
1. 电价预测的难点与“多变量”到底要多在哪1.1 电价不是“历史价格曲线”这么简单做电价预测之前我一度以为它和股票预测差不多把过去的价格序列喂进去就够了。真上手之后才发现电价序列比股价要“暴躁”得多。股价你可以用移动平均看个大概趋势但电价一天之内能走出好几个完全不同的形态凌晨低谷可能只有几十元每兆瓦时晚高峰瞬间拉到几百甚至上千遇到极寒极热天气、机组检修、风电出力骤减价格尖峰说来就来毫无预兆。所以我从第一个版本就确定了方向不能用纯历史价格做单变量预测必须上多变量。多变量电价预测的本质是把价格当成一个“结果”把影响价格的各路信息全部当成“原因”喂给模型。原因找得越齐预测就越靠谱。这套思路实践下来确实有效。我最开始只使用“前一天同一时刻电价”这一个特征模型在平时表现尚可但一到负荷起落剧烈的日子就直接失灵MAPE能飙到25%以上。后面我把负荷、气温、类型日等加进来同样的模型框架MAPE降到了10%以下。这就是多变量和单变量的直观差距。1.2 多变量特征清单与数据获取具体到电力现货市场我常用的特征大概是这么几类大家可以根据自己拿到的数据源做增减特征大类具体特征说明历史价格前一时刻电价、昨日同时刻电价、上周同时刻电价递推建模的“燃料”后面会细说负荷类系统负荷预测值、区域电网负荷、负荷变化率负荷是电价的直接推手很多市场出清模型的第一驱动变量就是负荷天气类气温、湿度、风速、辐照度气温影响制冷/供暖负荷风速和辐照度影响新能源出力日历类小时、星期几、是否节假日、节前节后电价有显著的日内周期、周周期和节假日效应新能源出力风电出力预测、光伏出力预测新能源渗透率高的市场这个特征甚至比负荷还重要燃料成本类天然气价格、煤价指数如果公开可得边际机组报价会随燃料成本变化这里面最容易踩坑的是天气数据的使用。做预测时拿到的天气一定是“预报值”而不是“实况值”并且要注意预报发布时间和预测目标时间的对齐关系。比如你预测明天中午12点的电价那你只能用今天发布的“明天中午12点气温预报”不能顺手把明天下午15点的实况气温拿来用——那叫信息泄漏是时间序列预测最忌讳的事。1.3 为什么要用递推策略而不是一场预测到底多变量特征准备好了接下来面临一个建模策略问题你要预测未来一天甚至未来几天的电价到底怎么个预测法常见方案有三种。直接多步预测训练一个模型直接输出未来24步的价格本质是一个多输出回归问题。递推多步预测只训练一个单步模型预测时把上一步的预测结果作为输入一步一步往后滚。多模型组合为未来每个步长单独训练一个模型即第1小时一个模型、第2小时一个模型……共24个模型。直接多步的模型结构复杂多输出回归在随机森林里并不原生支持要走多输出包装器解释起来也很费劲。多模型组合倒是稳定但训练开销大24个模型的特征分布还各自不同维护成本很高。所以我选了递推建模。主要原因有两个第一简单可解释。我只需要一个随机森林回归器写一个循环就能完成多步预测中间的每一步都能拆开看出了问题也好溯源。第二递推符合电价形成的时间逻辑。电价受“上一时刻价格惯性”影响很大前一小时电价高下一小时大概率也高。递推天然地把这种惯性传导下去了。当然递推的老毛病是误差累积这个我在第4节里专门讲并且有对应的止损思路。2. 为什么选随机森林从回归算法对比说起2.1 随机森林回归的工作原理简说随机森林回归的原理不算复杂用自助采样构建多棵决策树每棵树在生长时随机挑选一部分特征做最优分裂最后将全部树的预测结果取平均作为输出。这种“集体决策”的思路天然压制了单棵决策树过拟合的毛病。在电价预测这个场景里它有几个非常实用的特性。第一个是非线性拟合能力。电价和负荷、气温之间不是线性关系夏天超过35度后每升1度电价可能跳一个台阶随机森林的树结构能很好地表达这种分段关系。第二个是特征重要性输出。训练完直接告诉我“哪个特征对价格影响最大”这在向业务方解释模型时太关键了。第三是训练速度快、对超参数不敏感。相比深度学习的漫长调参随机森林用默认参数跑到后期效果也不会差到哪去。2.2 和LSTM、XGBoost、Prophet的对比选型的时候我不是没考虑过其他模型横向对比下来大致是这样LSTM在处理超长时序上很强但训练需要大量数据、GPU加速、精细调参而且黑箱程度比随机森林更高。对中小规模数据比如一年半载的小时级电价LSTM并不一定比随机森林强反而容易在局部过拟合。XGBoost和随机森林同为树模型家族预测精度通常XGBoost略高但它有几十个超参数要调调得不好很容易过拟合。随机森林的两个核心超参数就能跑出不错的基线。Facebook Prophet对强周期性序列拟合很好但它本质是趋势季节性分解对“负荷突变引发价格尖峰”这类事件性波动几乎无能为力。我最终选择了随机森林概括成一句话就是在数据量有限、希望快速上线、还要保持可解释性的生产环境里随机森林是性价比最高的起点。顺便说一句最近老看到有人讨论“遥感随机森林”之类的应用确实随机森林属于哪都能用的通用算法但时间序列预测和空间分类不一样它有自己的一套特征构造和验证规则不能把分类那套流程直接搬过来。2.3 随机森林在电价数据上的“脾气”用了一阵子之后我还摸到随机森林在电价序列上的一个脾气它预测长期均值很准但预测极端尖峰偏钝。因为随机森林的最终结果是多棵树平均平峰时期大家意见一致预测值漂亮一到尖峰时段树之间分歧大平均值就把尖峰“磨平”了。这个特性决定了如果你做的是日前价格申报参考随机森林完全够用如果你要做极端价格事件的精细化预测还得在残差、分位数上做文章。这部分我放到第6节展开。3. 训练侧的硬功夫特征工程、时间切分与防泄露3.1 滞后项构造递推建模的“燃料”多变量特征虽然多但真正和电价最贴近的永远是“过去的电价自己”。专业上叫滞后项。用随机森林做递推预测滞后项既是燃料也是容易翻车的地方。我构造滞后项时遵循一个原则覆盖三种时间尺度的记忆。短期记忆滞前1小时、滞前2小时价格捕捉前后时段的价格惯性。日记忆昨日同一时刻价格、昨日同时刻前后半小时价格捕捉日内模式。周记忆上周同一天同一时刻价格捕捉星期周期效应。在代码里就是给历史电价序列做shift操作import pandas as pd df[lag_1] df[price].shift(1) df[lag_2] df[price].shift(2) df[lag_24] df[price].shift(24) # 小时级数据取昨日同时刻 df[lag_168] df[price].shift(168) # 取上周同时刻这里有个小细节做节假日和周周期特征时单纯用“星期几”是不够的。周一对模型来说是周一但节假日后的周一和普通周一的用电曲线完全是两回事。我加了一个“日类型”分档特征普通工作日、周末、法定节假日、节前一天、节后一天五种状态。就这一个小改动节假日前后的预测误差下降非常明显。3.2 日历特征和天气特征的对齐问题日历特征看似简单但编码方式会影响结果。小时用0-23直接编码星期几用0-6直接编码模型能学出来但很难快速捕捉“早晚高峰”的周期性差异。更好的办法是用正弦余弦编码把时间拆成周期分量让模型理解“凌晨0点和深夜23点其实很近”。天气特征要注意的是空间尺度。电价预测不需要太细的网格天气我一般取预测区域主要城市的气温均值或负荷中心的气温。温湿度交互项也可以做比如“炎热的周末下午”这种组合往往对应空调负荷顶点。3.3 训练集测试集切分的正确姿势时间序列任务最忌讳随机打乱切分。随机切分等于把未来的信息混进了训练集模型在测试集上看起来特别漂亮上线就露馅。我见过好几个项目都栽在这上面。我的做法是按时间顺序切分用前70%的数据训练后30%按时间顺序验证。更进一步做模型调参时也得用滑窗交叉验证或扩展窗口交叉验证而不是K折随机划分扩展窗口法每次训练集只增不减逐步覆盖到验证窗。滑窗法固定训练集长度整体往后平移。滑窗法我实测下来更贴近生产场景——你不可能用三年前的数据去预测下周的电价模型对近期模式的敏感度才是关键。提示如果你用的是sklearn的GridSearchCV记得cross_val_score的cv参数不要用默认的KFold至少要传TimeSeriesSplit。这是做任何时序任务都要刻进肌肉记忆的一步。3.4 一个容易翻车的特征膨胀问题树模型对特征尺度不敏感所以归一化不是重点但“特征膨胀”值得警惕。随机森林在做特征分裂时存在特征选择偏差特征越多某些噪声特征被选中的概率越大模型整体稳定性会下降。我一开始把天气特征堆了十几个包括露点温度、体感温度、云量、降雨概率结果模型准确率反而比精简版低了。后来我按特征重要性排序观察发现有效的主要就是气温、风速、相对湿度这三个。其他天气特征是“有关系但不关键”。所以建议做一轮特征筛选把重要性低于阈值的特征剔除模型更快也更稳。随机森林“跑多长时间”的问题有很大一部分就由特征数量决定特征精简后训练时间能减少一半效果不降反升。4. 递推预测实现单模型多步滚动的完整代码逻辑4.1 递推预测的流程拆解递推建模的核心思想是把“预测未来24小时”这个任务拆成“预测下一个小时”并重复24次。每次产生的新预测值不作为最终答案而是作为下一步的“已知值”拼接回特征里。就像你走一条夜路每走一步把手电筒往前再照一步手电筒照不到的地方全凭当前视野推断。拆成步骤就是这样用训练好的随机森林模型基于当前已知特征预测第t1小时电价。把t1小时的预测值填入“滞前1小时电价”特征位。如果预测目标超过24小时还要把该值也填入“昨日同时刻电价”对应的滚动槽位。更新日历特征小时1重复上述步骤直到预测完整个窗口。在执行第二步时有一个关键选择你预测了t1时刻的电价之后是把这个预测值当作t1时刻的真实值继续滚下去内部一致性还是等真实值出来再更新重锚前者是标准递推简单但误差会像滚雪球一样越滚越大后者是混合策略适合短期预测。4.2 核心代码预测窗口逐小时滚动下面这段代码是我实际项目里的简化版完整展示了递推的核心循环import numpy as np import pandas as pd from sklearn.ensemble import RandomForestRegressor # 假设训练好的模型叫 rf_model特征列表叫 feature_columns # last_row 是最新一小时的已知特征行 # steps 是预测步长比如24 def recursive_predict(rf_model, last_row, feature_columns, steps24): current last_row.copy() predictions [] for i in range(steps): # 提取当前特征 X_next current[feature_columns].values.reshape(1, -1) # 单步预测 pred rf_model.predict(X_next)[0] predictions.append(pred) # 递推更新把预测值填入滞后特征 current[lag_1] pred current[lag_2] current[lag_1] # 上一步的实际值顺延 # 更严谨的做法是用一个价格历史队列来滚动 current[hour] (current[hour] 1) % 24 return np.array(predictions)这里看起来简单实际藏了两个细节值得展开。第一个是滞后特征不能只改lag_1。如果你用了lag_24、lag_168每一步都要维护一个“预测价格历史队列”把刚生成的预测值追加进去同时挤出最早的真实值保证lag_24取的确实是“24小时前”的预测值。简单粗暴地用current[lag_2]current[lag_1]只能是偷懒做法预测步数一多就会错位。第二个是日历特征的处理。hour加1取模没问题但如果你用了“星期几”特征跨天时还要同步更新。最稳妥的做法是用datetime索引一步步加timedelta再从日期对象上重新提取所有日历特征。代码会啰嗦一点但能避免跨周跨月的逻辑bug。4.3 误差累积的监控与止损机制递推预测最大的痛点是误差累积。第1步的预测误差如果偏大会污染第2步的滞后特征第2步又污染第3步……最后几步的预测基本就是“一场幻觉”。我的做法是给预测加两道保险。第一道是置信带估计。随机森林虽然没有原生的置信区间但可以用袋外预测的方差来近似不确定性。具体做法是拿到每棵树的预测结果计算它们之间的标准差。标准差越大说明树之间的分歧越大预测越不可靠。如果某一步的标准差超过历史水平的上限我就提醒自己从这一步往后的预测可信度已经很低了。第二道是重锚策略。递推预测不允许无限滚动。我一般限制连续递推步数不超过6步每6步触发一次“重锚”用最新的真实电价把滞后特征修正回来再继续往下预测。虽然这样失去了纯粹的递推属性但效果远好于让误差一路滚到底。真正上线的时候一天96个交易时段电力现货市场多为15分钟一个时段我按6步一组分成16组先预测6步真实值出来后立刻校准再预测下6步。这算是递推建模既保留“单模型多步预测”优雅性、又不被误差拖垮的折中方案。5. 实测避坑随机森林的训练时间、调参与过拟合5.1 “随机森林需要跑多长时间”的真实答案很多人问随机森林训练要多久这个问题的标准答案永远是“看你的数据规模”但展开说又有很多细节。我以自己的实测数据为例历史小时级电价数据约两年共计约17000个样本特征数量15个n_estimators设为500。在一颗普通桌面CPU8核16线程上用n_jobs-1并行训练耗时不到30秒。如果只预测未来24小时递推预测本身也就是毫秒级别的推理。所以随机森林在电价预测这个场景里训练时间完全不是瓶颈。真正耗时的是调参和验证环节。如果用TimeSeriesSplit做5折验证每折都要重新训练一次总时间大约放大到单次的4-5倍。再叠加GridSearchCV的网格搜索超参组合一多跑一两个小时很正常。我个人的建议是先用默认参数跑通全流程再根据特征重要性、残差分析决定是否调参不要在第一步就追求最优参数。另外还有个训练时间相关的隐形坑数据量随时间越来越大。如果你每年都重新全量训练训练时间会线性增长。我后来改成滚动训练只取最近365天的数据既保证了模型对近期模式的敏感度又控制了训练时间。5.2 n_estimators、max_depth、min_samples_leaf怎么定随机森林核心超参数不多逐个说下我的经验。n_estimators树的数量不是越多越好500棵之后边际收益递减训练时间却线性增长。我通常设300用袋外分数验证到500没有明显提升后就定在300。max_depth树的最大深度深度太深会记住个别极端样本深度太浅则欠拟合。电价数据模式丰富我实测10-15这个区间比较合适。min_samples_leaf叶子节点最小样本数这个是抗过拟合的关键也能让预测更平滑。考虑到电价尖峰的存在我设20-50保证每个叶子至少有足够样本做平均避免输出一个极端值。max_features每次分裂的特征数回归问题默认是特征总数的1/3我用默认值表现就很好一般不用动。如果发现训练集预测完美、测试集表现差几乎可以锁定为过拟合。优先加大min_samples_leaf其次是降低max_depth。不要一上来就砍n_estimators树多了抗过拟合能力反而是增强的。5.3 用特征重要性反向验证模型没学歪随机森林训练完第一件事不是看指标而是看特征重要性排序。这个步骤能帮你发现很多隐蔽问题。比如我有一回把“风速”特征的重要性异常拉高但业务逻辑上风速对电价的直接驱动没那么强。排查后发现是因为缺失值被我填充了-999树模型把这个填充值当成了有效分类。这就是典型的“模型学了不该学的东西”。修正成用前向填充加“是否缺失”标记后重要性排序才恢复正常负荷、昨日同时刻电价、气温稳居前三。所以特征重要性不光是解释工具更是数据质量的探测器。某个特征的重要性高到离谱先不要高兴去检查一下它的分布是不是存在异常标记或时间错位。6. 评估方法与结果解读6.1 MAE、MAPE、RMSE怎么选评价电价预测模型用哪个指标不能拍脑袋我一般三个指标一起看各自有各自的用处。MAE平均绝对误差最直观单位是元/兆瓦时业务侧能直接看懂。RMSE均方根误差放大了大误差的惩罚。电价尖峰预测失败时RMSE会暴涨适合用来盯“极端值表现”。MAPE平均绝对百分比误差看相对误差但低价时段会虚高。凌晨电价接近几十元绝对误差10元就对应20%以上MAPE而晚高峰绝对误差50元可能只有5%的MAPE。所以MAPE要按时段分组看不能看整体一个数。我评估时还会把一天拆成峰、平、谷三个时段单独看指标。峰时段的MAE最具业务参考价值因为现货市场的收益博弈主要发生在高峰时段。6.2 基线对比直接多步 vs 递推多步我不只跑了递推建模还跑了一个“直接多步”基线做对比方法是用多输出回归器包装随机森林。同样条件下实验结果是前6步递推模型的精度微微胜出6步之后递推模型的误差急剧上升直接多步的误差上升相对平缓但前期的起步精度要差一些。这就印证了一个规律如果你预测窗口很短递推优先如果预测窗口很长递推会被误差累积拖垮。在电价现货的交易场景里我更看重短期精度高的优势配合重锚策略把递推的弱点压制住。这也是我最终保留递推方案的原因。6.3 后续可做的扩展方向基线跑通之后我开始琢磨改进方向目前想到且值得尝试的有三条路。第一条是分位数随机森林。既然随机森林天然给出每棵树的预测分布那我们可以把每棵树在目标点的预测值做成经验分布取2.5%和97.5%分位数作为预测区间。这对于申报电力现货市场的价格区间非常有价值。第二条是残差序列的二次建模。先用随机森林预测主趋势把预测残差单独拎出来用一个线性模型或小网络去拟合残差的自相关模式最后再叠加还原。这能弥补随机森林对“短时价格惯性”的钝感。第三条是引入更细粒度的市场数据比如实时出清电量、备用容量、阻塞信息。这些数据越贴近日前市场出清的物理过程预测上限越高。我个人做完第一版递推模型的体会是这套方案最大的优点不是你模型有多聪明而是你每一步都看得懂、算得清、改得动。市场规则变了去掉一个特征再训练一轮就完事交易员质疑某个预测点拉出特征重要性就能解释清楚。这种“可解释、可复现、可快速迭代”的能力比某一天的预测精度多一个百分点重要得多。最后分享一个小习惯每次递推预测跑完我都会把关键节点的预测值和真实值画在一张图上重点盯尖峰处模型的响应滞后了几个时段。这个滞后的步数就是你下一次准备重锚的位置。