☰
电力负荷预测系统实战:ARIMA、GRU、LSTM、Transformer等七类模型选型与避坑指南
2026/10/31 14:18:40 网站建设 项目流程

简介:这份资源面向电力系统调度、能源管理及时间序列预测方向的学习者与工程人员,提供一套基于历史负荷数据预测每小时电力负荷变化趋势的完整代码集合,可用于电网调度优化与能源分配决策。压缩包共11个文件,约54KB,以8个Python脚本为核心,分别实现ARIMA、决策树、KNN、GRU、LSTM、随机森林与Transformer等模型的建模与预测流程,另附README说明、说明文件与附赠资源文档,便于理解各模型的原理、调用方式与适用场景。已有63人学习下载。读者可借助这些脚本对比传统统计方法与深度学习、集成学习在负荷预测上的表现,掌握从数据预处理、特征构建到模型训练与结果评估的完整链路,并可按需组合多种模型提升预测精度与鲁棒性,为电网调度和能源管理提供可复用的实践参考。

1. 电力负荷预测系统:从每小时数值到调度决策的那条链路

电网调度值班的人最怕什么?不是负荷高,而是负荷拐点来得比计划早两个小时。一座省级电网的日负荷曲线,从凌晨低谷到早高峰爬坡,再到午间回落、晚峰冲顶,一天之内要经历三到四次方向性反转。每一次反转背后都是机组启停、联络线调整、备用容量释放的连锁决策。如果预测偏差超过 3%,调度员就得靠旋转备用硬扛,代价是煤耗和碳排放同时上升。

这套「基于时间序列数据的电力负荷预测系统」要解决的,就是把每小时电力负荷数值预测未来变化趋势这件事做扎实,服务于电网调度优化和能源管理。标题里列了 ARIMA、决策树、GRU、KNN、LSTM、随机森林、Transformer 七类模型,这不是堆砌,而是覆盖了从统计基线到深度序列建模的完整光谱。适合谁读?做能源信息化、电力调度自动化、或者手上有历史负荷 CSV 想跑出可用预测的工程师。新手能照着搭出第一条基线,熟手能看清每个模型的边界在哪、什么时候该换。

2. 数据准备与特征工程:负荷曲线不是普通时间序列

2.1 电力负荷数据的三个特殊结构

普通时间序列预测的教程拿航空乘客数据或股票收盘价做例子,直接套到电力负荷上会翻车。电力负荷有三层结构必须显式处理。

第一层是强日周期。每天 24 个点的形状高度相似,早高峰和晚高峰的位置基本固定,但幅值随季节漂移。第二层是周周期。工作日和周末的曲线形状差异显著,周一早高峰爬坡比周三更陡,周六午间负荷明显偏低。第三层是节假日与极端天气的冲击。春节期间的负荷曲线会塌陷到正常水平的 60% 以下,持续一周以上;夏季持续高温日则会出现晚峰后移和二次爬坡。

这意味着特征工程不能只做 lag 和 rolling mean。常见做法是构造三类特征:日历特征(小时、星期、是否节假日、距最近节假日的天数)、周期编码(小时和星期的 sin/cos 编码,避免 23 点和 0 点被模型当成距离很远)、以及历史负荷的滞后特征(lag_24、lag_168、过去 7 天同时刻均值)。

import pandas as pd import numpy as np def build_load_features(df, target_col='load'): """ df: 至少包含 timestamp 和 load 两列,按时间升序 返回带特征的 DataFrame """ df = df.copy() df['timestamp'] = pd.to_datetime(df['timestamp']) df = df.sort_values('timestamp').reset_index(drop=True) # 日历特征 df['hour'] = df['timestamp'].dt.hour df['weekday'] = df['timestamp'].dt.weekday df['is_weekend'] = (df['weekday'] >= 5).astype(int) # 周期编码:让 23 点和 0 点在特征空间里相邻 df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24) df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24) df['weekday_sin'] = np.sin(2 * np.pi * df['weekday'] / 7) df['weekday_cos'] = np.cos(2 * np.pi * df['weekday'] / 7) # 滞后特征:昨天同时刻、上周同时刻 df['lag_24'] = df[target_col].shift(24) df['lag_168'] = df[target_col].shift(168) # 滚动统计:过去 7 天同时刻的均值和标准差 df['roll_7d_mean'] = df[target_col].shift(24).rolling(7).mean() df['roll_7d_std'] = df[target_col].shift(24).rolling(7).std() return df.dropna().reset_index(drop=True)

这段代码的关键在shift(24)和shift(168)的位置。lag_24 取的是昨天同一小时的值,lag_168 取的是上周同一小时的值。滚动统计必须先 shift 再 rolling,否则当前时刻的值会泄漏进特征里,模型在训练集上表现极好,上线后直接崩掉。hour_sin和hour_cos这一对编码是电力负荷预测里最容易被忽略但收益最明显的特征,它让模型理解 23 点和 0 点是相邻的,而不是数值上相差 23。

2.2 缺失值与异常值的处理边界

电力负荷数据几乎不可能干净。常见缺失来源包括采集终端掉线、通信中断、系统检修。缺失比例低于 5% 时,线性插值加前后向填充足够;超过 10% 的连续缺失段,插值会引入虚假的平滑曲线,这时候更稳妥的做法是把这段标记为缺失并在训练时用 mask 跳过。

异常值的判断不能只看 3-sigma。负荷曲线的正常波动本身就很大,凌晨低谷到晚峰可能差一倍以上。我一般用「同时刻历史分位数」做判断:如果某点的值超出过去 30 天同时刻的 1% 到 99% 分位范围,且连续出现超过 3 个点,才标记为异常。单点跳变大概率是采集噪声,直接替换为前后均值即可。

提示:处理完缺失和异常后,务必画一张「处理前后对比图」叠加在原始曲线上。很多隐蔽的错误——比如插值把周末曲线拉成了工作日形状——只有肉眼能发现。

3. 从 ARIMA 到 Transformer:七类模型的选型逻辑与最小实现

3.1 统计基线:ARIMA 和它的适用边界

ARIMA 在电力负荷预测里的角色是基线,不是主力。它的优势在于可解释性强、训练快、对小样本稳定。一个 SARIMA(1,1,1)(1,1,1,24) 模型,用过去 30 天的每小时数据训练,在平稳工作日的预测误差通常能控制在 2% 到 4%。但它的短板同样明显:无法利用外部特征(温度、节假日),对突变响应慢,参数定阶靠 ACF/PACF 图或 auto_arima 搜索。

from statsmodels.tsa.statespace.sarimax import SARIMAX # 取最近 60 天数据,按 24 小时周期建模 series = df.set_index('timestamp')['load'].asfreq('H').fillna(method='ffill') train = series[-60*24:] model = SARIMAX( train, order=(1, 1, 1), # 非季节性 ARIMA 阶数 seasonal_order=(1, 1, 1, 24), # 日周期 enforce_stationarity=False, enforce_invertibility=False ) result = model.fit(disp=False) forecast = result.forecast(steps=24) # 预测未来 24 小时

order=(1,1,1)里的三个参数分别是自回归阶数、差分次数、移动平均阶数。差分次数设 1 是因为负荷序列有明显趋势,不差分的话 ADF 检验过不了。seasonal_order的第四个参数 24 是日周期长度,这是电力负荷区别于普通经济时间序列的核心设置。如果数据是 15 分钟粒度,这里要改成 96。

ARIMA 什么时候该放弃?当你发现残差的 ACF 在滞后 24、48 阶仍然显著,说明日周期没被完全捕获,这时候要么加大 seasonal_order,要么直接换梯度提升或深度学习模型。

3.2 树模型:随机森林与决策树做特征重要性筛选

随机森林和决策树在负荷预测里很少单独作为最终模型,但它们有两个不可替代的用途:快速建立可解释基线,以及做特征重要性排序。

from sklearn.ensemble import RandomForestRegressor from sklearn.tree import DecisionTreeRegressor from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_percentage_error feature_cols = ['hour_sin', 'hour_cos', 'weekday_sin', 'weekday_cos', 'is_weekend', 'lag_24', 'lag_168', 'roll_7d_mean', 'roll_7d_std'] X = df[feature_cols].values y = df['load'].values # 时间序列交叉验证,不能用随机 KFold tscv = TimeSeriesSplit(n_splits=5) mape_scores = [] for train_idx, val_idx in tscv.split(X): rf = RandomForestRegressor( n_estimators=200, max_depth=12, min_samples_leaf=5, n_jobs=-1, random_state=42 ) rf.fit(X[train_idx], y[train_idx]) pred = rf.predict(X[val_idx]) mape_scores.append(mean_absolute_percentage_error(y[val_idx], pred)) print(f"RF MAPE: {np.mean(mape_scores):.4f}") print(dict(zip(feature_cols, rf.feature_importances_.round(3))))

这里必须用TimeSeriesSplit而不是KFold。随机切分会让未来数据泄漏到训练集,MAPE 看起来能到 1% 以下,实际部署时完全不可用。max_depth=12和min_samples_leaf=5是我在多个负荷数据集上比较稳的参数组合,再深就容易记住训练集的噪声。特征重要性输出里,lag_24和hour_sin通常排前两位,如果roll_7d_std排到前三,说明数据里存在明显的周内波动模式,值得进一步分析。

3.3 深度学习:LSTM、GRU 和 Transformer 的输入构造差异

LSTM 和 GRU 在负荷预测里的表现通常接近,GRU 参数少、训练快,LSTM 在长序列上略稳。真正的差异在输入构造:它们吃的是滑动窗口,不是单点特征。

import torch import torch.nn as nn from torch.utils.data import Dataset, DataLoader class LoadWindowDataset(Dataset): def __init__(self, series, window=168, horizon=24): """ series: 归一化后的负荷序列,shape (N,) window: 输入窗口长度,168 = 7天 horizon: 预测步长,24 = 未来一天 """ self.series = series self.window = window self.horizon = horizon def __len__(self): return len(self.series) - self.window - self.horizon + 1 def __getitem__(self, idx): x = self.series[idx: idx + self.window] y = self.series[idx + self.window: idx + self.window + self.horizon] return torch.FloatTensor(x).unsqueeze(-1), torch.FloatTensor(y) class GRUForecaster(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2, horizon=24): super().__init__() self.gru = nn.GRU(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_size, horizon) def forward(self, x): out, _ = self.gru(x) # out: (batch, window, hidden) last = out[:, -1, :] # 取最后一个时间步 return self.fc(last) # 直接输出未来 horizon 个点

窗口长度 168 是 7 天的小时数,这个选择让模型能看到完整的周周期。horizon 设 24 表示一次预测未来一天,也可以设 1 做滚动预测,但滚动预测误差会累积。GRU 的hidden_size=64和num_layers=2是起点,数据量超过 10 万条时可以加到 128 和 3 层。dropout=0.2放在层间,防止过拟合。

Transformer 的输入构造不同,它需要位置编码,且对窗口长度更敏感。在负荷预测里,标准做法是把窗口拉到 336(14 天)甚至 720(30 天),让自注意力机制捕捉跨周的依赖。但 Transformer 在小数据集上容易过拟合,数据少于 5 万条时,GRU 往往是更务实的选择。

3.4 KNN 在负荷预测里的正确用法

KNN 做负荷预测的思路是「找历史上最相似的几天,用它们的曲线加权平均」。它的优势是零训练、可解释、对突变响应快。关键是相似度度量:不能只用欧氏距离,要结合日历特征。

from sklearn.neighbors import KNeighborsRegressor # 用「同星期几 + 同时刻」的历史负荷作为特征 knn = KNeighborsRegressor( n_neighbors=5, weights='distance', # 距离越近权重越大 metric='euclidean' ) # 特征只用 lag_24 和 lag_168,避免引入噪声 knn.fit(df[['lag_24', 'lag_168']].values, df['load'].values)

n_neighbors=5是经验值,太少容易受单日异常影响,太多会平滑掉真实波动。weights='distance'让最近的历史日占更大权重。KNN 的预测在节假日附近往往比深度学习模型更稳,因为它直接找到了去年同期的相似日。

4. 避坑与排查:负荷预测系统上线前必须过的五道关

4.1 数据泄漏:MAPE 0.5% 的模型为什么上线就废

现象:离线验证 MAPE 低于 1%,部署后第一周 MAPE 跳到 8% 以上。

原因:特征工程里用了未来信息。最常见的是 rolling 统计没有 shift,或者归一化时用了全量数据的均值和方差。另一个隐蔽来源是TimeSeriesSplit之前做了随机 shuffle。

解决:所有滚动特征先 shift 再 rolling;归一化的均值和方差只能从训练集计算,然后应用到验证集和测试集;交叉验证必须用时间序列切分。上线前做一次「时间倒流测试」:把测试集的时间戳整体前移一天,如果预测结果发生剧烈变化,说明有泄漏。

4.2 节假日预测:模型集体失准的那几天

现象:春节期间连续 7 天预测偏差超过 15%,调度员直接弃用系统。

原因:训练数据里春节样本极少,模型没见过这种负荷塌陷模式。日历特征里is_holiday只给了 0/1,模型无法区分春节和普通周末。

解决:构造「距春节天数」特征,把节前、节中、节后分开标记。更稳妥的做法是对节假日单独训练一个模型,或者用去年同期的负荷曲线做模板匹配。我一般会在系统里加一条规则:检测到春节、国庆等长假期时,自动切换到「节假日模式」,用 KNN 找去年同期的相似日做预测。

4.3 温度突变:负荷曲线拐点比天气预报早到

现象:夏季高温预警发布当天,晚峰负荷比预测高出 8%,备用容量不足。

原因:负荷对温度的响应有滞后,但降温负荷的启动比气象预报的更新更早。模型只用了历史负荷特征,没有接入温度。

解决:把预报温度作为外生变量加入特征,同时构造「连续高温天数」和「温度变化率」两个衍生特征。LSTM 和 Transformer 可以直接吃多变量输入,树模型把温度特征拼进特征矩阵即可。注意温度预报本身有误差,特征里要保留温度预报的置信区间宽度。

4.4 多模型融合:简单平均为什么不如加权

现象:把 ARIMA、随机森林、GRU 的预测结果做简单平均,MAPE 反而比最好的单模型高。

原因:各模型在不同时段的表现差异很大。ARIMA 在平稳日好,GRU 在波动日好,简单平均把优势抵消了。

解决:用验证集上的分时段误差计算权重。比如早高峰时段给 GRU 更高权重,凌晨低谷给 ARIMA 更高权重。权重可以按小时固定,也可以用一个小型线性模型动态学习。我一般会保留一个「模型选择器」,根据当天的日历特征和温度预报,自动选择历史表现最好的 2 到 3 个模型做加权。

4.5 预测粒度与调度周期的错配

现象:系统输出每小时预测值,但调度计划是 15 分钟粒度,接口对不上。

原因:数据采集粒度、模型训练粒度、业务消费粒度三者不一致。

解决:在系统设计阶段就明确「预测输出粒度」和「调度消费粒度」。如果调度需要 15 分钟,模型要么直接训练 15 分钟粒度,要么在输出后做插值并标注置信度。插值不能简单线性,要用负荷曲线的形状约束——比如早高峰爬坡段的插值斜率应该大于午间平段。

5. 把预测误差压到 2% 以内的三个进阶技巧

第一个技巧是残差建模。先用 GRU 跑一版预测,然后把残差(真实值减预测值)拿出来,看它是否还有结构。如果残差的 ACF 在滞后 24 阶显著,说明日周期没被完全捕获,可以再训一个模型专门预测残差,最后把两个模型的输出相加。这个做法在多个省级电网数据集上能把 MAPE 从 3.5% 压到 2.2% 左右。

第二个技巧是分位数预测替代点预测。调度真正需要的不是「明天下午 3 点负荷是 8500 MW」,而是「有 90% 把握负荷不超过 8800 MW」。用分位数损失训练 LSTM 或 GRU,一次输出 P10、P50、P90 三条曲线。P90 用于备用容量决策,P10 用于机组下限安排。实现上只需把损失函数从 MSE 换成 pinball loss:

def pinball_loss(pred, target, quantiles=[0.1, 0.5, 0.9]): losses = [] for i, q in enumerate(quantiles): e = target - pred[:, i] losses.append(torch.max((q - 1) * e, q * e)) return torch.stack(losses, dim=1).mean()

模型最后一层输出维度改成 3,分别对应三个分位数。训练时每个分位数独立计算损失再平均。推理时 P50 作为点预测,P90 减 P50 作为上调备用需求。

第三个技巧是滚动更新与在线学习。负荷模式会随季节和政策缓慢漂移,一个训练于去年夏天的模型,到今年冬天误差会明显上升。我一般会保留最近 90 天的数据做增量微调,每周更新一次模型权重。微调时学习率调到初始值的十分之一,只解冻最后一层或最后两层,避免灾难性遗忘。

注意:在线学习必须有回滚机制。如果某次更新后连续 3 天验证误差上升,自动回滚到上一版模型。这个「后悔药」在真实调度环境里救过我两次。

这套系统值不值得做?如果你手上有两年以上的每小时负荷数据,并且调度侧愿意把预测结果接入实际决策流程,答案是值得。从 ARIMA 基线到 GRU 主力模型,再到分位数输出和在线更新,每一步都有明确的收益和可验证的指标。我自己的习惯是:任何新模型上线前,先和现有基线并行跑两周,用真实调度日志里的偏差记录做对比,确认稳定后再切换。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询