ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

电力负荷预测系统实战:ARIMA、随机森林、LSTM、Transformer多模型选型与避坑指南

电力负荷预测系统实战:ARIMA、随机森林、LSTM、Transformer多模型选型与避坑指南

简介:这份资源面向电力系统调度、能源管理及时间序列预测方向的学习者与工程人员,提供一套基于历史负荷数据预测每小时电力负荷变化趋势的完整代码集合,可用于电网调度优化与能源分配决策。压缩包共11个文件,约54KB,以8个Python脚本为核心,分别实现ARIMA、决策树、KNN、GRU、LSTM、随机森林与Transformer等模型,另附README、说明文件与附赠文档,便于理解各模型的建模思路与调用方式。已有63人学习下载。读者可对照脚本快速复现不同算法的预测流程,比较统计方法与深度学习模型在自相关性、长期依赖和全局序列建模上的差异,并参考说明文档完成环境配置与结果验证,为构建多模型融合的负荷预测方案提供可复用的代码基础与排错参考。

1. 电力负荷预测系统:从每小时数值到调度决策,多模型怎么选

做电网调度的人都有个共识:负荷曲线看着平滑,真到要提前一天排机组出力的时候,误差每多一个百分点,背后就是实打实的旋转备用成本。这套「基于时间序列数据的电力负荷预测系统」要解决的就是这件事——拿历史每小时电力负荷数值,预测未来一段时间的负荷变化趋势,输出直接喂给调度优化和能源管理模块。标题里列了 ARIMA、决策树、GRU、KNN、LSTM、随机森林、Transformer 一大串模型,很多人第一反应是「堆模型」,但真正落地时你会发现,模型多不是重点,重点是搞清楚每个模型吃什么样的数据形态、在哪个预测步长上更稳、以及怎么把它们组织成一条可复现的流水线。这篇笔记面向已经拿到负荷数据、准备动手搭预测系统的工程师,也适合想搞清楚「时间序列预测模型到底怎么选」的算法同学。我会按数据准备、特征工程、模型实现、避坑、进阶验证的顺序,把这条链路讲透,代码能直接抄。

2. 数据准备与时间序列特征工程:把原始负荷表变成模型能吃的样本

2.1 电力负荷数据的三个天然属性

电力负荷序列和普通时间序列不一样,它有三个绕不开的属性。第一是强周期性,一天 24 小时有早高峰和晚高峰,一周七天工作日和周末形态完全不同,一年还有夏冬两个空调负荷尖峰。第二是受温度、节假日、电价政策这些外生变量影响,纯靠历史负荷自回归,遇到极端天气就会翻车。第三是采样频率固定且缺失值有规律,电表采集通常是 15 分钟或 1 小时一个点,缺失往往成段出现,不是随机丢点。

理解这三点,决定了你后面特征怎么造。常见做法是把时间特征拆成小时、星期、是否节假日、是否周末这几类离散编码,再把温度、湿度作为外生变量拼进去。如果只有负荷一列数据,那就靠滑动窗口自己造滞后特征,这是时间序列预测最朴素也最有效的路子。

2.2 用 pandas 做重采样、缺失填充和滞后特征

下面这段代码把一张原始负荷表整理成监督学习能用的样本矩阵。假设原始数据是timestamp和load两列,采样间隔不规整。

import pandas as pd import numpy as np # 读取原始负荷数据,timestamp 解析为时间索引 df = pd.read_csv("load_raw.csv", parse_dates=["timestamp"]) df = df.set_index("timestamp").sort_index() # 重采样到整点,缺失用前向填充 + 线性插值兜底 df = df.resample("1h").mean() df["load"] = df["load"].interpolate(method="linear", limit=3) df["load"] = df["load"].ffill(limit=2) # 时间特征:小时、星期、是否周末 df["hour"] = df.index.hour df["weekday"] = df.index.weekday df["is_weekend"] = (df["weekday"] >= 5).astype(int) # 滞后特征:过去 1、2、3、24、168 小时的负荷 for lag in [1, 2, 3, 24, 168]: df[f"lag_{lag}"] = df["load"].shift(lag) # 滑动统计特征:过去 24 小时均值、标准差 df["roll_mean_24"] = df["load"].shift(1).rolling(24).mean() df["roll_std_24"] = df["load"].shift(1).rolling(24).std() df = df.dropna() print(df.shape)

逻辑说明:resample("1h").mean()把不规整采样对齐到整点,这是电力负荷预测的标准动作,因为调度计划本身就是按小时排的。插值用limit=3限制连续填充长度,超过 3 小时的空洞说明采集设备可能离线,硬填会引入假信号,这种段最好标记出来单独处理。滞后特征里lag_24和lag_168分别捕捉日周期和周周期,这是负荷预测里性价比最高的两个特征,很多场景下光靠这两个加一个线性模型就能拿到不错的基线。

参数说明:limit控制插值最大连续长度,一般设 2 到 4,看你的采集稳定性。rolling(24)的窗口对应一天,如果你做的是 15 分钟粒度,窗口要改成 96。滞后阶数的选择没有固定公式,先看自相关函数 ACF,拖尾到哪就取到哪,但工程上一般不超过两个完整周期,否则特征维度爆炸且容易过拟合。

2.3 训练集、验证集、测试集怎么切才不泄露未来信息

时间序列切分和普通机器学习最大的区别是不能随机打乱。常见做法是按时间顺序 7:1:2 切,但更严谨的是用滚动窗口做交叉验证。下面这个函数生成滚动切分索引。

def rolling_split(n_samples, n_splits=5, test_size=0.2): """按时间顺序生成滚动训练/验证索引,避免未来信息泄露""" test_start = int(n_samples * (1 - test_size)) fold_size = test_start // (n_splits + 1) splits = [] for i in range(n_splits): train_end = fold_size * (i + 1) val_end = train_end + fold_size splits.append((np.arange(0, train_end), np.arange(train_end, val_end))) return splits, np.arange(test_start, n_samples)

逻辑说明:每次训练集只用到当前时间点之前的数据,验证集是紧接着的一段,这样评估出来的误差才接近真实上线后的表现。测试集留最后 20% 完全不参与调参,只在最终验收时用一次。很多人图省事直接train_test_split(shuffle=False)切一刀,调参时反复看测试集,最后指标虚高,上线就露馅,这是血泪经验。

3. 统计模型与树模型:ARIMA、决策树、随机森林在负荷预测里的真实定位

3.1 ARIMA 的定阶与残差诊断

ARIMA 是时间序列预测的经典基线,优点是解释性强、训练快,缺点是只能捕捉线性关系,遇到节假日突变基本没辙。用statsmodels跑 ARIMA 的标准流程是先做平稳性检验,再定阶,最后看残差是不是白噪声。

from statsmodels.tsa.stattools import adfuller from statsmodels.tsa.arima.model import ARIMA import warnings warnings.filterwarnings("ignore") # 平稳性检验,p 值小于 0.05 认为平稳 adf_result = adfuller(df["load"].dropna()) print("ADF p-value:", adf_result[1]) # 一阶差分后定阶,p、d、q 分别试 0-3 best_aic, best_order = np.inf, None for p in range(4): for q in range(4): try: model = ARIMA(df["load"], order=(p, 1, q)) result = model.fit() if result.aic < best_aic: best_aic, best_order = result.aic, (p, 1, q) except Exception: continue print("Best order:", best_order, "AIC:", best_aic) # 用最优阶数拟合并预测未来 24 小时 final_model = ARIMA(df["load"], order=best_order).fit() forecast = final_model.forecast(steps=24) print(forecast.head())

逻辑说明:ADF 检验的 p 值大于 0.05 说明序列非平稳,需要差分,d参数一般取 1 或 2。定阶用 AIC 网格搜索是最省事的做法,虽然慢但不容易出错。残差诊断要看 Ljung-Box 检验,如果残差还有自相关,说明阶数不够或者模型没抓住某些结构。ARIMA 在负荷预测里通常作为基线,预测未来 1 到 6 小时还行,超过 24 小时误差会明显放大,因为它是纯自回归,没有外生变量。

参数说明:order=(p, d, q)里 p 是自回归项,q 是移动平均项,d 是差分次数。负荷数据一般 d=1,p 和 q 不超过 3,再大容易过拟合。如果你有温度数据,可以用 SARIMAX 把外生变量加进去,效果比纯 ARIMA 好一截。

3.2 决策树和随机森林:把时间序列当回归问题做

决策树和随机森林本身不是为时间序列设计的,但把滞后特征造好之后,它们就是普通的回归模型,而且对非线性关系捕捉得不错。随机森林比单棵决策树稳,因为它靠 bagging 降方差,在负荷预测这种噪声不小的场景里更实用。

from sklearn.ensemble import RandomForestRegressor from sklearn.tree import DecisionTreeRegressor from sklearn.metrics import mean_absolute_percentage_error feature_cols = [c for c in df.columns if c != "load"] X, y = df[feature_cols].values, df["load"].values splits, test_idx = rolling_split(len(X)) train_idx, val_idx = splits[-1] # 决策树基线 dt = DecisionTreeRegressor(max_depth=8, min_samples_leaf=5, random_state=42) dt.fit(X[train_idx], y[train_idx]) dt_pred = dt.predict(X[val_idx]) print("DT MAPE:", mean_absolute_percentage_error(y[val_idx], dt_pred)) # 随机森林 rf = RandomForestRegressor(n_estimators=300, max_depth=12, min_samples_leaf=3, n_jobs=-1, random_state=42) rf.fit(X[train_idx], y[train_idx]) rf_pred = rf.predict(X[val_idx]) print("RF MAPE:", mean_absolute_percentage_error(y[val_idx], rf_pred))

逻辑说明:max_depth控制树深,负荷预测里一般 8 到 15 之间,太深会记住训练集的噪声。min_samples_leaf设 3 到 5 能防止叶子节点样本太少导致预测抖动。随机森林的n_estimators设 300 以上指标基本稳定,再往上收益递减。树模型最大的好处是能直接输出特征重要性,你可以看到lag_24、hour、lag_168这几个特征排前面,验证特征工程做对了没有。

参数说明:n_jobs=-1用满所有 CPU 核,训练时间能砍一半以上。random_state固定住方便复现。树模型不需要做归一化,这是它比神经网络省事的地方,但滞后特征里的量纲差异还是建议统一处理,避免某些特征被稀释。

4. 深度学习模型:GRU、LSTM、KNN、Transformer 的输入构造与训练要点

4.1 把负荷序列切成三维张量

深度学习模型吃的是(样本数, 时间步长, 特征数)的三维张量。构造这个张量是 LSTM 和 GRU 落地时最容易出错的一步,维度对不上、时间步错位都是常见翻车点。

import torch from torch.utils.data import Dataset, DataLoader class LoadDataset(Dataset): def __init__(self, data, seq_len=24, pred_len=1): self.data = torch.tensor(data, dtype=torch.float32) self.seq_len = seq_len self.pred_len = pred_len def __len__(self): return len(self.data) - self.seq_len - self.pred_len + 1 def __getitem__(self, idx): x = self.data[idx: idx + self.seq_len] y = self.data[idx + self.seq_len: idx + self.seq_len + self.pred_len, 0] return x, y # 假设第一列是负荷,其余是特征 dataset = LoadDataset(df.values, seq_len=24, pred_len=1) loader = DataLoader(dataset, batch_size=64, shuffle=False)

逻辑说明:seq_len=24表示用过去 24 小时预测下一小时,这是负荷预测最常用的窗口。shuffle=False在时间序列里很重要,训练时如果打乱会破坏时间连续性,虽然对 LSTM 本身影响不大,但配合滚动验证时容易搞混。y取的是data[..., 0],即负荷那一列,确保预测目标明确。

参数说明:seq_len的选择看你的预测步长和周期长度,预测未来 1 小时用 24 够,预测未来 24 小时建议用 168(一周)。batch_size设 32 到 128 之间,显存够就大一点,训练更稳。

4.2 LSTM 和 GRU 的网络结构与训练循环

LSTM 和 GRU 是负荷预测里用得最多的两个循环网络,GRU 参数少、训练快,LSTM 表达能力强一点,数据量小的时候 GRU 往往更划算。

import torch.nn as nn class LSTMForecaster(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2, pred_len=1): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_dim, pred_len) def forward(self, x): out, _ = self.lstm(x) return self.fc(out[:, -1, :]) # 取最后一个时间步 class GRUForecaster(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2, pred_len=1): super().__init__() self.gru = nn.GRU(input_dim, hidden_dim, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_dim, pred_len) def forward(self, x): out, _ = self.gru(x) return self.fc(out[:, -1, :])

逻辑说明:batch_first=True让输入维度是(batch, seq, feature),和 DataLoader 输出对齐。out[:, -1, :]取最后一个时间步的隐藏状态,因为负荷预测是自回归任务,最后一步包含了整段序列的信息。dropout=0.2在循环层里能缓解过拟合,但注意 PyTorch 的 LSTM dropout 只在多层之间生效,单层设了没用。

参数说明:hidden_dim一般 32 到 128,负荷预测这种单变量或少量外生变量的任务,64 够用。num_layers设 2 层比较稳,再深容易过拟合且训练慢。学习率用 1e-3 配 Adam,训练 50 到 100 个 epoch,早停看验证集损失。

4.3 KNN 和 Transformer 在负荷预测里的适用边界

KNN 做时间序列预测的思路是找历史上形态相似的片段,用它们的后续值做加权平均。优点是简单、可解释,缺点是预测时要在全量历史里搜,数据量大了慢,而且对突变负荷不敏感。适合做短期基线或者作为集成模型的一个弱学习器。

Transformer 的优势是自注意力能捕捉长距离依赖,适合预测步长较长、周期性复杂的场景。但负荷预测数据量通常不大,Transformer 容易过拟合,需要配合位置编码、层归一化和较强的正则。实际落地里,Transformer 在负荷预测上的收益往往不如把特征工程做扎实的 LSTM,除非你有多年高频数据。

class TransformerForecaster(nn.Module): def __init__(self, input_dim, d_model=64, nhead=4, num_layers=2, pred_len=1): super().__init__() self.embed = nn.Linear(input_dim, d_model) self.pos = nn.Parameter(torch.randn(1, 168, d_model)) encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, batch_first=True) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers) self.fc = nn.Linear(d_model, pred_len) def forward(self, x): x = self.embed(x) + self.pos[:, :x.size(1), :] x = self.encoder(x) return self.fc(x[:, -1, :])

逻辑说明:位置编码用可学习参数,长度按最大序列 168 设。batch_first=True保持和 LSTM 一致的输入格式。Transformer 训练时学习率要调小,一般 1e-4 到 5e-4,配合 warmup 更稳。

参数说明:d_model和nhead要满足整除关系,64 配 4 头是常见组合。num_layers设 2 到 3,再深在小数据集上没意义。

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

5.1 现象:验证集 MAPE 很低,上线后误差翻倍

原因:验证集和测试集切分时用了随机打乱,或者调参阶段反复看了测试集,导致指标虚高。另一个常见原因是验证集落在平稳时段,上线后遇到节假日或极端天气,模型没见过。

解决:严格用滚动窗口切分,测试集只在最终验收时用一次。上线前用最近一个月的真实数据做一次盲测,包含至少一个周末和一个节假日。如果盲测误差明显高于验证集,说明模型泛化不够,需要加外生变量或换更稳的模型。

5.2 现象:LSTM 训练损失下降但验证损失震荡

原因:学习率太大,或者 batch size 太小导致梯度噪声大。负荷数据本身噪声不小,LSTM 对学习率敏感。

解决:学习率降到 5e-4 或 1e-4,加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)。batch size 提到 64 以上。如果还震荡,检查输入特征有没有做归一化,负荷数值动辄几千上万千瓦,不归一化 LSTM 很难收敛。

5.3 现象:ARIMA 预测出来是一条直线

原因:差分次数过多导致序列过度平稳,或者 p、q 都取了 0,模型退化成均值预测。

解决:检查 ADF 检验结果,d 一般取 1 就够。定阶时确保 p 和 q 至少有一个大于 0。如果数据本身周期性很强,考虑用 SARIMA 加季节项,或者直接换树模型和深度学习模型。

5.4 现象:随机森林特征重要性里滞后特征排最后

原因:滞后特征没造对,比如shift方向反了,或者重采样之后索引错位,导致特征和标签对不上。

解决:造完特征后手动检查几行,确认lag_1确实是前一小时的负荷。用df[["load", "lag_1"]].head(10)打印出来肉眼核对。重采样之后一定要sort_index(),否则 rolling 和 shift 都会错位。

5.5 现象:多模型集成后效果还不如单模型

原因:集成的模型之间相关性太高,比如 LSTM 和 GRU 在同一份数据上预测结果几乎一样,加权平均没有信息增益。或者集成权重是在验证集上过拟合出来的。

解决:选模型时看误差的相关性,相关性高于 0.9 的模型留一个就行。集成权重用简单的等权平均先试,往往比复杂加权稳。如果要做 stacking,元学习器的训练数据要用滚动切分生成,不能用全量数据。

6. 进阶验证与模型选择:用滚动回测和误差分解定最终方案

模型选到最后,不能只看一个 MAPE 数字。我一般会做两件事:滚动回测和误差分解。滚动回测是模拟真实上线场景,每次用过去 N 天训练,预测未来 24 小时,滚动前进,统计多轮误差的均值和方差。误差分解是把总误差拆成高峰时段、平段、低谷时段分别看,因为调度最关心的是高峰时段的误差,低谷差一点无所谓。

下面这个回测框架可以直接套用。

def rolling_backtest(df, model_fn, train_days=60, pred_hours=24, n_rounds=10): """滚动回测:每轮用 train_days 训练,预测 pred_hours""" results = [] total_hours = len(df) for i in range(n_rounds): test_end = total_hours - i * pred_hours test_start = test_end - pred_hours train_start = test_start - train_days * 24 if train_start < 0: break train = df.iloc[train_start:test_start] test = df.iloc[test_start:test_end] model = model_fn(train) pred = model.predict(test) mape = mean_absolute_percentage_error(test["load"], pred) results.append({"round": i, "mape": mape, "peak_mape": mape if test["hour"].max() in [18, 19, 20] else None}) return pd.DataFrame(results) # 用随机森林跑一轮回测 def rf_factory(train): feature_cols = [c for c in train.columns if c != "load"] model = RandomForestRegressor(n_estimators=300, max_depth=12, n_jobs=-1) model.fit(train[feature_cols], train["load"]) return model backtest_result = rolling_backtest(df, rf_factory, train_days=60, n_rounds=10) print(backtest_result[["round", "mape"]].describe())

逻辑说明:train_days=60表示每次用过去 60 天数据训练,这个窗口要覆盖至少两个完整周周期。n_rounds=10滚动 10 次,看 MAPE 的均值和标准差,标准差大说明模型不稳定。高峰时段单独统计是因为调度对高峰误差更敏感,如果高峰 MAPE 明显高于整体,说明模型在尖峰处欠拟合,需要加温度特征或换能捕捉突变的模型。

参数说明:train_days根据数据量调,数据多可以拉到 90 天,数据少 30 天也行,但要保证覆盖完整周期。pred_hours按实际调度需求设,日前调度就是 24。回测轮数至少 10 轮,太少统计意义不够。

选模型的最终建议:先用 ARIMA 和随机森林跑基线,如果随机森林的 MAPE 已经满足调度要求(一般日前预测要求 3% 以内),就不用上深度学习。如果误差卡在瓶颈,再上 LSTM 或 GRU,重点调seq_len和hidden_dim。Transformer 留到最后,数据量不够别碰。多模型集成优先选误差相关性低的组合,比如随机森林加 LSTM,比 LSTM 加 GRU 有意义。

我自己踩过最大的坑是早期太迷信模型复杂度,花两周调 Transformer,最后发现把lag_168和温度特征加进随机森林,误差就降了一半。后来养成习惯:每上一个新模型之前,先问自己特征工程还有没有空间。希望帮到你。

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

RELATED READING

延伸阅读

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