基于LSTM与SHAP的西班牙电价预测:特征工程与可解释性实践

发布时间:2026/10/20 16:56:23
基于LSTM与SHAP的西班牙电价预测:特征工程与可解释性实践
1. 项目背景与核心需求拆解1.1 电价预测到底难在哪里说到电价预测很多刚接触的人第一反应是这不就跟天气预测差不多吗一个时间序列喂给 LSTM 不就行了真做起来你会发现完全不是一回事。电价序列最让人头疼的是它不是一个“自然序列”而是经济系统、能源结构和人为撮合交易共同作用下的产物。一个典型特征是尖峰和极端值。西班牙市场的日前出清价日常在 0~100欧元/兆瓦时之间波动但每年都会出现数次突破 150欧元甚至 200欧元的尖峰时刻偶尔还会出现负电价。用 MAE 或 RMSE 做评估时这些极端点会直接把误差拉爆。更麻烦的是这种尖峰往往由“可再生能源出力骤降需求突增机组组合响应不及时”等多重因素叠加触发单靠历史价格序列很难提前捕捉。另一个特征是强周期性和非平稳性。电价有明显的日内双峰早晚高峰、周内差异工作日和周末负荷模式完全不同还有跨季变化夏季空调负荷、冬季供暖负荷。同时随着可再生能源渗透率逐年提高整体价格水平、波动形态都在缓慢漂移。这导致模型训练时不光要看“过去几天发生了什么”还要理解长期趋势的变化。所以电价预测项目不是一个简单的回归任务而是一个带有强结构化特征的时序建模问题。真正考验的是如何把价格信号之外的负荷、发电结构、时间上下文这些外部变量组织好再交给模型去拟合。1.2 为什么选西班牙市场作为研究对象西班牙电力市场是个很有意思的样本。它在市场化程度和可再生能源占比方面都属于比较靠前的。风电、光伏装机占比高意味着电力系统对天气条件极度敏感——阴天、无风天和晴好大风天的出清价可能差出三倍以上。这带来一个好处数据里的“因果信号”非常明显。你不需要刻意做复杂的特征挖掘光是把风电预测出力、光伏预测出力、系统总负荷这些公开变量对齐到价格序列上模型就能学到很强的规则。比如夜间风电大发时段价格被压低傍晚光伏退坡叠加需求爬升时价格快速拉高。这些模式在数据里重复出现非常适合做可解释性研究。另一个优势是数据公开度。市场运营方会发布逐小时的历史负荷、发电结构、日前出清价和预测信息粒度细、时间跨度长拿来直接做研究几乎不需要额外清洗。对一个想复现实验的读者来说这是非常友好的条件。这个研究思路其实是通用的。你换成其他市场化程度高的区域市场方法框架完全不变——数据源换一下、特征名称调整一下、模型超参数微调一下就能跑起来。1.3 这个项目最终交付什么整套研究做下来核心成果是三块一份完整的时序特征工程方案、一个基于深度学习的电价预测模型、一组基于 SHAP 的特征归因分析结论。我这么说可能有点干换个方式讲——你最后能清清楚楚地告诉别人模型为什么预测明天的价格会上涨是负荷涨了还是风电降了每个因素分别贡献了多少这才是这个项目和单纯调参跑 LSTM 的本质区别。如果你正准备入门电力市场数据分析或者已经在做时序预测但总觉得“模型像个黑盒说不清楚结论怎么来的”那这篇内容就是给你写的。我会把数据怎么处理、模型怎么搭、SHAP 怎么调通以及我踩过的那些坑全部摊开讲。2. 数据准备与特征工程方案2.1 原始数据格式与字段解析我用的数据来自市场运营方公开的逐小时数据主要包含以下字段字段说明单位系统负荷全系统实际用电需求兆瓦MW风电出力风电机组实际出力兆瓦MW光伏出力光伏电站实际出力兆瓦MW出清价格日前市场每小时出清电价欧元/兆瓦时EUR/MWh小时标记0~23 小时编号无星期标记周一至周日编号无这里要注意一点实际研究时最好把“实际出力”和“预测出力”都拿过来。理论上预测任务建模时只能用预测出力因为实际出力是事后才统计的。但很多公开数据集只发布实际出力这时做研究可以做一个近似使用当天可得的数值天气预报数据或当日预测曲线来构造特征。我为了复现方便在实验里以实际出力作为特征并明确标注“该实验假设出力序列在日前已知”——这是一般学术复现的常见设定。2.2 特征工程的三个关键动作先说结论在这个项目里做得好的特征工程比换一个更强的模型更能提升精度。第一个动作是构造滞后特征。电价和负荷的序列自相关性非常强——今天下午 5 点的价格和昨天下午 5 点的价格常常高度相关。所以我构造了滞后 1、2、3、24、25、48、168 小时的价格、负荷和风电出力序列。168 小时对应一周前同一时刻用来捕捉周周期模式。第二个动作是滚动统计特征。对最近 6、12、24 小时的价格、负荷和风电出力计算均值、标准差和最大值。这里有个细节滚动窗口只能用当前样本之前的数据也就是“未来不可知”原则。计算时我统一用shift()错位之后再做 rolling防止信息泄漏。第三个动作是周期性编码。对“小时”和“星期”做正弦余弦展开而不是直接塞给模型一个 0~23 的整数。原因是整数编码会让模型误以为小时 23 和小时 0 差别极大实际上它们只相隔一小时。sin/cos 编码能把时间的周期连续性表达出来。代码大概这样df[hour_sin] np.sin(2 * np.pi * df[hour] / 24) df[hour_cos] np.cos(2 * np.pi * df[hour] / 24) df[dow_sin] np.sin(2 * np.pi * df[dow] / 7) df[dow_cos] np.cos(2 * np.pi * df[dow] / 7)2.3 我踩过的特征工程坑这里想重点提醒三个坑。第一个坑是直接按随机比例切分训练集和测试集。时间序列数据不能像普通表格那样 shuffle 切分否则模型会“偷看”未来。你随机切分后测试集混杂在训练集时间区间内部模型相当于提前背了答案评估结果虚高得离谱。我一开始用随机切分测试集RMSE 漂亮得吓人后来改成按时间顺序切分误差立刻上来了——这才正常。第二个坑是滞后窗口不要贪大。滞后 168 小时的特征会让每个样本的构造需要至少一周的前置数据造成有效样本量损失。对于跨度只有两三年的数据集这个损失必须认真算。我实验中用了三年数据构造完特征后实际可用样本大约剩八千多条。第三个坑是归一化时要小心极端尖峰。MinMaxScaler 对全局最小值和最大值非常敏感如果训练集里出现一次 200 欧元的尖峰归一化后大部分正常价格会被压缩到 0 到 0.3 之间模型很难学习细节。我建议用分位数裁剪先把 99.5% 分位以上的值截断到分位点再归一化。这样既能保留尖峰信息又不会让数值分布过于畸形。3. 深度学习模型设计与训练细节3.1 为什么选了 LSTM 而不是其他模型模型选型时我对比过三档方案线性回归、梯度提升树、LSTM。线性回归在时序特征上表现一般主要因为电价与特征之间存在大量非线性交互——比如同样的负荷上涨 1000MW在风电出力低的时段会显著推高价格但在风电出力高的时段价格几乎不变。这种交互效应线性模型很难刻画。梯度提升树如XGBoost、LightGBM可以拟合非线性在表格特征上效果相当好但它天然忽略样本之间的顺序关系。而电价预测恰好很依赖“最近三小时的价格走势”这种序列上下文。LSTM 的长处就在于能通过循环结构把整个窗口内的特征顺序信息压缩成一个隐状态向量再把向量映射到明天的价格上。我做实验也验证了这一点在同样特征下LSTM 的 RMSE 比 LightGBM 低大约 5%~8%在尖峰时段的预测偏差更是明显缩小。这说明序列信息确实有帮助。3.2 网络结构与核心代码具体的模型结构并不复杂核心是一个双层 LSTM 全连接回归头import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.head nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): # x shape: (batch, seq_len, features) out, _ self.lstm(x) # 取最后一个时间步的输出 out out[:, -1, :] return self.head(out).squeeze(-1)训练超参数我这里给一组亲测比较稳的配置序列长度取 72最近 72 小时batch size 128初始学习率 0.001使用 Adam 优化器损失函数用 HuberLoss。Huber 损失对极端尖峰不像 MSE 那么敏感兼顾了正常区间精度和尖峰鲁棒性。训练时加了两个细节一是梯度裁剪clip_grad_norm_(model.parameters(), 1.0)防止 LSTM 在长序列上出现梯度爆炸二是学习率在训练到 60 个 epoch 后衰减为原来的十分之一。这里要注意epoch 数不建议设太大否则模型会把历史尖峰模式背下来造成测试集上尖峰时刻的预测偏差大得离谱。3.3 训练过程的动态观测训练时我习惯记录每个 epoch 的训练集和验证集 loss。验证集不参与梯度更新只用于观察是否过拟合模型最终用验证集 loss 最低的那个 checkpoint。这里的验证集不是测试集——我用按时间顺序切分的三段前 70% 训练、中间 15% 验证、最后 15% 测试。三段之间没有重叠时间也连续排列这样可以最大限度模拟真实的“滚动预测”场景。还有一个小细节输入模型的特征矩阵要做成三维张量(batch_size, seq_len, n_features)每个时间步包含那一时刻的全部特征维度。构建这个张量时把前面特征工程的 DataFrame 按时间顺序切片然后滑动窗口生成样本即可。4. SHAP 可解释性分析实战4.1 SHAP 是干什么的我理解的 SHAP像是给模型的每次预测做一次“责任分配”。它的核心思想来自合作博弈论把每个特征当成一个参与者计算出它在当前样本预测值中的边际贡献。打个比方一场足球比赛输了 0:1你要分析责任。不能简单说“前锋不行”要拆解门将扑救几次、中场丢了几次球权、后卫哪个位置漏人、教练换人时机是否合理。SHAP 就是在做这个拆解——只不过分析对象是电价预测模型每个特征就是场上的球员。SHAP 的值有正有负。正值代表该特征把预测价格往上推负值代表往下压。数值越大影响越大。这一点比单纯的特征重要性矩阵信息量大得多——特征重要性只能告诉你“谁重要”SHAP 还能告诉你“往哪个方向重要、重要多少”。4.2 时序模型做 SHAP 的三个实操坑把 SHAP 用到 LSTM 上和用到 XGBoost 上完全是两码事。我踩过的坑主要有三个一个比一个隐蔽。第一个坑输入形状不匹配。shap 的很多解释器接受二维表格输入但 LSTM 的输入是(batch, seq_len, features)三维张量。直接用DeepExplainer传入三维数据结果虽然能算但输出的 shap_values 也是三维的每个样本的每个时间步每个特征都有一个贡献值。这个结果理论上最完整但实际上很难分析和可视化——近百个时间步根本看不过来。我的处理办法用窗口最后一个时间步的特征矩阵作为代表构造一个二维数据集再传入 SHAP 解释器。因为 LSTM 在预测时最后一个时间步的隐状态已经编码了整个窗口的信息取最后一个时间步做解释是一个工程上合理的折衷。第二个坑计算量太大。LSTM 反向传播求 SHAP 值本身就很慢如果你把全部测试集都丢进去算可能要跑几个小时。正确做法是抽样。随机选 200~500 个覆盖不同时段工作日/周末、高峰/低谷、晴天/阴天的样本计算后汇总分析。我在实验里选 300 个样本计算时间大约 5 分钟效果已经足够稳定。第三个坑DeepExplainer 对模型输入有要求。传入的background_data必须是torch.Tensor并且要和模型在同一个设备上否则会报设备不匹配。代码示例如下import shap import torch model.eval() background torch.from_numpy(X_train_last_step[:200]).float().to(device) explainer shap.DeepExplainer(model, background) X_sample torch.from_numpy(X_test_last_step[:300]).float().to(device) shap_values explainer.shap_values(X_sample)注意shap_values是 ModelOutput 类型的数组转成 numpy 后 shape 是(300, n_features)每一列对应一个特征。画图时直接用shap.summary_plot(shap_values, X_sample.cpu().numpy(), feature_namesfeature_names)即可。4.3 从 SHAP 图里读出了什么这个项目里最有意思的部分就是解读 SHAP 结果。第一个明确信号是滞后价格的主导地位。滞后 1 小时价格、滞后 24 小时价格的 SHAP 绝对值最大这说明电价有极强的自相关性。打个比方昨天下午 5 点的价格基本决定了今天下午 5 点价格的大致区间。这符合市场直觉但它的解释含义要谨慎这不是“历史价格是未来价格的原因”而是“历史价格隐含了当天供需平衡状态的信息”。第二个信号是风电出力与价格的负向关系。在大量样本中风电出力特征为高值时SHAP 值为负预测价格降低风电出力低时SHAP 值为正预测价格升高。原因是边际出清机制下高风电出力替代了高成本的燃气机组把系统边际价格压低。这个结论几乎在每个月份的子样本中都成立。第三个信号是负荷特征在傍晚时段的交互效应。单独看负荷的 SHAP 值不算最大但是把负荷和光伏出力放在一起看能发现明显的协同作用当傍晚负荷上升而光伏出力下降时价格会显著跳升。SHAP interaction value 可以把这种交互量化出来——两个特征同时出现时对预测值的额外贡献显著高于各自贡献的简单加总。这些结论对交易员或电力市场分析师来说是很有价值的。它不光是事后解释还能反过来指导特征选择如果风电和光伏的信息量足够大甚至可以考虑用数据再加工的方式把“净负荷 系统负荷 - 风电 - 光伏”作为一个新特征放进模型直接编码这种交互关系。5. 评估结果与场景扩展5.1 多维度评估不能只用 RMSE 糊弄我在项目收尾时做了四维评估总体误差、分时段误差、尖峰样本误差和方向准确率。总体误差用 RMSE 和 MAE。整体 RMSE 大约在 8~10 欧元/兆瓦时MAE 在 5~6 欧元/兆瓦时。考虑到西班牙市场日常价格区间在 0~100 欧元这个误差水平说明模型能把大趋势抓住但还做不到精确到个位数的程度。分时段误差里有意思的发现是午间时段误差最小因为光伏出力让这个时段的价格模式最稳定晚间高峰误差最大因为机组爬坡约束和需求突增带来了非线性跳变。尖峰样本测试单列了一组——把测试集中价格超过 120 欧元的样本单独统计。这些样本的预测普遍偏低说明模型学到了“总体规律”但还没有充分掌握“极端切换机制”。方向准确率指的是“明天比今天涨/跌”的预测正确比例。我用这个指标评估模型在交易场景下的实用性测试集上大约 72% 的方向准确率这已经能提供不错的交易决策参考。5.2 把框架迁移到其他市场这套工作流的迁移成本比我预想的低。核心原因是它不依赖某个市场特有的字段只依赖四类通用信号负荷、可再生能源出力、价格历史和周期性时间编码。换到其他区域市场时需要做的调整集中在三处小时粒度是否与目标市场一致、是否有负电价需要单独处理、以及是否有冷热电联产等额外系统特征。比如有些市场执行 15 分钟出清粒度这时候序列长度、滞后步长都要整体重新设计。负电价占比高的市场可以考虑先分类再做回归——先预测“是否出现负电价”再预测负电价时的具体值这种做法在极端样本上的表现往往比单模型好。5.3 后续可以扩展的方向这个项目做完之后我觉得最有价值的扩展方向是概率预测。目前模型输出的是确定性价格但实际交易中更关心的是“90% 置信区间落在哪个范围”。你可以用分位数回归的思路把输出层改成三个神经元分别预测 10%、50%、90% 分位数用分位数损失函数训练。这样预测结果从一条曲线变成一条区间带对风险管理场景更实用。另外SHAP 分析已经帮我们识别出了风电和光伏渗透率是关键变量下一步可以尝试在训练数据中按“高风电日/低风电日”分组训练多个专家模型再做一个门控网络来决定每个专家模型的权重。这种结构在极端天气样本比较多的市场里通常能有更稳定的表现。6. 实操中遇到的典型问题排障6.1 高频问题速查表症状可能原因解决办法验证集 loss 远高于训练集模型过拟合增大 dropout、减少 LSTM 层数、提前早停测试集误差异常大训练/测试切分时间顺序混乱确认按时间切片禁止随机切分预测结果整体偏低归一化时被极端尖峰带偏使用分位数裁剪后再归一化SHAP 计算时报维度错误输入三维数据未降到二维取最后一个时间步特征作为代表负电价样本预测完全不准回归模型对极端值不敏感考虑分类回归两阶段方案训练时 loss 出现 NaN学习率过高或梯度爆炸调低学习率、加梯度裁剪6.2 两个值得多聊几句的排查案例有个参数我之前一直调不对LSTM 的序列长度。一开始我用 168 小时想着“一周的完整周期信息总该更全吧”结果验证集误差反而比 72 小时差。后来发现原因是序列太长导致有效样本变少——数据首周被窗口吃掉了序列尾部的高阶特征也被模型当成噪声吸收了。最后定在 72 小时这是有效样本量和信息量之间的一个比较舒服的平衡点。另一个是 SHAP 分析时出现的“特征自相矛盾”现象同一特征在不同样本里 SHAP 值正负出现频率差不多一平均接近零。开始我以为算错了后来发现这是正常现象——说明该特征的作用是“调节变量”它的影响完全依赖其他特征的状态。比如负荷特征在白天的样本里 SHAP 为正深夜的样本里可能为负因为低价时段负荷增加不一定推高价格。遇到这种情况不要怀疑代码要去看交互效应或者按时间分组再画一次摘要图。最后分享一点实操体会这个项目前前后后我迭代了大概三个版本。第一版模型精度还行但解释性为零别人问我“为什么这个点预测涨了”我只能说“模型算出来的”。第二版加了 SHAP能解释特征贡献但特征工程做得糙有些结论经不起推敲。第三版重新梳理了特征和时间切分逻辑才算形成了一个“数据—模型—解释”闭环的作品。我个人的建议是如果你是第一次做这类项目一定要从数据可视化开始花两周时间把价格曲线、负荷曲线、风电光伏出力曲线在不同时间尺度上认真看一遍再动手建模。很多特征工程的灵感就是盯着图表看出来的。直接跳进调参训练看似进展快后期返工的成本反而更高。