☰
区域电力负荷预测实战:从数据清洗到LSTM多步预测
2026/10/7 13:23:18 网站建设 项目流程

简介:这是一份基于深度学习的区域电力负荷预测模型完整项目,面向机器学习或电力系统方向的课程设计、期末大作业场景,适合具备一定Python基础、希望直接获取可运行高分方案的学习者。项目以Python实现,涵盖数据预处理、模型构建、训练评估与可视化等关键环节,压缩包内共62个文件,其中39个py源码文件按功能模块组织,覆盖数据加载、网络定义、训练测试等完整流程;17个jpg与1个png为结果图表和结构示意图,便于直观理解;另有md说明文档和pyc缓存文件,整体大小仅3.72MB,目录结构清晰,可快速定位所需内容。目前已有150人学习使用,项目经导师指导获97分的高分评价,下载后无需修改即可直接运行。除完整源码外,还附带项目说明文档和性能展示图表,能帮助使用者快速掌握设计思路与实验效果,非常适合作为课程设计或期末大作业的可靠参考模板。

1. 区域电力负荷预测:为什么这项深度学习应用值得认真做

「基于深度学习的区域电力负荷预测模型」听起来像课程作业,但放到真实场景里,它是电网调度、分时电价和需求响应都要依赖的前置环节。你需要在未来 15 分钟到未来几天的时间尺度上估计某个区域的用电负荷,误差直接决定调度员是少开一台机组还是多储备一份备用容量。传统统计方法擅长处理平稳线性序列,可负荷曲线里有天气突变、节假日、工厂排产,还有早晚高峰的尖刺,这些都是深度学习模型相对擅长的模式。这篇笔记就围绕怎么用 Python 把这套模型从数据清洗、特征工程一路做到多步预测和误差诊断,适合想把课程项目或开源代码真正跑通、并迁移到自己数据上的工程师。

2. 拿到负荷数据后先做什么:清洗、特征工程与时间切分的关键步骤

2.1 原始负荷数据常见的三个脏点:缺测、异常尖峰与节假日标记

拿到压缩包后,先别急着找 model.py,先把 data 目录下的 CSV 打开看前 20 行,确认三件事:时间戳列是不是标准格式、采样频率是 15 分钟还是 1 小时、有没有可用的温度列。国内公开负荷数据集大多是 15 分钟一个点,一天 96 个点;有些电网内部数据是 5 分钟或 1 小时,这会直接影响后面所有滑动窗口参数。我一般会把数据频率写死在项目说明里,并写一个函数校验相邻时间戳的间隔,防止后面切窗口时出现重复或空洞。

原始表从来不会干净。先说缺测:采集终端掉线会让某些时间点变成 NaN。我一般不会直接删除,因为固定频率采样的序列一旦删除数据,就会留下断点,滑动窗口切到断点附近时,模型会取到错位的时间上下文。常见做法是前后线性插值,连续缺测超过一定数量时用上周同一天同时刻的值替换。再说异常尖峰:负荷曲线偶尔会出现比正常值高 30% 的毛刺,这多半是上报错误而不是真实用电。我会用滚动中位数加绝对中位差识别,超过阈值就替换成中位数,而不是把所有高值当异常删掉。最后是节假日标记,很多模型在节假日这天预测翻车,是因为日历特征里只有星期几,没把法定节假日和调休补班日标出来。

import pandas as pd import numpy as np df = pd.read_csv("regional_load.csv", parse_dates=["timestamp"]) df = df.set_index("timestamp").sort_index() # 1) 缺测修复 df["load"] = df["load"].interpolate(limit_direction="both") # 2) 基于滚动中位数的异常尖峰过滤 median = df["load"].rolling(96, center=True, min_periods=1).median() mad = (df["load"] - median).abs().rolling(96, center=True, min_periods=1).median() thresh = 5 * (1.4826 * mad + 1e-6) df.loc[(df["load"] - median).abs() > thresh, "load"] = median # 3) 节假日标记:holiday_dates 从项目说明维护的日历表读取 df["is_holiday"] = df.index.normalize().isin(holiday_dates).astype(int)

rolling(96, center=True) 里的 96 对应 15 分钟一个点、一天 96 个点,窗口跨一整天,能识别持续较长的真实高峰,又不会把一整段早高峰误判成毛刺。limit_direction="both" 表示首尾缺测也插值填补,避免样本被削掉。为什么用中位数而不是均值?均值容易被极端值带偏,中位数对单点毛刺更稳。

2.2 把时间戳构建成模型特征:周期编码、温度与滑动窗口

模型不认识时间戳,它只认识数值。第一步是把时间戳拆成小时、星期几,但直接把这些整数喂给神经网络会引入一个隐藏问题:23 点和 0 点之间数值差 1,语义上只隔一个小时;周一和周日也是同样。更好的做法是把「小时 + 星期几」折算成一周内的总小时数,再做 sin/cos 周期编码。温度是区域负荷预测里最有效的外生变量,夏季空调负荷几乎和温度呈指数关系;拿到温度序列后我会对缺测做前向填充,同时保留原始温度和滞后温度两个版本。

def build_features(df, temp_col=None): df = df.copy() week_hour = df.index.dayofweek * 24 + df.index.hour df["week_hour_sin"] = np.sin(2 * np.pi * week_hour / 168) df["week_hour_cos"] = np.cos(2 * np.pi * week_hour / 168) if temp_col and temp_col in df.columns: df["temp"] = df[temp_col].ffill() df["temp_lag1"] = df["temp"].shift(96) # 前一天同一时刻温度 return df

周期编码的意义在于让模型学到「0 点和 24 点是连续的」这个常识,sin/cos 两个维度一起给,模型才能同时知道小时和星期。temp_lag1 为什么取前一天同一时刻?因为负荷本身有很强的 24 小时周期性,加上滞后 24 小时版本的温度,模型可以对比今天与昨天同时刻的温度变化,捕捉天气系统过境带来的负荷爬升。做完这一步,特征列包括周期编码、节假日标记、温度等;原始负荷列作为预测目标单独保存,不要混进特征矩阵。

特征不是越多越好。温度、湿度、光照在部分区域强相关,全塞进去会让模型容量浪费在冗余信息上,训练时间变长,泛化能力反而下降。我常用的策略是先保留周期编码、节假日、温度这三组基础特征跑一版基线,再逐步加入湿度和负荷一阶差分作为候选特征,用验证集 MAPE 的变化决定留不留。要注意负荷的一阶差分属于目标变换,不能和目标同时进入同一根训练管线,否则泄漏问题会更难排查。

2.3 训练/验证/测试划分:按时间顺序切分并留出一个步长间隙

时序预测最忌讳随机打乱。随机切分会让模型在训练时看到未来信息,验证集上分数虚高,到线上预测就原形毕露。正确做法是按时间顺序切三段:训练集、验证集、测试集,比例大概 7:1:2 或 8:1:1。我通常在训练集和验证集之间留出一个步长间隙,避免「训练窗口末尾贴着验证窗口开头」的共享状态。

train_end = "2023-03-31 23:45:00" val_end = "2023-05-31 23:45:00" train_df = df.loc[:train_end] # 注意:从 train_end 后一个采样点开始,留出 15 分钟间隙 val_df = df.loc[train_end + pd.Timedelta(minutes=15):val_end] test_df = df.loc[val_end + pd.Timedelta(minutes=15):] print(train_df.shape, val_df.shape, test_df.shape)

这里每个集合都按时间顺序保留,验证集起点故意和训练集终点错开 15 分钟,相当于多一个 margin。如果后续把数据频率改成 60 分钟一个点,这个 gap 也要跟着改成 60 分钟。切分完成后要做归一化,但归一化的均值、标准差只能在训练集上计算,这一点放到避坑章节展开,属于最容易翻车的位置。

划分完以后,一个常见疑问是样本量到底够不够。假设你有三年 15 分钟数据,总计约 105120 个点,seq_len 取 96,那么可用样本数是 105120 - 96 = 105024 个,约 7 成给训练集,大约 73000 条。对 LSTM 这种参数规模在几十万量级的模型来说,这个样本量足够;如果只有三个月数据,样本数不到两万五,此时优先考虑把 seq_len 降到 48,或者干脆换成参数更少的 GRU。

3. 模型选型:LSTM、GRU 还是轻量 Transformer,参数如何起步

3.1 为什么传统预测方法在区域负荷上效果变差

区域负荷序列有三个让传统方法头疼的特征:强周期性、非平稳性、对天气敏感。ARIMA 这类方法假设序列经过差分后平稳,但负荷会跟随气温整体爬升,夏季尖峰和冬季采暖负荷让方差一直变化;节假日和突发事件的脉冲又让残差显著偏离正态。机器学习方法如 XGBoost 能做非线性拟合,但它默认每个样本独立,要自己构造滞后特征来隐式表达时间顺序,滞后阶数一多,特征维度爆炸。循环神经网络的价值在于把「过去 96 个时刻的序列」整体作为输入,在参数共享的前提下自动提取长期依赖。换句直白的话:传统方法是在用手工特征猜明天,LSTM 是在用过去一周的波形推算明天的波形。

3.2 基线 LSTM 的 PyTorch 实现

先从一个能跑出不错结果的基线开始。负荷预测里的 LSTM 不需要花哨,最常见配置是两层 LSTM 加一个全连接输出头。hidden_size 从 128 起步,num_layers 取 2,dropout 取 0.2,这个组合在大多数区域负荷数据上都能在一小时内看到可靠结果。

import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden_size=128, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, ) self.regressor = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, 1), ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden) return self.regressor(out[:, -1, :]) # 取最后一个时间步

为什么只取最后一个时间步的输出?因为预测目标是未来一个点,整个窗口的信息经过 LSTM 已被压缩到最后一步的 hidden state 里,out[:, -1, :] 等价于取最后一个时刻的短期记忆,再接两层全连接做非线性映射。batch_first=True 让输入形状变成 (batch, seq_len, n_features),对刚接手的人更直观,不容易把维度搞反。dropout 只作用于层与层之间,不会作用于最后一层输出,所以 regressor 里不需要再加 dropout。

3.3 升级方向:GRU 和自注意力模块

如果数据量不大,或者想让训练更快,我会把 LSTM 换成 GRU,改动只有一行:nn.LSTM 换成 nn.GRU。GRU 少了一个门控单元,参数约少四分之一,在负荷这种中等规模序列上精度几乎不会下降。另一个升级方向是在 LSTM 顶部加一个自注意力层,让模型自己学习「窗口里的哪几个时刻对明天同一时刻最重要」,比如捕捉温度突升前 12 小时的特殊模式。轻量 Transformer 也可以做,但区域负荷序列长度通常只有 96,自注意力对 96 步的建模优势并不明显,训练配平反而更麻烦。我的建议是把 Transformer 放在第二版再试,第一版用 LSTM 或 GRU 把基线和数据管线跑通。

3.4 选型对比:精度、训练速度与内存

模型验证集 MAPE 通常范围训练速度内存占用适用场景
LSTM2% – 4%中等中等数据充足,追求精度
GRU2% – 4.5%较快较低数据量在 2 万条以内,快速验证
LSTM + Attention1.8% – 3.5%较慢较高特征多、需要解释关键时间步
轻量 Transformer1.8% – 3.5%慢高有充足 GPU 预算的进阶实验

表格里的 MAPE 是以 15 分钟为粒度的区域负荷预测;日累计负荷预测的 MAPE 通常会低一半。如果你的验证集 MAPE 落在 5% 以上,先别怀疑模型结构,大概率是数据或特征的问题。训练速度按单张消费级 GPU 估算,LSTM 在 7 万样本、seq_len=96 场景下每个 epoch 大约 20 秒,配上早停整体在 15 分钟内能跑完。轻量 Transformer 慢的原因主要不是参数量,而是注意力矩阵计算和更小的 batch size 导致的显存瓶颈。

4. 最小可复现方案:项目目录、数据管线与训练循环

4.1 项目目录怎么组织才能让「项目说明」读得快

一份项目说明能不能让人十分钟进入状态,取决于目录是否把数据、配置、代码、输出分开。我见过不少把模型定义、训练脚本和图片全堆在一个文件里的项目,跑是能跑,但改参数和复现结果都靠猜。常见做法是这样组织:

load_forecast/ ├── data/ # 原始 CSV、节假日字典 ├── configs/ # 训练参数 JSON ├── src/ │ ├── data.py # 清洗与特征工程 │ ├── dataset.py # Dataset 与 DataLoader │ ├── model.py # LSTM/GRU 定义 │ └── train.py # 训练与评估脚本 ├── checkpoints/ # 权重文件 └── outputs/ # 预测结果和图表

源码与输出分离的最大好处是:跑实验时不需要反复改动训练脚本,改配置文件就行,也不会因为误改模型代码污染上一次的结果。项目说明里如果没有目录结构,拿到压缩包后先按这个思路重新整理。

4.2 构造 LoadDataset 与 DataLoader:窗口边界别搞错

Dataset 的核心是「用连续 seq_len 个时刻的特征,预测下一个时刻的负荷」。最容易犯的错是让预测目标等于窗口内最后一个值,那模型等于在做「预测现在」,验证集上自欺欺人。

from torch.utils.data import Dataset, DataLoader class LoadDataset(Dataset): def __init__(self, features, target, seq_len=96): self.features = features self.target = target self.seq_len = seq_len def __len__(self): return len(self.features) - self.seq_len def __getitem__(self, i): x = self.features[i:i + self.seq_len] # 96 个历史时刻 y = self.target[i + self.seq_len] # 未来 1 个时刻 return x, y

len 返回的是 len(features) - seq_len,而不是 len(features)。因为要保证从起点 i 取 96 个特征后,i+96 这个索引仍然在 target 范围内。如果把 len 写成 len(features),最后一个样本会越界,训练在最后一个 epoch 报 IndexError。这里 y 是一维还是二维取决于 target 的初始形状,通常我会把 target 先 reshape 成 (n_samples, 1),再在训练循环里 squeeze。

4.3 训练循环与验证逻辑:分开看 Train Loss 和 Val MAPE

训练时监控 loss 用的是 MSE 或 Huber,评估时用的是 MAPE。两者分开的原因是:loss 只告诉模型梯度往哪走,MAPE 才是业务关心的「平均偏差百分之几」。如果只看 train loss,模型可能在绝对值大的夜间低谷上表现很好,但在高峰上误差很大。

import torch import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0.0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() pred = model(x).squeeze(-1) loss = criterion(pred, y) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() total_loss += loss.item() * len(x) return total_loss / len(loader.dataset)

clip_grad_norm_ 是循环神经网络训练的必要保护。负荷序列里偶尔会有极端样本,梯度一冲,hidden state 的数值范围就容易爆炸;把梯度范数截断到 1.0 后,训练会更平顺。criterion 我通常用 nn.SmoothL1Loss(beta=1.0),也就是 Huber loss,它对小误差用平方、大误差用线性,不像 MSE 那样被高峰决堤式的大误差带着跑。

验证逻辑不在这里展开,但要记住:验证时用 torch.no_grad() 包裹前向计算,并在每个 epoch 结束时计算验证集 MAPE;早停的触发条件是验证 MAPE 连续 10 个 epoch 不再下降。

4.4 训练超参数速查表与建议起点

超参数建议值调整方向
seq_len96数据是 1 小时粒度时应降到 24
hidden_size128数据量小降到 64,数据量大升到 256
num_layers2超过 3 层收益极小
dropout0.2验证集 MAPE 高于 5% 可降到 0.1
batch_size256OOM 时降到 64
learning_rate0.001AdamW 配 cosine 退火
epochs60配合 early stopping,不必跑满
clip_grad_norm1.0梯度爆炸时再降到 0.5

这些参数的起点来自大多数区域负荷项目的共性:数据量几万到十几万条,特征 5 到 10 个。seq_len 取 96 是因为一天 96 个 15 分钟点,模型能看到完整的一天波形;如果改成 48,模型只能看到 12 小时,夜间和日间的衔接会差。learning_rate 用 0.001 起步,训练到中段用 cosine 退火慢慢降,避免后期在最优解附近震荡。

5. 避坑与排查:负荷预测翻车的 6 个典型问题

5.1 时间泄漏:验证集被训练集的归一化信息污染

现象:验证集 MAPE 做得很好,测试集或线上预测一塌糊涂。

原因:很多人在整个 df 上做归一化,用全局 mean/std 缩放后再切分数据集。验证集和测试集的归一化参数包含了它们自身的均值信息,等于模型在验证时间接看到了未来的统计量。这是时序预测里最隐蔽的泄漏。

解决:只拿训练集计算 mean/std,验证集和测试集复用这组参数。代码上养成习惯,把归一化器单独 fit 在 train_df 上,再 transform 到 val_df 和 test_df。

5.2 预测曲线滞后:模型只是搬了昨天的负荷

现象:把预测值和真实值画在一起,预测曲线整体向右平移了几个点,像「慢半拍」。

原因:负荷序列自相关极高,模型发现最简单省力的做法是复制前一个时刻的观测值。如果目标构造错了,比如 y 取成了窗口内最后一个值,或者特征里没有足够的外生变量,模型就会走这条捷径。

解决:先检查目标索引,确认 y = target[i + seq_len] 而不是 target[i + seq_len - 1];再检查特征里有没有温度、节假日这类能打破自相关的外生变量;最后把训练目标从原始负荷改成负荷的一阶差分,预测完再累加回去,滞后现象通常会明显缓解。

5.3 训练 Loss 很低,验证 MAPE 却很难看

现象:train loss 一路下降到接近 0,验证 MAPE 卡在 8% 不往下走。

原因:两个方向。一是过拟合,模型把训练集的噪声也背下来了;二是 MAPE 的尺度陷阱,夜间低谷负荷很小,绝对误差 0.1 MW 在低谷上算出的百分比误差很大,模型如果牺牲低谷去保高峰,MAPE 就不好看。

解决:先把 dropout 从 0.2 提到 0.3 看验证集变化;再把验证指标改成「分段 MAPE」,分别计算 0-6 点、7-9 点、18-21 点的误差,定位到底哪一段在拖后腿。

5.4 显存 OOM:batch_size、seq_len 与梯度累积

现象:训练刚开始就报 CUDA out of memory,或者跑到一半被系统 kill。

原因:batch_size 256 乘上 seq_len 96,再乘 6 个特征,每批数据本身不大;占显存的大头是 LSTM 的反向传播需要保留的中间状态。hidden_size 越大、num_layers 越多,OOM 概率越高。

解决:第一个操作是把 batch_size 降到 64;还不行就把 hidden_size 从 128 降到 64,把 num_layers 从 2 降到 1;最后的手段是梯度累积,每 4 个 batch 更新一次梯度,模拟 batch_size=256 的统计效果。

5.5 高峰总是被低估:损失函数与样本权重

现象:MAPE 控制住了,但早高峰和晚高峰的峰值预测普遍偏低 5% 左右。

原因:MAPE 对低负荷敏感,模型如果优先压低低谷误差,整体 MAPE 会下降得很快,代价就是高峰期被牺牲。另一个原因是 MSE 训练出来的模型天然趋于均值回归,极端峰值会被拉平。

解决:给损失函数加峰值权重,w = 1 + alpha * (load 的归一化值),高峰样本的梯度更大;或者改用 pinball loss 做分位数预测,输出 p=0.5 的中位数作为点预测。我一般先用前一种,改动最小。

5.6 拿到项目说明先看哪三处:数据、模型定义、训练输出

现象:压缩包解压后文件很多,不知道从哪读起,跑脚本还报错。

原因:项目说明没有按数据、模型、输出组织,或者作者默认你懂他的目录结构。

解决:先读 data 目录的 README 或 CSV 列头,确认时间戳和特征列名;再打开 model.py 看 forward 返回的是什么形状,是 (batch, 1) 还是 (batch, horizon);最后看 train.py 里加载数据的方式,是直接读全量 CSV 还是已经做了切分。大部分报错都出在列名不一致、时间戳没解析、归一化器没有保存这三个点上。

6. 让预测真正被调度采纳:多步预测策略与误差诊断

6.1 直接多步预测的模型改动

如果业务要的是未来 4 小时或者 24 小时的负荷曲线,只输出未来一个点的模型不够用。常见做法有两种:递归预测是把预测值当成下一个时刻的输入喂回模型,误差会随时间累积,预测到第 8 步时曲线通常会变得平滑到失真;直接多步预测是把输出维度直接改成预测步长 h,让模型一次输出未来 h 个时刻的值。

class DirectMultiStepLSTM(nn.Module): def __init__(self, n_features, horizon=24, hidden_size=128, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout, ) self.regressor = nn.Linear(hidden_size, horizon) def forward(self, x): out, _ = self.lstm(x) return self.regressor(out[:, -1, :]) # (batch, horizon)

训练时 target 从单个标量变成未来 24 个值。损失函数可以逐时刻独立计算 MSE,也可以对不同时刻用不同权重,比如越近的时刻权重越高。直接多步的好处是每一步都有独立的输出头,不会累积误差;缺点是 horizon 增大后模型复杂度上升,预测远期曲线往往偏保守。我个人的习惯是:预测未来 12 个点以内用直接多步,超过 24 个点就分两段做,先预测未来 24 小时,再用第一段的末端值作为下一段序列窗口的输入。

6.2 三个可上手的验证技巧

第一,把验证集的预测曲线与真实曲线画在同一张图里,叠加 7 天,人眼比 MAPE 更能发现问题。预测曲线如果和真实曲线形状一致但整体偏低,说明模型偏差稳定,可以做简单的偏差修正;如果曲线明显滞后,问题多半在特征或目标构造。

第二,按时刻统计 MAPE。把验证集所有样本按小时分组,画出一天 24 个小时的 MAPE 柱状图。通常凌晨 2-5 点的 MAPE 最高,早高峰 7-9 点和晚高峰 18-21 点次之,午后相对平顺。如果某个高峰时段的误差异常突出,就该检查这个时段是否叠加了温度突变或特殊排产。

第三,做误差的自相关分析。如果残差在时间上仍然呈现出明显的周期性或自相关,说明模型没有把序列中的决定性信息抽干净,还有可用特征没加进来。这一步不用写复杂代码,pandas 的 autocorr 函数可以直接看滞后阶数。

6.3 一段我踩过的坑收尾

我最早做负荷预测时,第一版模型直接套用了成熟的 Transformer,结果验证集 MAPE 比 LSTM 还高,前三个 epoch 训练 loss 甚至不下降。后来发现不是模型不行,是温度特征做完归一化之后没有保留原始尺度,注意力机制对特征尺度非常敏感,特征缩放不一致直接影响了注意力权重的计算。之后我都先把基线 LSTM 和手工特征管线跑通,再考虑上复杂结构,这套顺序让很多项目少走了弯路。把数据、模型、评估三条线理顺后,你会觉得负荷预测真正难的地方不在网络结构,而在每一个平凡细节是否做对。希望帮到你。

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

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

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

立即咨询