简介:基于BP神经网络的电力负荷预测研究资料包,面向电气工程、数据科学方向的学生与相关从业者,聚焦电网短期负荷预测场景,系统展示从历史负荷数据采集到预测模型构建、训练与误差分析的完整流程。压缩包共334个文件,主体为329个Excel数据文件,涵盖2018年多日负荷记录与天气数据,并配有main.m、my_map.m、read_load_data_from_excel.m等MATLAB脚本及README.md说明文档,整体约5.32MB,结构清晰、便于快速上手。已有2459人学习下载,内容涉及数据预处理、特征选择、网络结构设计、梯度下降与反向传播调参、模型验证及均方误差等指标分析,既可直接运行复现案例,也可依据代码和数据结构学习BP神经网络在时间序列预测中的应用,同时了解天气、历史负荷等因素如何融入模型,适合作为课程设计、毕业设计或论文实验的基础参考资料。
1. 电力负荷预测:机器学习不是万能药,但确实是当下最靠谱的路
电力负荷预测这件事,说大也大,它直接决定电网调度、机组启停和现货市场报价;说小也小,落到一个具体园区、一台变压器上,就是回答"明天上午十点这条馈线大概跑多少千瓦"。过去十几年行业里最常用的方法是时间序列模型和回归分析,近几年机器学习方法逐步上位。别急着神话它——神经网络不是一切问题的答案,但机器学习框架下的特征工程、交叉验证和误差分析,确实把负荷预测从一个凭经验的"玄学"变成了可量化、可迭代的工程问题。这篇笔记我会按自己实际做过的方案来讲,基本思路是:先弄清负荷预测在预测什么、用什么数据,再选模型,再谈工程落地和那些容易翻车的细节。适合电网调度、售电公司、园区能源管理平台的技术人员,也适合准备做相关课程设计的同学照着搭一套基线系统。
2. 从问题定义到数据集构建:先把负荷预测翻译成监督学习
2.1 预测步长与粒度:多做点预测和短期预测的工程分界线
做负荷预测第一件事不是选模型,而是把问题定义清楚:预测未来多久、颗粒度多大、滚动更新还是定时更新。常见的分法是超短期(未来15分钟到1小时)、短期(未来1到7天)、中期(未来数周到数月)。机器学习方法最常落地在短期和超短期,因为中期以上涉及气象长期预报、节假日安排和经济增长等不确定性,纯数据驱动模型很难压住误差。
粒度方面,电网调度常用15分钟和1小时;楼宇和园区能源管理常用30分钟或1小时;做售电预测时还会看日总量。不同的粒度对数据量要求差别很大:15分钟粒度一年约有35040个点,小时粒度约8760个点。数据量越小,越要克制模型复杂度——用个几千棵树的LightGBM在小样本上没太大问题,但深度网络就容易过拟合。
我给自己的工程约定是:短期预测用小时粒度,步长24小时;超短期用15分钟粒度,步长4到8个点。这个搭配既能覆盖大多数业务场景,又不至于让数据预处理和特征工程过于复杂。
2.2 数据清洗与对齐:拿到原始负荷数据后的前三个处理动作
原始负荷数据从SCADA系统或电表采集系统导出后,几乎不可能直接用来训练。常见的问题包括:缺失点(通讯中断)、跳变点(采集异常)、时区混乱(有些系统存的是北京时间,有些是当地时区)。我一般会做这样三步:
第一步是对齐时间轴。把所有数据统一到同一时区、同一个时间戳格式,重采样到目标粒度。比如原始数据是5分钟一条,要聚合到小时粒度,可以用取平均或取最大值——取平均适合做电量推算,取最大值适合做需量管理,看业务口径选。
第二步是处理缺失值。对小于等于连续两个点的缺失,用前后线性插值;对更长的缺失段,用同类型日(比如都取工作日)的同一时刻均值填充。这里有个易错点:不能用未来数据填充历史缺失,否则做时序交叉验证时会造成信息泄漏,验证集上误差会很好看,一上真实环境就现原形。
第三步是识别和处理异常跳变。最简单的方法是用滚动窗口的均值加减三倍标准差做粗筛,再结合业务判断。比如某地区夏季空调负荷大,某天午后出现一个远超历史极值的读数,既可能是真实需求(高温+工作日+电价激励),也可能是采集故障。这时候我会把候选异常点导出到一个Excel里人工排查一遍,完全不自动删除。
import pandas as pd import numpy as np def load_and_clean(raw_path, freq='1h'): df = pd.read_csv(raw_path, parse_dates=['timestamp']) df = df.set_index('timestamp').sort_index() # 去重:同一时间戳只保留最后一个值 df = df[~df.index.duplicated(keep='last')] # 重采样,缺失点置为NaN df = df.resample(freq).agg({'load': 'mean'}) # 连续缺失不超过2个点,线性插值 df['load'] = df['load'].interpolate(limit=2, limit_direction='both') # 滚动窗口异常值标记,不直接替换 rolling_mean = df['load'].rolling(window=24, center=True).mean() rolling_std = df['load'].rolling(window=24, center=True).std() df['anomaly'] = (df['load'] - rolling_mean).abs() > 5 * rolling_std return df先说一下上面的逻辑。limit=2和limit_direction='both'保证了只填充短缺口,而且是双向填充,避免凌晨数据单边缺失时产生明显偏置。center=True让滚动窗口以当前时刻为中心,两边的数据都参与统计,这对标记异常点来说是合理的——但注意,训练特征里不能这么用,后面预测时当前时刻之后的数据不可得,特征只能从过去取。异常值标记出来之后,我通常会做一个散点图确认,确认是真实事件就保留,确认是坏数据才用前一天同时刻的值替换。
2.3 构建监督学习样本:滑动窗口、预测目标与防止数据泄漏
负荷预测的原始形态是时间序列,但机器学习模型吃的是表格数据,所以要把时间序列转换成样本对(特征,目标)。假设用过去7天的同时刻负荷去预测未来1小时负荷,那么每条样本的特征是168个历史小时值加上天气、日期等外生变量,目标是未来某个时刻的负荷值。
窗口滑动构建样本时有一个非常容易踩的坑:训练集和验证集必须按时间顺序切分,不能在构建样本时打乱。另一个坑是,滚动窗口的重叠度过高会让相邻样本高度相关,模型很容易记忆而不是学习。我的做法是:对小时粒度数据,窗口长度24或168,步长1小时;对15分钟粒度,窗口长度96(一天),步长1个点。这样相邻样本之间存在信息重叠,但业务上预测任务就是需要连续输出,所以重叠本身不是问题,问题是切分不干净。
def build_samples(df, feature_cols, target_col, window=168, horizon=1): X, y = [], [] values = df[feature_cols].values targets = df[target_col].values for i in range(window, len(df) - horizon): X.append(values[i - window:i]) y.append(targets[i + horizon]) return np.array(X), np.array(y)这个函数有三个参数要特别注意。window是历史窗口长度,取168意味着利用过去整整一周的负荷模式,对周期性强、工作日和休息日差异明显的场景很有效;如果数据只有一个月,那窗口取24更稳妥,否则前168个点都只能放弃不做样本。horizon是预测提前量,取1就是预测下一时刻,取24就是预测明天同一时刻。特征矩阵的维度是(样本数, window, 特征数),如果特征是168个小时负荷加温度、湿度、星期几、节假日标记,那第三维就是4到6个通道,后面的机器学习模型要把二维矩阵展平或做特征聚合,直接三维灌进LightGBM是不行的。
3. 特征工程是负荷预测的主战场:时序特征、气象特征与节假日编码
3.1 负荷序列的三种周期:日周期、周周期与季节周期怎么编码
做负荷预测的人常说一句话:负荷数据最稳定的信息不是数值本身,而是它的周期性。居民负荷、商业负荷、工业负荷都有明显的日周期——早上和晚上各有一个峰,午间和深夜是谷;周周期也很明显,工作日和休息日形态完全不同;季节周期更不用说,冬季取暖和夏季制冷的负荷曲线相差甚远。
把周期性喂给机器学习模型,有三种做法。第一种是直接取历史同一时刻的值作为特征,比如预测明天上午10点的负荷,就把今天上午10点、昨天上午10点、上周同一天的上午10点作为特征——这是最简单的,效果通常也很好。第二种是用时间戳的周期编码,比如把小时转换成sin(2π*hour/24)和cos(2π*hour/24),把星期几用同样的方式编码成周周期。第三种是把周期信息做成类别特征,比如"第几小时""星期几"直接喂进树模型,LightGBM和XGBoost能自己找到合适的分裂点,但也容易过拟合一些冷门组合——比如某节假日恰好在周二、正好又碰上极端天气,这种组合样本量极少。
我一般会把三种做法混用:历史同时刻值是必选,周周期和周几同时给,周期编码则根据模型选——树模型给原始类别,线性模型给sin/cos,神经网络直接嵌入。
3.2 温度与湿度特征:用感受温度代替单一温度值
负荷对气象的敏感度很高,但直接用单一温度值做特征往往效果不理想。原因有二:一是有滞后性,连续三天高温时第三天的负荷通常比第一天高,因为建筑物和地面蓄热;二是湿度的影响不能单独看,闷热天即使温度不是极高,空调负荷也可能很大。
实操中我会构造一个"感受温度"特征,用温度加湿度做一个简单加权,再叠加累积效应——过去48小时的小时温度加权平均值,权重按小时递减。这样做的效果比单纯加一个温度列好很多,尤其在夏季场景下能把午后负荷峰值的拟合误差压低几个百分点。另外,光照对光伏渗透率高的地区也很重要,如果手头有辐照度数据,也同样要做累积特征。
3.3 节假日与特殊日处理:为什么重大节假日总是预测翻车
节假日是负荷预测的老大难。春节、国庆这类长假期间,工厂停工、商场歇业,负荷水平可能比正常工作日低20%到40%,而且每天的形态完全不同于常规。如果训练数据里没有足够的同类样本,模型会把长假误判成普通周末甚至工作日,导致预测偏差非常大。
处理节假日有几个工程上常见的做法。第一种是加"距最近节假日天数"和"节假日第几天"特征,让模型知道当前日期离节假日的远近,这在节前和节后尤其有效。第二种是单独建一个节假日样本集,节假日期间用专门的模型或规则修正。第三种是直接剔除重大节假日样本,然后另做一套节假日预测方案。我一般会先用第一种做通用模型,然后看节假日期间的实际误差——如果平均绝对百分比误差超过常规时段的1.5倍,就会给节假日单独建一个模型。
一个容易犯的错是在节假日特征里把调休日也当作普通工作日或休息日。中国特有的调休制度意味着有些周六上班、有些周日放假,如果只用周几做特征,模型永远学不会这种规则。要把日历中的"是否调休"作为一个独立特征输入,这个数据可以从公开节假日接口获取,也可以每年年初人工维护一张日期表。
4. 选型与训练:从线性回归到LightGBM再到LSTM,怎么选不翻车
4.1 基线模型先立住:线性回归和决策树回归的定位
机器学习方法做负荷预测,第一步不是直接上深度学习,而是先跑一个可解释的基线模型。我通常先做线性回归和决策树回归两个基线,前者用于理解特征的线性相关性,后者用于理解非线性切分的潜力。线性回归有个很大的好处是可以直接看每个特征的系数——过去几天同时刻负荷的特征权重通常是最大的,温度特征的权重是正的(夏天)或负的(冬天),日期特征的权重能反映工作日和休息日的差异。
基线模型不需要精细调参,主要用来建立后续对比的基准。如果后面复杂模型比基线的提升不到5%,就要怀疑是不是特征工程出了问题,而不是模型不够强。这个习惯帮我避免了不少无用功——有次在某个园区的数据上,LSTM调了一周参数,最后效果只比线性回归好3%,检查下来发现是温度特征的滞后效应没做好,把累积温度特征加进去后LightGBM直接把差距拉开到12%。
from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_percentage_error def train_baseline(X_train, y_train, X_test, y_test): model = LinearRegression() model.fit(X_train, y_train) y_pred = model.predict(X_test) mape = mean_absolute_percentage_error(y_test, y_pred) * 100 coef = sorted(zip(feature_names, model.coef_), key=lambda x: abs(x[1]), reverse=True) return model, mape, coef[:10]这段代码里有个细节:mean_absolute_percentage_error对接近零的真实值非常敏感,如果某些时段负荷趋近于零,MAPE会被极小的分母推到一个很大的数。工程上我更常用RMSE加MAPE两个指标一起看,并且在做误差统计时剔除负荷低于某个阈值的样本。另外线性回归对特征尺度敏感,如果进入模型的温度是两位数、负荷是四位数,最好先做标准化或至少统一量纲。
4.2 树模型是当下的主流选择:LightGBM的四个必调参数
如果只能选一个模型做负荷预测,我推荐LightGBM。它在表格数据上的表现稳健、训练快、对特征工程的要求比深度网络低,而且在相对小的数据集上不容易过拟合。XGBoost也很好,但LightGBM在训练速度和内存占用上更友好,尤其是特征维度到几百之后差别明显。
负荷预测的LightGBM模型,四个参数最值得调:num_leaves控制树的复杂度,数据量在几万条量级时取31到127之间;learning_rate一般取0.03到0.1,调低后可以配合更大的n_estimators;max_depth限制树深,防止在噪声特征上学到太细的分裂;min_child_samples设到20以上,防止叶子节点样本太少。还有一个容易被忽视的点:feature_fraction和bagging_fraction可以缓解时间序列样本间的相关性带来的过拟合,我通常把两者都设在0.8左右。
import lightgbm as lgb params = { 'objective': 'regression', 'metric': 'rmse', 'num_leaves': 63, 'max_depth': 8, 'learning_rate': 0.05, 'n_estimators': 800, 'min_child_samples': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, } model = lgb.LGBMRegressor(**params) model.fit(X_train, y_train, eval_set=[(X_valid, y_valid)], callbacks=[lgb.early_stopping(50), lgb.log_evaluation(100)])注意这里用了early_stopping,它在验证集上评估每一次迭代的性能,当连续50轮没有提升时自动停止。这个机制对有较强周期性但偶尔出现异常样本的负荷数据特别有用——模型会在前几百轮快速下降,然后进入一个平台期,如果没有早停,继续训练只是在记住噪声。
4.3 深度学习要不要上:LSTM和TCN在不同场景下的取舍
深度学习在负荷预测领域的名声有点两极分化。做时序预测的论文几乎必上LSTM、GRU、Transformer,但在实际工程项目里,深度模型能稳定打赢LightGBM的场景并不多。我的经验是:数据量超过5万条、特征中包含多条长时间依赖的序列(比如同时给过去96个时刻的负荷、温度和电价)、并且允许投入较多时间做调参和数据预处理时,深度学习才有明显的优势。否则,树模型的性价比更高。
如果决定上深度模型,我建议先从LSTM开始。它对中等长度序列(几十到几百个时间步)的建模能力是足够的,而且Keras和PyTorch都有很成熟的API。要注意三个坑:第一,输入需要做归一化,而且归一化的统计量必须从训练集计算,验证集和测试集用同一组参数转换;第二,序列样本的构建方式和前面提到的滑动窗口一致,但训练时必须打乱样本顺序——这一点和时间序列交叉验证不矛盾,因为LSTM处理的是样本内的时序依赖,而样本间的顺序不影响这个依赖关系;第三,验证集的切分仍然要按时间顺序,模拟真实预测时的样本分布。
TCN(时间卷积网络)是另一个值得尝试的方向,它的训练比LSTM稳定,不存在梯度消失问题,而且可以并行计算。在超短期负荷预测中,TCN往往能取得和LSTM相当或更好的效果,训练时间还短很多。如果团队没有专人负责调参,我一般建议先做TCN,把LSTM留给后续研究。
4.4 时序交叉验证:普通K折在这里是错的
机器学习的常规做法是K折交叉验证——把数据随机分成K份,轮流做验证。但负荷数据是时间序列,随机切分会把未来的样本混进训练集,验证集上当然表现很好,但那是虚假的。正确做法是按时间顺序切分,训练集永远在验证集之前。
具体实现上,我会用"滚动推进"的方式:假设有24个月数据,第一次用前12个月训练、第13个月验证,第二次用前13个月训练、第14个月验证,以此类推。这样做既能模拟真实预测时"用过去预测未来"的约束,又能通过多次评估判断模型在不同季节的稳定性。
import numpy as np from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, val_idx in tscv.split(X): X_train_t, X_val_t = X[train_idx], X[val_idx] y_train_t, y_val_t = y[train_idx], y[val_idx] model.fit(X_train_t, y_train_t) preds = model.predict(X_val_t) mape = mean_absolute_percentage_error(y_val_t, preds) * 100 print(f"fold MAPE = {mape:.2f}%")TimeSeriesSplit是sklearn内置的时间序列切分器,它保证训练集始终是验证集之前的连续片段。但注意:它默认的切分间隔是等宽的,不会考虑季节跨度,如果你的数据是小时粒度并且只切5折,可能每折只有几个月,模型学到的季节性有限。我通常会在切分前先把数据按"连续X周"组织成块,再手动切分,确保每折都覆盖完整的周周期。
5. 负荷预测的常见翻车现场:五个高频踩坑点与排查办法
5.1 数据泄漏:验证集误差很小,上线就崩
现象:离线测试时MAPE只有3%,投入在线预测后第一周误差飙到10%以上。原因:特征构建或预处理过程中用了未来信息,最常见的有三类——用全局均值和标准差做归一化时统计量包含验证集数据;缺失值填充用了前后插值且时间窗口跨过了切分点;日期特征里包含了预测时刻之后才确定的气象预报值。解决方案:统一封装特征构建逻辑,测试集和验证集走完全相同的管道(pipeline),归一化统计量只从训练集计算。一个更彻底的检查方法:把数据的后1/3随机抹掉,用前2/3训练后重新预测后1/3,如果误差明显高于交叉验证结果,基本可以判定泄漏。
5.2 季节性漂移:模型在夏天拟合得很好,一到冬天就偏
现象:春夏训练的模型在秋冬验证集上误差变大,而且不是轻微变大,是整体抬升。原因:负荷的季节性特征(采暖负荷、制冷负荷)在训练集中占比小或者完全没出现,模型相当于面对一个没见过的数据分布。解决方案:构建训练数据时保证覆盖至少一个完整年度;如果数据不到一年,就明确声明模型的适用季节,不要盲目相信一整年的测试结果。另一个补救措施是把"月份"或"季节"作为特征输入,模型至少能学到不同季节的整体差异。
5.3 多步预测误差累积:预测24小时后,后半段误差越来越大
现象:做24小时滚动预测,前几个小时的误差很小,但第18到24小时误差明显增大。原因:单步模型在每一步输入的都是真实历史值,而真实在线预测时输入的是前一步的预测值,误差会逐点累积。解决方案:训练时加入"多步拼接"策略——把前一步的预测值当作下一步的特征来训练,让模型适应误差累积的输入分布;或者退一步讲,预测24小时输出时,直接用多输出模型(同时输出24个未来值)而不是递归滚动。前者效果好但训练复杂度高,后者实现简单、在负荷场景通常也够用。
5.4 极端天气日整体失准:寒潮大风一来,模型集体失灵
现象:平日预测误差5%,寒潮来袭当天误差冲到20%以上。原因:训练集里极端天气样本太少,模型没见识过这种输入组合;或者是气象特征不够直接——寒潮的影响不仅体现在温度绝对值上,还体现在温度变化速度上。解决方案:构造"温度24小时变化量"特征;对极端天气样本单独加权或做上采样;备一条规则兜底——当天气预报出现极端天气预警时,使用一个简单但鲁棒的经验模型(比如按历史同期温度-负荷回归曲线直接查表)替换机器学习预测。
5.5 样本加权失效:试图让模型更关注近期的数据,反而失真
现象:给近期样本分配更高权重,验证集误差没降反升。原因:负荷分布不是平稳的,近期样本的形态不一定能代表未来——比如刚过去的国庆假期会压低模型对未来普通工作日的预测。解决方案:权重只应放在业务上明确相关的时段,比如知道未来一周有新项目投产,才需要把相似的投产历史样本加权。平时不需要做时间衰减加权,树模型对时间远近天然不敏感,LSTM可以通过训练时调整序列内的注意力机制来关注近期趋势,不需要显式加权。
6. 模型上线前的验证技巧:残差分析和滚动回测帮你少交点学费
模型训练完之后,先别急着部署到生产环境。我习惯再做两件事:残差可视化分析和滚动回测。残差分析能帮助判断误差是否在时间轴上均匀分布;滚动回测能模拟连续预测场景,暴露数据泄漏和误差累积的问题。这两步加起来半天内就能完成,但往往能发现前面所有指标都看不出来的隐患。
残差分析有一个特别实用的做法:把预测残差按小时、星期几、月份各画一张分布图。如果某个时段残差明显偏大,就说明特征工程没捕捉到这个时段特有的模式。我有一次就是通过这个分析发现,晚间20点到22点居民负荷在周五和周六差异极大——平时用"星期几"一个特征根本不够,加上"是否为周末前夜"之后误差立刻降下来。
另外,可以做一个"滚动起点"的快速回测脚本:从验证期的每一天作为起点,依次预测未来24小时;每天只保留当天的预测结果,汇总成一条连续预测曲线,再和真实值逐点对比。这样做比一次性预测更接近真实的在线运行逻辑,因为每一天用的训练数据都在前一天之后。要注意,这个回测脚本会跑很多次模型训练,所以先确保单次训练时间在可接受范围内。
def rolling_backtest(df, model, feature_cols, target_col, horizon=24): errors = [] for start in range(len(df) - horizon - 168): train = df.iloc[:start + 168] test = df.iloc[start + 168:start + 168 + horizon] model.fit(train[feature_cols], train[target_col]) pred = model.predict(test[feature_cols]) true = test[target_col].values errors.append(np.abs(pred - true) / (true + 1e-6)) errors = np.concatenate(errors) print(f"rolling MAPE = {np.mean(errors) * 100:.2f}%")这里起始训练数据不能是空的,所以从第168个点之后才作为第一次预测起点。1e-6是为了防止负荷接近零时计算比值为无穷大,实际业务里大部分时段负荷都有底数,这个保护仍然建议保留。运行这个回测会比较耗时,但它是上线前最有价值的一步——如果滚动MAPE和单次验证MAPE差距超过两个百分点,基本可以确认流程里还有泄漏或特征错位问题。
最后说一下我的习惯:上线后我会把每天的预测值和真实值存成一张对比表,每周画一次趋势图,哪怕不做任何调整也至少坚持看两个月。因为负荷模式会随季节、经济活动和用电行为缓慢变化,一个模型在某个时间段看起来很准,不代表三个月后依然可靠。机器学习负荷预测的本质是持续迭代,不是一次训练一劳永逸。这个方案的方向是对的,关键是用好交叉验证、控制好数据泄漏、盯紧残差。希望帮到你。
本文还有配套的精品资源,点击获取