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"], utc=True) df["timestamp"] = df["timestamp"].dt.tz_convert("Europe/Lisbon") # 按需调整 df = df.sort_values(["zone_id", "timestamp"]).reset_index(drop=True) 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(drop=True) 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/2014 | UCI 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(), marker="o") axes[0].set_title("Average Load by Hour") axes[1].plot(zone.groupby("weekday")["load"].mean(), marker="o") axes[1].set_title("Average Load by Weekday") plt.tight_layout() plt.show()如果你看到日负荷曲线是典型的“双峰”——早上8-10点一个峰、晚上18-21点一个峰,那说明这个区域居民和商业负荷占比都不低。这种结构信息在后面特征工程里非常有用,比如你可以把峰值时段单独编码成一个分类变量。
3.3 缺失值处理:先看结构,再选方法
缺失值处理没有万能公式,核心思路是“先看缺失结构,再选填补方法”。我一般的处理优先级是:
- 如果是零散的单个点缺失,
ffill+bfill组合就能处理; - 如果是连续几个小时缺失,用线性插值更平滑:
df["load"] = df["load"].interpolate(method="linear", limit_direction="both") - 如果是连续多天缺失,线性插值会很假,这时候可以用“同区域同时段均值”来填补。比如缺失周一早上8点的数据,就填这个区域所有周一早上8点的平均值;
- 如果某个区域的数据缺失超过30%,我建议直接放弃这个区域,强行插补反而会给模型注入噪声。
GEFCom 2012里面确实有几个区域的缺失情况比较严重,如果你不想花太多时间在数据清洗上,可以选择缺失率低的区域作为主研究对象,把剩余区域作为辅助特征来源。这个策略在竞赛和实际项目里都很常见。
4. 时空预测的关键:怎么把“空间”变成模型能吃进去的特征
4.1 从单时序到时空矩阵:理解维度扩展的必要性
单站点负荷预测,你只需要看目标站点自己的历史负荷。但现实中电网是一个互联系统,A区域负荷飙高,往往B区域也会跟着波动——因为天气系统是移动的,经济活动是联动的。这就是“空间相关性”的直觉来源。
实现层面,要做两步:
- 计算区域间的相关性矩阵,确认空间结构值得建模;
- 把相关区域的历史负荷、温度作为目标区域的额外特征,拼进特征矩阵。
下面这段代码用皮尔逊相关系数来检测GEFCom 2012各区域之间的负荷相关性:
pivot = df.pivot_table(index="timestamp", columns="zone_id", values="load") 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 = f"zone_{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_splits=5) 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_estimators=500, learning_rate=0.05, num_leaves=64, subsample=0.8, colsample_bytree=0.8, random_state=42, ) 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_first=True) 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, squared=False) 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) | 训练耗时(CPU) |
|---|---|---|---|
| LightGBM(默认参数) | 45.2 | 68.3 | 12秒 |
| LightGBM(调参后) | 41.8 | 62.5 | 30秒 |
| LSTM(单层,hidden=64) | 52.4 | 78.9 | 90秒 |
| LSTM(两层,hidden=128) | 49.1 | 74.6 | 180秒 |
从这个简单对比可以明显看出,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的学习路径,我自己走过两遍。第一遍是在刚接触负荷预测的时候,踩了无数坑;第二遍是在给团队做内部培训的时候,把所有流程重新梳理了一遍。现在回头看,这两套数据承载的不只是算法技巧,更是一整套“时序问题怎么结构化思考”的方法论。如果你也刚入这个方向,希望这篇文里的代码和思路能帮你少走一段弯路。