电价预测这个话题我一直觉得是时间序列里最有嚼头的一类问题——它不像气象预测那样纯粹“靠天吃饭”也不像股票预测那样充满无规律噪音它有清晰的供需框架、有政策驱动的结构性突变、也有真实的交易价值。最近我把一套基于深度学习与 SHAP 可解释性分析的西班牙电力市场电价预测研究完整跑了一遍从数据清洗到模型训练再到用 SHAP 逐特征拆解预测来源整个过程走下来收获非常大。这篇文章就把整体思路、最关键的特征处理逻辑、核心代码片段以及踩过的坑都整理出来给准备进场做能源数据分析、电力市场研究或者模型解释工程的同学做一个参考。1. 项目背景与整体思路拆解1.1 为什么锁定这个电力市场做电价预测欧洲的电力市场普遍采用区域边际定价机制西班牙所处的这个市场也不例外发电机组按报价排队出清最后一个被调用的机组决定所有机组的统一结算价格。这意味着电价不是简单的“成本加利润”而是在一个供需博弈的拍卖过程中形成的天然带有强烈的非线性特征。更棘手的是当地电源结构。风电和光伏在装机中占比非常高天气好、风大的时段边际成本接近零的可再生能源会把价格压得很低甚至出现负电价反过来无风无光且负荷拉满的时候燃气机组顶上价格会迅速冲上高位。我拉出来的历史序列里一天之内从几欧元到上百欧元的波动时有发生。再加上燃料价格、碳排放配额价格、跨境输电状况以及节假日导致的负荷形态变化这些因素叠在一起让电价序列具备长记忆、突变和异方差特性。用传统统计模型去拟合这种数据往往要设计大量干预项和分段切换逻辑。而深度学习模型可以从历史样本中自动学习复杂映射这也是我最终把主线放在深度学习上的根本原因。1.2 方案选型深度学习为什么比统计模型更合适先说明态度我并不排斥传统方法做基线对比或者快速验证时ARIMAX、GARCH族和树模型都能用。但在这个场景里目标不是单纯拟合而是要同时利用多变量、长时间依赖和多重季节性。树模型处理表格特征很强但它把时间关系展平成静态特征后对长程依赖的表达完全靠手工构造滞后项LSTM、GRU、TCN这类结构天然按序列处理更容易捕捉时间上的连续依赖。我在这个项目里把候选模型收敛到三个方向做横向对比LSTM、CNN-LSTM和Transformer。LSTM是经典递归结构处理24到72步的窗口非常稳CNN-LSTM先用一维卷积提取局部模式再交给LSTM建模长期依赖收敛速度和精度都优于纯LSTMTransformer理论上注意力最强但这个数据量级下优势不明显反而对训练超参数更敏感。下面是本次实验的实际记录以欧元/MWh为单位模型MAERMSE训练耗时备注线性回归基线9.8814.22秒级只能刻画线性趋势XGBoost7.2411.05分钟级滞后特征要手工堆量LSTM6.419.78分钟级深度基线CNN-LSTM5.969.12分钟级本方案主力Transformer6.129.31中等数据有限时不稳定最终我以 CNN-LSTM 为主模型。横向对比里它精度最高训练也快后续做 SHAP 解释时它的结构兼容性也更好。1.3 加一层 SHAP 可解释性到底解决了什么如果只追求预测精度模型训练完项目其实就可以交付了。但电力市场预测的使用方是交易员、调度员和政策研究人员他们不会接受一个不可解释的黑箱直接给报价建议。这也是我在这个项目里把 SHAP 可解释性分析放到核心位置的原因。SHAP 的理论基础是合作博弈论中的 Shapley 值核心思想是把最终预测值当作所有特征共同创造的“收益”然后计算每个特征在所有特征组合下的平均边际贡献。它比 LIME 稳定有数学上的一致性保证既能看全局特征重要性也能看单条样本的归因。对深度学习模型我使用 GradientExplainer它基于期望梯度思想把预测值归因到输入特征上不需要像 KernelExplainer 那样反复采样模型推理速度上更适合神经网络。实际跑完之后我发现 SHAP 给出的结论不只是“哪个特征重要”还能暴露错误特征、提示交互效应这在后面会展开讲。2. 核心细节解析数据清洗与特征工程怎么才靠谱2.1 数据源与预处理三道工序不能省数据建议直接通过公开电力市场接口获取一般是逐小时的系统负荷、电价、各电源出力序列。拿到手别急着训练先做三件事时间轴对齐、缺失值处理、异常值甄别。时间轴对齐最容易被忽略。这个市场本地时间和常见气象数据的 UTC 时间存在冬夏令时差异如果用 UTC 直接对齐市场数据每天 24 小时序列里会出现两个小时错位模型学到的会是错误的对应关系。我的做法是统一转为市场当地时间后再合并并额外保留“小时序号”特征供模型学习日内节奏。缺失值要看具体成因。个别小时档位的缺失用线性插值即可如果出现连续多天空档我会用同星期几同时段的历史均值填充或者干脆裁剪掉这段数据避免污染训练集。最忌讳的是拿整个序列的均值去填空那会把波动整体压低模型学到的信号完全失真。异常值要区分“脏数据”和“真实极端事件”。负电价不是脏数据它是市场真实状态必须保留而某个小时录成负数、前后小时完全正常高值基本可以判定为录入错误这种才需要修正。我在项目里用前后24小时的滚动中位数做初步筛查再结合人工抽检确认。2.2 特征工程五组特征对应五条市场逻辑我把特征整理成五组每一组背后都对应明确的市场逻辑负荷侧系统总负荷的历史数据与预测值。负荷是电价最直接的牵引力早晚高峰负荷抬升边际机组切换电价随之上跳。电源侧风电出力、光伏出力、水电和燃气出力。这个市场可再生能源占比高大风时段电价被压制尤其明显而且存在若干小时的滞后效应。价格传导类天然气价格、碳排放配额价格。边际定价机组成本的重要组成部分尤其无风无光时段这两项直接决定高价区水位。日历特征小时序号、星期几、是否节假日。电力市场有清晰的日内和周循环工作日与周末负荷形态差异很大。天气类温度、风速、云量和辐照度特别是滞后若干小时的风速因为它先影响风电出力再间接传导到电价。构造特征时有一个反复要问的问题这个变量在预测时刻是否真实可知比如预测 t 时刻电价t 时刻的负荷真实值通常是拿不到的只能用系统发布的负荷预测值风光出力同理模型输入里要使用预报值而不是实测值否则会造成严重的数据泄露测试误差会假性低到离谱。这是做预测项目最容易翻车的地方。小时特征我记得不要直接用 0 到 23 的整数喂给模型否则 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[dow_sin] np.sin(2 * np.pi * df[dayofweek] / 7) df[dow_cos] np.cos(2 * np.pi * df[dayofweek] / 7)2.3 滑窗构造与数据划分时间序列的三条铁律时间序列建模的样本构造和普通表格数据不一样这个项目里我严格遵循三条铁律。第一条滑窗不能混信息。假设用过去 72 小时预测未来一小时每个样本的输入是 [t-72, t-1]标签是 t 时刻电价。构造样本时输入窗口和标签窗口不能重叠更不能把标签拼进输入特征。我见过有人为了“增强特征”把前一天的同一小时电价放进去结果模型直接记住周期性复制验证集误差漂亮得吓人一上真实场景就崩。第二条归一化参数只能在训练集上计算。很多同学习惯用全量数据的 min 和 max 做 MinMaxScaler这在普通机器学习里问题不大但在时间序列里是未来信息泄露。测试集的值已经参与了归一化尺度计算模型在训练时就“偷看”了测试分布。标准做法是先按时间切分再只在训练集上 fit再分别 transform 验证集和测试集。第三条按时间切分而不是随机切分。随机切分会把未来信息混进训练集模型看似精度很高泛化能力一塌糊涂。我采用 7:1:2 的比例按时间顺序切分前 70% 训练中间 10% 验证最后 20% 测试保证验证集和测试集在时间上都晚于训练集。这三条做到位后面的训练才谈得上可信。3. 实操过程从模型搭建到 SHAP 可视化全流程3.1 环境准备与项目结构依赖库主要是pytorch、numpy、pandas、scikit-learn、shap、matplotlib。我把工程按模块拆开避免后续换模型时推倒重来electricity_price_forecast/ ├── data/ # 原始数据与特征缓存 ├── src/ │ ├── data_prep.py # 数据加载、清洗、特征工程 │ ├── model.py # 模型定义 │ ├── train.py # 训练与评估 │ └── explain.py # SHAP 分析脚本 └── output/ # 模型权重、图、结果表数据管道和模型分开管理是最重要的特征迭代的频率远高于模型迭代有清晰的边界能省下大量时间。3.2 模型搭建与训练配置CNN-LSTM 模型定义我用 PyTorch 实现核心结构如下import torch import torch.nn as nn class CNNLSTM(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, dropout0.2): super().__init__() self.conv1 nn.Conv1d(n_features, 32, kernel_size3, padding1) self.conv2 nn.Conv1d(32, 32, kernel_size3, padding1) self.lstm nn.LSTM(32, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.head nn.Sequential( nn.Linear(hidden_size, 16), nn.ReLU(), nn.Linear(16, 1) ) def forward(self, x): # x: (batch, seq_len, n_features) x x.permute(0, 2, 1) x torch.relu(self.conv1(x)) x torch.relu(self.conv2(x)) x x.permute(0, 2, 1) out, _ self.lstm(x) out self.head(out[:, -1, :]) return out.squeeze(-1)这里有几个参数选择不是随便定的。一维卷积的kernel_size3相当于只看前后各一小时把局部趋势先抽出来再用两层卷积加深感受野最后接 LSTM 建模更长的依赖。head只取最后一个时间步的隐状态对应预测下一个时刻的电价。训练配置上损失函数我选 MAE 而不是 MSE。MSE 对异常尖峰过于敏感会把模型带偏训练出来的曲线容易被极端值拉扯MAE 更平稳预测曲线不容易被个别尖峰带跑偏。优化器用 Adam初始学习率 1e-3配合ReduceLROnPlateau验证损失连续 5 个 epoch 不下降就把学习率减半。batch_size64epochs 上限 100通过 early stopping 在验证集连续 10 轮不更新时停止。还要做梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)这个动作在电价这类带尖峰的数据上非常重要不裁的话偶尔一个异常大梯度就会让训练直接发散。3.3 评估指标怎么选预测效果怎么验证常规指标我同时报告 MAE、RMSE 和 MAPE另外增加一个Peak_MAE只统计每天 18 点到 21 点峰值时段的绝对误差。为什么单列这个指标因为交易策略最关心高峰时段的价格峰值判断不准整体误差再低也没有业务价值。MAPE 在这个场景里需要小心电价偶尔会接近甚至等于零直接计算会出现无穷大。我的做法是在分母加一个平滑项比如(|y_true| 1e-3)同时单独记录正式 MAPE 值避免指标失真。效果验证除了看整体误差还要拆两个维度看一是分小时的误差分布画出 24 小时 MAE 柱状图后基本会发现高峰时段误差最大二是分场景把正常日、负电价日、极端尖峰日分开统计。这种拆分能帮助判断模型是“整体均衡但平庸”还是“日常精准但极端场景失效”后者往往更符合业务预期。本次实验的具体水平CNN-LSTM 整体测试 MAE 约 5.96、RMSE 约 9.12峰值时段 MAE 约 8.4。这个精度在跨日预测场景里已经能支撑策略层面的进一步应用。3.4 SHAP 可解释性分析实战与结果解读SHAP 分析的核心代码并不复杂关键在前面铺垫的数据准备工作。我的实现如下import numpy as np import shap # 使用 KMeans 从训练集中挑选 50 个代表样本作为背景数据 background shap.kmeans(X_train, k50) explainer shap.GradientExplainer(model, background) # 对测试集前 200 个样本做解释 shap_values explainer.shap_values(X_test[:200]) # 蜂群图查看全局特征影响排序 shap.summary_plot(shap_values[0], X_test[:200], feature_namesfeature_names, max_display12)背景数据为什么用 KMeans 采样到 50 个因为 GradientExplainer 内部要对背景样本做积分路径计算背景样本越多耗时越长50 个已经能覆盖数据分布的主干结构又不会让计算卡住。这是我在多次实验后认为性价比最高的配置。这次跑出来的 SHAP 结果有几个很有意思的发现。排名第一的特征是“过去 24 小时均价”但它的影响方向并不是单调的价格处于不同水位时它对未来价格的推动方向会变化这是模型自己学出来的机制。风电出力排在第二方向为负直接印证了“大风压价”的市场直觉。小时序号排在第三贡献呈现明显的分段特征凌晨时段负贡献、早晚高峰正贡献。碳排放价格的贡献则主要体现在无风时段依赖图能清楚看到它和风电出力之间的交互效应。这些结论对业务方来说比单纯丢一个预测数字有用得多。交易员可以据此判断当前预测是主要由价格惯性驱动还是由可再生出力驱动从而决定要不要对预测结果做人工修正。4. 常见问题与排查技巧实录4.1 训练阶段最常踩的几个坑第一个坑是特征里混入未来信息。预测 t 时刻电价时把 t 时刻的负荷实测值放进输入特征测试误差会低到不可思议但这只是自欺欺人。排查方法是逐个特征检查业务时间线凡是目标时刻才能拿到的实测值一律换成预报值或滞后值。第二个坑是归一化参数泄露。MinMaxScaler的fit必须只作用在训练集上如果对全量数据fit_transform测试集信息已经被编码进训练过程模型评估结果偏乐观。这个错误在代码 review 时很难发现需要在数据管道里用 Pipeline 强制保证顺序。第三个坑是模型“记忆”而不是“学习”。典型案例是训练集 MAE 极低、验证集快速上升这时候先检查滑窗是否错位、是否有标签复用。我记得有一次手工循环构造样本索引从 0 开始写错了偏移量导致标签恰好和输入的最后几位重叠模型直接记住了答案。4.2 预测“滞后”和“低估”的针对性修复滞后现象是很多时间序列模型共通的毛病预测曲线总比真实曲线慢一拍尤其在价格拐点区域。根源在于模型学到的是“用最近值外推”的保守策略从损失函数角度看这能把整体误差压到最小但结构性偏移依然存在。我的缓解方法是拉长输入窗口从 24 拉到 48 甚至 72 小时让模型看到更早的趋势再加入相邻小时差分特征帮助模型识别拐点。低估现象则是所有点估计模型的通病MSE 和 MAE 都天然倾向于预测条件均值尖峰被平滑掉。业务上如果要用预测做交易决策我建议引入分位数损失直接预测 0.95 这类高分位相当于拿到一个偏高但保守的估计或者使用加权损失把高峰时段的 MAE 权重提高 1.5 倍付出一点非高峰精度换取关键时段的表现。4.3 SHAP 实施中的细节问题背景样本数量不是越多越好我用 KMeans 采样到 50 个实例解释结果已经稳定计算时间也控制在可接受范围内。特征很多时summary_plot会挤成一团需要用max_display限制显示条数再按feature_names排序只展示贡献度靠前的十几个特征。还要注意递归层的解释速度会明显慢于卷积层。实测中先把X_test转成 numpy 数组再把 tensor 放到同一设备上可以有效减少逐样本的设备切换开销。DeepExplainer 对某些自定义结构兼容性一般我最终还是选 GradientExplainer它在 LSTM、卷积混合结构上的稳定性明显更好。4.4 这个项目还能怎么扩展这套框架换场景的成本主要在于特征工程模型和解释链路是通用的。可以扩展到日内市场预测日前价格出来后直接以日内增量为目标做第二阶段预测也可以改分位数损失做区间预测电力交易天然需要区间而不是点估计。规模上还可以考虑多市场联合预测把跨境交换容量和邻国电价纳入特征毕竟互联线路两侧的价格会相互影响。落地环节可以用滚动窗口的方式做在线更新每隔一天用最新数据重训一个 epoch让模型跟随市场状态漂移同时保留 SHAP 可视化的监测面板一旦特征归因出现异常漂移能及时发现市场结构变化或者数据质量问题。最后说一点个人体会。电价预测项目的难点从来不在模型结构多高级而在于是否愿意花时间理解市场机制和清洗数据。特征工程里的每一个假设最终都会在 SHAP 的可视化里得到验证或被打脸这种“假设、建模、解释、修正”的循环是提升预测系统最扎实的路径。如果你刚开始接触这类项目我的建议是先下载某个公开市场三年的数据跑通一条最小可行链路——数据、训练、SHAP 可视化再回头优化特征和模型。整个过程完整走一遍之后你会对“电价的每一次波动都有迹可循”这句话有非常具体的感受。