
做这个项目之前我一直觉得电价预测和普通时序预测没什么两样无非是拿历史价格套一个模型看看 RMSE 能压到多少。直到我真正面对欧洲某典型现货市场的逐小时电价数据才意识到问题比想象中难得多价格凌晨可能只有几欧元中午光伏满发时能跌到负数傍晚气电顶上来又瞬间冲到上百欧元。这种“日常化”的极端波动让 ARIMA、GARCH 甚至普通树模型都显得力不从心而换深度学习模型虽然能压住误差却很难回答业务方那句灵魂拷问——“模型到底看到了什么才敢预测出这个价格”于是这个项目把两条线拧在一起做一条是用深度学习做未来 24 小时电价预测另一条是用 SHAP 可解释性分析把黑盒模型的输出拆解成“每个特征的贡献”。项目跑通之后我的体会是准确率提升只是结果真正有价值的是能够向别人说清楚“预测为什么成立”以及“模型学会了哪些市场规律”。这篇就按我实际动手的顺序从方案选型、数据清洗、模型训练到 SHAP 解读把完整链路和踩过的坑一次讲透。适合正在做电力市场预测的算法工程师、对冲能源风险的量化同学以及想给深度学习项目加上可解释性的研究者参考。1. 项目定位与研究目标拆解1.1 为什么选这个市场场景这个项目选择的研究对象是欧洲最具代表性的现货电力市场之一特征是新能源占比极高、市场化程度成熟、价格波动大。熟悉欧洲电力市场的读者可能立刻想到伊比利亚半岛那个“天天上头条”的市场但为了避免挂靠具体机构下面统一用“目标市场”来称呼。目标市场采用典型的统一边际出清机制发电资源按报价从低到高排队系统负荷需求曲线与供给曲线的交点决定了全体市场成员统一成交的现货价格。这个机制带来一个很有意思的结果——风电和光伏的边际运行成本几乎为零只要它们出力足够大就会把电价推向极低甚至负值而天然气机组和储能则成为晚高峰价格的主要支撑。所以电价预测的本质其实是预测“发电结构博弈”的结果而不是预测某一条独立的时间序列。我选择这个场景有三个理由。第一数据可得性高市场运营方公开的透明度数据接口里有逐小时出清价、系统负荷、风电光伏出力预测等核心字段天然适合做研究。第二目标市场经常出现负电价和尖峰样本内包含了“电价概率分布的尾部”这对检验深度学习模型的鲁棒性很有价值。第三可再生能源占比高意味着电价受天气和燃料成本双重驱动特征工程可挖掘的空间很大做 SHAP 解释时能讲出的故事也足够丰富。1.2 整体技术方案的选型逻辑先交代为什么淘汰传统方案。ARIMA 类模型要求序列近似线性且平稳而电价序列有明显的时变方差、尖峰厚尾和多重季节周期做差分和对数变换也只能勉强应付预测能力很弱。GARCH 族模型擅长描述波动率聚集但点预测效果平平而且它对“新能源出力冲击”这种外生突变几乎无能为力。树模型能用特征工程做得好时效果不错但它本质上是按特征切分空间天然不擅长建模“未来 24 小时连续输出的时序依赖”外推能力也弱。深度学习之所以成为主线是因为它能把“过去一周的价格形态、今日外生预测、日历节奏”整体作为序列输入端到端学习从窗口到未来 24 小时的映射关系。我在项目里用 ConvLSTM 混合结构后面会细说。但深度学习换来了精度也带来了解释性空白。交易台的人不会只因为 RMSE 好看就相信预测他们会追问“为什么中午这小时你预测了负价依据是光伏还是负荷”项目评审也会关心模型是否学到了合理市场逻辑而不是记住了噪声。SHAP 正好补上了这一环它计算每个特征对每个预测样本的边际贡献既能画全局重要性排序也能拆解单条预测的形成过程。这套“高性能预测 可解释分析”的组合是项目在技术选型上最核心的决策。最后是管线规划。整个项目拆成五个模块数据清洗与重采样、特征构造与目标定义、模型设计与训练、预测评估、SHAP 解释。每个模块单独验证通过之后再串联避免后期排查问题时无从下手。2. 数据链路搭建从原始序列到可训练样本2.1 原始数据字段与清洗细节项目使用的原始数据主要来自市场运营方公开的透明度接口核心字段包括逐小时现货出清价、系统实际负荷、系统负荷预测、风电出力预测、光伏出力预测、天然气价格、碳排放配额价格以及气温和日历信息。这里有一个非常重要的概念要提前明确——建模时能用的“外生变量版本”到底是什么。举个例子预测当天第 14 个小时的电价如果特征里用了“当天 14 时系统实际负荷”那属于严重的未来信息泄漏因为实际负荷要到那个小时结束后才会被统计发布。实际可用的应该是前一日发布的负荷预测或者临近时刻的更新预测。训练样本可以包含很多维度但每条特征必须严格满足“在预测时刻已知”的约束。清洗上我踩过三个比较典型的坑。第一是时间索引问题欧洲市场跨夏令时切换某些天只有 23 个小时或 25 个小时直接用本地时间做滞后会自动错位。处理方式是数据处理阶段全部统一成 UTC保留一个本地时区字段仅用于展示滞后特征一律按 UTC 时间轴构造。第二是缺失值逐小时序列偶尔会出现单点缺失1~2 个缺失点用线性插值问题不大但如果连续缺失超过 6 个小时最好整段剔除否则插值会制造出虚假的平滑段模型会误学。第三是负电价和极端尖峰很多新手第一反应是当成异常值删掉这是大忌——负电价是新能源高发时段的正常市场现象极端尖峰也承载了供需紧张的信息删掉它们等于把模型最需要学习的关键机制抹掉了。2.2 特征工程与预测目标的定义特征工程我分成三类来设计历史价格特征、日历节奏特征、外生变量特征。历史价格特征里滞后项最核心的是前 1/2/3 小时、昨日同时刻lag_24h、上周同时刻lag_168h再加一个过去 7 天同时刻均值。为什么重点保留这几个现货市场有极强的日内节奏昨天下午 2 点的光伏压价效应大概率会在今天下午 2 点重演一周前的同小时还能捕捉周季节模式。只保留密集的逐小时滞后并不会带来明显提升反而会让特征矩阵变胖、训练变慢SHAP 分析时还会出现特征共线性问题。除了原始滞后价我还构造了滚动窗口统计量过去 3 小时均值、过去 3 小时标准差、过去 24 小时极差。这批特征刻画的是市场“当前状态”比如过去三小时价格剧烈震荡模型就应该对当前时刻的预测保留更多不确定性。日历特征用 sin/cos 周期编码而不是直接填整数小时。直接填 hour23 和 hour0 这两个整数在模型看来距离是 23 个单位但真实时间上它们只差 1 小时用线性编码会给模型制造虚假的“断点”。hour_sin/hour_cos以及dayofweek_sin/dayofweek_cos能平滑表达这种周期性。节假日则单独做一个二值特征欧洲市场在法定节假日期间需求结构会发生显著变化。外生变量特征包括负荷预测、风电出力预测、光伏出力预测、天然气价格、碳价、气温。外生变量的发布版本要统一用“日前预测快照”不能混用不同批次的日更数据否则模型会发现同一时刻的输入口径不一致预测结果会跟着跳。目标变量定义为未来 24 小时逐小时电价的多步输出即模型一次输出 24 个值而不是迭代滚动预测。直接多步预测避免了误差在递归中累积训练时间也更短缺点是失去递归修正能力但实测结果更稳定。构造样本时还有一条铁律窗口滑动方向严格沿时间轴推进训练集和验证集的划分边界要发生在生成窗口之前。如果先把整段数据切出验证集再在全局生成滑动窗口训练窗口的末端会“看到”验证集开头的数据评估结果会虚高这在第 5 章还会展开讲。3. 深度学习模型的设计与训练纪律3.1 模型结构为什么要用 Conv LSTM 混合主模型结构是一层 1D 卷积接双层 LSTM最后接全连接层输出 24 个价格点。先上代码import tensorflow as tf model tf.keras.Sequential([ tf.keras.layers.Conv1D(filters32, kernel_size3, paddingsame, activationrelu), tf.keras.layers.BatchNormalization(), tf.keras.layers.LSTM(units64, return_sequencesTrue), tf.keras.layers.Dropout(0.2), tf.keras.layers.LSTM(units32, return_sequencesFalse), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(units24) ])为什么第一层用卷积而不是直接上 LSTM电价相邻小时之间往往有很强的短时惯性例如凌晨 2 点到 4 点的价格通常平稳滑行Conv1D 的局部卷积核能快速提取这类短时模式还能压缩序列长度。我实测过加这一层后 LSTM 层的压力明显减小训练时间缩短约三分之一测试集误差略有改善。反过来如果只用纯 LSTM模型需要自己从长序列里摸索出短时模式参数量和训练成本都会上去。LSTM 单元堆两层是性能和复杂度之间的平衡点第一层 64 个单元负责学习时间模式第二层 32 个单元在更高抽象层组合这些模式超过两层之后收益很小反而更容易在长序列上过拟合。代码里每个 LSTM 层后面接了 Dropout(0.2)这是处理时序模型过拟合最直接的手段比正则化系数更顺手。3.2 训练细节、归一化与超参选择训练配置里最关键的一步是归一化。电价序列包含负电价和极端尖峰如果用 MinMaxScaler极端值会把大部分正常样本压缩到一个很窄的区间模型无形中把精度花在了区分极值上。我用的是 RobustScaler按中位数和四分位距缩放对异常值不敏感负电价落在零点两侧也很自然。from sklearn.preprocessing import RobustScaler scaler RobustScaler().fit(X_train_2d) # 只在训练集上拟合 X_train scaler.transform(X_train_2d) X_val scaler.transform(X_val_2d) X_test scaler.transform(X_test_2d)这里必须强调fit只能作用在训练集上验证集和测试集只能用训练集的统计量做transform否则就属于把未来数据的信息“捎带”进了训练流程。训练超参数我固定如下超参数取值选择依据输入窗口长度168 小时覆盖完整 7 天周周期模型能识别周日和周一差异卷积核尺寸3捕捉相邻 3 小时的短时惯性LSTM 单元数64 / 32两层结构控制参数量在合理区间Dropout0.2防止时序过拟合损失函数Huberdelta1.0兼顾 MSE 的敏感性和 MAE 的鲁棒性优化器Adam初始学习率 0.001常规稳健搭配Batch Size256平衡梯度稳定性与训练速度最大 Epoch80配合早停策略早停 Patience10验证集 loss 连续 10 轮不降即停损失函数没有用默认的 MSE。电价的尖峰会让 MSE 主导梯度模型为了降低二次惩罚倾向于把预测压向“平滑的中间值”结果就是峰谷时刻全被抹平。Huber 损失在误差较小时接近 MSE、误差大时接近 MAE既保留了对大误差的敏感度又不会因为个别尖峰带偏整个训练方向。训练时配合 ReduceLROnPlateau 做学习率衰减patience 设 5、衰减系数 0.5模型保存以验证集指标为基准早停触发时恢复到验证集最优权重。所有随机种子固定保证实验可复现。3.3 评估指标与误差观察口径第一批评估指标我用了 RMSE、MAE、R² 和 sMAPE。RMSE 对灾难性大误差敏感适合盯“有没有离谱预测”MAE 反映平均误差水平R² 看模型对目标变异的解释比例。MAPE 在这个场景里直接失效——真实电价会出现 0 和负值MAPE 的分母不是 0 就是负数计算值毫无意义所以用对称的 sMAPE 作为替代。基线选择非常关键。我用了两个基线持久性基线用昨日同小时价格作为明日预测值和线性回归。持久性基线在电价这种强惯性序列里一点都不弱如果深度学习模型不能让 RMSE 相对它下降 20% 以上那这个深度学习就不合格。项目跑出来的结果是测试集上深度学习模型的 RMSE 相对持久性基线下降约四分之一MAE 改善略小R² 在 0.92 附近。误差不能只看整体均值按小时误差拆开看才有意义。实测中正午 12 点到 15 点的误差明显偏大这正对应光伏出力高、价格容易被打到负区间的时段。如果外生特征里的光伏预测数值偏保守模型就会在午后持续性高估价格。这个结论后来通过 SHAP 依赖图得到了印证。预测曲线可视化时可以明显看到早高峰和晚高峰的尖峰形状被模型学到了但遇到风电骤减或者气价突涨的日子预测还是会滞后这类样本正是后续需要单独建子模型或引入额外特征的方向。4. SHAP 可解释性分析把预测逻辑打开给业务看4.1 SHAP 原理与深度学习配适SHAP 的核心是 Shapley 值来自合作博弈论把每个特征看作参与游戏的一方模型输出是全体特征合作产生的收益Shapley 值就是一种公平分配收益的方法——它在所有可能的特征组合里计算某个特征的边际贡献再按加权平均得到该特征对当前预测的贡献。简单说就是模型给这个样本预测出的价格可以拆成“基线预测值 每个特征的贡献值”把所有贡献加起来就还原了预测结果。用一个生活类比理解朋友聚餐后 AA但有人吃得多有人吃得少怎么算出谁该出多少才公平SHAP 的方法是让每个人分别加入不同的人群组合看他加入前后账单变化了多少把所有组合的变化加总求平均最后得到每个人的“公平贡献”。放到模型里特征就是聚餐的人模型输出就是账单金额。深度学习模型配 SHAP 有几种方式。TensorFlow 环境里可以用DeepExplainer或GradientExplainer做基于梯度的近似计算也可以用模型无关的KernelExplainer做采样。对 LSTM 类模型我的实测体会是GradientExplainer的稳定性和速度更均衡先从训练集随机抽 200 条样本作为背景数据集再对测试集样本计算 SHAP 值。背景数据集必须全部来自训练集否则解释过程泄漏了未来信息得到的贡献值会有偏。import shap explainer shap.GradientExplainer(model, X_train_background) shap_values explainer.shap_values(X_test_explain) shap.summary_plot(shap_values[0], X_test_explain, feature_namesfeature_names)4.2 全局解释特征重要性和影响方向SHAP 全局分析最有价值的是 summary plot。横轴是 SHAP 值大小纵轴按特征平均绝对 SHAP 值排序样本点的颜色代表该特征的实际取值高低。读图时先看排序再看颜色分布的方向关系就能知道“谁在影响预测”以及“怎么影响”。这个项目里最重要的三个发现如下。第一lag_24h昨日同小时价格的重要性一骑绝尘且与 SHAP 值呈正相关昨日同小时价格高模型就倾向于推高今日预测。这是市场惯性的体现现货价格在相同时段之间有极强的自相关。第二光伏出力预测的重要性排在前列整体方向为负——光伏出力越高模型越倾向压低价格。这个方向不是线性恒定午后高光伏区间尤其明显样本点出现大量左端延伸说明模型学到了“光伏过剩会把价格打到负区间”这一段非线性机制。第三天然气价格的影响不是独立常数它在冬夜高负荷时段的贡献被明显放大。气价是边际机组的成本锚模型捕捉到的是“高价燃料 高负荷 价格容易被推升”的交互效应。再往下看依赖图。画dependence_plot时会发现光伏出力预测特征对 SHAP 贡献的斜率在不同负荷颜色下并不一致高负荷日同一份光伏出力预测压低价格的作用更强。这说明市场博弈中光伏对价格的边际影响依赖系统需求——光伏在需求旺盛时压低价格在需求惨淡时直接制造负电价。这类交互特征在线性模型里很难显式构造深度学习 SHAP 能把它自动找出来是这套组合最大的竞争力。4.3 局部解释拆解单个时段的预测全局解释回答“模型总体靠什么”局部解释回答“这一个小时为什么这么预测”。用 waterfall 图看单个样本最直观从基线预测值出发每个特征往上加贡献或往下减贡献最后得到模型输出。我挑过一个正午负电价样本。模型预测 -12 欧元/兆瓦时拆开看光伏出力预测高贡献 -18 欧元昨日同小时低价贡献 -6 欧元系统负荷预测低贡献 -3 欧元但天然气价格走低让预测回拉了 5 欧元节假日效应又贡献 3 欧元几项加总就是 -12 欧元。这种拆解给交易员看对方几乎不需要培训就能理解“模型认为光伏是打价格的主力”信任感会大幅提升。夜晚峰值样本则是另一个图景滞后价格正贡献 40 欧元气价正贡献 22 欧元负荷预测正贡献 8 欧元光伏出力近似为零。面对这两类差别极大的样本模型分别动用了不同特征组合做决策这不是人工规则能轻松覆盖的。我用 SHAP 做局部解释的另一个用途是审计如果某一天模型预测出异常高价点开 waterfall 立刻能查到是哪个输入变量在“撑场面”而不是对着黑盒干瞪眼。5. 实测踩坑记录与常见问题速查5.1 数据泄漏与时序错位这个项目最大的坑也是电力预测最经典的坑就是未来数据泄漏。我第一次建模时把“当日全天实际负荷均值”直接作为当天所有时点的特征结果验证集效果极好测试集也漂亮但把特征换成“预测负荷”之后误差立刻打回原形。原因在于实际负荷均值是次日才知道的统计量把它塞进当天输入等于让模型作弊。排查方法是做特征发布延迟审计对每个外生变量标注“最早可用时间点”逐项确认它在预测时刻确实已经发布。窗口构造阶段的边界泄漏同样隐蔽。如果先随机切验证集再生成滑动窗口训练集最后一个窗口的标签可能落在验证集时间范围内信息会跨边界流动。解决方式是先设定好时间断点再构造窗口并让训练集结束时间和验证集开始时间之间至少空出一个窗口长度的冷却间隙。这个间隙不会影响数据量但能彻底阻断跨集合的时间依赖。夏令时错位是最折磨人的问题。欧洲市场每年两次切换本地时区原始数据如果用本地时间标记shift(24)得到的“昨日同小时”在切换日会错位一小时。强迫自己把所有时间列统一为 UTC 之后这类错位彻底消失。为了业务展示可以保留一个本地时区转换字段但一切滞后、滚动计算都基于 UTC。5.2 模型训练与 SHAP 解释的坑训练阶段的坑主要是损失函数和早停选择。默认用 MSE 会让模型把精力花在拟合极端尖峰上预测曲线整体变得迟钝。换 Huber 之后正常时段的精度几乎没有损失峰谷时段的追涨能力反而更好。早停不要盯着最后一个 epoch 的权重要显式保存验证集指标最好的 epoch 权重否则训练后期模型已经开始过拟合测试集误差会被拖累。SHAP 解释也有自身陷阱。特征共线性是最常见的一种滞后价格和滚动均值高度相关时SHAP 会在它们之间随机分摊贡献单次计算的单个特征数值不稳定换一次背景样本结果可能差别不小。解决办法是先用相关性矩阵排查特征再进行分组 SHAP把全部滞后类特征合并成“历史价格组”算总贡献再细看组内拆分。另外DeepExplainer对 RNN 的近似有时会波动我习惯把GradientExplainer的结果和KernelExplainer在少量样本上的计算结果交叉验证方向一致才采用。要记住 SHAP 表达的是“模型如何决策”不是“数据真实因果”向业务汇报时用词要准确避免说“因为光伏所以价格低”要说“模型基于光伏特征做出了压低预测的决策”。5.3 常见问题速查表症状可能原因解决方案预测曲线整体比实际滞后一天滞后特征以 24 小时为主模型过度依赖昨日同小时增加 1/2/3 小时短滞后特征引入滚动统计量午后光伏高发时段误差显著偏大光伏预测特征口径不准或数据版本混用统一使用日前预测快照核对发布延迟验证集指标很好实盘效果崩塌某特征存在未来数据泄漏逐项审计特征最早可用时间点夏令时切换日预测混乱时间索引错位导致滞后特征错 1 小时时间统一为 UTC展示层再转本地SHAP 单特征数值稳定性差特征共线性导致贡献分摊随机化分组计算 SHAP去除高相关冗余特征SHAP 值全部接近 0背景样本没有正确取自训练集或解释器类型不适配重设背景样本更换 Gradient/Kernel Explainer6. 项目复盘哪些经验能复用到下一个预测项目把这个项目做完我最大的感受是预测类项目的成败往往不在模型结构而在数据口径和验证纪律。模型从 LSTM 换成 ConvLSTM、调大调小 DropoutRMSE 的变化幅度其实有限但一次夏令时错位或者一个未来泄漏特征就能让评估结果失真 30% 以上。所以项目推进过程中我把“数据版本时间线”当成和模型精度同等重要的交付物每个特征都标注了发布时间、适用时区、预测窗口整套流程立刻变得可控。另一个值得复用的经验是可解释性分析应该前置而不是后置。很多同学先把模型调到满意再补一个 SHAP 图当说明材料但我在这个项目里其实是靠 SHAP 早期就发现过错误特征——有个外生变量重要性很高查了发布延迟才发现它根本不是预测时刻已知的信息那时候模型还没最终定型改起来成本很低。把 SHAP 当成模型调试工具比当成论文装饰品实用得多。后续扩展方向也比较明确。当前模型输出的是单点预测值实际交易决策需要的是概率分布可以基于同一套特征加 Quantile Loss 或 MC Dropout 做区间预测。目标市场的气价、碳价、新能源出力之间存在动态耦合下一步可以尝试多市场联合建模或引入注意力机制让模型自动聚焦关键时段。如果要做成生产系统还需要加入在线增量训练因为市场机制会随时间演化静态模型的预测能力会逐渐衰减。最后说一点个人心得。做这类高波动时序预测最忌讳的是拿测试集反复试模型试到指标好看为止——那其实已经造成信息泄漏只是你感觉不到。我要求自己测试集只准跑一次完整实验所有调参都看验证集SHAP 分析也单独保留一套“审计样本”。事实证明这个习惯帮我避免了很多后期返工。如果你准备动手复现这个项目先把数据清洗和特征构造框架搭扎实再碰模型结构。模型可以随时换数据错位一天后面所有工作都会跟着白做。