ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SVM短期电价预测实战:特征工程、参数调优与避坑指南

SVM短期电价预测实战:特征工程、参数调优与避坑指南 简介面向电力现货市场短期电价预测场景该代码包聚焦基于支持向量机SVM的次日电价方向变化预测适合电力市场研究人员、量化分析者及机器学习初学者参考。压缩包共8个文件核心为3个MATLAB脚本覆盖自回归模型调优与相关变量增强两个层面2个mexw64编译函数用于加速预测计算测试数据xlsx与mat及说明文档txt方便直接复现实验整体仅542KB轻量且结构清晰。已有532人学习。资源完整复现了在欧洲能源交易所德国和奥地利控制区Phelix指数上的测试流程200天内预测准确率达76.12%代码保留了数据读取、特征构造、模型训练与结果评估的完整逻辑可直接运行或稍作修改用于其他电价数据集。通过该案例可系统掌握SVM参数寻优、核函数对比及外部特征融合的方法同时也能理解短期电价预测从方案设计到效果验证的关键步骤为后续研究提供扎实的代码起点。1. 短期发电市场电价预测为什么都说先试SVM刚拿到短期发电市场电价预测这个题目时直觉是上深度学习。真正跑过一轮才发现日度样本只有24个点一年也就八千多个样本波动还夹着尖峰和负电价深度学习在这种数据量上很容易过拟合而且慢。反倒是SVM支持向量机配RBF核在小样本、强非线性的回归任务上常常先交出一版能看的预测。这个标题里的研究项目核心就是把SVM当主力模型对短期发电市场的电价做回归预测。SVM擅长什么样本量几千到几万、特征几十维时它用一个核函数把电价这种非线性关系映射到高维空间又没有深度模型那么重的调参负担。适合的读者很明确要搭电力市场电价预测、负荷预测手头有历史电价序列、数据量还没到海量级别的团队。后面从特征构造、核函数选型、参数调优到避坑把整套流程说透。2. 从电价序列到监督学习样本滞后特征与归一化的第一道坎SVM不能直接吃一串连续的时间序列它吃的是特征矩阵X和标签y。所以第一步是把电价历史序列改造成监督学习数据集。这一步做得对不对直接决定后面SVM跑出来的是可用模型还是玄学。我一般先把序列画出来看周期发电侧现货电价有明显的24小时日内周期和168小时周周期——早高峰、晚高峰、周末低谷这是选滞后特征的基本盘。2.1 滑动窗口怎么开lag_24、lag_168 这些滞后特征在说什么import pandas as pd def make_lag_features(df, targetprice, lag_hours(1, 2, 24, 25, 48, 168)): df df.copy() for h in lag_hours: df[flag_{h}h] df[target].shift(h) df[hour] df.index.hour df[is_weekend] (df.index.weekday 5).astype(int) return df.dropna() # df 为按时序索引、含 price 列的原始电价表 feature_df make_lag_features(df) X feature_df.drop(columns[price]) y feature_df[price]逻辑说明shift(h)是把当前时刻往前推h小时的那个价格挪到当前行。lag_24就是「昨天同一时刻的电价」lag_168是「上周同一时刻的电价」。保留1、2、25小时这种邻近窗口是为了让模型感知到电价的短期变化趋势保留168是让它感知周周期。SVR不像树模型那样能做自动特征交互所以这一步宁可显式把周期信息铺开。参数说明lag_hours这个元组要根据市场特性调。如果尖峰固定出现在早晚时段就把对应时段的lag加进来但也不要贪多几十个滞后特征塞进去RBF核的距离计算会被无关维度稀释训练时间变长、误差反而变差。一般控制在6到12个滞后特征。dropna()会丢掉序列开头的若干行样本数小于理论值这是正常的。一年8760个小时点扣掉开头168个滞后缺失行训练样本仍然是够的。2.2 归一化必须落在训练集上MinMaxScaler 的 fit/transform 分开写SVR对特征的数值尺度极其敏感。RBF核算的是样本间的欧式距离电价是百元级、小时数是24、星期几是1到7不归一化的话电价这一个维度会完全主导距离计算核函数等于白选。归一化是硬前提但标准流程我看过很多人写错from sklearn.preprocessing import MinMaxScaler # 先按时间顺序切分再归一化scaler 只在训练段上 fit train_len int(len(X) * 0.8) X_train, X_val X.iloc[:train_len].copy(), X.iloc[train_len:].copy() y_train, y_val y.iloc[:train_len].copy(), y.iloc[train_len:].copy() scaler MinMaxScaler(feature_range(0, 1)) X_train_scaled scaler.fit_transform(X_train) X_val_scaled scaler.transform(X_val)逻辑说明fit_transform里的fit是只针对训练集计算每个特征的最小值和最大值transform则是把训练集上算好的缩放参数套到验证集上。验证集、测试集只能被transform不能被fit。如果先对完整数据集做fit再切分验证集的价格极值会提前参与训练特征的缩放——这就是最常见的归一化泄漏离线分数虚高上了真实数据立刻现原形。这个坑在第5章会再展开。参数说明feature_range默认是(0,1)。电价里有负电价时MinMax会把负值映射到0附近这是正常的映射结果不要额外截断。StandardScaler零均值单位方差也可以用但在电价这种含尖峰的长尾数据上MinMax的边界更直观、更好解释。预测完成后要把数值还原成电价单位用scaler.inverse_transform处理预测结果。2.3 时间特征与外部变量把星期几、预测日类型编码成 SVM 能懂的数SVR没有时间概念它不知道3月1日和8月1日有什么区别也不知道周五和周日之间隔了多久。除了滞后价格还得喂时间特征。常见做法是hour直接作为数值特征电价预测里hour进模型一般就够用不必非做sin/cos变换is_weekend用0/1节假日单独一列is_holiday供暖季和非供暖季再用一列季节特征。如果手里有公开的负荷或温度数据加一列同期负荷预测值或温度对电价预测的提升往往比多加三道滞后特征还明显——价格本质上由供需决定负荷就是需求侧最直接的代理变量。我一般会建议团队先把基础特征跑通再加外部变量。原因很简单SVR的核函数对每一维特征都做距离计算加了没用的维度会把噪声也放大进去效果未必比少加好。外部变量缺失时用「同一时刻的滞后价格星期几小时」已经能解释电价序列的大部分方差。特征造好之后顺手看一眼每列特征的min/max和缺失率缺失超过30%的列直接去掉缺失少的用前向填充再进归一化。3. SVR核函数选型与三组必调参数C、epsilon、gamma怎么试很多人在SVR上调参像抽签今天C调成10明天换成100看哪个分数高用哪个。实际上SVR的损失函数设计和普通回归完全不同把「epsilon不敏感带」这件事想明白参数怎么调就顺理成章了。3.1 先搞清楚 SVR 在干什么epsilon 不敏感带与支持向量SVR回归的目标不是最小化均方误差而是找到一个函数f(x)让大部分样本的预测误差落在±epsilon的「管道」内。管道内的样本不产生任何损失只有超出管道的样本才成为支持向量并产生惩罚。这意味着epsilon直接决定了模型对误差的容忍度epsilon设大了模型对尖峰、负电价这类离群点视而不见预测曲线会非常平滑设小了模型被噪声带着跑训练时间暴涨。C是管道外样本的惩罚权重和分类里的C一个逻辑。理解了这两条再看RBF核里的gamma。gamma决定单个样本的影响半径gamma越大决策边界越紧贴训练数据越容易过拟合落回电价回归尖峰时段的价格样本不多却往往决定交易盈亏gamma太小会把尖峰当噪声抹掉gamma太大又容易把尖峰附近个别样本单独拟合出来次日常规时段跟着翻车。SVR的结构不像线性回归那样能把每个特征系数直接读出来调参时确实有点黑匣子感所以更依赖一套系统的搜索流程。3.2 核函数怎么选RBF 优先但线性核在电价上常有惊喜RBF核是SVR的默认选择它能把特征映射到无穷维理论上可以拟合任意非线性关系电价这种强周期性、尖峰多的数据用RBF不会错。线性核则适合特征和标签关系接近线性的场景——如果只靠滞后特征做电价预测序列本身的周期性会让线性核也能拿到不差的分数线性核训练快、外推时不会像高次多项式那样爆发。多项式核在电价预测里我基本不推荐次数稍微高一点碰到尖峰样本就会剧烈外溢预测出离谱的负值。一个实用的选型策略是先跑线性核做基线再跑RBF看提升幅度。提升在5%以内生产环境为了速度和可解释性就留线性核提升超过10%用RBF值得付出训练成本。核函数的对比要固定C和epsilon否则分不清是核的功劳还是参数的功劳。sklearn的SVR里kernel参数直接填linear或rbfgammascale时RBF会自动按特征数放缩这个默认值在电价特征规模下是安全的起点。3.3 网格搜索与 TimeSeriesSplit别用随机 K 折from sklearn.svm import SVR from sklearn.model_selection import TimeSeriesSplit, GridSearchCV param_grid { C: [0.1, 1.0, 10.0, 50.0], epsilon: [0.01, 0.05, 0.1], gamma: [scale, 0.01, 0.1], } tscv TimeSeriesSplit(n_splits5) search GridSearchCV( SVR(kernelrbf), param_grid, cvtscv, scoringneg_mean_absolute_error, n_jobs-1, ) search.fit(X_train_scaled, y_train) print(best params:, search.best_params_) print(best CV MAE:, -search.best_score_)逻辑说明GridSearchCV会遍历4×3×3共36组参数做5折时序交叉验证scoring用neg_mean_absolute_error是因为sklearn的 scoring 默认越大越好MAE取负之后越大相当于MAE越小。TimeSeriesSplit按时序递增切分前折的数据永远在时间上早于后折不会出现随机K折把未来样本混进训练集的问题。在电价预测这种时序任务里随机K折的搜索结果再漂亮都不能用。参数范围怎么定C从0.1到50是因为电价数据尖峰多、噪声大惩罚太小模型会过于平滑epsilon从0.01到0.1是看价格尺度——归一化后价格在0到1之间0.1已经算很宽的管道gamma用scale做基线再试0.01和0.1两个数量级。更精细的做法是先粗粒度跑一遍观察最优值落在边界还是中间。落在边界说明范围没给够继续外扩落在中间说明附近可以加密网格再搜一轮。注意RBF核的SVR在高C、小epsilon组合下训练很慢先拿一半数据试跑确认耗时能接受再全量搜。4. 不只是MAE短期电价预测的误差拆解与分时段评估一个模型交出去之前只报一个总体MAE是很不负责任的。电价预测的误差分布极不均匀——把一天24小时的预测误差混在一起算峰时段的大误差会被谷时段的小误差掩盖。所以评估至少要做三件事选对指标、分时段记账、和持久性基线对比。4.1 MAPE 在负电价时会翻车改用 MAE/RMSE 与 sMAPEMAPE是很多回归任务的默认指标但发电侧现货市场现在有负电价——光伏大发、需求疲软的午间批发价可能跌到负数。MAPE的分母是真实值真实值为负时MAPE要么为负、要么除以零趋近无穷完全没法用。sMAPE是常见的替代口径import numpy as np def smape(y_true, y_pred): denominator np.abs(y_true) np.abs(y_pred) denominator np.where(denominator 0, 1e-6, denominator) return np.mean(2 * np.abs(y_true - y_pred) / denominator) * 100逻辑说明sMAPE的分子是两倍绝对误差分母是真实值和预测值绝对值之和。真实值为-20、预测值为-15时误差占比是2×5/(2015)仍然有界。denominator接近0时的防御处理是必须的否则个别预测和真实值同时接近0的点会除出无穷大。业务汇报时同时报MAE单位是元/MWh和sMAPE百分比口径RMSE在需要重点惩罚大误差时再补上。4.2 分时段误差峰段永远比谷段难要分别记账把预测结果按小时拆开统计每个时段的误差比只看一个总量有用得多。下表是我常用的一种分桶方式量级是相对示意时段典型价格水平误差相对量级说明夜间低谷0-6时低较低价格低位、波动小早高峰7-10时高偏高价格爬坡快滞后特征反应慢午间11-15时中低中等光伏出力影响天气波动晚高峰17-21时高最高双峰叠加SVR最难拟合的段深夜22-23时中中等负荷回落价格下行做法是逐小时计算每天的平均绝对误差按月份画一张热力图一眼能看出模型在哪几个钟点系统性跑偏。峰时段误差大未必是模型问题——如果持久性基线在这个时段误差同样大说明这个时段本身方差就大模型能做的只是别比基线差太多。反过来如果谷时段误差反而偏高多半是特征里有东西没对齐比如工作日和周末的样本被混在同一个模型里。4.3 和持久性基线对比跑不赢「昨天」说明模型白做了持久性基线persistence forecast是电价预测的傻瓜基线直接拿前一天同一小时的电价作为今天的预测。这个基线在强周期性数据上强得离谱尤其在平稳天气、无突发机组的市场里。SVM的任何收益都要和它对比着看df[persist_pred] df[price].shift(24) # 昨天同一时刻 df[svm_pred] svm_predictions # SVR 输出的预测序列 for col in [svm_pred, persist_pred]: mae np.mean(np.abs(df[price] - df[col])) print(col, MAE:, mae)逻辑说明shift(24)直接构造持久性基线它不消耗任何训练成本。SVR模型哪怕只比这个基线低5%的MAE在业务上都是有意义的因为电价套利的边际利润率常常只有几个百分点。反过来如果SVR跑不赢持久性基线第一反应不是调参而是回去查特征是不是lag_24占了绝对主导、模型实际在学「复制昨天」这和第5章的5.3是同一个病根。4.4 评估集要跨过完整的市场状态变化评估集的长度不能拍脑袋定。只看春季一个月的平稳数据模型表现会很漂亮但换到夏季高温或冬季寒潮时段价格结构和波动幅度完全变样误差会突然拉大。我一般要求评估集覆盖至少一个完整季度最好跨过一种极端市场状态——比如包含几段价格尖峰和几段负电价时段。如果训练数据里没有这些状态模型在极端日的表现就是纯外推这时候要把极端日单独分出来看而不是混在总体MAE里一带而过。5. 避坑数据泄漏、尖峰削平与 SVM 参数玄学的五条记录以下五条是从特征、交叉验证到参数的真实踩坑记录。调SVM模型翻车基本都能归到这几类里面。每一条按「现象→原因→解决」写清楚比背一堆调参口诀有用。5.1 全量归一化后 CV 分数漂亮上线第二天翻车数据泄漏现象训练集、验证集、测试集的归一化一起做完之后交叉验证分数很漂亮部署后真实预测误差直接放大一倍以上。原因全局MinMaxScaler在全部数据上fit验证集和测试集的价格极值提前参与了训练集特征的缩放。模型训练时的特征分布和真实环境特征分布不一致离线的低误差是假的。解决归一化只在训练段fit验证段和测试段只transform按2.2节的写法执行。改完之后再做一次walk-forward回测确认误差量级和离线评估一致再谈上线。5.2 TimeSeriesSplit 忘记留 gap交叉验证分数虚高现象已经用TimeSeriesSplit替换随机K折分数还是偏乐观尤其当滞后特征里有lag_1、lag_2这种短滞后时间窗时。原因前折训练集末尾的样本它自身的滞后特征可能就指向后折验证集开头的时间点——极端情况下验证集第一个样本的lag_1就是训练集最后一个样本本身。信息从训练集渗到了验证集这属于切分引入的泄漏。解决TimeSeriesSplit里设置gap参数让训练集和验证集之间空出至少24个点。例如tscv TimeSeriesSplit(n_splits5, gap24)。gap的取值参考滞后特征的最大跨度滞后特征里有lag_168时gap至少给到48或72才有意义。5.3 预测曲线是昨天的平移SVR 退化成了 naive 基线现象SVR预测的曲线和前一天价格高度重合误差评估一看主要误差来自小时级别的相位偏移整体滞后一个大周期。原因lag_24在特征里占绝对主导SVR发现「复制昨天」能把损失降到很低于是把其他特征全部当噪声忽略。这是特征工程问题不是参数问题。解决不要用lag_1到lag_24连续一整套滞后特征保留lag_1、lag_2、lag_24、lag_168这种稀疏组合把is_weekend、hour这些时间特征保留有条件就加入外部变量。训练完删掉lag_24再跑一遍如果误差没有明显变大说明模型真的学到了周期结构而不是在复制粘贴。5.4 尖峰永远被削平epsilon 太大、C 太小现象预测曲线整体平滑早晚高峰被削成圆顶价格尖峰日的预测值离真实值差一大截峰时段分桶误差明显高于常规时段。原因SVR的epsilon是「不敏感管道」的宽度管道越宽尖峰越容易被当作可忽略的噪声。C是管道外样本的惩罚C小了模型宁可让尖峰落在管道外也不在乎。解决把epsilon从0.1逐步调到0.01或0.005同时把C从1升到1050观察峰时段MAE的变化。注意epsilon下调后训练时间会明显变长这是正常的计算代价。还有一招针对尖峰时段比如早高峰7-10点单独再训一个小模型主模型负责常规时段这个组合在峰谷差异大的市场上效果明显。5.5 把星期几当成数字 17SVR 的距离假设被坑现象周一和周日的预测在边界附近出现奇怪的突变误差呈周期性抖动工作日和周末的界限被模糊。原因weekday用数字编码后SVR的核函数按数值距离理解它——1和7之间的距离是6模型以为「周一」和「周日」是两个极端实际上它俩是相邻的七天循环。数值编码破坏了循环结构。解决把weekday拆成布尔特征is_weekend一列、is_holiday一列最多再补is_monday到is_friday中的几列够用就好。更精细的做法是hour和weekday一起做sin/cos周期编码但电价预测实践中稀疏的布尔特征往往更抗噪、更好解释。6. 让预测真正能上线walk-forward滚动回测与每日重训静态的train/val/test切分只能证明「这版参数在历史上不差」不能证明每天都能正常出数。上线前要做walk-forward滚动回测从历史某个时点出发用截至该时点的数据训练预测未来24小时然后窗口向前滚动重复直到覆盖整个测试周期。这个流程比单次切分更接近真实生产环境。6.1 walk-forward 滚动回测循环怎么写train_window 365 * 24 # 用过去一年数据训练 step 24 # 每次滚动一天 preds, truths [], [] for end in range(train_window, len(scaled_df), step): train scaled_df[end - train_window:end] val scaled_df[end:end step] X_tr, y_tr train.drop(columns[price]), train[price] X_va, y_va val.drop(columns[price]), val[price] svr SVR(kernelrbf, C10, epsilon0.05, gammascale) svr.fit(X_tr, y_tr) preds.extend(svr.predict(X_va)) truths.extend(y_va.values) print(walk-forward MAE:, np.mean(np.abs(np.array(preds) - np.array(truths))))逻辑说明这里的scaled_df是已经构造好滞后特征、按时序索引的完整DataFrame训练前只做当轮归一化、不提前fit全量数据。循环里每次用滚动窗口内的数据重训一次SVR再预测下一天24小时窗口向前移动一天。这样得到的误差近似真实部署时的误差。数据跨度大时这一步很慢可以先每隔7天滚动一次评估误差趋势确认稳定后再按天滚动。6.2 每日重训还是增量更新SVR 没有现成的增量路径SVR的增量学习不常用sklearn里的SVR也不支持partial_fit。常见做法是每天凌晨用「最近一年」的数据全量重训一次训练几分钟、预测几十毫秒代价完全可接受。数据量超过几十万条时按天降采样成每小时一个点或者只保留最近三个月——电价的周期性会随时间漂移太久以前的数据反而拖后腿。模型更新后把前一天的预测误差记录下来按周看误差是否随模型版本漂移比每次都盯着参数调要省心。6.3 一份能直接照做的验收 checklist逐项确认按时序切分训练、验证、测试归一化只fit训练集TimeSeriesSplit带gap做参数搜索离线评估对比持久性基线并记录优势比例分时段统计MAE确认峰时段误差在可接受范围完整跑一轮walk-forward回测误差和离线评估量级一致模型连续运行两周观察市场进入极端状态时误差是否失控。有一说一我最早那版直接把sklearn默认参数的SVR塞进电价数据预测曲线平滑得像均值回归尖峰全被抹平当时还以为是市场数据噪声大。后来把epsilon从默认调小两个数量级、C拉大又补上星期特征才算是真正能用的模型。参数这件事多少有点玄学但每一步踩坑都对应一个明确的特征或切分问题查清楚比盲目调参有用得多。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进