资讯动态

深度学习结合SHAP的西班牙电力市场价格预测实战

发布时间:2026/10/20 4:07:28 来源:尧图企业网站定制
在电力市场数据分析这个方向上我最近完成了一个让我收获很大的项目基于公开的西班牙电力市场数据用深度学习模型做电价预测再配合SHAP做可解释性分析。说实话一开始我只是想验证一下混合模型在这个高波动市场里的上限到底在哪但做下来发现真正有价值的并不是预测精度提升了多少而是SHAP把黑盒模型的“决策依据”摊开后整个分析链条瞬间变得可信了不少。这篇内容我就把整个项目的思路、数据加工、模型选型、评估方式以及SHAP落地的坑按我实际操作的顺序完整梳理一遍代码都是Python想复现的朋友可以顺着走。这类项目的典型痛点其实很明显电价的非线性和突发性很强尤其在新能源高渗透的市场里模型光“准”还不够业务侧和论文评审都会问一句“为什么”。所以这个项目的定位不是单纯刷精度而是要把“预测结果为什么是这样”这件事说清楚。适合正在做能源数据分析、电力市场预测、或者需要用可解释性给深度模型补一份说明材料的人参考。1. 为什么拿西班牙电力市场开刀以及这个预测任务到底怎么定义1.1 典型的高波动场景深度学习才有发挥空间西班牙电力市场的现货价格在很多人眼里是“很难预测”的代名词。风电和光伏装机占比很高天气一变化供给曲线就会大幅波动价格可以在短时间内冲到很高的位置也可以在某天凌晨跌成负值。你看需求侧其实相对稳定真正搅动价格的是可再生能源的出力不确定性。这类市场特征让传统的统计模型相当难受。ARIMA和线性回归擅长抓平稳趋势但面对尖峰、负电价、日内价差扩大这些非线性片段往往只能给出一个含糊的平均值。深度学习在这里的优势是能直接学习特征之间的交互关系不预设函数形式尤其是当输入维度拉长到几百个小时的连续序列时循环结构和卷积结构都能保留不同尺度的时序信息。我选西班牙市场还有一层考虑公开数据质量相对规整历史价格、负荷、新能源出力、期货价格都有标准的开放接口时间粒度能到小时级这对做深度学习项目非常友好数据清洗成本低可以更专注在模型和解释性上。1.2 预测目标不是单点而是次日24小时价格曲线这个项目里我没有做简单的“下一小时价格预测”而是直接把任务定义为“基于过去168小时的运行状态预测未来24个价格点位”。也就是说输入过去7天的小时级数据输出未来24小时的现货价格曲线。之所以这样定是因为电力市场在业务上按天为单位进行出清真正有用的决策信息是“明天的价格曲线长什么样”而不是“下一小时是多少”。单步预测的精度再高如果午夜到清晨的峰谷结构全乱了对调度和交易员的价值也有限。因此在模型设计上输出层就是24维向量。你可以理解为模型学的是“给定最近一周的市场状态明天的价格分布应该长什么样”。这种多步多输出结构在训练时更稳也不会出现递归预测多步时误差逐渐放大的问题。1.3 数据字段与公开来源这里我主要使用了以下几个数据维度电力价格每日24点现货价格单位是欧元/兆瓦时。负荷数据全系统实际用电负荷小时级。新能源出力风电出力、光伏出力。市场交易电量出清电量。期货或远月价格作为成本侧预期的一个参考。日历信息月份、星期、是否节假日。这类数据基本都能在电力交易机构和电网调度机构的公开开放数据接口里拿到具体字段名可能不同但核心内容是一致的。我做的时候是分接口下载再统一拼接时间跨度为连续约三年训练集、验证集、测试集按时间顺序划分为约8比1比1。1.4 项目产出的完整形态这个项目做完以后主要会形成这么几样东西一份可复现的数据预处理脚本、一个训练好的深度学习模型、一套模型评估结果以及一组SHAP可解释性图表。模型文件本身可以用Python的保存机制归档SHAP的图表则可以导成常见图片格式用于汇报或论文插图。我的建议是在项目初期就把“最终要交付什么”想清楚。如果只是自己跑一版数字那后面做不做可解释性都无所谓但如果你要把模型结果拿给非技术背景的同事看或者要在研究文档里写清楚特征影响SHAP这部分就是刚需。2. 数据清洗与特征工程实战比模型设计更影响最终精度2.1 缺失值、负电价与异常尖峰不能一刀切电力数据表面看着干净实际上坑不少。价格序列偶尔会有缺失负荷和新能源出力也有传输延迟导致的空值。我处理缺失值的策略是如果缺口小于连续6个小时使用前后24小时同窗口的中位数填充如果缺口较长就用线性插值兜底因为长区间内用同窗口均值会抹掉真实趋势。更需要注意的是负电价和尖峰价格。很多人第一次见到负电价会下意识以为是脏数据实际上在新能源大发的时段供给过剩导致出清价为负是正常现象必须保留。而真正的异常是那种短时间内价格跳变超过前后7天价格分布4倍标准差的点这种我会用边界值缩尾处理而不是删除因为删除会切断时间序列的连续性。另外还有一个容易忽略的问题夏令时切换。欧盟市场每年有两次时间调整导致那条序列并不严格等价于24小时均匀间隔。做时间索引的时候一定要统一成UTC时间戳最后展示结果时再转回当地时间否则特征对齐会出现一个小时间的偏移训练时不明显误差分析时却能看出来。2.2 特征清单与构建逻辑我把特征分成四类历史状态特征、周期特征、滚动统计特征、外部预期特征。一张表看下来比较清楚特征类别具体字段构建逻辑历史状态过去各小时价格、负荷、风电出力、光伏出力直接把原始序列按滞后窗口放入模型让模型自己提取不同时间尺度的规律周期特征小时索引、星期几、是否节假日现货价格有明显的日内、周内季节性节日负荷下降会影响价格滚动统计过去24小时价格均值、标准差、最大最小值描述近期的波动环境和价格水平外部预期远月或期货价格表达市场对远期成本的一致预期有一定先行性构建滞后特征时最大的纪律是不能“偷看未来”。所有统计窗口只允许使用当前预测时刻之前的数据这一点在代码里一定要反复检查。另一个经验是滚动窗口不要全用同一个长度24小时窗口能刻画日内周期168小时窗口能刻画周规律两者结合的信息量更大。2.3 时序切分 vs 随机划分这个坑踩一次就会印象深刻机器学习常规训练里随机划分数据集很常见但在时间序列问题上这是绝对不能做的。随机打乱会让模型在训练时见到测试时段附近的信息测试集里的某一天可能被分到训练集这会导致模型“记住了”未来趋势验证指标好看得离谱一上真实环境立刻现原形。我在项目里采用的是严格的时序切分。按时间顺序把数据切成三段训练集取前80%验证集取中间10%测试集取最后10%。为了进一步降低季节性过拟合我专门留意了训练集和测试集的季节覆盖情况保证两端都至少包含完整的春夏秋冬周期。这样评估出来的误差基本能代表模型在未来真实滚动预测中的表现。2.4 归一化与逆归一化的正确打开方式深度学习模型对输入尺度敏感必须做标准化。标准做法是只在训练集上计算均值方差然后把这组统计量应用到验证集和测试集。这里最容易犯的错误是有人会在整个数据集上统一做标准化导致验证集和测试集的统计信息提前泄漏进训练过程同样会让评估结果虚高。输出价格这个目标变量我单独做了一个MinMaxScaler将价格压缩到0到1的区间。原因很简单现货价格会出现比较极端的尖峰标准化后更容易让模型聚焦在小幅波动上。预测结束后再使用同一个训练集上拟合的scaler逆变换回实际价格单位。这个逆变换步骤千万别搞错如果误用验证集或测试集的统计量整个误差分析都会失真。3. 从单层LSTM到CNNLSTM结构选型与训练细节复盘3.1 为什么最终选了CNNLSTM一开始我用的是单层LSTM隐藏单元64训练很快但预测曲线有个明显问题在价格快速跳变的时段模型总比真实值慢半拍。造成这种情况的很大一部分原因是价格序列里的“突变模式”通常只持续很短几个时间步这类局部特征单靠循环结构去记忆反应不够灵敏。后来我把结构改成CNNLSTM的组合先用一维卷积核对输入序列做卷积提取短期局部涨跌模式再把卷积输出送入LSTM让循环结构继续捕获跨时段的依赖。这个组合在时间序列预测里很常见核心思路就是“先用局部感受野提取冲击特征再用序列记忆组织上下文”。实际效果上尖峰时段的滞后明显减轻训练收敛速度也快了一些。3.2 输入输出张量设计与数据管道模型输入形状固定为“样本数×168×特征数”其中168代表过去7天的小时数。假设特征维度是8那么每个样本就是一个168行8列的矩阵。输出形状是“样本数×24”代表未来24小时的价格预测。数据管道就是典型的滑窗切片。从完整的时间序列里按每小时滑动一次生成样本同时记录对应的24个后续价格点作为标签。这里有一个取舍如果用全部历史天数的数据样本量能到两万以上足够训练一个中等规模的深度模型但样本之间有很强的重叠训练时如果直接按顺序喂给模型容易产生过拟合。我一般会在训练过程中使用小的batch并配合轻微的dropout来缓解样本重叠带来的影响。3.3 训练策略和超参数训练配置没有很花哨但每一项都有实际作用优化器Adam初始学习率0.001。损失函数HuberLoss也就是平滑L1它对极端峰值不太敏感比MSE更稳。Batch大小64。早停监控验证集损失连续10轮不下降就停止。学习率衰减验证损失进入平台期后学习率自动降为原来的三分之一。训练过程里最容易出现的两个问题分别是梯度爆炸和过拟合。梯度爆炸表现为loss突然变成NaN原因多是学习率过大或序列长度过长解决办法就是降低学习率或者给梯度加裁剪。过拟合则表现为训练损失持续下降但验证损失不动我通过加大dropout、缩减CNN的卷积核数量来压制。3.4 用LightGBM做基线防止深度学习自嗨深度学习模型很容易给人“参数多就是厉害”的错觉所以我还用同一套训练特征跑了一个LightGBM多输出回归模型作为基线。这里有一个明显的对比价值梯度提升树对特征交互和非线性关系也有很强的拟合能力如果Deep模型的测试集误差不能明显优于它说明所谓深度特征提取并没有真正发挥出来。我这份数据上的结果显示LightGBM的验证集MAE大概在8.4左右而CNNLSTM组合的MAE在7.2附近。深度模型确实占了优势但幅度没有想象中夸张。这也提醒我做项目报告的时候一定要放上基线对比不然单独一个深度模型的结果没有参照系说服力会打折扣。3.5 模型对比结果模型测试集MAE欧元/兆瓦时测试集RMSE欧元/兆瓦时LightGBM多输出8.4213.27单层LSTM8.1112.95CNNLSTM7.2411.38CNNLSTM SHAP分析7.2411.38最后一行其实就是同一套模型只是增加了可解释性分析环节显示在这里是为了说明加SHAP不会改变模型精度但会显著提高结果的可信度。4. 不只是看MAE分时段误差拆解与模型真实水平4.1 评价指标怎么选电价预测常用的指标是MAE、RMSE和MAPE。我的实际体会是RMSE对少数极端误差非常敏感适合用来判断模型有没有离谱预测MAE更适合衡量整体水平MAPE在电价场景下并不好用因为当真实价格接近零时哪怕误差只有0.1百分比也会被放大成无穷大。所以如果论文或者报告里非要用百分比指标我建议改用sMAPE或者直接报告分时段的绝对误差。我还额外关注了“方向准确率”这个指标也就是模型预测的价格走势方向与真实走势是否一致。对于交易决策来说预测值和真实值差多少固然重要但明天的价格是涨还是跌往往更关键。4.2 分时段误差分析同一套模型在不同时段的误差差异很大。我统计了测试集上各小时的平均绝对误差结果呈现出很清晰的规律凌晨0点到5点负荷低、价格波动小MAE在4到5之间。上午时段随着负荷爬升误差略有增加。傍晚和晚间尖峰时段也就是18点到21点MAE最高能到9以上。周末与工作日相比误差整体小一些因为负荷曲线更平坦。这个结果表明模型在平稳区间已经做得不错真正的难点依然集中在价格快速抬升和风电出力骤变的时段。做误差分析时如果只给一个总均值这些结构性问题就全被掩盖了。4.3 负电价与极端值时段的误差规律测试集里出现负电价的时段并不少。模型在这些时段的误差表现很有意思多数时候预测也能给出负值说明特征里的风电出力和负荷信息确实传达了一部分信号但幅度往往不够极端。也就是说模型倾向于把负电价预测成“轻度负”而真实值偶尔会到负得比较深的位置。这说明不是模型不理解负电价的形成机制而是它在训练中为了降低整体损失主动选择更“保守”的预测。如果业务上特别关心深负电价时段就需要在损失函数上做文章比如给负电价样本加权重或者单独对这部分样本做二次建模。4.4 滚动评估比一次性预测更真实我在最终评估里还做了一组滚动测试从验证集开始每次用模型预测未来24小时然后等真实值出来后把真实值并回历史序列再滑动生成新的输入特征预测下一天。这样连续滚动测试了一个季度。和你可能想的一样滚动评估的结果会比静态测试差一点因为误差会通过特征一部分传导到后续预测中。如果你要在报告里对模型的“实操水平”做一个诚实的评价推荐用这种滚动方式而不是直接在完整测试集上一次预测所有天。我论文里最终采用的是滚动评估口径静态结果只是作为参考。5. SHAP可解释性怎么落到电价预测项目上5.1 Shapley值与SHAP到底在做什么SHAPSHapley Additive exPlanations的核心思想来自合作博弈论中的Shapley值。可以这样理解把模型里的每一个特征看成参与合作的成员某一次预测值就是整个团队获得的收益而每个特征对这个收益的边际贡献就是它的Shapley值。在不同样本里同一个特征的作用方向和大小会不一样。SHAP把这些贡献值汇总就能得到全局特征重要性也可以单独挑出某一天的某个时段看每个特征到底把预测值往上推还是往下压。相比传统的feature importanceSHAP的意义在于它既给出了方向又能精确到单样本叙事能力非常强。5.2 深度模型该用哪种SHAP算法SHAP库提供了多个解释器TreeExplainer专门用于树模型而深度学习模型一般用GradientExplainer或DeepExplainer。我在项目里使用的是GradientExplainer它通过梯度归因近似估计Shapley值对Keras和PyTorch都有支持。实际调用时需要准备一批背景样本也就是用于参考的典型输入。背景样本数量不用太大我的经验是50个以内就已经足够稳定。样本选得太少解释结果会比较毛糙选太多计算时间会明显上升。5.3 3D输入的坑这一步是我在整个项目里踩过最大的一个坑。LSTM类模型的输入是三维张量形状为“样本数×时间步数×特征数”而SHAP内部在处理解释器时会把时间步也当成一个维度去计算贡献值。当你拿到shap_values后它的形状可能会是“样本数×时间步数×特征数”这显然不是你第一直觉认为的“样本数×特征数”。我当时看到shap_values的shape直接愣了几秒后来才意识到需要把时间步维度的贡献聚合起来。实操上我是这样处理的先取shap_values的绝对值再对时间步维度求平均或者求和最后得到每一样本在每个特征上的平均贡献。这里细节是直接求和和求平均对各特征排序影响不大但绝对值和原始值的区别很大不要用原始值去画bar图否则正负贡献会互相抵消。5.4 三种常用图的业务解读SHAP跑完之后我最常用三种图全局特征重要性bar图把所有特征的平均绝对SHAP值画出来排在前面的特征就是模型最依赖的信息。beeswarm图和依赖图可以看特征数值从低到高变化时对预测的影响是怎么变化的。比如风电出力从小变大SHAP值整体为负说明新能源出力越大价格预测越低这符合市场直觉。force plot适合单个样本的归因。某一天夜间预测价格明显偏高force plot会显示是“过去24小时价格均值”和“风电出力偏低”同时在推高预测值这个结果拿去做复盘汇报特别直观。SHAP的输出并不只是为了“好看”它的真正价值是让结论可验证。如果业务专家说“那天风电出力高所以价格应该低”SHAP能定量告诉他这个因素贡献了多少这就是黑盒模型与业务经验对齐的过程。5.5 可解释性反哺特征工程做完SHAP全局分析后有一个发现直接改进了特征工程光伏出力在前一天的同一小时时段的SHAP贡献排名非常靠前但我最初只用了当前小时的光伏出力。于是我在特征里增加了一组“前一日同一小时”的对照特征测试集MAE又降了一点。这种反馈是可解释性分析最值钱的部分。SHAP不是解释完就收工而是拿解释结果去审视特征组合看哪些特征是重复信息哪些缺失维度还需要补充。这样模型精度和解释质量能形成正向循环。6. 可复现的代码骨架与关键经验6.1 数据预处理与数据集构建代码下面的代码只保留了核心流程实际使用时按你的字段名替换即可import numpy as np import pandas as pd from sklearn.preprocessing import StandardScaler, MinMaxScaler # 假设df是拼好的DataFrame索引为UTC时间列包括 # price, load, wind, pv, futures, holiday df df.sort_index() features [load, wind, pv, futures, holiday] df df.dropna(subset[price]) # 缺失特征值用同窗口中位数填充 window 24 * 7 for col in features: df[col] df[col].fillna( df[col].rolling(window, min_periods1, centerTrue).median() ) # 时序切分不要随机打乱 train_end int(len(df) * 0.8) val_end int(len(df) * 0.9) train_df df.iloc[:train_end] val_df df.iloc[train_end:val_end] test_df df.iloc[val_end:] # 标准化只能fit在训练集上 scaler_x StandardScaler() scaler_x.fit(train_df[features]) scaler_y MinMaxScaler() scaler_y.fit(train_df[[price]]) def make_samples(data, history168, horizon24): X, y [], [] values_x scaler_x.transform(data[features]) values_y scaler_y.transform(data[[price]]) for i in range(history, len(data) - horizon 1): X.append(values_x[i - history:i]) y.append(values_y[i:i horizon].reshape(-1)) return np.array(X), np.array(y) X_train, y_train make_samples(train_df) X_val, y_val make_samples(val_df) X_test, y_test make_samples(test_df)注意make_samples函数内部直接用全局scaler来处理单段数据在实际工程里建议把scaler作为参数传入避免不同数据集共用一个闭包变量时产生混淆。6.2 CNNLSTM模型训练代码模型用TensorFlow/Keras实现结构非常紧凑import tensorflow as tf from tensorflow.keras import layers, models history_len X_train.shape[1] n_features X_train.shape[2] horizon 24 model models.Sequential([ layers.Input(shape(history_len, n_features)), layers.Conv1D(filters64, kernel_size7, paddingsame, activationrelu), layers.Dropout(0.1), layers.LSTM(units64, return_sequencesFalse), layers.Dropout(0.2), layers.Dense(64, activationrelu), layers.Dense(horizon) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), losstf.keras.losses.Huber(), metrics[mae] ) early_stop tf.keras.callbacks.EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) reduce_lr tf.keras.callbacks.ReduceLROnPlateau( monitorval_loss, factor0.33, patience4 ) history model.fit( X_train, y_train, validation_data(X_val, y_val), epochs100, batch_size64, callbacks[early_stop, reduce_lr], verbose1 )如果训练中出现loss震荡不稳我一般先把卷积核数量从64降到32再或者把学习率降到0.0003两种方案都能让训练曲线平滑下来。6.3 SHAP调用代码shap_values拿到之后的维度排布是关键import shap # 背景样本取50个足够 background X_train[np.random.choice(len(X_train), 50, replaceFalse)] explainer shap.GradientExplainer(model, background) shap_values explainer.shap_values(X_test[:200]) # 当模型输出维度为24时shap_values可能是list if isinstance(shap_values, list): shap_values np.stack(shap_values, axis-1) # 重点关注第一个预测时间步的归因结果 # 也可以用后面的步骤聚合所有输出步 sv shap_values[..., 0, :] # sv原始shape: (样本数, 时间步数, 特征数) agg_sv np.mean(np.abs(sv), axis1) shap.summary_plot(agg_sv, X_test[:200].mean(axis1), feature_namesfeatures, showFalse)这里我给一个细节如果直接对完整输出24维都做解释图会太复杂。常规做法是先对输出时间步维度算平均或者单独挑几个关键预测时刻做解释。比如你要复盘晚高峰就只取预测维度中对应晚高峰小时的那一列。6.4 参数汇总与环境版本最后把本次复现用到的关键参数汇总一下方便对照参数取值历史窗口168小时7天预测输出24小时卷积核数量64卷积核大小7LSTM单元数64Dropout0.1 / 0.2批大小64初始学习率0.001早停轮数10损失函数HuberSHAP背景样本数50环境版本参考Python 3.10TensorFlow 2.12SHAP 0.44.0pandas 2.0NumPy 1.24。这几个版本组合跑起来没有什么兼容问题。做完整套流程后我最大的体会是这个项目的核心工作其实不在深度学习模型本身而在数据清洗、特征工程和误差归因这些“不漂亮”的环节里。SHAP也不是锦上添花的可视化它更像一台显微镜能把模型那些隐蔽的偏好照出来。每次跑完一批实验我都会先把SHAP的图打开看一遍确认模型学到的规律没有违背基本物理直觉然后再决定要不要继续调参。这种“性能指标归因验证”双轨并行的方式是我在后续所有类似预测项目里都会坚持的。如果你手头也有类似的高波动时序预测任务建议不要一开始就扎进复杂网络结构里。先把一套像样的基线模型跑通把SHAP解释链路打通再根据归因结果去改特征、改结构整个项目推进起来会顺畅很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑