ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

SVM短期电价预测实战:SVR特征工程与参数调优

SVM短期电价预测实战:SVR特征工程与参数调优 简介面向电力市场量化研究者与机器学习初学者这份资源聚焦SVM在短期发电市场电价方向预测中的应用。作者基于欧洲能源交易所EEX德国与奥地利控制区的Phelix日价格指数构建自回归SVM模型并引入多种相关变量进行增强在200天测试窗口内取得76.12%的预测准确率完整呈现了从特征构造、模型训练到误差评估的流程。压缩包共8个文件以MATLAB源程序.m为主包含2个编译好的mexw64加速模块、1个Excel测试数据、1个mat数据文件及readme说明文档合计约542KB结构紧凑便于直接运行验证。已有533人学习下载适合正在研究电价预测、现货市场或SVM回归/分类应用的读者可参考其特征选择思路与并行训练脚本用于自身实验对比或方法改进。1. 短期电价预测为什么绕不开SVM这个zip在研究什么在电力现货市场里短期电价预测是交易员报价之前必须做的一步。基于SVM的短期发电市场电价预测研究.zip标题拆开就是一句话用支持向量机去预测未来24小时以内的发电侧节点电价。SVM在教科书里总被当分类器讲但电价是连续数值落到工程上用的是SVR也就是支持向量回归。这个方案能被长期采用不是因为新而是在样本量有限、特征非线性强、又需要可解释性的场景里SVR比线性模型更能逼近尖峰比LSTM更容易调参数也不容易因数据量少而过拟合。以下按老套路来先把历史数据整理成特征标定SVR的核函数和超参数给出可复现的Python代码再重点讲几个让新手血泪的坑。2. 把历史电价变成SVM能用的特征矩阵时间粒度、特征构造与归一化拿到zip源码包解开之后第一步不是急着训练模型而是把数据整理成特征矩阵。很多现成包里的数据文件已经整理好但你要拿它去预测自己的市场还是得按自己的口径重新做一遍。只会调用model.fit不看特征的人第一次跑出自己的预测结果时多半会对着曲线发愣。2.1 数据来源与时间粒度怎么选SVM预测电价需要的输入只有两类历史电价序列本身以及能解释电价波动的其他序列。国内现货试点省份大多公布15分钟或1小时粒度的出清价国外不少市场是5分钟到1小时不等。我不建议一上来就追求高粒度。1小时粒度一年8760个点足够SVR训练而且和报价决策的最小窗口一致。15分钟粒度虽然点数多但相邻样本相关性极高SVR的epsilon不敏感区间反而更难调容易把一个小时的价格波动拆成四段互不连续的预测。先把1小时粒度跑通全流程确认特征和参数有效再往下加密。电价序列本身也要选对对象。日前出清价和实时出清价是两个序列混在一起喂给SVM会直接翻车。我一般先做日前价格预测日前价格由机组报价和负荷预测决定波动比实时价格小SVR的准确率更容易做高。等到要把预测用于日内交易时再单独建一个实时价模型。数据时间范围不要贪长。很多研究喜欢取三五年历史但市场机制、供需结构和机组组合几年内可能已经变过几轮早期数据反而成了噪音。我习惯取最近一年到一年半的数据至少覆盖一个完整的春夏秋冬。如果某段时间市场规则改了比如新增了现货结算规则直接把那段数据排除比留它在训练集里更安全。2.2 特征构造滞后项、滚动统计与日历特征把原始电价序列直接丢给SVM模型学不到“今天是周几”“现在是几点”这些关键信息。SVM是在高维空间里找回归面特征里必须显式带出时间结构。我常用的特征分四组。第一组是滞后特征这是电价预测的骨干。t-1、t-2、t-24、t-25、t-48、t-168六个滞后项足够t-1和t-2抓短期惯性t-24和t-25抓日前差分t-48和t-168抓周周期。注意t-24和t-25要一起用而不是只用t-24这样模型才能看出“前一天同一时刻的电价是在涨还是在跌”。第二组是滚动统计。过去6小时平均价、过去24小时最高价和最低价三个就够。滚动平均给出局部趋势最高最低给出波动区间这两者对判断下一小时会不会出现尖峰有直接帮助。滚动窗口不宜过多6小时和24小时两个窗口即可否则窗口间高度相关反而稀释有效信息。第三组是日历特征。小时编号0到23、星期几0到6、是否工作日0或1这三个几乎必加。电价有典型的日内双峰双谷结构工作日和周末的峰谷位置不同节假日的曲线又和工作日完全两样。节假日样本量太小SVM基本学不出东西常见做法是并进周末类。第四组是负荷与新能源。系统负荷和电价高度相关数据源里有负荷实测值的话肯定要加。但天气、光伏、风电这些数据如果本身来自数值天气预报预报值和实测值之间有不可忽略的误差直接当特征喂进去相当于把第二层误差导进模型。我的做法是有可靠的实测负荷就加负荷天气数据除非确认历史口径与预报口径一致否则先不加。2.3 归一化与时间切分两个决定结果的细节SVR的RBF核通过距离计算样本相似度特征尺度不统一量级大的特征会压制量级小的特征。归一化不是可选项。三种常见缩放方式里MinMaxScaler把特征压到0和1之间适合分布均匀的特征StandardScaler减均值除以标准差适合有正有负、分布接近正态的特征RobustScaler用中位数和四分位距缩放对尖峰和离群点不敏感。电价序列的典型特点是大部分时间在正常范围内波动极小部分时间冲到几倍或拉成负值。MinMax会被尖峰把区间拉得很开平日的数值被压到很窄的范围SVM在那些窄区间的样本上很难精细拟合。我对比过几组公开市场数据RobustScaler的RMSE大致比MinMax低3%到6%。但MinMax也有它的用场预测时遇到训练区间外的电价映射值会超出0到1这个越界信号恰恰提醒模型有极端情况。所以我的习惯是默认用RobustScaler在验证集上跑一次对比哪个RMSE低就用哪个。切分训练集和测试集必须按时间。电价序列有强自相关随机打乱等于把未来的信息泄露到训练集里验证集指标会虚高。常见做法是取前70%的时间段训练、后30%测试要模拟真实交易环境则用滚动窗口。第4章的代码里我会演示前70%后30%的切分第6章再给滚动验证的具体做法。3. 理解SVR的三个关键旋钮epsilon损失、RBF核与参数标定SVM做分类和做回归底层共享同一套“间隔最大化”的思想但目标函数差得很远。如果不先把SVR的三个旋钮弄清楚参数搜索就是无头苍蝇。这一章用白话把原理、选型理由和参数之间的配合关系讲透。3.1 先从原理上理解SVR不是分类器的那个SVMSVM分类的核心理念是找一个最大间隔的超平面把两类样本分开。到了回归问题思路变成寻找一个回归函数让大多数样本的预测误差落在设定的管道宽度epsilon以内同时对超出管道的样本做惩罚。这个epsilon就是SVR和普通回归最本质的区别。普通最小二乘回归对每一个点的误差都严格惩罚SVR则允许小误差存在只要误差不超过epsilon就不计入损失。这对电价预测非常契合电价曲线充满噪音你并不想让模型去拟合每一个无意义的微波动epsilon把“不值得学的抖动”过滤掉模型能更专注于大趋势和尖峰。损失项之外SVR还带一个正则化项通过参数C控制。你可以把epsilon理解为“拟合精度要求”把C理解为“对超差样本的容忍度”。C越大越不允许样本超出管道C越小模型越倾向保留简单平滑的拟合面。电价预测里C通常取0.1到100之间具体要靠搜索确定。做这个小节时经常有人问为什么不直接上LSTM或Transformer我的回答是如果你只有几千个样本、几个到十几个特征、又需要给交易团队解释预测依据SVR在一个小时内就能完成特征工程加参数标定且效果和中等规模的LSTM相当。深度学习优势在大量数据和复杂特征组合上才能体现出来电力市场这种小样本高噪音场景SVR的“少即是多”在运维层面特别实用。3.2 RBF核以及它的C、gamma、epsilon怎么配合核函数的选择决定了特征被映射到多高维的空间。线性核自然不行电价和特征之间的关系明显非线性。多项式核容易在小样本上剧烈振荡实际用得少。径向基函数RBF核是SVR在电价预测里的默认选择它只有一个gamma参数控制单个样本影响力衰减的速度。gamma小的时候每个样本的影响范围大回归面平滑但容易忽略局部细节gamma大的时候样本只影响很近的区域回归面对每个局部都敏感噪音也被学进去了预测值可能大幅震荡。我一般会把gamma设在0.001到1之间搜索。sklearn里有个实用默认值“scale”它按特征数量自动设定gamma在特征数量不多、样本几千量级时往往已经不错可以作为搜索范围的中点而不是起点。C和gamma不是独立起作用的。C大gamma小模型平滑但严格容易欠拟合局部波动C大gamma大模型复杂容易过拟合C小gamma小模型过于宽松预测曲线基本就是一条直线。两者必须一起做网格搜索。epsilon对SVR电价预测的影响容易被忽略。epsilon设得太大小波动全被忽略尖峰也被抹掉设得太小模型去学习噪音泛化变差。电价预测里我很少让epsilon低于0.01常用0.05到0.2之间但要结合损失值的单位来看。如果电价是几十到几百元的量级epsilon取0.1对应的绝对误差要求是十几元可能过宽如果先把电价归一化到0到1epsilon取0.01到0.05更合理。这也是为什么参数搜索必须把epsilon纳入。3.3 网格搜索与时间序列验证参数不是拍脑袋定的参数标定最稳妥的手段是网格搜索配交叉验证。sklearn里的GridSearchCV默认用KFold随机切分但电价预测不能这么干——随机切分会把未来数据用到训练折里造成数据泄漏。我会自己做时间序列的交叉验证把训练集按期数切成几段例如训练集有250天就切成前200天训练加50天验证依次往前滚动在每一折上计算RMSE取平均值作为参数评估结果。参数搜索的网格我分两步。第一轮粗搜把范围带出来比如C[0.1,1,10,100]gamma[0.001,0.01,0.1,1]epsilon[0.01,0.05,0.1,0.2]。得到粗优值以后在优值附近再细化。每个模型训练一次的时间在秒级几百个组合也就几分钟跑得起不需要急着上贝叶斯优化。等样本量到了数万以后再考虑更昂贵的搜索方法也不迟。有一个容易踩的细节交叉验证的评分指标要和最终业务目标一致。如果最终关心的是尖峰时段的预测效果验证应当在RMSE基础上再看分时段的MAE如果最终是拿预测去报价宁可平均误差稍大也不能在极端时段系统性跑偏。搜索的评分指标不要只用默认的r2加一个负MAE或负RMSE的评分项结果更贴近实际。4. 用scikit-learn跑通SVM电价预测最小实现与滚动预测代码不是项目最后才写的东西而是需要先跑通一个能给出预测结果的最小管道再在这个管道上迭代。scikit-learn是最顺手的工具和pandas配合不需要额外引入重量级框架。这一章的代码可以直接复制修改注释和参数说明放在每个块后面。4.1 数据管道读入CSV、构造特征、切分并归一化起步代码分成四步读数据、构造特征、按时间切分、缩放。看起来简单但每个步骤都有容易写错的地方。import pandas as pd import numpy as np from sklearn.svm import SVR from sklearn.preprocessing import RobustScaler from sklearn.metrics import mean_absolute_error, mean_squared_error # 1. 读入1小时粒度的电价历史数据 # csv至少要包含 time 和 price 两列price单位为元/MWh df pd.read_csv(price_data.csv, parse_dates[time]) df df.set_index(time).sort_index() # 2. 构造特征矩阵 def make_features(data, lags(1, 2, 24, 25, 48, 168)): data data.copy() for lag in lags: data[fprice_lag_{lag}] data[price].shift(lag) data[hour] data.index.hour data[weekday] data.index.weekday data[is_weekend] (data[weekday] 5).astype(int) data[rolling_mean_6h] data[price].rolling(6).mean() data[rolling_max_24h] data[price].rolling(24).max() data[rolling_min_24h] data[price].rolling(24).min() return data.dropna() data make_features(df) feature_cols [c for c in data.columns if c ! price] # 3. 按时间顺序切分前70%训练后30%测试 cut int(len(data) * 0.7) train, test data.iloc[:cut], data.iloc[cut:] # 4. 特征和目标值分别缩放目标值要reshape成二维 scaler_X RobustScaler() scaler_y RobustScaler() X_train scaler_X.fit_transform(train[feature_cols]) y_train scaler_y.fit_transform(train[price].values.reshape(-1, 1)).ravel() X_test scaler_X.transform(test[feature_cols]) y_test test[price].values代码逻辑不复杂但有两处必须留意。shift(lag)产生的滞后项会让最早168小时的样本在dropna时被丢弃这部分数据不能用于训练如果数据量本身小用fillna(0)或者fillna(中位数)代替dropna也可以但要意识到这些小时的特征并不真实。RobustScaler对电价尖峰的稳健性前面提过这对应的是缩放那两行。目标值也做了缩放这样SVR的epsilon可以按0到1的尺度理解而不是直接换算成几十元的绝对误差。4.2 训练SVR并滚动预测未来24小时训练模型后做未来24小时的逐时预测。滚动预测的意思是每预测完一个点把预测值接回历史序列用它构造下一小时的滞后特征。这样模型的每一步都能利用最新信息。# 训练SVR模型 model SVR(kernelrbf, C10.0, gammascale, epsilon0.05) model.fit(X_train, y_train) # 滚动预测函数输入已训练模型、缩放器和历史dataframe def forecast_horizon(model, scaler_X, scaler_y, df, feature_cols, start_time, horizon24): df df.copy() preds [] for i in range(horizon): # 每轮用当前序列重建特征然后取最后一行作为预测输入 df make_features(df) last_row df[feature_cols].iloc[-1:].copy() # 特征里含NaN时做前向填充防止滚动窗口未满导致预测中断 last_row last_row.ffill().fillna(0.0) X scaler_X.transform(last_row) y_scaled model.predict(X)[0] y scaler_y.inverse_transform([[y_scaled]])[0][0] preds.append(y) # 把预测值追加到序列末尾下轮用它生成新的滞后特征 new_time start_time pd.Timedelta(hoursi 1) df.loc[new_time, price] y return preds start_time test.index[0] preds_24h forecast_horizon(model, scaler_X, scaler_y, test, feature_cols, start_time, horizon24)这里有个容易被忽略的工程细节。make_features内部用了rolling(24).max()这类窗口统计滚动预测到第1到第24小时之间历史序列尾部其实还没有完整的24小时窗口因为预测值是逐步拼接上去的所以代码里对NaN做了前向填充。如果忽略这一行预测会在某个位置产生NaN错误并不显眼但结果全乱。另一个细节预测值接回序列后滞后特征里会包含前一小时的预测值本身。这是自回归式滚动的正常行为让模型在没有真实数据的情况下一步步推下去但也意味着误差会累积。第6章的残差修正就是针对这一点。如果只预测下一个单点输入全用真实历史特征误差会小很多这也是实时交易预测里多数用单步预测而不是一次性滚24小时的原因。4.3 怎么读评估指标MAE、RMSE、MAPE都有边界# 对测试集整体做单步评估 y_pred_all scaler_y.inverse_transform(model.predict(X_test).reshape(-1, 1)).ravel() mae mean_absolute_error(y_test, y_pred_all) rmse float(np.sqrt(mean_squared_error(y_test, y_pred_all))) # MAPE的分母保护真实电价接近0时用一个小底数代替 denom np.maximum(np.abs(y_test), 1e-3) mape np.mean(np.abs((y_test - y_pred_all) / denom)) * 100 print(fMAE{mae:.2f}元/MWh, RMSE{rmse:.2f}元/MWh, MAPE{mape:.2f}%)这段代码对MAPE加了分母保护防止真实电价接近0时指标爆炸。真实的电价序列经常在夜间跌到个位数甚至负值MAPE在负电价和近零电价上几乎失去意义。我实际项目里以MAE和RMSE为主MAPE只做辅助参考并且会按小时段分别统计看看夜间是不是系统性偏大。RMSE对大误差平方放大能反映尖峰时段的预测表现但只报RMSE而不报MAE很容易让一个整天预测偏平缓的模型蒙混过关——它把尖峰都削掉了RMSE看起来不错MAE却说明平均偏差很大。两个指标一起看才不会被单一数字带偏。5. 短期电价预测避坑指南五个让模型翻车的隐蔽问题模型能跑通和模型能用于报价之间隔着一打坑。以下五点是我在多个数据集上实际踩过或看别人踩过的每条按现象、原因、解决的顺序写方便直接对照排查。5.1 预测曲线整体滞后一天现象预测曲线和真实曲线的形状高度相似但在时间轴上向后错位看起来就是“慢了一天”。原因电价有非常强的日周期性t-24滞后项在模型里权重占比最高模型学到的捷径是“明天这一小时等于今天这一小时”。一旦某天价格因为天气或检修发生整体偏移预测就比真实变化慢。解决加入t-25滞后项并把t-24与t-25的差值作为独立特征让模型感知相邻小时的变化方向。同时把小时编号变成数值特征允许它与滞后项交互例如构造“hour × price_lag_24”的交叉特征。这样模型才能区分“昨天这个时间的水平”和“这个小时自身的周期位置”。5.2 夜间MAPE异常放大现象整体MAPE看起来在10%以内按小时段一拆凌晨两点到六点MAPE达到100%甚至更高。原因MAPE的分母是真实电价凌晨低价时段真实值接近零哪怕绝对误差只有5块钱百分比也被放大到几倍。这不是模型变差了是指标选错了场景。解决改用带底价的MAPE变体比如把绝对值电价低于10元/MWh的时刻从MAPE计算里剔除业务上重点看这些时段的绝对误差因为夜间低价的绝对偏差对交易盈亏影响很小。把分时段MAE和RMSE一起列出来是更诚实的汇报方式。5.3 特征越加越多精度反而下降现象初始模型只用滞后和日历特征RMSE在30附近加了光伏出力、风电出力、温度等八个特征后RMSE反而跳到40。原因SVR在RBF核下用距离度量样本相似度冗余特征会稀释核距离的判别力。天气和新能源出力如果来自数值天气预报引入的是预报误差模型学到的是一层有偏信号。最直接的表现是滚动平均特征和滞后特征之间的相关性往往超过0.9把同质信息重复喂进去没有收益。解决控制特征数量在10到15个以内新特征先单独和预测目标做相关性验证再加进模型做环比测试。加特征不改善验证集就去掉不要舍不得。5.4 电价尖峰被平滑掉现象真实电价在某个时段冲到300元/MWh模型预测只有60元峰值被压得很平。原因SVR的epsilon管道和C正则化天然偏好平滑回归面尖峰样本在训练集里占比可能只有1%损失贡献被淹没参数搜索时也因为尖峰被压低而更容易获得更低的整体RMSE。解决训练前把尖峰样本单独标记对它们施加更高的样本权重或者把训练目标从原始电价改成电价的平方根或对数变换降低尖峰的绝对量级也可以在偏重尖峰的时段单独训练一个SVR和平日模型并行预测。需要明白尖峰不是随便就能学的——历史上尖峰的原因各不相同模型能学到的是“什么时候容易尖”具体高度由市场供需决定对尖峰预测要有一个合理的期望上限。5.5 RMSE在不同测试周之间剧烈波动现象同一个模型在测试集前三周RMSE只有20后两周跳到45。原因静态切分把不同市场状态混进了测试集。某一段时间发生机组检修或极端天气时电价曲线形态全变模型在训练里从没见过这种状态单次切分的验证结果不具备代表性。解决把测试集按周分段连续取4到5个周段分别评估报告RMSE的均值和标准差模型发布时附带这个稳定性区间。更进一步用第6章的滚动验证方式每次训练窗口后向前验证一周把验证分数串成一条时间线模型什么时候开始失效一目了然。6. 让SVM电价预测更稳的进阶方案残差修正、滚动重训与可视化验证模型不是训练一次就结束。电价市场的状态会变SVM的回归面也需要跟上。我常用的进阶手段按性价比排序有三个。第一个是残差修正。SVR第一轮预测会漏掉不少系统性偏差比如周末的峰谷时段总是偏平。把第一轮预测值和真实值相减得到残差序列用另一个简单模型线性回归或轻量GBDT去拟合特征到残差的映射最终预测值等于SVR预测值加上残差模型输出。前提是残差里确实有可学的模式先用自相关函数看残差是否还有显著的时序相关。如果残差已接近白噪音说明SVR把信息榨干了强行叠加只会引入噪音。第二个是滚动重训节奏。每周重训一次比每24小时重训一次更划算。每天重训的代价是模型参数会随着偶然波动跳来跳去而且SVR在几千样本上训练虽然只要几秒但整套流程里还有特征重算和验证运维成本会翻上去。我习惯每周日收盘后拉取过去8周数据重训并把未来一周作为验证周记录RMSE的变化趋势。市场规则变更后立即重训并对比旧模型看RMSE是否跳变是判断规则影响最直观的方法。第三个是可视化验证。数字指标能说明模型差多少但不容易说明差在哪里。把滚动预测的曲线按周画出来和真实电价叠在一张图上标出尖峰和低谷时段import matplotlib.pyplot as plt plt.figure(figsize(12, 4)) plt.plot(y_test[:168], label实际电价) plt.plot(y_pred_all[:168], labelSVM预测) plt.xlabel(小时) plt.ylabel(电价(元/MWh)) plt.legend() plt.show()我先看三件事预测曲线有没有整体偏移、夜间是否贴地、尖峰时段是延迟还是削平。画图不需要花哨matplotlib一个折线图就够。懒得做可视化的话至少把分时段的误差热力图打出来那能一眼看出哪个小时段的预测常年偏差最大这是让新同事最快理解模型行为的手段。最后说一个我的习惯改特征或参数时一次只改一处记录验证集RMSE和曲线形态变化不要同时动多个旋钮否则你永远不知道是哪个改动让结果变好的。这套流程用下来SVM电价预测在多数现货市场数据上做到MAE在正常电价水平5%到10%之间并不是难事。希望这些趟过的坑能帮你在自己的数据上少走一遍弯路。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

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