简介:精准的电力负荷预测能够优化发电计划、减少能源浪费,这组资料聚焦基于机器学习、特别是反向传播神经网络的负荷预测方法研究,面向电力系统相关专业学生、研究人员及对智能预测感兴趣的开发者,既可支撑课程设计,也可作为科研入门的参考。压缩包内共计334个文件,其中329个为Excel格式数据文件,包含历史负荷、天气、节假日等影响因子,另有3个Matlab脚本负责数据读取、模型训练和结果可视化,1个Markdown说明文档梳理研究思路,包体仅5.32MB,当前已有2460人学习下载,热度较高。学习这份资料,可系统理解从原始数据清洗、特征选择、网络拓扑构建到梯度下降反向传播的完整流程。真实可跑的数据与脚本,让读者能直接上手复现关键实验,并通过替换或扩充数据进一步验证模型,为后续改进预测精度或探索深度学习打下基础。
1. 电力负荷预测:为什么机器学习方案值得认真做
电力负荷预测是电网调度、需求响应、现货交易和配电规划共同依赖的底层能力,预测偏差直接决定经济性和安全性。做负荷预测时,很多团队上来就套深度学习模型,结果发现效果并不比简单模型好,原因往往是数据边界没理清、特征构造粗糙,甚至训练集和测试集泄漏了未来信息。机器学习的价值在于它能把温度、日历、历史负荷这些信息揉进同一个模型里,自动找到那些人工经验很难总结的规律。我这里分享的是一条可以直接落地的技术路线,覆盖任务定义、数据清洗、特征工程、模型选型、滚动预测和上线避坑,适合正在做能源数据分析或负荷预测项目的开发者,也适合准备把预测能力接入调度系统的团队。目标是让你按这份笔记走一遍,能稳定跑出一个可用的预测模型。
2. 数据准备与特征工程:先把预测的天花板抬起来
数据质量决定了模型效果的上限,模型算法只是尽量逼近这个上限。负荷预测尤其如此,因为负荷序列有强周期、强温度相关性和明显的日历效应。这一章从任务定义开始,把数据清洗和特征构造的每一步都拆开说明,这些工作做完,后面的模型选型才有意义。
2.1 定义预测任务:粒度、步长与目标变量
做负荷预测之前,有三个问题必须想清楚:预测粒度、预测步长、目标变量。它们直接影响后面所有特征和模型的选择。
第一个问题是粒度。负荷数据常见的有15分钟级、小时级、日级。电网调度通常需要小时级或15分钟级的短期预测,用于机组组合和备用安排;设备检修和规划类业务只需要日级或周级预测。粒度不同,可用的特征完全不同,15分钟级数据能利用最近一个时点的负荷特征,小时级数据则要更多依赖日周期和温度变化。
第二个问题是步长。要预测未来1小时,还是未来24小时?单步预测只要搭建一个回归模型就能跑,多步预测则要面对误差累积或者为每个步长单独建模型。我一般会先做一个预测未来1小时的单步模型,把特征、验证流程跑通,再扩展成24步。这样容易定位问题出在模型还是出在多步策略上。
第三个问题是目标变量。大多数场景直接预测原始负荷,单位是MW或kW。有些团队喜欢对负荷做差分,预测增量的好处是能去掉趋势,但还原回原始负荷时会累积误差,反而不如直接预测稳定。如果数据有明显的长期增长趋势,更推荐把原始负荷做归一化或去趋势后再预测,但归一化要在训练集上拟合,不能把验证集数据也放进来算。
另外,时间戳必须统一。不同系统导出的数据,有的带时区偏移,有的用本地时间,如果不处理,后面构造“前一天同一时刻”这类特征时会错位几个小时,这种问题很难排查。我通常先把时间列解析成标准UTC时间,再转回业务时区,保证所有特征在同一套时间体系下。
2.2 数据清洗:缺失值、异常值与节假日冲击
电力负荷数据通常来自SCADA或电表采集系统,质量参差不齐。最常见的问题有三类:缺失、突变、节假日。
缺失值处理要看规模。单点缺失可以直接线性插值;连续一段缺失,尤其是超过6个小时的,就不能简单插值,否则会抹平真实的负荷形态。我会优先用历史相似日填充,相似日的选择逻辑是:相同星期几、相同时段、温度相近。注意不要用缺失点之后的数据回填,那会引入未来的信息,在离线验证时看起来准确,上线后完全没有对应数据可用。
异常值处理要区分设备故障和真实冲击。设备故障通常表现为单点跳变或连续多个点变成0;真实冲击比如大型工厂集中启停,负荷曲线会出现短时尖峰,那是模型应该学习的规律。我的习惯是先用可视化把所有时序曲线看一遍,再用统计阈值兜底,比如计算相邻时刻负荷差分的标准差,超过4倍标准差就标记出来,逐个判断。如果是采集故障,用周围正常点均值替代;如果是真实冲击,保留原值。
节假日冲击是最容易被忽视的问题。电网负荷在春节、国庆期间会明显下降,而且不同节假日形态差异很大。最简单可靠的做法是维护一份节假日列表,为每个样本打上是否属于节假日;更细一点的做法是加入“节假日第几天”“假期前第几天”这类特征,比如春节这种长假期,节前一两天的负荷下降和节中的低谷是不同的模式,只用0/1标签无法区分。
import pandas as pd import numpy as np df = pd.read_csv("load_data.csv", parse_dates=["timestamp"]) df = df.set_index("timestamp").sort_index() # 单点缺失用线性插值,连续缺失超过6个点先置空 df["load"] = df["load"].interpolate(method="linear", limit=6) # 异常值诊断:对相邻时刻差分计算 z-score diff = df["load"].diff() z = (diff - diff.median()) / diff.std() df.loc[abs(z) > 4, "load"] = np.nan df["load"] = df["load"].interpolate(method="time")逻辑说明:第一处interpolate(method="linear", limit=6)只对连续不超过6个缺失点的片段做线性插值,更长的缺失段保留NaN,方便后面用相似日填充。第二处计算相邻时刻负荷差分,再标准化成z-score,超过4倍标准差点先置空,最后用时间插值补齐。阈值4不是固定值,如果数据本身波动很大,可以适当放宽。
参数说明:limit的取值要结合采样间隔来看,小时级数据6个点是6小时,建筑负荷在这个尺度内变化相对平缓,可以接受;15分钟级数据连续缺6个点就不建议直接插值。z-score阈值太小会把正常尖峰误判成异常,我通常先试4,再结合曲线图确认误判比例,如果误判多就放宽到5。
2.3 特征构造:让模型看到日历与温度
特征工程决定模型能学到什么。负荷和人的作息高度相关,模型必须看到日历信息,也要看到温度、湿度这类气象信息以及历史负荷本身。
日历特征至少包括小时、星期几、月份、是否工作日、是否节假日。小时和星期几可以直接用数值,但树模型和线性模型的处理方式不同。树模型对数值编码不敏感,给0到23的数字也能正确分裂;线性模型则很容易被“23点和0点距离很接近”这种假设误导,所以我给线性模型做小时周期编码,用sin和cos两个分量表达小时在一天里的循环位置。
温度特征是负荷预测里信息量最大的外部特征,夏季降温和冬季采暖都会让负荷与温度呈现很强的相关关系,而且是分段线性:温度低于采暖拐点时,越低负荷越高;温度高于制冷拐点时,越高负荷越高。如果只用线性模型,可以人为加入双向温差特征,比如取温度超过18度的部分和低于18度的部分分别作为两个特征;如果用树模型,温度特征直接给进去,模型会自动找到拐点。
历史负荷特征是短期预测里最有效的特征。我会构造前1小时负荷、前一天同一时刻负荷、前一周同一时刻负荷,以及最近3小时和最近24小时的移动平均值。对小时级负荷预测来说,前1小时的实际负荷携带大量信息,模型靠这类滞后特征就能把误差压得很低。但要记住,真正上线时预测未来多个小时,前1小时负荷是拿不到的,需要用递归方式替换成预测值,这一点会在滚动预测部分再展开。
气象数据的接入要根据业务范围来判断。如果有区域气象站的温度、湿度、风速数据,可以把它们作为特征;如果没有,用公开气象数据也能凑,但要注意网格预报值和实测值之间的偏差,比如某地区夏季温度预报普遍偏高1到2度,如果不做偏差校准,负荷预测也会系统性偏高。
特征量要控制住。很多滞后特征之间高度相关,比如前1小时负荷和前3小时均值相关系数可能超过0.9,树模型还能处理,线性模型则会出现明显的系数不稳定。我一般先计算相关性矩阵,把相关系数超过0.95的特征删掉一组,再通过特征重要性验证。
def build_features(df, holiday_dates): df = df.copy() df["hour"] = df.index.hour df["weekday"] = df.index.dayofweek df["month"] = df.index.month df["is_holiday"] = df.index.isin(holiday_dates) # 小时周期编码,避免线性模型觉得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["lag_1h"] = df["load"].shift(1) df["lag_24h"] = df["load"].shift(24) df["lag_168h"] = df["load"].shift(168) # 滚动平均值 df["mean_3h"] = df["load"].rolling(3).mean() df["mean_24h"] = df["load"].rolling(24).mean() return df逻辑说明:shift(1)是取上一小时负荷,shift(24)是前一天同一时刻,shift(168)是前一周同一天同一时刻。rolling(3).mean()是最近3小时均值,能平滑短时波动;rolling(24).mean()是最近24小时均值,代表最近一段时间的整体水平。周期编码里2 * np.pi * hour / 24,把0到23映射到0到2π之间。
参数说明:滞后窗口的选择可以先用自相关图确认,小时级负荷通常在24小时和168小时出现明显峰值,所以滞后24和滞后168往往是树模型里重要性最高的特征。移动平均窗口不宜太短,否则会跟前1小时滞后特征高度重迭;也不宜太长,超过48小时会把太久远的信息混进来。如果用线性模型,还建议对标准化后的特征做方差膨胀因子检查,避免共线性把系数推断带偏。
3. 模型选型与建模:从线性基线与树模型起步
模型选型不需要一开始就追求复杂。负荷预测是典型的表格回归问题,特征以时间、温度、历史负荷为主,机器学习的常用方案里,线性回归和梯度提升树是最稳的两个起点。这一章讲清楚怎么用基线模型建立参照,再切换树模型提升精度,以及怎么选择评估指标。
3.1 线性回归与岭回归:基线必须先做
基线模型的价值是给后续模型一个参照系。如果岭回归在验证集上的误差已经不错,说明当前特征和负荷的关系接近线性,再去调树模型的收益可能很小;如果岭回归误差明显偏大,说明存在非线性特征交互,树模型才有发挥空间。很多人跳过高漂回归直接上XGBoost,结果模型跑完都不知道提升到底来自模型结构还是来自特征工程,这样很难做工程决策。
岭回归是线性模型里最实用的选择,它通过L2正则化控制系数向零收缩,避免高维特征下的共线性和过拟合。唯一需要调的主参数是alpha,它控制正则化强度。alpha太小,模型接近普通最小二乘,方差大;alpha太大,所有系数都被压向零,偏差增加。我通常在0.1到100之间取对数网格搜索。
使用线性模型时,特征标准化是必需的。特征里既有“小时”这种量级小于100,也有“负荷”这种上千的量级,如果不标准化,正则化会按特征尺度不均地约束系数。注意标准化要用训练集的均值和方差,验证集和测试集上不能重新计算,否则就泄漏了分布信息。
from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline pipe = Pipeline([ ("scaler", StandardScaler()), ("model", Ridge(alpha=10.0)) ]) pipe.fit(X_train, y_train) y_pred = pipe.predict(X_valid)逻辑说明:Pipeline把标准化和岭回归串在一起,确保验证和推理都套用同一套标准化参数。alpha=10.0是初始值,真实场景要在验证集上通过网格搜索确定。标准化器会在整体数据上计算均值和方差,要求传入的X必须只包含建模用的数值特征。
参数说明:如果特征数量只有几十个,alpha不用设得太大,否则模型的偏差会明显,尤其在温度影响强烈的季节。我用的搜索范围是[0.1, 1, 10, 50, 100],选验证集MAE最小的值。线性回归的误差出了异常点会变差,可以先看残差图确认,不要急着换成非线性模型。
3.2 树模型:LightGBM 与 XGBoost 的优势和参数
梯度提升树在电力负荷预测里是目前最稳妥的机器学习选择。它对特征量纲不敏感,不需要标准化,能自动处理非线性关系和特征交互,而且对缺失值有一定鲁棒性。在我接触的项目里,LightGBM和XGBoost最终精度差别不大,LightGBM训练更快,XGBoost在小样本上更稳,各有适用场景。
树模型和线性模型的关键差异在于它能处理复杂的局部规律。比如温度对负荷的影响在拐点前后斜率不同,树模型可以通过多次分裂把温度区间切成多段,每段学习不同斜率;而线性模型必须手动构造拐点特征。再比如某个工厂的检修让某段时期负荷形态异常,树模型能把这段时期单独分到一个叶子节点,避免影响其他时段。
参数中最重要的四个是learning_rate、max_depth、n_estimators、min_child_samples。learning_rate控制每一步更新的步长,越小越稳定但需要的迭代次数越多;max_depth控制树的深度,太深容易把噪声当成规律;n_estimators是树的棵数,通常和learning_rate配合调整;min_child_samples限制叶子节点最少样本数,防止极端值落在样本量过小的叶子节点上。
LightGBM里还有一个容易忽略的细节:它直方图算法对连续特征的切分和XGBoost的预排序不同,在缺失值处理上也有差异,但默认参数下两者差异不大,不需要刻意调整。如果训练数据超过百万量级,LightGBM的优势会体现得更明显。
import lightgbm as lgb model = lgb.LGBMRegressor( objective="regression", learning_rate=0.05, max_depth=6, n_estimators=600, subsample=0.8, colsample_bytree=0.8, min_child_samples=20, random_state=42 ) model.fit( X_train, y_train, eval_set=[(X_valid, y_valid)], callbacks=[lgb.early_stopping(50)] )逻辑说明:subsample=0.8是每次迭代随机抽取80%样本训练,colsample_bytree=0.8是每次建树只用80%的特征,这两个子采样参数能显著降低过拟合并加速训练。early_stopping(50)在验证集误差连续50轮不下降时停止训练,返回最优迭代轮数。训练结束后可以通过model.best_iteration_查看实际用得上的树的数量。
参数说明:max_depth=6对小时级负荷数据通常足够,树太深会把节假日的个别天当作长期规律来学。min_child_samples=20是经验起点,如果验证误差曲线早期就开始反弹,优先调高到40,而不是降低learning_rate。如果数据量只有几千行,n_estimators可以设到300以内,配合早停基本够用。
3.3 评估指标:别只盯 RMSE,峰值时段才是痛点
负荷预测的评估如果只看RMSE,很可能会掩盖真实痛点。RMSE对异常点敏感,适合衡量大偏差,但它会把凌晨低谷的小误差和傍晚峰值的大误差混合在一起;MAE更稳健,但无法突出峰值时段的系统性偏差;MAPE在负荷接近零时会虚高,凌晨时段的绝对误差不大,百分比却能到20%以上。
我常用的做法是把验证集误差按小时分层统计,分别看早高峰、晚高峰和白天的MAE。这样的原因很简单:负荷预测的价值大多体现在峰值时段,峰值时段如果系统性偏低,机组备用不足的风险就会上升。评估时我会区分“全天MAE”和“高峰时段MAE”,目标是让两个指标同时下降,而不是只优化其中一个。
MAPE计算还要避免除零。当真实负荷很低时,即使绝对误差只有0.5 MW,MAPE也可能超过20%,这个数字会污染整体报告。我习惯在MAPE计算里给分母加一个下界,并且额外统计负荷高于第25百分位时的MAPE,这样得到的百分比误差更稳定。
import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate(y_true, y_pred, hour=None): mae = mean_absolute_error(y_true, y_pred) rmse = np.sqrt(mean_squared_error(y_true, y_pred)) safe_true = np.maximum(y_true, 1.0) mape = np.mean(np.abs((y_true - y_pred) / safe_true)) * 100 p95 = np.percentile(np.abs(y_true - y_pred), 95) result = {"MAE": mae, "RMSE": rmse, "MAPE": mape, "P95": p95} if hour is not None: mask = hour == 8 result["MAE_peak"] = mean_absolute_error(y_true[mask], y_pred[mask]) return result逻辑说明:np.maximum(y_true, 1.0)是给MAPE的分母加了下界,避免负荷接近0时产生夸张的百分比。p95表示95%的绝对值误差都小于这个值,比RMSE更直接地反映尾部风险。hour参数用于指定关注时段,比如早上8点,可自行换成晚高峰。
参数说明:评估前要把负荷还原到原始单位。如果训练时对目标做了归一化,预测值也要还原后再算指标,否则MAE和RMSE失去物理意义。对比不同模型时,要固定同一时间段和同一指标口径,不要A模型用MAPE、B模型用MAE,这样没法定位差距。
4. 从离线模型到可上线预测:验证划分、滚动窗口与调优
模型训练只是第一步,真正落地负荷预测,还要处理验证划分、预测窗口和模型更新这几个工程关键点。时间序列问题比普通回归问题更容易踩到“指标好看、上线翻车”的坑,所以要提前把验证框架搭对。
4.1 时间序列验证:随机打乱在预测里是大忌
普通回归任务随机划分训练集和测试集没有问题,但时间序列数据如果随机打乱,会让模型看到测试集所在时间段的信息,考察的不是外推能力。更严重的是,负荷数据有季节周期,如果训练集和测试集来自同一时期的随机样本,模型实际上学到了数据分布的整体形态,预测的难度被严重低估。
正确的做法是按时间顺序划分。最简单的方案是前70%训练、后30%验证,但这样只能验证一段连续时间,如果那段验证期恰好处于平季,误差会好于全年平均。更可靠的方案是walk-forward验证,让窗口不断向前滚动,把每个窗口的验证误差累积起来取平均。
sklearn的TimeSeriesSplit可以直接使用,它保证每次划分的训练集都在验证集之前,而且验证集之间不重叠。使用时还要考虑是否包含时间特征带来的边界效应,比如在切分后训练和验证集各自独立构造滞后特征,不要跨切分线取滞后值。
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_idx, valid_idx in tscv.split(X): X_train, X_valid = X.iloc[train_idx], X.iloc[valid_idx] y_train, y_valid = y.iloc[train_idx], y.iloc[valid_idx] model.fit(X_train, y_train) y_pred = model.predict(X_valid) print(mean_absolute_error(y_valid, y_pred))逻辑说明:TimeSeriesSplit(n_splits=5)会把数据切分成5段,每段循环里训练集位于验证集之前,验证集互不重叠。这样得到的5个MAE取平均,比单次划分更能反映模型在不同时间段的稳定性。
参数说明:n_splits的选择取决于历史长度。两年以上的小时级数据可以设5到6折;如果只有几个月数据,3到4折更安全,因为训练集太短时模型学不到季节规律。还可以考虑每次训练集的起点如何处理,比如固定起点不更新,或者用滑动窗口只保留最近365天。
4.2 滚动预测与多步预测:警惕误差累积
单步预测是基础,但业务上经常需要未来24小时甚至更长的曲线。多步预测的常用方案有三种:递归预测、直接预测、多输出预测。
递归预测把上一时刻的预测值作为下一时刻的滞后特征,循环预测24步。它的优点是只训练一个模型,缺点是误差会随着步长累积。比如第一步误差3%,第二步在此基础上再叠加3%,到了第24步曲线可能严重偏移,尤其在温度突变时。
直接预测是为每个预测步长训练一个独立模型,预测小时0用一个模型,预测小时1用另一个模型。每个模型只用真实历史特征,避免了误差在时间步之间传递。缺点是训练成本高,24步就要训练24个模型。实际中不需要为每个步长都建一个,可以把24小时分成几个区间,比如预测1到6小时用递归,7到24小时用直接模型。
多输出预测是利用回归器的multi_output能力,同时输出未来多个时刻的预测值。对树模型来说,每个输出列是独立建模的,本质上是多个模型并行的直接预测,不会引入误差累积。但输出之间存在相关性,模型无法利用它们之间的约束。
离线回测滚动预测时要特别注意特征边界。预测起点为t,那么 t 时刻及之前的历史负荷都是真实值,t+1及之后如果还用滞后特征,就要用预测值替换。如果在回测时不小心用了后续真实值,误差会被严重低估。
def backtest(model, X, y, predict_start, horizon=24): model.fit(X.loc[:predict_start], y.loc[:predict_start]) y_pred = [] current_pred = None for step in range(horizon): # 预测步长大于1时,滞后特征需要滚动替换 if step == 0: X_step = X.loc[predict_start:].iloc[[0]] else: X_step = X.loc[predict_start + step].copy() X_step["lag_1h"] = current_pred current_pred = model.predict(X_step)[0] y_pred.append(current_pred) return np.array(y_pred)逻辑说明:这个回测循环只是演示了滚动预测的思路,第0步使用预测起点的真实历史特征,第1步开始把上一步的current_pred填入lag_1h特征。注意X里其他时间特征和温度特征仍使用真实遍历值,这在线下回测中是允许的,因为预测时这些外部特征来自预报。
参数说明:递归步数太长时,建议不要超过24步,超过的话误差累积会很明显。如果必须预测更长时间,可以分成两段:前24小时用递归模型,后续用日均值或趋势外推。horizon=24是针对小时级数据的常见选择,如果预测15分钟级,建议设为96步,但每一步对应的滞后特征要从小时级粒度换算。
4.3 超参数调优与特征筛选:让模型不容易过拟合
树模型的参数多,一个一个手动试很浪费时间。我习惯分两步调参:先用较低learning_rate固定迭代次数,然后调整树的深度和叶子节点最小样本数,最后再调子采样比例。这种策略比一次性网格搜索更高效,也更不容易被随机噪声干扰。
使用sklearn的RandomizedSearchCV时,要注意交叉验证必须使用时间序列切分方式,不能默认用随机KFold。否则调出来的参数在时序外推时依然会偏乐观。搜索过程本身也要控制时间,n_iter建议在30以内,每次训练把n_estimators降低到200,选完参数再加回来。
特征筛选方面,树模型的特征重要性可以作为线索。LightGBM的特征重要性有split和gain两种口径,split表示特征被选为分裂点的次数,gain表示特征带来的平均增益。我通常看gain,因为它更直接反映对损失的贡献。把gain低的特征删掉后,模型精度往往不变,训练速度反而更快。
from sklearn.model_selection import RandomizedSearchCV param_dist = { "max_depth": [4, 6, 8, 10], "min_child_samples": [10, 20, 40], "subsample": [0.7, 0.8, 0.9], "colsample_bytree": [0.7, 0.8, 0.9] } search = RandomizedSearchCV( lgb.LGBMRegressor(learning_rate=0.05, n_estimators=200), param_distributions=param_dist, n_iter=30, cv=tscv, scoring="neg_mean_absolute_error", random_state=42 ) search.fit(X, y)逻辑说明:RandomizedSearchCV从参数分布中随机抽取30组组合,用前面定义的TimeSeriesSplit做交叉验证,以负MAE作为评分。搜索完成后通过search.best_params_查看最优参数,再用这个参数配合更大的n_estimators重新训练。
参数说明:n_iter=30是速度与覆盖范围的折中。如果数据集大,每组训练耗时长,可以减到20;如果数据集较小,可以增加到50。搜索时先固定learning_rate=0.05,选完参数后再根据需要调整学习率。评分用负MAE是因为sklearn的搜索默认取最大值,MAE越小越好,所以要取负。
5. 负荷预测避坑指南:五个高频翻车点
这一章记录的是实际项目中反复出现的问题。每条按“现象、原因、解决”展开,我自己至少碰到过其中四条,每一条都值得在上线前排查。
5.1 短期预测误差不大,但早晚峰值时段系统性偏低
现象:整体MAE看着不错,把误差按小时统计后发现,早上7点到9点、晚上18点到22点,预测负荷普遍比实际值低1到2个百分点,曲线波峰被削平。
原因:训练样本里峰值时段只占一天的少数小时,模型为了最小化平均损失,会更倾向把平滑曲线拟合好,而不是去追逐峰值。再加上峰值时刻通常叠加了更强的温度和生产活动因素,特征交互不够时,模型学不到峰值抬升的机制。
解决:最直接的做法是给峰值时段的样本在损失函数里加权,比如把9点和18到20点的样本权重设为1.5到2。也可以单独训练一个峰值时段模型,只在峰值时段做预测,主模型负责其余时段。目标函数也可以换成按负荷水平的相对误差,让峰值时段权重自然提高。实测中峰值时段MAE能下降10%到15%。
5.2 节假日预测崩溃,误差放大好几倍
现象:普通日预测稳定,一旦到春节、国庆这类长假,预测误差变成平时的3倍以上,有的树模型在长假首日直接把负荷预测低了30%。
原因:节假日样本在训练集里占比不到2%,模型几乎没见过这种负荷形态。春节长假期间工厂停工会让工业负荷消失,商业和居民负荷成为主导,与普通日负荷结构完全不同,模型只能按普通日的规律外推。
解决:先把节假日标签做成特征,并提高节假日样本的训练权重。更进一步是把节假日的相对日期做成特征,比如“距春节前5天”“春节第3天”“假期结束后第2天”,让模型学习长假前后的负荷变化过程。如果历史数据超过三年,可以用过去三年的同期数据单独微调一个节假日模型,避免污染普通日模型。
5.3 离线指标虚高,上线后误差突然变大
现象:离线回测MAE只有2%,部署上线后MAE变成6%,而且偏差集中在预测起点附近。
原因:特征构造时泄漏了未来信息。最常见的是用当天日均温度预测当天逐时负荷,或者在构建滞后特征时,测试阶段的lag_1h竟然来自真实未来值。离线验证时这些信息是存在且完整的,所以指标好看;上线时预测起点之后的真实负荷根本拿不到,模型只能依赖不完整的特征。
解决:严格检查特征时间戳。所有特征必须早于或等于预测起点,温度特征要使用预测时刻之前发布的气象预报,不能用事后观测值。构造代码时分出训练和预测两个分支,预测分支以预测起点为边界,只允许使用起点时刻已经存在的数据。写完特征函数后,可以用一个简单测试:在特征里加入“当天真实负荷”作为特征,如果模型指标大幅提升,说明其他特征里混入了类似的信息,需要排查。
5.4 每周重训一次,效果反而越来越差
现象:模型每隔一周用最近数据重训一次,头几天误差下降,然后某一天误差暴涨,持续好几天才恢复。
原因:滚动重训练频率过高,让模型把最近一两周的极端天气或者临时事件当成了长期规律。比如上周某两天工厂减产导致负荷偏低,模型重新训练后记住了这个现象,而这周负荷恢复正常,模型反而预测偏低。还有一种情况是训练集窗口设得太短,只用了最近一两个月数据,丢失了季节模式。
解决:给重训练设置最小训练集长度,比如至少保留过去365天。每次重训练前后,用固定的最近30天验证集做检查,如果验证误差相比上一版明显升高,就回滚到上一版。线上更新要有灰度发布意识,可以先让旧模型和新模型并行运行几天,对比后再切换。
5.5 MAPE在凌晨时段爆炸,报告没法看
现象:白天MAPE在3%左右,凌晨2点到5点的MAPE突然到20%,但绝对误差可能只有几百千瓦。
原因:MAPE的分母是真实负荷,凌晨低谷时负荷很小,同样的绝对误差会被放大成很大的百分比。模型如果用MAPE做早停,低负荷时段的梯度会主导整个验证过程,导致模型为了降低凌晨百分比误差而牺牲高峰时段的精度。
解决:改用对称MAPE,公式里同时考虑真实值和预测值的关系。如果只是评估,可以排除凌晨低负荷时段,或者按负荷分位数分层计算MAPE。训练时建议使用加权MAE或加权MAPE,给高峰时段更高权重。报告里同时给出MAE和RMSE,避免只报一个MAPE。
6. 再进一步:分位数预测与两模型融合的实用做法
当单点预测稳定后,下一步值得做的是输出预测区间和简单模型融合,这两个技巧对实际业务帮助很大。
6.1 用分位数回归输出预测区间
调度部门需要的不只是一个期望值,还需要知道负荷可能落在哪个范围,以便安排备用容量。LightGBM支持分位数回归,通过设置objective="quantile"和alpha就能拟合指定分位数。
lower_model = lgb.LGBMRegressor(objective="quantile", alpha=0.1, learning_rate=0.05, n_estimators=300) upper_model = lgb.LGBMRegressor(objective="quantile", alpha=0.9, learning_rate=0.05, n_estimators=300) lower_model.fit(X_train, y_train) upper_model.fit(X_train, y_train) y_lower = lower_model.predict(X_test) y_upper = upper_model.predict(X_test)逻辑说明:alpha=0.1拟合第10百分位,alpha=0.9拟合第90百分位。两个模型独立训练,预测时输出一个80%区间。分位数回归比残差法更适合负荷数据,因为负荷在高峰时段波动更大,区间也应该随之变宽。
参数说明:分位数回归对异常值更敏感,learning_rate建议比回归模型调低一点,比如0.03到0.05,否则少数尖峰会主导损失。训练后可以统计测试集里真实值落在区间内的比例,控制在80%附近。
6.2 线性模型加树模型做简单融合
我试过几种融合方式,最实用的是让岭回归和LightGBM各出一个预测,再做一次线性加权。岭回归擅长整体趋势,树模型擅长局部形态,两者互补性比同类型模型融合更强。
权重可以用验证集网格搜索,从0到1之间按0.05步长取组合,选验证MAE最小的一组。手工设定也可行,比如树模型0.7、线性模型0.3。融合前要确保两个模型使用完全相同的验证区间,误差口径一致。
我这些年做负荷预测最深的感受是:模型复杂度不是关键,预测任务定义和验证边界才是决定成败的环节。之前有一个项目花了大量时间在调树模型参数,却一直没发现温度特征用了事后观测值,离线指标漂亮但线上始终对不上,后来把特征时间戳修正,模型没做大改动,误差反而明显下降。另一个习惯是每次模型迭代前先写一页验证计划,写清训练区间、验证区间、评估指标和更新频率,再动手写代码。这个习惯帮我规避了很多无效调参。希望帮到你。
本文还有配套的精品资源,点击获取