简介:面向电力市场量化研究者与机器学习初学者,这份资源聚焦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:不是分类器的那个SVM
SVM分类的核心理念是找一个最大间隔的超平面把两类样本分开。到了回归问题,思路变成:寻找一个回归函数,让大多数样本的预测误差落在设定的管道宽度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到1,epsilon取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[f"price_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(kernel="rbf", C=10.0, gamma="scale", epsilon=0.05) model.fit(X_train, y_train) # 滚动预测函数:输入已训练模型、缩放器和历史dataframe def forecast_horizon(model, scaler_X, scaler_y, df, feature_cols, start_time, horizon=24): 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(hours=i + 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, horizon=24)这里有个容易被忽略的工程细节。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(f"MAE={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], label="SVM预测") plt.xlabel("小时") plt.ylabel("电价(元/MWh)") plt.legend() plt.show()我先看三件事:预测曲线有没有整体偏移、夜间是否贴地、尖峰时段是延迟还是削平。画图不需要花哨,matplotlib一个折线图就够。懒得做可视化的话,至少把分时段的误差热力图打出来,那能一眼看出哪个小时段的预测常年偏差最大,这是让新同事最快理解模型行为的手段。
最后说一个我的习惯:改特征或参数时,一次只改一处,记录验证集RMSE和曲线形态变化,不要同时动多个旋钮,否则你永远不知道是哪个改动让结果变好的。这套流程用下来,SVM电价预测在多数现货市场数据上做到MAE在正常电价水平5%到10%之间并不是难事。希望这些趟过的坑能帮你在自己的数据上少走一遍弯路。
本文还有配套的精品资源,点击获取