☰
电力负荷预测实战:特征工程与多模型对比全解析
2026/10/19 5:23:14 网站建设 项目流程

简介:电力负荷预测是电力系统规划与调度的重要基础,直接关系机组启停、运营检修与电网扩建等决策。面向电力数据分析、机器学习建模或相关课程设计的学生与研究者,围绕短期(10天15分钟粒度)与中期(未来三个月)负荷预测场景,提供可运行的 Python 代码、配套分析报告及中间数据,帮助理解特征提取、统计分析、模型评估与结果可视化流程。资源包共 9 个文件,包含 3 份 csv 中间/结果数据、2 份 xls 未来预测表格、3 份 html 可视化分析与 1 份 docx 报告,其中 csv 保存十天每15分钟等关键输出,xls 对应未来三个月的最大/最小值预测,html 展示建模过程与结果,docx 则为完整报告文档;压缩包仅 455KB,轻量易下载,便于直接复现和对比验证。目前已有 3330 人学习,代码与报告结构清晰,涵盖气象等因素影响下的负荷变化分析及传统模型局限性的应对思路,是一份适合入门到进阶的完整负荷预测参考资料。

1. 电力负荷预测:别把时间序列当回归硬撸,这份代码包里全是套路

做过电力负荷预测的人都知道,真正烦人的不是模型跑不起来,而是数据本身一堆“坑”——节假日突变、极端天气尖峰、工作日与周末的双峰形态完全不一样。如果直接把原始负荷序列丢给机器学习模型,十有八九要翻车。这套“电力负荷预测分析”资源包,包含完整 Python 代码、分析报告和中间处理数据,正是围绕“特征工程 + 多模型对比 + 统计校验”这条主线展开的。适合刚接触负荷预测、想跑通全流程的从业者,也适合需要一份可复现模板、快速套到自己业务数据上的算法工程师。它解决的核心问题是:在数据质量参差不齐的前提下,如何让预测结果既统计上站得住脚、又业务上解释得通。

2. 负荷数据的三个隐藏特征:趋势、周期与突变,决定了模型上限

2.1 为什么先做时间序列分解:趋势项不拆,模型学到的全是假规律

负荷数据不是平稳序列,这一点在动手建模前必须认清楚。很多人上来就直接用随机森林或 XGBoost 拟合,结果训练集上 R² 高达 0.98,一到跨月预测就崩。原因很简单:模型把时间趋势当成固定规律记住了,而不是真正学到了负荷的动态变化逻辑。

代码包里第一步就是做分解。常见做法是用statsmodels的seasonal_decompose,把原始负荷拆成趋势项、季节项和残差项三部分。实际跑下来你会发现,电力负荷的“季节项”其实有两个尺度:一个是年度季节——夏季制冷、冬季取暖导致的高负荷;另一个是日内周期——每天 24 小时中,早高峰和晚高峰的形态完全不同。

import pandas as pd from statsmodels.tsa.seasonal import seasonal_decompose # 假设 load_data 是两列:ds(时间戳),y(负荷值) load_data['ds'] = pd.to_datetime(load_data['ds']) load_data = load_data.set_index('ds').resample('H').mean() # 统一重采样到小时 # 周期设为 24,原因是电力负荷有典型的日内周期 decomp = seasonal_decompose(load_data['y'], model='additive', period=24) # 把分解结果写回中间数据,供后续特征工程使用 load_data['trend'] = decomp.trend load_data['seasonal'] = decomp.seasonal load_data['residual'] = decomp.resid

这里有个关键参数:period=24。如果数据是半小时粒度,这个值要改成 48;如果是 15 分钟粒度,改成 96。model='additive'意味着把负荷看成趋势、季节项和残差的简单相加。对大部分城市级负荷数据,加法模型就够了;但如果负荷量级随季节明显放大,比如冬季峰值是夏季的两三倍,就要考虑model='multiplicative'。判断方法很简单:把趋势项和季节项分别画出来,如果残差项还存在明显的季节性波动,说明分解方式选错了。

分解完以后,trend列直接作为特征喂给模型是没问题的,但seasonal和residual要谨慎。seasonal本质上是一个可预测的确定性模式,直接加入特征容易让模型过拟合到历史周期上;而residual里往往藏着噪声和突发事件信息,放入特征反而会带入不可复现的成分。我一般的做法是:只把trend作为衍生特征,seasonal用日历特征(小时、星期、月份)隐式表达,residual只用来做残差分析、检查模型是否遗漏了重要模式。

2.2 特征工程不只是加几列:温度、滞后值、节假日的“组合拳”怎么打

负荷预测的特征工程和普通回归有很大区别。普通回归关心的是“当前状态”,负荷预测关心的是“过去状态如何延续、外部因素如何干扰”。这套代码包里构造的特征大致分四类:时间类、滞后类、滚动统计类、外部因素类。

时间类特征包括小时、星期、月份、是否周末、第几周等。其中hour和weekday的交互项特别重要——同样是上午 10 点,周一的负荷和周日完全不同。滞后类特征是最能提升模型精度的单项,代码包里默认构造了lag_1、lag_2、lag_24,也就是前 1 小时、前 2 小时、前 24 小时的负荷值。lag_24尤其关键,它捕捉了“昨天的这个时刻负荷是多少”,对于有强日周期性的电力数据,这个特征几乎决定了 baseline 的下限。

滚动统计类特征如过去 6 小时均值、过去 24 小时最大值,用来平滑短期波动。外部因素类特征里,温度是最常规的,部分场景还会加入湿度、风速。但要注意:温度数据的时间粒度和负荷数据对不上时,千万别直接 merge,先做前向填充。

def build_features(df, temp_df=None): df = df.copy() # 时间类特征 df['hour'] = df['ds'].dt.hour df['weekday'] = df['ds'].dt.weekday df['is_weekend'] = (df['weekday'] >= 5).astype(int) df['month'] = df['ds'].dt.month # 滞后类特征 df['lag_1'] = df['y'].shift(1) df['lag_2'] = df['y'].shift(2) df['lag_24'] = df['y'].shift(24) # 滚动统计类特征 df['roll_mean_6'] = df['y'].rolling(6).mean() df['roll_max_24'] = df['y'].rolling(24).max() # 温度特征:按时间戳对齐后前向填充 if temp_df is not None: temp_df['ds'] = pd.to_datetime(temp_df['ds']) df = df.merge(temp_df, on='ds', how='left') df['temp'] = df['temp'].ffill() return df.dropna()

滞后值构造完成后必须dropna(),因为前 24 行数据没有lag_24,留在训练集里会让模型学到“缺失就是 0”的错误映射。roll_mean_6和lag_1有着高度相关性,有些代码里两者都保留,实际跑出来共线性并不严重,树模型无所谓,但如果是线性回归或 ARIMA 类模型,建议只保留其一。

2.3 数据集切分必须“按时间切”,随机切分是负荷预测里最隐蔽的翻车点

很多人在负荷预测上第一次翻车,就是栽在train_test_split的默认参数上。负荷数据是时间序列,相邻样本高度相关,随机切分会让训练集里混入“未来”的数据,模型相当于提前偷看了答案。跨时间预测时误差立刻放大,表现好的离谱的验证集指标全是假象。

正确的做法是严格按时间顺序切分,比如用前 80% 的数据训练、后 20% 验证。代码包里有现成的切分函数,核心逻辑是取时间戳的某个分位点作为切分边界:

def temporal_split(df, test_ratio=0.2): df = df.sort_values('ds').reset_index(drop=True) split_idx = int(len(df) * (1 - test_ratio)) train_df = df.iloc[:split_idx].copy() test_df = df.iloc[split_idx:].copy() return train_df, test_df

这里有一个容易忽略的细节:如果数据里包含多个不同片区的负荷,切分前必须按ds排序,否则同一时刻的数据会被拆到两个集合里,造成信息泄漏。另外,如果是预测“未来一周”的场景,不能只切一个点,而是要用滚动窗口切分:每次用前 30 天训练、预测下一天,然后窗口前移。代码包里默认的是单次切分,适合离线场景;如果要模拟真实在线预测,建议自己改造为滚动窗口。

3. 多模型对比:从线性回归到 XGBoost,选模型的思路比调参更重要

3.1 设置 Baseline 的正确姿势:为什么先跑线性回归而不是直接上树模型

拿到特征矩阵后,第一件该做的事不是调参,而是跑一个最简单的线性回归作为 baseline。原因有两个:一是线性回归的可解释性强,能直接看到每个特征对负荷的影响方向和量级;二是它的误差水平给了后续复杂模型一个“及格线”——如果 XGBoost 还不如线性回归,那一定是特征工程出了问题,而不是模型不够强。

代码包里对线性回归的用法比较克制,但保留了完整的评估逻辑:计算 MAE、RMSE、MAPE 三个指标,并画出真实值与预测值的对比曲线。这里面 MAPE 是电力负荷预测最常用的业务指标,因为调度部门关心的是“误差百分比”而不是绝对误差。需要注意,MAPE 在负荷值很低的时段(比如凌晨三四点)会被分母放大,如果出现异常大的 MAPE 值,先检查是不是低负荷时段造成的。

from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error # 特征列:按代码包内的 feature_cols 指定 feature_cols = ['hour', 'weekday', 'is_weekend', 'lag_1', 'lag_2', 'lag_24', 'roll_mean_6', 'roll_max_24', 'temp'] X_train = train_df[feature_cols].values y_train = train_df['y'].values X_test = test_df[feature_cols].values y_test = test_df['y'].values lr = LinearRegression() lr.fit(X_train, y_train) y_pred_lr = lr.predict(X_test) mae = mean_absolute_error(y_test, y_pred_lr) rmse = mean_squared_error(y_test, y_pred_lr, squared=False) mape = np.mean(np.abs((y_test - y_pred_lr) / y_test)) * 100 print(f"LinearRegression baseline -> MAE: {mae:.2f}, RMSE: {rmse:.2f}, MAPE: {mape:.2f}%")

y_test里如果存在零值或接近零的负荷,分母趋近于零会导致 MAPE 溢出。遇到这种情况,有两种常见处理:一是用np.maximum(y_test, 1)把下限兜住;二是直接用 MAE 作为主要对比指标。我自己的习惯是两个指标都看,MAE 衡量绝对偏差、MAPE 衡量相对偏差,如果两者趋势一致,说明模型整体稳定;如果 MAE 很小但 MAPE 很大,说明误差集中在低负荷区,这时要检查低负荷时段的特征表达是否缺失。

3.2 树模型与梯度提升的实战参数:XGBoost、LightGBM 怎么在这份数据上收敛

树模型在负荷预测里的优势是能吃下大量离散特征和交互项,不用手工构造太细的交叉特征。代码包里同时给出了 XGBoost 和 LightGBM 的配置,两者在大多数负荷数据集上精度相近,但 LightGBM 的训练速度快得多,适合需要频繁重训的滚动预测场景。

这里最大的坑在时间类特征的编码方式上。hour如果直接用 0-23 的整数喂给树模型,模型会学到“23 和 0 差异很大”,但实际上 23 点和凌晨 0 点都是低负荷时段,差异很小。更合理的方式是用np.sin(2 * np.pi * hour / 24)和np.cos(2 * np.pi * hour / 24)做周期编码,把小时的连续性表达出来。代码包里没有内置这个转换,但强烈建议自己加上。

import lightgbm as lgb # 周期编码小时和星期 for df_tmp in [train_df, test_df]: df_tmp['hour_sin'] = np.sin(2 * np.pi * df_tmp['hour'] / 24) df_tmp['hour_cos'] = np.cos(2 * np.pi * df_tmp['hour'] / 24) df_tmp['week_sin'] = np.sin(2 * np.pi * df_tmp['weekday'] / 7) df_tmp['week_cos'] = np.cos(2 * np.pi * df_tmp['weekday'] / 7) feature_cols_ext = feature_cols + ['hour_sin', 'hour_cos', 'week_sin', 'week_cos'] lgb_params = { 'objective': 'regression', 'metric': 'mae', 'learning_rate': 0.05, 'num_leaves': 31, 'max_depth': 6, 'min_data_in_leaf': 30, 'feature_fraction': 0.85, 'bagging_fraction': 0.85, 'bagging_freq': 1, 'verbose': -1, 'n_estimators': 500, 'early_stopping_rounds': 50 } lgb_model = lgb.LGBMRegressor(**lgb_params) lgb_model.fit( train_df[feature_cols_ext], train_df['y'], eval_set=[(test_df[feature_cols_ext], test_df['y'])], callbacks=[lgb.early_stopping(50)] )

num_leaves=31对应深度约 5 到 6 层的树,再大容易过拟合。min_data_in_leaf=30是防止树在数据稀疏的分支上做出极端预测。feature_fraction和bagging_fraction都是 0.85,相当于每棵树只用 85% 的特征和 85% 的样本,这是梯度提升里最有效的正则化手段。early_stopping_rounds=50表示验证集指标连续 50 轮不提升就提前终止,可以省掉大量无效训练时间。

另一个值得注意的参数是learning_rate=0.05。如果追求极致精度,可以先按 0.05 粗跑,然后降到 0.01 并增加n_estimators到 1500-2000。但学习率越低、树越多,过拟合验证集的风险也越大,最终要以验证集 MAE 为准,而不是训练集。

3.3 模型评估不能只看一个指标:误差分布与峰值时段单独拆开看

负荷预测的业务痛点通常不在平均误差,而在“尖峰时刻”。夏季空调负荷集中爆发、工作日上午 9 点的上班潮,这些时段的预测偏差直接影响调度决策。所以模型对比要把评估维度拆开:整体误差看 MAE RMSE,业务满意度看分时段误差。

代码包的报告里给出了按小时分组的误差热力图,这一步很值得保留。具体做法是:把测试集的预测残差按hour分组,统计每个小时的 MAE,然后画成柱状图。你大概率会看到某个固定时段误差特别大,比如傍晚 18-20 点,这时段的负荷曲线斜率最大,模型反应跟不上。解决办法不是盲目调参,而是单独加一个is_rush_hour特征,或者把该时段的数据在训练集中加重采样权重。

import numpy as np results = test_df[['ds', 'y']].copy() results['y_pred'] = y_pred_lgb results['hour'] = results['ds'].dt.hour results['error'] = np.abs(results['y'] - results['y_pred']) # 按小时统计平均绝对误差 hourly_mae = results.groupby('hour')['error'].mean() # 找出误差最大的三个小时段 worst_hours = hourly_mae.sort_values(ascending=False).head(3) print("误差最大的时段:") print(worst_hours)

如果误差集中在高峰时段,还有一种常见修法:对训练样本按负荷值加权,高负荷时段的样本权重提高到 1.5-2 倍,让模型把容量花在“难样本”上。LightGBM 可以通过sample_weight参数传入,XGBoost 对应sample_weight也有同样的接口。这个方法的效果通常比直接调num_leaves或learning_rate更明显。

4. 避坑指南:数据泄漏、缺失值、异常尖峰,负荷预测的四个典型翻车现场

4.1 滞后特征带入验证集导致“未来穿越”

现象:训练集 MAPE 只有 2%,验证集 MAPE 却飙到 15% 以上,而且验证集误差曲线和真实负荷曲线存在一个固定的相位偏移。

原因:构建lag_1、lag_24时是在全量数据上做的shift(),切分训练集和验证集之后,验证集第一行的lag_1实际上来自训练集最后一行,相当于模型已经看到了未来信息。

解决:先切分数据、再构造滞后特征,或者在切分后对训练集和验证集分别执行shift()。代码包里默认是先构造特征再切分,但在自定义场景里务必把顺序反过来,或者至少在评估时用“纯滚动预测”的方式重新验证一遍。

4.2 节假日负荷形态突变导致预测系统性偏高

现象:工作日模型在节假日当天的预测值明显高于真实值,误差方向一致。

原因:节假日负荷曲线和工作日完全不同,早晨没有通勤高峰、白天商业用电量大减,模型只在训练集里见过工作日模式,遇到节假日自然外推失败。

解决:把节假日单独标记成一个分类特征,并在训练集中确保节假日样本不掉队。处理方法上,一是用is_holiday二元特征,二是把“节假日前一天”“节假日后一天”也编码进去,因为这两天的负荷形态也有过渡特征。如果节假日样本太少,可以考虑用去年的同期数据做插值,或借用相邻周末的负荷形态做填充。

4.3 温度传感器缺失导致特征列大量为空

现象:训练集特征矩阵里temp列出现大段 NaN,模型直接报错,或者某些库自动填充 0 后预测值整体偏低。

原因:温度数据通常来自气象站,而气象站并非每个小时都有记录,遇到设备维护或传输中断,一段时间的数据就是空的。直接把 NaN 扔给模型是最差做法。

解决:先判断缺失比例。缺失少于 5% 用ffill()或线性插值;缺失超过 20% 就不要用它做特征,改为把温度分成冷/暖/热三档离散特征,容忍一定的信息损失。代码包里的中间数据已经处理过这个环节,但如果替换成自己的数据,需要重新处理。

4.4 极端天气导致尖峰负荷成为孤立异常点

现象:某个下午 3 点负荷突然冲到全年最高,模型完全没跟上,单点误差是平时的 5 倍以上。

原因:极端高温或寒潮导致空调/取暖负荷集中释放,这种尖峰在历史样本中出现的次数极少,模型无法从有限样本中习得规律。

解决:一是用“温度阈值 + 滞后温度”构造交互特征,比如temp > 35时标记为极端高温日;二是对尖峰样本单独建模,用一个分类器先预测“今天是否会出现尖峰”,再用回归器预测具体负荷值;三是用分位数损失函数训练模型,比如quantile=0.9,让模型更关注高负荷尾部。代码包没有内置这一层,但在生产系统中这一步往往是提升峰值精度的核心。

5. 进阶:把预测结果变成可解释的业务结论——误差溯源与可视化闭环

代码包里附带的分析报告,如果你只看文字部分,可能觉得只是常规的“数据概况 + 模型效果说明”。但真正有价值的是报告里那几张图和对应的分析思路,它们把预测问题闭环到了业务侧。

首先是“特征重要性排序图”。LightGBM 训练完成后,feature_importances_会告诉我们模型最依赖哪些特征。在实际负荷场景中,排第一的几乎总是lag_24,其次是hour,然后是temp。这个排序本身就是一个结论:负荷预测的本质是“延续昨天的模式,再按时刻做修正”。如果lag_24的特征重要性低于hour,说明数据可能存在异常,或者时间戳对齐出了问题。

其次是“预测残差与时间的关系图”。把残差按时间绘制成散点图,如果残差在某个时间区间内持续为正或持续为负,说明模型在该时段存在系统性偏差。比如下午 14 点到 16 点持续偏低,可能是因为模型没有捕捉到下午的气温累积效应。这种情况下,加入“过去 3 小时平均温度”作为特征,通常能缓解。

最后是“分时段 MAE 柱状图与业务阈值的对比”。调度侧通常有一个可接受的误差范围,比如峰时不超过 5%、谷时不超过 8%。把模型的分时段误差和这个阈值放在同一张图上,一眼就能看出哪些时段需要重点优化。这个“阈值对比”思维,是代码报告里最有借鉴价值的部分——它把机器学习模型的输出,翻译成了调度工程师听得懂的语言。

import matplotlib.pyplot as plt # 特征重要性可视化 feat_imp = pd.Series(lgb_model.feature_importances_, index=feature_cols_ext).sort_values(ascending=True) feat_imp.plot.barh(figsize=(10, 6)) plt.xlabel('Feature Importance') plt.tight_layout() plt.savefig('feature_importance.png', dpi=150)

我自己的经验是:模型误差到 5% 以内之后,再花时间去调超参数收益很小,但花时间分析“哪个时段误差大、为什么大”收益很大。负荷预测的项目交付通常不只交付模型,还要交付“预测结果为什么可信”——这三张图的组合,就是回答这个问题的完整叙事。

从那以后,我每接到一个负荷预测的数据集,都会先花半小时把这三件事跑完:按时间切分确认无泄漏、看特征重要性确定主导因素、按小时分桶定位误差热点。强制走完这一遍,再决定要不要调参。希望这份代码包和分析报告能帮你把同样的流程快速跑通,少走几个我当年踩过的坑。

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

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

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

立即咨询