简介:这是一份围绕“智造扬中”电力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 | 长序列、样本量大 | 高 | 多变量长时序预测 |
| 集成(LSTM+LightGBM) | 追求稳定排名 | 中高 | 最终提交主力方案 |
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_hours=3, iqr_multiplier=3, freq='15min'): # 计算滚动中位数和IQR,window由时间决定而非固定行数 rolling = series.rolling(window=f'{window_hours}h', min_periods=6) median = rolling.median() q1 = series.rolling(window=f'{window_hours}h', min_periods=6).quantile(0.25) q3 = series.rolling(window=f'{window_hours}h', min_periods=6).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(method='linear') df['load_clean'] = median_filter_outliers(df['load'])这里有两个参数要注意:window_hours要按数据集的采样频率调整,如果数据是1分钟一条,3小时窗口是180条记录,滚动计算量不小;iqr_multiplier在电力场景下用3比较合适——我们验证过用2会把正常的早晚高峰尖峰全砍掉,损失掉模型最需要的峰值信息。min_periods=6保证窗口内至少有一小时数据支撑统计量,否则前几个点会被置空。
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_len=24, pred_len=6): X, y = [], [] for i in range(len(data) - seq_len - pred_len + 1): X.append(data[i:i+seq_len]) y.append(data[i+seq_len:i+seq_len+pred_len]) return np.array(X), np.array(y) # data是二维数组 [样本数, 特征数] # seq_len=24 表示用过去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_len=96,此时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_size=128, num_layers=2, pred_len=6): super().__init__() # 双层LSTM,第一层输出给第二层,hidden_size从64调到此值后MAPE降了约1.2 self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.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_size=128和num_layers=2是电赛这种数据规模下的甜点值,再往上加到256和3层,验证集MAPE几乎不动,训练时间翻倍。dropout=0.2只加在两层LSTM之间,如果加到输入层会抑制模型学习短时波动。训练时我一般固定batch_size=256,学习率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, eps=1.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, min=eps))eps=1.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; a=pd.read_csv('submit.csv'); b=pd.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.05,GRU同区间,余下权重全给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_mape=10.2, gru_mape=10.5, lgb_mape=11.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_ratio=0.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_ratio=0.4这个阈值对15分钟粒度的负荷数据是我们的验证集调出来的。设太大起不到修正作用,设太小(比如0.1)会削平真实的高峰爬坡,在带储能或电动汽车接入的台区数据上反而制造误差。如果你不知道赛题数据里有没有这类突发负荷,先按0.4跑一遍,再看修正前后本地MAPE的变化方向,若修正后变差就调回0.6。
6.3 自建评测脚本:电赛专属的“后悔药”
最后分享一个我自己的习惯:专门为每次电赛写一个evaluate.py,把数据加载、格式校验、指标计算全封装起来。每次训完模型、生成完预测,都强制走一遍这个脚本才允许自己提交。脚本里除了算MAPE,还会自动检查预测值有没有NaN、有没有负值、行数对不对、列名是否匹配。这个脚本在这份资源里也被我整理好附带了,建议你拿到后先跑一遍示例验证数据,确认脚本输出正常,再替换成自己的模型和真实测试数据路径。
从那以后,我每次提交前都强制走一遍评测脚本,确认行数、列名、值域全过才敢点提交按钮。这套流程帮我规避了至少三次因格式问题被记零分的事故,希望帮到你。
本文还有配套的精品资源,点击获取