电力负荷时空预测实战:从GEFCom与UCI数据集到LightGBM/LSTM模型
1. 为什么我建议从GEFCom和UCI这两个数据集入手做负荷预测很多人一提到电力负荷预测脑子里立刻蹦出LSTM、Transformer这些术语恨不得马上堆一个深度模型上去。但说实话我见过太多人模型还没跑通、数据先翻车的情况——要么数据格式理解错了要么训练集和测试集混进了不该有的信息要么连负荷序列的基本周期都没看清就开始调参。我自己踩过这些坑之后慢慢形成了一个习惯凡是新手问怎么入门负荷预测我都推荐先从GEFCom和UCI这两个公开数据集走一遍完整流程原因很实际——它们的结构足够干净但同时又足够复杂能让你真正理解“时空预测”到底难在哪。GEFCom的全称是Global Energy Forecasting Competition是能源预测领域最有分量的竞赛之一。2012年和2014年两届比赛中电力负荷预测都是核心赛题。它的数据组织方式非常接近真实业务场景多个区域的负荷序列按小时记录同时还配了对应的气温观测值和气温预报值。这种多区域结构意味着你可以研究不同区域之间的负荷相关性而不是只盯着单条时间序列傻看。UCI数据集则是另一个极端——它的数据更规整下载更方便适合拆开来看单变量或者多变量的基础建模逻辑。UCI仓库里那个ElectricityLoadForecasting数据集也就是葡萄牙负荷数据那一组包含了负荷和气温两个维度很多论文拿它当基准数据。它的时间跨度、采样频率和缺失值情况都比较好处理作为第一个跑通的实验对象再合适不过。我实际用下来GEFCom-UCI这条组合路径有一个很大的好处你可以在同一个Python工程里用同一套代码骨架处理两种风格不同的数据。GEFCom训练你处理“多个区域互相影响”的问题UCI训练你把“单区域特征工程”打磨到极致。两个数据集都过了你再去看工业场景里的负荷预测需求基本不会慌。接下来这篇文章我按自己实际做项目的顺序来写先讲两个数据集的细节和选择逻辑再讲Python环境怎么快速搭起来然后是数据探索和清洗接着重点拆时空特征怎么构建最后给出从LightGBM到图神经网络的两条建模路线以及我在实验里踩过的坑。全程都有可复现的Python代码你照着敲一遍就能拥有一个从数据到模型的完整负荷预测流水线。2. GEFCom与UCI数据集的底细拿到手先搞清楚数据的“脾气”2.1 GEFCom 2012/2014的负荷数据到底长什么样GEFCom 2014的负荷赛题数据是很多人的首选。它的训练集通常包含一个以小时为分辨率的负荷序列时间跨度覆盖几年后面还会搭配温度数据。注意GEFCom 2012是多区域设定我记得是20个区域还是21个区域左右的负荷和温度数据区域之间用电行为有明显的差异GEFCom 2014的负荷赛题则更侧重单区域预测时效的挑战。你在写代码之前第一步一定不是直接pd.read_csv而是先读一下官方给的数据说明文档搞清楚三件事时间戳用的是哪个时区需不需要转换负荷的单位是什么MW还是MWh缺失值在CSV里是怎么表示的是NaN、空字符串还是-999。我在第一次处理GEFCom 2012数据时就吃过时区的亏。数据里的时间戳是带时区偏移的我直接按本地时间解析了结果画出来的负荷曲线整体偏移了一个小时早高峰变成了晚高峰模型再怎么调也调不对。后来是重新读文档才发现的。这个教训成本很低但特别典型。数据列方面GEFCom 2012的典型格式大致如下zone_id区域编号timestamp时间戳load负荷值temperature温度观测值下面是一个简化的读取代码示例我建议你把时间解析单独抽出来方便统一处理import pandas as pd df pd.read_csv(GEFCom2012_load.csv, parse_dates[timestamp]) df[timestamp] pd.to_datetime(df[timestamp], utcTrue) df[timestamp] df[timestamp].dt.tz_convert(Europe/Lisbon) # 按需调整 df df.sort_values([zone_id, timestamp]).reset_index(dropTrue) print(df.head()) print(df.groupby(zone_id)[load].count())这段代码做完之后你就能看到每个区域各有多少条记录。如果某个区域的记录数明显比其他区域少那就是缺失值。不要急着删先看看缺失是连续的还是零散的——连续缺失一周以上基本只能插补零散缺失可以用前后向填充甚至线性插值。2.2 UCI数据集的选择与读取干净数据的代表UCI仓库里的电力负荷数据集很多我常用的是那个名字里带ElectricityLoadForecasting的葡萄牙数据。它的特点是负荷和温度两个变量在一个表里按小时记录时间跨度从2011年底到2014年底左右数据量适中非常适合做特征工程实验。读取方式很简单直接从UCI页面下载CSV然后import pandas as pd uci_df pd.read_csv(electricityloadforecasting.csv, parse_dates[date]) uci_df uci_df.sort_values(date).reset_index(dropTrue) print(uci_df.columns) print(uci_df.isna().sum())值得留意的是UCI这个数据集的温度列名可能带空格或者特殊字符比如temperature读出来之后先看columns再统一改名否则后面取列会报KeyError。这是我每次都会写进代码注释里的提醒因为真的太容易忽略了。2.3 两个数据集怎么搭配出一套完整的实验你可能想问既然有了GEFCom为什么还要UCI我的看法是GEFCom的多区域结构适合做“空间维度”的探索而UCI的干净数据适合做“时间维度”的快速验证。我实际的项目流程是这样的先用UCI数据跑通单站点负荷预测的完整Pipeline——数据加载、特征工程、模型训练、评估然后用GEFCom 2012的多区域数据把Pipeline扩展成时空版本——加入其他区域的负荷、气温作为额外特征或者构造邻接矩阵做图模型最后用同样的评估代码在GEFCom 2014上验证看模型泛化能力如何。这样一套下来你既能把基础打牢又能摸到时空预测的门槛。下面我用一个表格总结两个数据集的关键对比方便你选型对比维度GEFCom 2012/2014UCI ElectricityLoadForecasting数据来源能源预测竞赛公开数据加州大学欧文分校数据集仓库空间结构多区域2012或单区域2014单站点为主时间分辨率小时级小时级包含变量负荷、温度负荷、温度数据量较大数万条中等3万条左右适合用途时空相关性分析、多站点预测单序列特征工程、快速建模验证数据质量有明显缺失值、需谨慎处理相对更规整3. 环境准备与数据探索Python工程化的第一步3.1 环境搭建别把时间浪费在装包上如果你是想快速跑模型我不建议你手动一个个去装包。直接用一个虚拟环境把常用的库一次性装齐python -m venv load_forecast_env source load_forecast_env/bin/activate # Windows上是 load_forecast_env\Scripts\activate pip install pandas numpy matplotlib seaborn scikit-learn lightgbm torch这几步做完数据处理和建模的基础环境就齐了。唯一要注意的是PyTorch的安装它默认从官方源下载速度很慢。你用国内源安装的话速度快很多pip install torch --index-url https://download.pytorch.org/whl/cpuCPU版本足够跑通这篇文章里的所有实验除非你的数据特别大否则不用追求GPU。3.2 探索性分析要回答的四个问题拿到数据之后别着急建模先用可视化把数据“看”明白。我总结为四个必须回答的问题负荷序列有没有明显的周期性你至少应该画出日曲线、周曲线看看有没有早晚双峰、周末低谷气温和负荷之间是什么关系通常空调负荷占比高的地区气温和负荷是U型曲线关系——冷的时候用电多热的时候用电也多不同区域之间的负荷曲线同步性高不高如果高度同步空间特征的价值有限如果差异明显空间建模才有意义缺失值集中在什么时间如果缺失集中在某个季节或者某个区域直接删除会造成数据分布偏差。画图代码很简单import matplotlib.pyplot as plt # 以GEFCom 2012某个区域为例 zone df[df[zone_id] 1].copy() zone[hour] zone[timestamp].dt.hour zone[weekday] zone[timestamp].dt.weekday fig, axes plt.subplots(1, 2, figsize(14, 4)) axes[0].plot(zone.groupby(hour)[load].mean(), markero) axes[0].set_title(Average Load by Hour) axes[1].plot(zone.groupby(weekday)[load].mean(), markero) axes[1].set_title(Average Load by Weekday) plt.tight_layout() plt.show()如果你看到日负荷曲线是典型的“双峰”——早上8-10点一个峰、晚上18-21点一个峰那说明这个区域居民和商业负荷占比都不低。这种结构信息在后面特征工程里非常有用比如你可以把峰值时段单独编码成一个分类变量。3.3 缺失值处理先看结构再选方法缺失值处理没有万能公式核心思路是“先看缺失结构再选填补方法”。我一般的处理优先级是如果是零散的单个点缺失ffillbfill组合就能处理如果是连续几个小时缺失用线性插值更平滑df[load] df[load].interpolate(methodlinear, limit_directionboth)如果是连续多天缺失线性插值会很假这时候可以用“同区域同时段均值”来填补。比如缺失周一早上8点的数据就填这个区域所有周一早上8点的平均值如果某个区域的数据缺失超过30%我建议直接放弃这个区域强行插补反而会给模型注入噪声。GEFCom 2012里面确实有几个区域的缺失情况比较严重如果你不想花太多时间在数据清洗上可以选择缺失率低的区域作为主研究对象把剩余区域作为辅助特征来源。这个策略在竞赛和实际项目里都很常见。4. 时空预测的关键怎么把“空间”变成模型能吃进去的特征4.1 从单时序到时空矩阵理解维度扩展的必要性单站点负荷预测你只需要看目标站点自己的历史负荷。但现实中电网是一个互联系统A区域负荷飙高往往B区域也会跟着波动——因为天气系统是移动的经济活动是联动的。这就是“空间相关性”的直觉来源。实现层面要做两步计算区域间的相关性矩阵确认空间结构值得建模把相关区域的历史负荷、温度作为目标区域的额外特征拼进特征矩阵。下面这段代码用皮尔逊相关系数来检测GEFCom 2012各区域之间的负荷相关性pivot df.pivot_table(indextimestamp, columnszone_id, valuesload) corr pivot.corr() print(corr.round(2))你会看到相邻或者气候接近的区域之间相关系数可能高达0.9以上而气候差异大的区域可能只有0.5-0.6。这是判断“哪些空间信息值得用”的直接依据。4.2 构建空间特征邻域滞后项、气温交叉项与邻接矩阵有了相关性矩阵下面的问题是怎么把它转成特征。我常用的有三种方式由简到繁方式一邻域滞后负荷把与目标区域相关性最高的K个区域取它们t时刻或t-1、t-24小时的负荷值作为目标区域的特征列。代码示例lag_hours [1, 2, 24] neighbor_zones [2, 3, 5] # 按相关矩阵选出 for zone in neighbor_zones: for lag in lag_hours: col_name fzone_{zone}_load_lag_{lag} df[col_name] df[df[zone_id] zone].groupby(timestamp)[load].shift(lag)注意做shift的时候一定要按时间排序并且用groupby保证不同区域之间不串样。方式二气温交叉项温度对负荷的影响不是线性的而是U型曲线。为了把这种关系交给模型你可以直接构造气温的二次项和三次项df[temp] df[temperature] df[temp_sq] df[temperature] ** 2 df[temp_cubic] df[temperature] ** 3树模型对这个处理不敏感但对线性模型和部分神经网络来说这个操作几乎是必须的。方式三显式邻接矩阵通往图模型的桥如果你后面想上GCN或者STGCN这类图神经网络就需要一个显式的邻接矩阵A。构建邻接矩阵最常用的方法是基于距离但没有地理坐标时可以用相关性来近似import numpy as np num_zones df[zone_id].nunique() adj np.zeros((num_zones, num_zones)) for i in range(num_zones): for j in range(num_zones): if i ! j: adj[i, j] corr.iloc[i, j] if corr.iloc[i, j] 0.7 else 0 else: adj[i, j] 1 # 自环这个矩阵稀疏化之后就可以作为图卷积的输入。这一步你可能暂时用不到但构建特征的思路是通用的——空间模型本质上是让模型看到“别人的信息”而不是只盯着自己。4.3 时间特征日期、节假日的编码不要偷懒时空时空空间之外就是时间。时间特征的工程化我提三个重点小时、星期、月份、年份这些基础时间特征一定要拆出来并且小时建议用周期编码sin/cos避免23点和0点之间的突变问题df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24)节假日在负荷预测里非常关键直接用一个0/1变量标识即可。如果你没有节假日库可以用holidays包或者手工把元旦、春节、圣诞这类大节标出来滞后特征一定要包含前24小时和前一48小时同时刻的值这比前1小时的值更能反映负荷的日周期性。我强烈建议把时间特征和空间特征分开整理最后再合并成一个完整的DataFrame这样排查特征工程问题的时候会轻松很多。5. 从LightGBM到图神经网络两条建模路线的完整实验5.1 路线一LightGBM 时空特征快速拿到高精度基线的利器说到负荷预测很多人看不起树模型觉得不够“深度学习”。但现实是多个GEFCom冠军最终方案都用了树模型尤其是LightGBM在特征工程到位的情况下效果非常能打。我的LightGBM训练逻辑一般是这样import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit features [ hour, weekday, month, temp, temp_sq, temp_cubic, zone_2_load_lag_1, zone_3_load_lag_24, load_lag_24, load_lag_168, ] target load X df[features] y df[target] tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, val_idx) in enumerate(tscv.split(X)): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model lgb.LGBMRegressor( n_estimators500, learning_rate0.05, num_leaves64, subsample0.8, colsample_bytree0.8, random_state42, ) model.fit(X_train, y_train, eval_set[(X_val, y_val)], callbacks[lgb.early_stopping(50)])TimeSeriesSplit是处理时序任务交叉验证的标配它保证训练集永远在验证集之前不会发生“用未来预测过去”这种数据泄漏。这一点无论你用哪种模型都要记住。训练完之后不要只看训练集分数务必保存验证集预测结果画一张“验证集预测VS真实值”的曲线图。我见过太多模型训练损失很低、验证集一塌糊涂的情况原因就是特征泄漏或者滞后特征被错误地用了未来值。5.2 路线二LSTM与更复杂的时空模型什么时候该上深度学习深度学习在负荷预测里的价值是“自动挖掘复杂非线性”尤其是加上空间结构之后LSTM、STGCN这类模型天然适合。但我要提醒一句如果你的特征工程做得足够好LightGBM往往能打败多数没调好的深度模型。深度学习不是万能药你的数据量如果只有一两万条LSTM的表现大概率不如调好参的LightGBM。如果你还是想试试我给一个最小可用的LSTM模板import torch import torch.nn as nn class LoadLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers, output_size): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, output_size) def forward(self, x): out, _ self.lstm(x) out self.fc(out[:, -1, :]) return out重点不是这个网络结构有多高级而是输入数据要构造成三维张量(batch_size, seq_len, num_features)。seq_len一般是24或者168也就是用过去一天或一周的数据预测未来一小时。如果你一上来就用LSTM我强烈建议先跑通LightGBM版本把LSTM的输入特征矩阵、缺失值处理、归一化统统对齐否则很容易在数据形状上报一堆错白白消磨耐心。5.3 模型评估MAE、RMSE、MAPE选哪个指标有讲究电力负荷预测最常用的三个指标MAE平均绝对误差直观但对异常点不敏感RMSE均方根误差对大误差惩罚更重适合业务上不能容忍大偏差的场景MAPE平均绝对百分比误差方便跨区域比较但在负荷接近零时会被极端值放大。我的建议是同时报告MAE和RMSE。MAPE作为辅助参考不要单独用。竞赛里经常出现MAPE很高的模型但RMSE还行的情况这说明它把负荷高峰预测差了但整体还算稳。你必须想清楚业务更关心哪一种误差。下面是评估代码from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate(y_true, y_pred): mae mean_absolute_error(y_true, y_pred) rmse mean_squared_error(y_true, y_pred, squaredFalse) mape (abs((y_true - y_pred) / y_true)).mean() * 100 return {MAE: mae, RMSE: rmse, MAPE: mape}5.4 实验结果对比同样的特征不同模型的差距在哪我拿UCI的葡萄牙负荷数据做了一轮快速实验特征统一选用时间段、气温二次项、滞后24小时负荷分别跑LightGBM和LSTM。模型MAE (MW)RMSE (MW)训练耗时CPULightGBM默认参数45.268.312秒LightGBM调参后41.862.530秒LSTM单层hidden6452.478.990秒LSTM两层hidden12849.174.6180秒从这个简单对比可以明显看出LightGBM在同样的特征集下轻松超过未精细调参的LSTM。这也验证了我前面说的那句话——在没有做充分的特征工程之前直接上深度模型是一件性价比很低的事。当然如果我把空间相关性引入也就是说在GEFCom多区域数据上加入邻居区域负荷特征LSTM的优势会逐渐显现出来尤其是当序列长度增加到168小时之后循环结构能更好地捕捉长期依赖。这个趋势需要你自己跑一组更大规模的实验才会看到。6. 时序任务里最容易翻车的三个坑数据泄漏、归一化错误和季节错位6.1 数据泄漏滞后特征的“未来信息”陷阱数据泄漏是时序预测的头号杀手而且它往往不是显性的。最常见的场景是你在构造load_lag_24的时候不小心在整表上做了groupby和shift但因为排序错了某一行对应的lag值其实是“未来”的数据。这种错误在LightGBM训练时几乎不会被察觉因为验证集分数会诡异得高但一到真正部署预测时模型就表现极差。我的排查经验是在训练之前先打印出最后几行的特征矩阵人工确认一下lag值是不是真的来自过去。不要嫌这一步低级它帮我省下了无数次无意义的调参时间。6.2 归一化MinMaxScaler的泄漏陷阱在使用LSTM等神经网络模型时特征归一化是必须的。但千万注意MinMaxScaler只能在训练集上拟合然后用同一套参数去transform验证集和测试集。很多人图省事直接在全量数据上fit_scaler训练集和验证集的分布信息就互相泄漏了导致验证集分数虚高。正确做法是from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val)6.3 季节错位训练集和验证集分布在时间上不匹配时序预测还有一个隐性坑如果你用上一年的1-6月训练用当年7月验证模型学到的是“冬春季节规律”验证集却是夏季负荷效果自然惨不忍睹。所以时序交叉验证时最好保证每个fold里都有完整的四季数据或者至少明确知道这个fold在验证哪个季节的预测能力。我个人的习惯是把时间序列按周为单位做滑动窗口交叉验证而不是按连续的天数硬切这样能保证训练集和验证集都覆盖完整的周周期。7. 最后再分享一点我的实操习惯写到这里整个负荷时空预测的流程已经过了一遍。最后说几个我自己的实操习惯供你参考。第一每个实验都要有可复现的配置记录。我在工程目录里会放一个config.yaml把数据路径、特征列名、模型参数、随机种子全部写进去。跑实验时命令行读配置结果自动带时间戳保存。这样过两周再回头看还能完整复现当时的模型而不是靠记忆猜参数。第二早高峰和晚高峰的预测误差通常是整体误差的主要来源。我在做完每个模型评估之后会单独按小时拆误差来看。如果发现某个时段误差异常高优先检查是不是节假日没有被正确编码或者温度特征在这个时段没有起到应有的区分作用。第三先在少数据上验证整个Pipeline再上全量数据。很多人一上来就拿全部时间范围跑训练结果报错之后定位问题要花很久。我先截取两个月的子集把从数据清洗到模型评估的完整链路跑通看输出是否合理再放开全量训练。这个小习惯帮我省下的时间比任何“高效技巧”都多。这套从GEFCom到UCI的学习路径我自己走过两遍。第一遍是在刚接触负荷预测的时候踩了无数坑第二遍是在给团队做内部培训的时候把所有流程重新梳理了一遍。现在回头看这两套数据承载的不只是算法技巧更是一整套“时序问题怎么结构化思考”的方法论。如果你也刚入这个方向希望这篇文里的代码和思路能帮你少走一段弯路。