ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

电力AI赛题实战:从数据清洗到LSTM集成建模全流程

电力AI赛题实战:从数据清洗到LSTM集成建模全流程 简介这是一份围绕“智造扬中”电力AI大赛的完整Python实战源码包面向电气电子类竞赛参赛者及电力AI学习者旨在展示从数据拆分、特征提取到模型训练与预测的完整工程链路。压缩包共18个文件约835KB以6个Python脚本、4个CSV数据表、2个XML工程配置及readme、sh运行脚本等构成其中data_split、feature_extract、model.py等模块清晰划分了数据处理、特征工程与建模环节run_all.sh可用于一键复现流程。目前已学习44人适合具备一定Python基础、希望快速上手电力负荷预测或电网智能管理项目的读者。通过阅读源码可掌握天池电力预测表的清洗与特征构造思路、模型调参逻辑及输出预测结果csv的完整做法为参赛或毕业设计提供可移植的代码模板。1. 电赛电力AI赛题这份压缩包把从数据清洗到提交的全流程摊开了电赛这几年最让队伍头疼的不是控制题而是带“AI”字样的电力方向题目25年B题、E题和26年G题、F题里都能看到负荷辨识、电力预测、故障诊断的影子。拿到一份不知道从哪下手的JSON或CSV很多队伍第一反应是套个LSTM上去结果验证集刷得挺高提交分数却掉得离谱。这份「大航杯智造扬中电力AI大赛_1.zip」的完整解题包正好把这条链路补齐赛题数据长什么样、特征怎么做、模型怎么选、结果按什么格式交。适合三类人正在备赛电赛电力AI方向的学生队伍、要做电力数据分析的课题组、以及想拿真实电力数据集练手但被数据清洗劝退的工程师。下文所有操作我按这套资源里最典型的流程拆开讲。2. 从赛题到方案选型电力数据为什么不能直接套通用机器学习流程2.1 电力AI赛题到底在考什么电力AI赛题和普通的数据竞赛有个明显区别数据带有强时间依赖性和周期性而且采集端经常出问题。负荷预测、台区辨识、电压异常检测这几类题数据通常来自智能电表或SCADA系统采样间隔从1分钟到15分钟不等文件里除了负荷值还有时间戳、温度、湿度、电价等多维字段。赛题表面让你“预测未来时段负荷”实际考察的是两件事能不能把时间序列特征吃透以及你的提交格式能不能被评分脚本正确解析。我在拆这份资源时先看了它的数据目录结构典型的电赛-style布局训练集按日期分片存CSV测试集是连续时间段标签列单独放一个文件。这种“分段给数据”的模式在电赛里非常常见意图就是逼你手动对齐时间索引而不是直接读进来就训练。数据里还留了不少空值和尖峰毛刺这是故意埋的预处理考点。2.2 模型选型时序深度模型与树模型的取舍很多队伍一上来就上Transformer但电力负荷数据的样本量通常只有几万条时间长度也短Transformer在小样本时序上很容易过拟合收敛还慢。我一般建议先做两组基线一组是LSTM/GRU这类循环模型另一组是把时序数据展平成特征表丢给LightGBM。两份资源里给的参考方案也是这个思路深度学习模型负责捕捉短期依赖树模型负责吃统计特征、外部因素特征。选型判断标准看三点数据量、序列长度、外部特征占比。数据量低于5万条时LSTM比Transformer稳GRU又比LSTM轻量序列长度超过48个时间步考虑注意力机制或直接上TCN温度、湿度、电价这些外部特征占比重时树模型的上限往往更高因为深度模型对这类异构特征的拟合效率不高。具体取舍可以直接参照表2-1。模型适合场景训练成本电赛电力题常见用途LightGBM特征工程充足、外部变量多低负荷预测、异常检测LSTM/GRU序列模式强、样本量适中中短期负荷预测、电压序列建模Transformer长序列、样本量大高多变量长时序预测集成LSTMLightGBM追求稳定排名中高最终提交主力方案2.3 评价指标与提交格式先对齐评价指标决定了损失函数怎么设计。电力预测题用得最多的是MAPE平均绝对百分比误差和RMSE个别题会用分类准确率或F1-score判断“辨识”型任务。这份资源在文档里明确写了按MAPE打分这点非常关键——如果按RMSE做损失函数模型会对峰值负荷投入过多注意力而MAPE对近零值的小绝对误差非常敏感两者的最优解完全不同。实操上损失函数用HuberLoss或MAPE的平滑版本输出层不加激活函数训练时监控验证集的MAPE早停阈值设在20个epoch内不下降即停。提交格式一般是一个CSV两列预测时间点、预测值。行数必须和测试集严格一致索引错位是电赛提交最常见的问题第5章我会专门讲。3. 数据预处理与特征工程把现场采集的电力数据变成能训的样本3.1 缺失值与异常值处理电力数据里的缺失值分三种单点缺失、连续缺失、整段缺失。单点缺失用前后时刻线性插值就能补连续缺失超过3个采样点时线性插值会抹掉趋势我一般用同星期类型、同时段的均值填充整段缺失比如某一天文件整个没有只能放弃这一段不要让模型自己去“脑补”一整天。异常值要区分是采集噪声还是真实尖峰。负荷突增可能是设备启动也可能是抄表错误。一个稳妥的判断方法是滚动中位数滤波某个时刻的值偏离前后3小时中位数超过3倍IQR就标记为异常替换成中位数。代码实现很简单import pandas as pd import numpy as np def median_filter_outliers(series, window_hours3, iqr_multiplier3, freq15min): # 计算滚动中位数和IQRwindow由时间决定而非固定行数 rolling series.rolling(windowf{window_hours}h, min_periods6) median rolling.median() q1 series.rolling(windowf{window_hours}h, min_periods6).quantile(0.25) q3 series.rolling(windowf{window_hours}h, min_periods6).quantile(0.75) iqr q3 - q1 lower median - iqr_multiplier * iqr upper median iqr_multiplier * iqr # 超出边界的置为NaN后续统一插值 cleaned series.where((series lower) (series upper)) return cleaned.interpolate(methodlinear) df[load_clean] median_filter_outliers(df[load])这里有两个参数要注意window_hours要按数据集的采样频率调整如果数据是1分钟一条3小时窗口是180条记录滚动计算量不小iqr_multiplier在电力场景下用3比较合适——我们验证过用2会把正常的早晚高峰尖峰全砍掉损失掉模型最需要的峰值信息。min_periods6保证窗口内至少有一小时数据支撑统计量否则前几个点会被置空。3.2 时间特征与滑窗构造电力负荷有典型的“三周期性”一天24小时、一周7天、一年四季。模型没法直接理解时间戳字符串必须拆成数值特征。我习惯先把时间戳拆出小时、星期、是否工作日、距离最近节假日的天数再加一个hour_sin和hour_cos这样的周期编码。注意星期特征不要用顺序整数周一到周日是1到7用one-hot或者周期编码更好因为周六和周一在语义上不相邻。滑窗构造是时序模型的第二步。对LSTM来说窗口长度我们试过6、12、24、48电赛这种15分钟粒度的数据24步即6小时效果最好太长反而把噪声也装进去。滑窗代码要注意步长训练时滑动步长设为1来最大化样本数但构造测试集预测时步长必须等于预测长度否则会产生重叠预测。def make_sequences(data, seq_len24, pred_len6): X, y [], [] for i in range(len(data) - seq_len - pred_len 1): X.append(data[i:iseq_len]) y.append(data[iseq_len:iseq_lenpred_len]) return np.array(X), np.array(y) # data是二维数组 [样本数, 特征数] # seq_len24 表示用过去24个时间步预测未来6个时间步 X_train, y_train make_sequences(df[[load,hour_sin,hour_cos]].values, 24, 6) print(X_train.shape, y_train.shape) # 输出类似 (89352, 24, 3) (89352, 6, 3)这段代码有个容易踩的地方pred_len是预测步数它决定了输出层的神经元个数和MAPE的计算方式。如果赛题要求预测未来一天而数据是15分钟粒度pred_len96此时LSTM输出层的96个神经元直接对应96个时间点的值这种映射关系要提前憋清楚否则后面对齐标签时必乱。3.3 归一化与数据划分防止正态分布统计量泄漏归一化是电赛数据处理里翻车率最高的环节。最大的坑用全量数据的均值、方差去做归一化再划分训练集和验证集。这等于把验证集和测试集的统计信息泄露给了模型验证集上永远好看但一到真正未知的测试集就露出原形。正确做法是先切分、再单独对训练集拟合scaler然后transform验证集和测试集。此外电力数据是时间序列划分不能随机打乱必须按时间顺序前80%作训练、后20%作验证。随机shuffle在你第一次拿到数据时几乎一定会手滑一旦shuffle模型学到的是“记住了所有时间点的统计规律”而不是“根据过去推未来”的能力。from sklearn.preprocessing import StandardScaler # 先按时间切分再做归一化 split_idx int(len(df) * 0.8) train_df df.iloc[:split_idx].copy() val_df df.iloc[split_idx:].copy() scaler StandardScaler() # 只fit训练集transform全部 train_df[[load]] scaler.fit_transform(train_df[[load]]) val_df[[load]] scaler.transform(val_df[[load]])一个我每次都检查的操作fit_transform和transform别写反写反了等于用验证集的均值方差去归一化训练集整个特征分布直接错乱。这种错误在代码review时很难一眼看出来因为训练loss还是能降只是验证曲线会产生异常的抖动。4. 模型训练与调参把基准方案跑到榜单中位以上4.1 训练脚本骨架与关键参数这份资源的训练脚本主体是PyTorch写的一个双层LSTM输出层接全连接。选PyTorch而不是Keras或纯sklearn的理由是电赛环境经常要求选手在本地离线GPU上跑题PyTorch的显存控制和断点续训机制比Keras好调试出错时能直接看到梯度流向。整个脚本分五段配置读取、数据处理、模型定义、训练循环、验证保存。import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset class ElectricityLSTM(nn.Module): def __init__(self, input_size, hidden_size128, num_layers2, pred_len6): super().__init__() # 双层LSTM第一层输出给第二层hidden_size从64调到此值后MAPE降了约1.2 self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0.2 # 两层之间的dropout最后一层后的输出不加 ) self.fc nn.Linear(hidden_size, pred_len) def forward(self, x): out, _ self.lstm(x) # 取最后一个时间步的hidden state 做预测 return self.fc(out[:, -1, :])hidden_size128和num_layers2是电赛这种数据规模下的甜点值再往上加到256和3层验证集MAPE几乎不动训练时间翻倍。dropout0.2只加在两层LSTM之间如果加到输入层会抑制模型学习短时波动。训练时我一般固定batch_size256学习率1e-3配AdamW权重衰减1e-5跑30个epoch后手动降学习率到3e-4再跑20轮。4.2 损失函数与评估指标的对齐逻辑刚才第2章提过评分是MAPE但直接用MAPE值做损失函数有个问题当真实值非常接近0时MAPE的梯度会爆炸。电赛数据里凌晨时段的负荷确实会掉到很低的量级所以这份资源用的是平滑MAPE变体在分母上加一个小常数防止除零。def smooth_mape_loss(y_pred, y_true, eps1.0): # eps取1.0量级和负荷值域(几十到几千)对齐 # 如果直接把eps设成1e-6凌晨低负荷样本的loss会主导整个训练 diff torch.abs(y_true - y_pred) return torch.mean(diff / torch.clamp(torch.abs(y_true) eps, mineps))eps1.0这个值不是随手填的。如果把它设成1e-6低负荷时段的样本每个都能产生巨大梯度模型会为了拟合凌晨的低值牺牲白天高峰段的精度而白天高峰段恰好是评分权重最大的部分。反过来eps设得过大100以上所有样本的误差都被压缩成一个量级模型学不到区分度。跑实验时可以先统计一下训练集负荷的5%分位数把eps设成比这个值再小一档我们这套数据5%分位约在8.5左右取1.0这个保守值效果最好。4.3 本地评测脚本与模型保存训练结束不急着提交本地先出一个评测报告。验证集上的MAPE、RMSE、以及按小时分段的误差曲线都要看一眼。如果某个小时段的误差明显高于其他段说明特征和模型对该时段捕捉不足回3.2补特征。# 按小时统计验证集误差 val_df[hour] val_df.index.hour val_df[abs_err_ratio] (val_df[pred] - val_df[load]).abs() / val_df[load] hourly_mape val_df.groupby(hour)[abs_err_ratio].mean() # 常见的异常早上6-8点误差飙高说明模型没学到早高峰斜坡特征 print(hourly_mape) # 保存时同时存模型结构和参数避免PyTorch版本不一致导致load失败 torch.save({ model_state: model.state_dict(), config: {input_size: 13, hidden_size: 128, pred_len: 6} }, best_model.pth)保存时顺手把模型配置写进同一个文件这点很值得养成习惯。电赛现场经常换机器跑PyTorch版本一换直接load模型权重会报奇怪的键名错误有了config字段可以随时重建网络结构再load。模型用哪个epoch的文件也有讲究保存验证集MAPE最优的那个不保存最后一个epoch——时序模型最后一个epoch大概率已经过拟合。5. 电赛电力AI实战避坑训练泄漏、时区对齐与结果格式三个老大难5.1 五条真实踩坑记录第一条验证集MAPE只有8%提交分数却到15%。原因出在数据划分时用了train_test_split默认的shuffle把未来数据泄露给了训练集。解决无条件按时间顺序划分划分后分别打印首尾时间戳确认没有重叠。从那以后我对每个队伍都强调时间序列的验证集不允许随机抽样这是红线。第二条提交CSV的行数始终比官方样例多出来几十行。原因是生成预测时数据里有重复索引groupby后没去重导致多行。解决提交前做一次行数校验。wc -l submit.csv wc -l sample_submit.csv python -c import pandas as pd; apd.read_csv(submit.csv); bpd.read_csv(sample_submit.csv); print(a.shape, b.shape, a[time].is_unique)第三个命令里is_unique是False的话说明索引重复先把duplicated的行找出来删掉再复检。第三条预测值整体比真实值偏低15%。原因是归一化时对训练集和测试集分别做了fit_transform测试集里出现训练集没见过的峰最大值域transform后标准值被压到异常区间。解决用训练集拟合scaler后保存测试集只transform并检查测试集最大值是否超出训练集最大值3个标准差。超出的话说明测试数据分布漂移需要加日志特征或做差分。第四条模型训练loss下降但验证MAPE在20个epoch后开始锯齿状震荡。原因是学习率偏大AdamW虽然自带自适应但1e-3对这个数据集还是太高。解决换成余弦退火调度器从1e-3降到1e-4周期长度设为总epoch数的1/2我们实测震荡完全消失最终MAPE还降了0.8。第五条提交格式被拒系统报“列名不匹配”。原因是官方要求列名是timestamp,predict我们输出成了time,load。解决写一个格式对齐函数读取官方样例的列名和顺序强制把结果的列名改成样例名再输出。每次提交前都跑这个校验脚本列名、行数、数据类型、有无NaN全查一遍。5.2 评分脚本黑匣子怎么应对电赛的评分脚本通常不公开你只能在提交后拿到一个总分。这个黑匣子让很多人焦虑但换个角度想它也有规律可循。你可以在本地尽量仿真它按官方给的样例格式写一个自己的评分函数用验证集跑一遍记录本地MAPE提交后再对比官方分数。两次分数的差异如果稳定在一个固定偏差比如本地10%、官方12.5%说明官方的测试集和你的验证集分布接近你完全可以把这个偏差当作校准常数。反过来如果两次差异忽大忽小多半是提交结果里存在异常预测值回头检查凌晨时段的输出看看有没有负值或离谱的尖峰。6. 进阶把单模型换成集成与后处理白捡两三个百分点6.1 集成策略LSTM、LightGBM、GRU三个模型加权单模型的提升空间有限集成才是电赛拿稳排名的关键。最省事的方案是三模型加权平均LSTM和GRU在序列建模上各有侧重LightGBM则擅长捕捉外部特征的交叉效应。权重的搜参方式不复杂网格搜索就能搞定LSTM从0.2到0.5步长0.05GRU同区间余下权重全给LightGBM以验证集MAPE为最低目标。def ensemble_predict(models, weights, x_seq, x_tab): preds np.zeros(len(x_seq)) for i, (model, w) in enumerate(zip(models, weights)): if isinstance(model, torch.nn.Module): # 深度模型喂序列数据树模型喂展平特征表 model.eval() with torch.no_grad(): pred model(x_seq).cpu().numpy().flatten() else: pred model.predict(x_tab).flatten() preds w * pred return preds # 权重示例先各自单独跑出MAPE按误差反比初始化再微调 # lstm_mape10.2, gru_mape10.5, lgb_mape11.8 # 初始权重约 [0.38, 0.36, 0.26] best_weights [0.37, 0.35, 0.28] pred ensemble_predict([lstm, gru, lgb], best_weights, x_seq, x_tab)集成有个前提条件三个模型必须保证用同一套时间步划分逻辑否则预测值在时间轴上对不齐。我实践下来最简单的是三个模型共享同一个训练集和归一化参数深度模型吃原始序列LightGBM吃经过时序统计聚合的特征表最后在预测阶段求和。6.2 后处理规则修正与输出平滑电赛电力题还有一个容易得分的小技巧物理约束后处理。电力负荷有明确的物理边界——不能为负不会瞬间跳变过大。模型预测值偶尔会违反这些常识直接提交就扣分。我会在输出后加两道修正负值截断为0相邻时间步跳变量超过前值40%的按前值40%封顶。def rule_postprocess(pred, max_jump_ratio0.4): pred np.maximum(pred, 0) for i in range(1, len(pred)): prev pred[i-1] diff pred[i] - prev limit prev * max_jump_ratio if diff limit: pred[i] prev limit return pred pred rule_postprocess(pred, 0.4)max_jump_ratio0.4这个阈值对15分钟粒度的负荷数据是我们的验证集调出来的。设太大起不到修正作用设太小比如0.1会削平真实的高峰爬坡在带储能或电动汽车接入的台区数据上反而制造误差。如果你不知道赛题数据里有没有这类突发负荷先按0.4跑一遍再看修正前后本地MAPE的变化方向若修正后变差就调回0.6。6.3 自建评测脚本电赛专属的“后悔药”最后分享一个我自己的习惯专门为每次电赛写一个evaluate.py把数据加载、格式校验、指标计算全封装起来。每次训完模型、生成完预测都强制走一遍这个脚本才允许自己提交。脚本里除了算MAPE还会自动检查预测值有没有NaN、有没有负值、行数对不对、列名是否匹配。这个脚本在这份资源里也被我整理好附带了建议你拿到后先跑一遍示例验证数据确认脚本输出正常再替换成自己的模型和真实测试数据路径。从那以后我每次提交前都强制走一遍评测脚本确认行数、列名、值域全过才敢点提交按钮。这套流程帮我规避了至少三次因格式问题被记零分的事故希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进