☰
深度学习+SHAP:西班牙电力市场次日现货电价预测与可解释性实战
2026/10/20 19:12:27 网站建设 项目流程

电价预测这个话题,这几年做的人越来越多,但真正能拿来讲的门道其实不在模型多复杂,而在于你如何处理数据和解释结果。最近我在能源数据分析方向做了一项专题研究,研究对象是西班牙电力市场,核心任务就一条:用深度学习模型预测次日24小时的现货电价,然后叠加 SHAP 可解释性分析,把模型为什么给出这个预测拆解得明明白白。

西班牙市场有个非常“讨喜”的特点:可再生能源渗透率高得吓人,光伏、风电在发电结构里的占比很大,电价因此表现出典型的“鸭子曲线”——白天光伏大发时电价被压得很低,傍晚光伏功率掉下来以后电价又快速反弹。再加上电力市场普遍采用的边际定价机制,可再生能源近乎零边际成本,某些时段甚至会出现负电价。这种强波动、强周期、带尖峰的价格序列,正好是传统统计模型的噩梦,却是深度学习的用武之地,也是做可解释性分析的绝佳题材。

这篇内容我把完整路径捋了一遍:从为什么要做这个方向、怎么做特征工程、怎么搭建并训练深度模型,再到用 SHAP 如何把黑盒模型讲清楚,最后附上实操中容易踩的坑和扩展思路。无论你是做现货市场分析、电力交易辅助决策,还是想找一个带完整解释链路的时序预测案例,这篇应该都能给你实在的参考。先说清楚思路,再讲具体做法。

1. 项目定位与整体思路拆解

1.1 为什么选西班牙市场作为研究对象

选研究对象这件事,看起来稀松平常,实际上决定了问题难度的上界。我见过很多人一上来就挑历史电价很平稳的市场做预测,模型精度刷得很漂亮,但业务上一问“波动大的时候怎么办”就卡壳。西班牙市场在这一点上属于“困难模式”,很能暴露方法的真实水平,研究结论也更容易迁移到其他波动性较高的市场。

首先是数据可得性。欧洲不少电力市场的交易数据、负荷数据、新能源出力数据都是公开的,西班牙市场的数据体系也比较完整——小时级的历史负荷、风力发电、光伏发电、现货电价都能拿到,时间跨度足够长。数据是深度学习的“口粮”,如果数据不完整,后面模型再花哨也白搭。

其次是市场行为足够复杂。西班牙电价序列里你能同时看到强日周期(24小时节奏非常明显)、强周周期(工作日与周末模式不同)、气象冲击(风电突然大发导致电价往下跌)、以及偶发的尖峰和负电价。这意味着模型不能靠一两个滞后项就把问题搞定,它需要同时捕捉多尺度时间依赖和外部变量交互,正好是验证深度学习的合适场景。

最后,从业务角度看,现货电价预测是电力交易、风险管理、储能策略优化的底层依赖。预测误差直接关系到收益和成本,业务方对精度和可解释性的要求同时存在。这也引出了项目里第二个关键选型:为什么深度学习之外一定要搭配 SHAP。

1.2 技术选型逻辑:深度学习解决什么,SHAP 补上什么

传统电价预测里,ARIMA、GARCH、带外生变量的回归模型用得很多,原理清晰、计算轻量。但这类方法对多变量非线性交互的建模能力有限。你今天不只要用电价的历史序列,还要把负荷需求、风光出力、日历时间这些外生信号一起揉进预测;明天还想看这些信号如何共同影响预测结果。传统统计模型处理这种“高维变量+多尺度时间依赖”的场景会非常吃力,特征工程做起来也极其繁琐。

深度学习则把这些事简化了很多。以 LSTM 和 Transformer 为代表的序列模型,可以直接吃原始特征序列,让网络自己去学滞后依赖和非线性关系。实践下来,至少在两个点上深度模型的优势是明显的:一是它能隐式建模“过去一周同一时刻电价→当前时刻电价”这类周期性模式,二是在光伏、风电出力突然变化时,模型能够捕捉到价格对供给端冲击的非线性响应。

但深度学习有个绕不开的毛病:黑盒。交易员拿到一个预测值,脑子里第一反应是“这个预测凭什么这么给”。如果你只能说“这是神经网络算出来的”,那这套系统很难真正被器重。业务侧需要的解释不是“用了什么算法”,而是“本次预测中哪个因素影响最大、影响方向是什么、和过去典型情况是否一致”。这就是 SHAP 的用武之地。

SHAP 做的事情从直观上讲,是把单个样本的预测值拆解为各个特征的贡献之和。它源于合作博弈论中的夏普利值思想,在模型解释这块已经是非常通用的工具。项目里我的做法很简单:先用深度学习模型把预测精度做到位,再用 SHAP 对模型行为做全局和局部的解释。模型负责“准”,SHAP 负责“说得清”,两者一配合,整个技术链路在业务上才真正完整。

2. 数据准备与特征工程:把时序问题变成监督学习

2.1 数据字段与时间粒度

先交代一下我最终用到的数据集合。这个项目的数据全部来自公开可获取的电力系统运行数据,时间粒度统一为1小时。原始字段包括以下这些:

  • 系统总负荷(MWh),反映整体需求水平;
  • 风力发电出力(MWh),西班牙风电装机大,出力波动非常剧烈;
  • 光伏发电出力(MWh),光伏出力有明显的白天“钟形”曲线;
  • 日前市场电价(EUR/MWh),也就是模型的预测目标;
  • 对应小时的计算日历特征,包括小时序号、星期几、是否节假日。

光有这些还不够,特征工程的核心是把原始序列转换成模型需要的形式,并构造出“周期、趋势、突变”三类信息。

2.2 滑窗设计:为什么要看过去168小时

深度时序模型首先需要确定一个输入窗口长度。我尝试过24小时、72小时、168小时三档,最终选择的是168小时输入 + 24小时输出。

这个选择的逻辑说起来很简单:电价序列存在明显的日周期和周周期。24小时窗口只能覆盖单日模式,遇到“今天是周三”和“上周三”的模式相似性就无法建模;168小时即过去整整一周,模型能同时看到完整的日周期、周周期以及周内工作日与周末的差异。从实际效果看,窗口从24小时扩到168小时带来的精度提升非常可观,尤其对周末和节假日前后的预测改善明显。

输出侧我直接预测未来24个连续小时的电价,也就是一次性输出24个点的序列,而不是递归地一步推一步。递归预测误差会累积,序列到序列的直接多步输出在实践中更好训练,也更容易控制.

构造监督学习样本时,数据组织成这样的形式:每个样本包含一个168小时的特征矩阵X,和对应未来24小时的目标向量y。相邻样本之间我做了滑动切分,步长可以设置为24小时,避免样本之间过度重叠导致冗余。

2.3 数据集切分、归一化与负电价处理

数据切分这块是时间序列项目最容易出错的地方。绝不能像普通分类任务那样随机打乱。我的切分方式是严格按照时间顺序切:取最早70%作为训练集,中间15%作为验证集,最后15%作为测试集。验证集用于早停和超参选择,测试集只用于最终评估,保证“预测未来”这个场景的真实感。

归一化上,我推荐对连续数值特征用Z-score标准化,而不是Min-Max缩放。原因是电价序列里存在明显的尖峰,Min-Max会把绝大多数正常区间压缩得非常窄,模型学习效率下降。Z-score对离群值的鲁棒性相对更好。不过有一点极其重要:标准化所用的均值和标准差必须在训练集上计算,验证集和测试集直接复用训练集的统计量,绝对不能在预处理时把全量数据的统计量算进去——这属于典型的特征泄漏,后面我会再强调。

西班牙市场会有负电价,这不是脏数据,是真实的市场信号。负电价通常出现在风光大发且负荷低迷的时段,模型如果能把这种事件预测出来,业务价值反而很高。处理原则是:不删除负电价样本,不把它们截断为零,保留原始市场信息。唯一要小心的是评估指标对接近零的值非常敏感,这一点需要在下游单独考虑。

2.4 评估指标:MAE、RMSE 与 MAPE 的取舍

电价预测任务里,评估指标选不好,结果很容易自欺欺人。常用指标有三个,我的建议如下:

  • MAE(平均绝对误差):最稳的指标,对尖峰不过度惩罚,适合作为主评估指标。
  • RMSE(均方根误差):对大误差惩罚更重,适合用于关注极端预测失误的业务场景。
  • MAPE(平均绝对百分比误差):在电价任务里非常容易爆炸,因为真实值一旦接近零,百分比误差就会变得无穷大;负电价情况下更是直接失真。不建议作为主控指标,顶多作为参考。

除了整体指标,我还会分时段看误差——高峰时段、低谷时段、平段分别计算MAE。这样能定位模型的薄弱点,比如“原来晚上20点的尖峰预测误差最大”,然后再针对性地调整特征。

3. 深度学习模型搭建与训练:LSTM 与 Transformer 对照

3.1 从原始序列到可训练样本

在模型代码之前,先把数据管道说清楚。我用PyTorch写数据加载器,核心逻辑如下:

import torch from torch.utils.data import Dataset class PriceDataset(Dataset): def __init__(self, features, targets): self.features = features # shape: (样本数, 168, 特征维度) self.targets = targets # shape: (样本数, 24) def __len__(self): return len(self.features) def __getitem__(self, idx): return ( torch.tensor(self.features[idx], dtype=torch.float32), torch.tensor(self.targets[idx], dtype=torch.float32) )

特征矩阵的形状是 (样本数, 时间步长, 特征数),其中时间步长168小时,特征数大约在10个左右。模型输入直接喂这个三维张量即可。

3.2 LSTM 基线模型

LSTM作为序列预测的经典基线,我通常在模型对比中保留一个。它的优势是参数量适中、训练稳定、对小规模数据更友好。核心结构是这样:

import torch.nn as nn class LSTMPriceModel(nn.Module): def __init__(self, input_size, hidden_size=128, num_layers=2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=0.2 ) self.head = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, 24) ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, 168, hidden) last_hidden = out[:, -1, :] # 取最后一个时间步 return self.head(last_hidden)

这里有几个细节值得讲。第一,取最后一个时间步的隐藏状态作为整段序列的总结;第二,LSTM层数选了2层,再深容易过拟合且收益有限;第三,Dropout放在LSTM层间和输出层前,用于缓解过拟合。这个基线模型的参数量不大,训练速度也快,适合作为后续所有改进的对照基准。

3.3 Transformer 对比模型

Transformer在长序列建模上的优势这几年已经被验证得很充分了。电价序列里“上周三同一时段电价”对当前预测有很强的参考价值,而这种长距离依赖正是Transformer注意力机制擅长的。我实现的版本是参考标准Transformer编码器结构做了简化,去掉了Decoder部分,直接让编码器输出进入回归头。

import torch.nn as nn class TransformerPriceModel(nn.Module): def __init__(self, input_size, d_model=128, nhead=8, num_layers=3): super().__init__() self.input_proj = nn.Linear(input_size, d_model) self.pos_encoding = PositionalEncoding(d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=512, dropout=0.1, batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.head = nn.Sequential( nn.Linear(d_model, 64), nn.ReLU(), nn.Linear(64, 24) ) def forward(self, x): x = self.input_proj(x) x = self.pos_encoding(x) x = self.encoder(x) # 对整条序列做池化,综合全局信息 x = x.mean(dim=1) return self.head(x)

位置编码是实现Transformer时很容易漏的一环。电价序列顺序本身就包含着信息,如果不加位置编码,模型会把所有时间步当成无序集合来处理,效果会打很大折扣。我在这里特地保留了一个简单的正弦位置编码实现,实践里效果明显优于没有位置编码的版本。

关于注意力层数我试过1层到4层,3层时训练时间和效果平衡得最好。再深的话,在这个体量的数据集上收益很小,反而训练时间翻倍。

3.4 训练配置与对比观察

训练细节对深度模型最终效果的影响,有时候比模型架构本身还大。我的配置大致如下:

  • 损失函数:MAE(L1Loss),对电价尖峰更稳;
  • 优化器:AdamW,初始学习率1e-3,权重衰减1e-5;
  • 批大小:64;
  • 轮数:最多60轮,配合早停;
  • 学习率调度:ReduceLROnPlateau,验证损失连续3轮不降就把学习率乘以0.5;
  • 早停:验证损失连续8轮不降就停止训练;
  • 梯度裁剪:max_norm=1.0,防止Transformer训练初期出现loss爆炸。

实验对比的思路也要说清楚。不是随便训练两个模型就结束,而是固定同一套训练集、验证集、测试集和数据预处理流程,保证唯一变量是模型架构。我在实际项目里最常见的结果模式是:Transformer在长周期模式上的表现略优于LSTM,尤其是在周六、周日这类与工作日模式差异较大的日子;LSTM在贴近训练分布的极端尖峰时段则会追得更紧。整体上Transformer通常能带来几个百分点的RMSE改善,但代价是训练时间明显增加。因此如果你的计算资源有限,LSTM仍然是个完全可用的方案;如果你追求更好一点的精度,Transformer值得上。

4. SHAP 可解释性分析:把黑盒模型一层层拆开

4.1 SHAP 原理:一句话人话版

SHAP 的数学源头是合作博弈论里的夏普利值,但抛开数学,它的核心思想可以这样理解:对于一个具体的预测样本,模型的预测值是由各个特征共同作用的结果,SHAP 做的事情是把“最终预测值相对于平均预测值的那部分差异”,公平地分配到每一个特征头上。所有特征的贡献加起来,刚好等于这个样本的预测值减去基线值。

这样一个一个样本去做归因,最后再汇总,就能得到全局层面“哪些特征最重要、影响方向如何”的结论。它和我们常说的特征重要性最大的区别就在于:特征重要性只告诉你“这个特征重不重要”,SHAP 还告诉你“当特征值变大时,预测是往上走还是往下走”,以及这个影响在什么区间最明显。

4.2 时序深度模型上怎么用 SHAP

对深度模型做SHAP分析,最常用的是DeepExplainer。它专门针对深度学习网络,利用反向传播的梯度来计算近似贡献值,速度快,但对模型结构有一定要求。另一种通用方案是KernelExplainer,它可以解释任何黑盒模型,但计算量太大——对样本量稍多一些的数据集,可能要跑非常久。实际项目中,我一般先用DeepExplainer在验证集上做抽样分析。

具体操作有几个关键细节:

import shap # 1. 从训练集随机抽样,作为背景数据集 background = train_dataset.features[:200] # 2. 用 DeepExplainer 包住模型 explainer = shap.DeepExplainer(model, torch.tensor(background, dtype=torch.float32)) # 3. 在测试集上选一批样本做解释 test_sample = torch.tensor(test_dataset.features[:50], dtype=torch.float32) shap_values = explainer.shap_values(test_sample)

这里第一个坑是背景数据的选择。背景数据集的作用是近似“没有这个特征时模型的预期输出”,样本数太多会非常慢,太少则近似误差大。我一般取200条上下,考虑到电价序列的周期性,背景样本会尽量覆盖到一周内的不同小时。

第二个坑是解释结果的维度。因为我们的输出是24个小时的预测,所以每个样本会得到24组SHAP值。拿到这个多维数组之后,需要决定自己是逐个时刻看局部解释,还是把24个时刻的SHAP值按特征维度做平均或加和,得到整段预测的全局解释。两种视角都有价值,我在项目里两种都做了,下面细说。

4.3 三类图怎么读:摘要图、依赖图、力图

摘要图(Summary Plot)解决的是“全局哪些特征重要”的问题。把测试集所有样本的SHAP值按特征汇总,按平均绝对SHAP值从大到小排序,同时用颜色表示特征值高低,就能一眼看到每个特征的重要程度和影响方向。

在电价预测场景里,我见到的高频规律通常是:历史滞后电价的重要性极高,尤其是对应时刻、对应一周前的滞后值;系统负荷和光伏出力排在第二梯队;风力出力在特定时段会突然冒到前列,通常对应风电大发的日子。摘要图能帮我们快速验证模型学到的东西是否符合电力市场常识。

依赖图(Dependence Plot)关注的是单个特征与预测值之间的非线性关系。比如看光伏出力这个特征,SHAP依赖图通常会显示出一种这样的关系:光伏出力从小到大的过程里,对预测电价的负向影响持续累积,但当光伏出力已经很高时,继续增加出力的边际压低效应会减弱。这就把西班牙市场里的“光伏对电价的压制”这个业务直觉用数据曲线验证了下来。

力图(Force Plot)是单样本解释工具,用来回答“为什么这个时刻模型给出了一个特别高或特别低的预测”。比如面对一个预测电价明显偏高的样本,Force Plot会把主要贡献者列出:可能是当前小时正好是晚高峰,负荷特征把预测往上推;而同时光伏出力已经降到接近零,也在往上推。这种解释对于业务侧用户来说,比单纯一个数字有说服力得多。

我后来还把SHAP值落到一张报表里:每个小时对应的预测值、以及贡献最大的前三个特征和贡献方向。这比任何模型精度指标都更能让业务侧建立信任感,也是可解释性分析落地价值的最佳体现。

5. 实操中的坑与排查经验

5.1 特征泄漏:最隐蔽也最容易犯的错误

特征泄漏在时序项目里是个大坑,它不会让你的程序报错,只会让你在测试集上得到一个“虚假的好成绩”,等到真实部署时模型立刻失效。我总结出三种常见的泄漏形式。

第一种是归一化统计量泄漏。预处理时如果把所有数据的均值、标准差都算好再切分,测试集的信息在训练阶段就已经泄露了。正确做法是先切分,再只基于训练集计算统计量。

第二种是特征构造泄漏。比如你想用“当天平均负荷”做特征,但这个信息在当天还没结束时是不可能拿到的。任何用到未来信息的特征都会让模型学会“偷看答案”。

第三种是缺失值填充泄漏。用全量数据的中位数或均值去填充缺失值,同样引入了未来信息。填充时也必须只用训练集的统计量。

我在项目里自查泄漏的方式很简单:训练集和测试集分别统计每个特征的真实值分布,如果测试集某个特征的统计量被异常对齐到训练集分布,就要警惕是不是预处理阶段出了问题。

5.2 模型训练不稳定的应对方法

Transformer 在电力数据这种长序列上训练,一个典型问题是初期会偶发 loss 突然飙高。我总结下来,两个手段最有效:一是梯度裁剪,设置max_norm=1.0;二是把预热轮数加上,学习率在头几个epoch从很小的值逐步爬升到目标值。不管哪个模型,学习率太大都会出现前期震荡,宁可把学习率调低一些,多训练几轮。

还有一个容易被忽略的点是批量大小的选择。在时间序列任务里,我建议每个batch尽量覆盖不同的星期特征,而不是让一个batch里全是某个相近时段的样本。具体做法是构造DataLoader时随机抽样打乱样本顺序,确保每个batch有足够的多样性。

5.3 常见问题速查表

现象可能原因解决方法
测试集指标远好于验证集归一化或特征构造存在泄漏检查预处理是否只用了训练集统计量
MAPE指标爆炸真实电价接近零/负值改用MAE或对价格区间分段评估
Transformer loss突增学习率过高加梯度裁剪、加学习率预热
SHAP计算极慢背景样本太多或使用了KernelExplainer减少背景样本,改用DeepExplainer
周末预测误差明显增大周末样本在训练集中占比天然偏低针对周末样本单独评估,必要时加权采样
模型对尖峰时段反应迟钝尖峰样本少,损失被平段样本稀释可采用分层损失或做极端时段专项分析

这张表看起来简单,每一条背后都是实际项目中真金白银的教训。尤其是特征泄漏那一条,我见过太多次“离线指标好看、上线就拉胯”的情况,最后翻来覆去查,十有八九是预处理阶段的未来信息泄漏。

6. 项目扩展方向:从点预测走向决策支持

6.1 从点预测升级到区间预测

目前这个项目输出的是逐小时电价的点预测,也就是“最可能的取值”。但风险管理场景往往更关心“电价有多大可能落在什么范围”,这就从点预测扩展到了概率预测。

一个常见的做法是把模型输出从确定性回归改成分位数回归。把损失函数换成Pinball Loss,分别训练多个头,每个头预测不同的分位点(比如10%、50%、90%),就能得到预测区间。这个方法改动不大,但对模型结构要做一些调整,比如输出层的维度从24变成24乘以分位数个数。

我自己的经验是,分位数回归对业务侧的吸引力非常大,因为它给了明确的“不确定性”度量。无风险的决策通常不需要不确定度,但电力交易本质上就是在不确定性里做选择,只给一个点值,业务侧其实很难直接下决策。

6.2 把预测和解释打包进决策流程

项目做到最后,我最大的体会是:预测精度和可解释性不是两个割裂的事情,它们需要一起推向生产环境。我在项目里最后搭了一个小流程:每天定时拉取最新数据,重推理模型得到次日24小时电价预测,同时计算对应的SHAP解释值,生成一份HTML解释报告。报告里既有预测曲线,也有每个时段的主要影响特征和贡献方向。

这个流程跑通之后,业务侧再讨论预测结果时,话题就不再是“这个模型准不准”,而是“今天这个高点主要受什么因素影响、光伏出力怎么变化、我们应该怎样调整策略”。当对话变成这个样子,技术方案才算是真正在业务中扎下了根。

坦白说,目前市面上做电价预测的公开方案很多,但把深度学习模型、特征工程、SHAP解释、工程化部署串成一条完整链路的参考材料并不多。这个项目做完后,我自己最大的收获不是把模型精度刷到了多少个点,而是搞清楚了一个问题:模型要能被真正信任、被真正用起来,光靠精确度远远不够,还得把模型的思考过程翻译成业务听得懂的语言。希望这篇总结也能帮你省下一点走弯路的时间。

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

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

立即咨询