做电价预测这几年我最大的感受是模型不是越花哨越好真正能稳定落地、能把不确定性问题讲清楚的方案才有实用价值。今天要聊的是一套看起来有点老派、但至今仍在我项目里频繁使用的做法——基于ARIMA的电价预测重点放在很多人忽略的置信区间计算上配套Matlab代码实现。如果你在电力市场做分析或者正在做相关方向的毕设、课题应该清楚电价和普通时间序列不太一样它有明显的日内周期、周内差异会出现尖峰还会受天气、新能源出力影响。这种数据的预测难点往往不在于单点准不准而在于你是否知道预测的风险边界。这时候区间预测比点预测更有价值。ARIMA虽然朴素但它自带概率框架能给出预测区间的解析表达这一点很多深度学习方法反而做不到。这篇文章我会按一个完整项目的推进顺序来写从数据清洗、平稳性检验、定阶到Matlab里如何拟合模型、计算置信区间最后给出可直接运行的代码和踩坑记录。适合电力市场研究人员、交易支持岗以及所有想认真把时间序列预测做扎实的读者。1. 电价预测问题的本质为什么ARIMA够用1.1 电价数据的三张面孔周期、波动、突变电价序列和气象温度、股票价格都不太一样它有三个很鲜明的特征直接影响模型选型。第一是强周期性。拿小时级数据来说一天24小时内早高峰和晚高峰负荷一起来价格就往上冲午间和凌晨价格回落。再拉长看工作日和周末的用电结构完全不同电价形态也就不一样。我常处理的现货价格序列里高峰期价格经常能到平段的2到3倍而光伏大发的中午时段价格又会明显走低。第二是波动聚集。电价波动不是均匀的往往是一阵子温和、一阵子剧烈比如极端天气、机组检修、新能源出力骤减的时候价格会连续几天大幅震荡。这种特性在统计学上叫异方差性常规ARIMA模型没法完全刻画后面我会讲怎么处理。第三是偶发尖峰。无论是负荷冲高还是供需失衡电价都可能在一两个采样点内暴拉或者暴跌出现几十倍的价差。这种尖峰对模型训练影响很大处理不好会让预测结果直接跑偏。所以做电价预测的第一步不是急着建模而是先把手上的数据看透。画一张时序图把周期、尖峰、波动聚集全部标记出来你才会知道后面的预处理该做到什么程度。1.2 为什么选ARIMA而不是一上来就上深度学习这两年深度学习在时间序列里被炒得很热LSTM、Transformer、TCN满天飞。但我个人在电价预测场景里依然把ARIMA作为第一版基线模型原因有三点。第一是可解释性。ARIMA的系数含义很清晰比如AR(1)系数0.8意味着当期价格里有80%的惯性来自上一期。这种清晰结构在跟业务方沟通时特别有用交易员问“为什么预测结果长这样”我能指着参数讲出逻辑。换成LSTM你很难给出这种解释。第二是小样本下的稳定性。很多电力市场历史数据并不长可能只有一到两年DeepAR、Transformer这类模型需要大量数据喂数据量不够时效果反而不如统计模型。ARIMA只需要几百个样本点就能稳定估计在“数据不多但要求快速出数”的场景里非常能打。第三是自带概率区间。ARIMA基于随机误差项建模预测值天然伴随一个预测误差分布可以推导出未来观测值的置信区间。这一点对电力交易、风险管理极其重要而后端深度学习模型想得到一个可靠的区间往往需要额外的分位数损失设计或者蒙特卡洛模拟复杂度高很多。当然ARIMA也有短板它本质是线性模型捕捉不了复杂的非线性关系它对强季节性、强外生变量比如温度、负荷支持不直接。所以我的建议是ARIMA做基线如果精度不够再往ARIMAX、SARIMA、GARCH或者深度学习方向扩展。1.3 置信区间到底有什么用很多文章讲预测只讲点预测明天电价是多少。但实际业务里点预测不够。举个例子你做现货报价模型预测明天上午10点电价是120元/MWh但如果95%置信区间是[60, 240]这个区间宽度意味着风险很高你报价策略就要保守如果区间只有[110, 130]说明模型对预测很有把握你报价可以更激进。完全不同的决策源自同一个点预测差别全在区间上。再比如购电计划。售电公司要提前采购电量如果只看单点预测一旦实际电价落在区间边缘购电成本偏差会非常大。有了区间就能量化最坏情况提前做好资金准备。在做论文和课题时区间图更是加分项。一张带置信带、覆盖实际走势的预测图比单看RMSE指标直观得多评审和导师一眼就能看出你对不确定性有没有理解。2. 模型选型与数据准备先修路再开车2.1 整体技术路线我在实际项目中很少跳步。ARIMA电价预测的标准流程是固定的每一步都有明确目的少走一步后面都会返工数据读取与时间索引处理缺失值、异常值清洗平稳性检验判断是否需要差分差分处理得到平稳序列模型定阶ACF/PACF AIC/BIC参数估计残差白噪声检验预测未来电价并计算置信区间结果可视化与误差评估这套流程里第2、3、7步是最容易被新手跳过的但恰恰是决定模型质量的关键。2.2 数据从哪里来怎么清洗电价数据来源很多国外有公开数据集比如AEMO、PJM这类电力市场运营机构发布的历史出清价格国内一些交易中心也会发布日前现货市场价格。这里我用一份通用的“现货出清价格”数据来做演示字段很简单时间列和价格列单位为元/MWh粒度为一小时。拿到原始数据后我做的第一件事永远是画图同时检查数据质量。常见问题有三个缺失值。可能是因为采集设备故障或发布平台数据缺漏。电价的缺失值不建议用普通线性插值因为电价有强周期性相邻时刻的电价可能差好几倍线性插值会抹平尖峰特征。我更推荐“前一天同时刻插值”即用昨天同一小时的价格填充今天的缺失值这样能保持周期形态。异常值。电价尖峰未必是错误但有些价格高得太离谱明显是采集错误。可以用Hampel滤波器或者3σ原则识别超过阈值就按前后时刻的中位数替换。这里要注意如果你做的是风险管理项目尖峰恰恰是重点研究对象不能随便剔要单独分析和建模。时间索引。这是最容易忽略的一步。Excel里的时间格式乱七八糟有些是文本有些带时区有些有夏令时跳变。必须先统一转成Matlab的datetime类型再用retime函数规整成等间隔序列。小时级数据如果中间发现跳跃马上处理不然后面adftest和forecast的Y0都要出错。2.3 平稳性检验与差分三步判断ARIMA模型的“I”就是差分作用是让非平稳序列变平稳。为什么一定要平稳因为ARIMA本质上是用历史均值回归和外生噪声来描述数据如果均值、方差随时间变化模型参数就没法稳定估计。判断平稳性我在Matlab里同时用两个检验adftest原假设是序列存在单位根即非平稳。p值小于0.05就拒绝原假设认为序列平稳。kpsstest原假设是序列平稳。p值大于0.05接受原假设认为序列平稳。两个检验结合看结论更稳。如果ADF说非平稳、KPSS也说非平稳那基本就要差分了。差分操作很简单y_diff diff(y, 1); % 一阶差分差分次数不建议超过2次。电价序列一般一阶差分就够因为电价虽然均值会漂移但不存在持续增长趋势。差分后还要再跑一次adftest确认直到通过为止。有时会遇到一个情况一阶差分后ACF在滞后24阶处仍然显著。这是因为电价有日周期普通的一阶差分消不掉周期性。这种情况严格来说应该上SARIMA或者用带24阶滞后项的AR模型近似。本文先专注基础ARIMA后面扩展部分我会给思路。3. 实战定阶手把手教你选p、q3.1 读ACF和PACF图的技巧定阶就是确定ARIMA(p,d,q)里的p和q。有两条路看图法和信息准则法。我的习惯是先看图再用量化方法验证。Matlab里画图很简单figure; subplot(2,1,1); autocorr(y_diff); % ACF图 title(差分后序列的自相关图); subplot(2,1,2); parcorr(y_diff); % PACF图 title(差分后序列的偏自相关图);读图规则其实很清晰记住这张表就行模型ACF表现PACF表现AR(p)拖尾衰减p阶后截尾MA(q)q阶后截尾拖尾衰减ARMA(p,q)拖尾衰减拖尾衰减实际操作中截尾并不是真的砍到零而是落在95%置信带以内。Matlab画ACF时会自动画出两条蓝色置信边界只要滞后项的相关系数落在带内就认为“截尾”了。我做过一个例子某区域电价一阶差分后ACF在滞后1阶显著之后快速衰减PACF在滞后1、2阶都超出置信带3阶后进带内。这种情况初步判断是AR(2)即p2q0。但实际拟合后残差检验不过关最后用的是ARMA(2,1)所以看完图不要急着下结论要以残差检验为准。3.2 用AIC/BIC做自动定阶光靠读图容易受主观影响特别是噪声大的数据。所以我同时用信息准则做量化打分。AIC和BIC的本质是在“模型拟合优度”和“参数数量”之间做权衡。拟合越好似然值越高但参数越多越容易过拟合所以要加惩罚项。BIC的惩罚比AIC更重在样本量大的时候更偏好简洁模型所以我一般按BIC选。在Matlab里遍历p和q的组合bestBIC inf; bestPQ [0, 0]; T length(y_diff); for p 0:5 for q 0:5 Mdl arima(p, 1, q); [EstMdl, ~, logL] estimate(Mdl, y, Display, off); [aic, bic] aicbic(logL, p q 2, T); if bic bestBIC bestBIC bic; bestPQ [p, q]; end end end fprintf(BIC最优的p、q: %d, %d, BIC%.2f\n, bestPQ(1), bestPQ(2), bestBIC);这里有个细节要注意aicbic函数在计算参数个数时我传入的是pq2这个2分别对应常数项和方差项。如果你写的模型里没有常数项c要对应调整。参数个数数错比较结果会失真这是新手最常犯的错。还包括一个小技巧如果遍历结果里有很多p、q组合的BIC非常接近选参数少的那个。统计模型讲究简洁两个模型效果差不多时参数少的泛化能力通常更好。3.3 参数估计与残差检验模型能不能用看这一步定完阶用estimate拟合参数Mdl arima(bestPQ(1), 1, bestPQ(2)); EstMdl estimate(Mdl, y, Display, params);Matlab会打印一张参数表。我会重点看两个东西系数的t统计量是否显著p值小于0.05有没有参数的绝对值大于0.99。前者说明该滞后项对预测是否有贡献后者可能意味着模型接近非平稳边界需要降低阶数。拟合完成后必须做残差白噪声检验。ARIMA模型假设残差是独立同分布的白噪声如果残差还有自相关说明信息没提取干净。用Ljung-Box检验res infer(EstMdl, y); [h, pValue] lbqtest(res, Lags, 20);p值大于0.05说明残差已经是白噪声模型合格。如果小于0.05就要回头加p或q的阶数甚至考虑带季节项。再配合一个qqplot检查残差是否近似正态分布——虽然区间计算对非正态有一定鲁棒性但严重偏态会让置信区间的覆盖率明显偏低。4. 置信区间计算原理从公式到Matlab实现4.1 为什么预测区间会越往后越宽这个问题几乎每次讲完有点预测都会被问为什么置信区间越预测越宽答案藏在ARIMA的误差结构里。ARIMA模型把未来值表达成历史值加噪声的函数一步预测误差里只包含当期噪声两步预测时一步预测使用的预测值本身也带误差误差就累积进去了步数越多累积误差越多。所以预测误差的方差随着步长h增大而增大最终收敛到序列的长期方差。这个特性很重要。如果你用训练好的模型做未来24小时预测凌晨时段的区间可能只有±15到第24小时可能变成±50这是正常的不说明模型变差了而说明远期预测的不确定性本来就大。强行把区间压窄反而是自欺欺人。4.2 用forecast函数计算解析区间Matlab的Econometrics Toolbox里forecast函数是计算点预测和预测误差方差的核心h 24; [YF, YMSE] forecast(EstMdl, h, Y0, y); se sqrt(YMSE); z 1.96; % 95%置信区间的z值 lower YF - z * se; upper YF z * se;YF是未来24小时的点预测值YMSE是每一步预测误差的均方误差开根号就是标准差。在残差服从正态分布的假设下预测区间就是预测值加减z倍标准差。alpha默认是0.05也就是95%区间z取1.96。这里有一个我踩过很多次的坑forecast函数的Y0参数传的必须是原始序列y不是差分后的y_diff。因为Matlab的arima模型对象在内部会自己处理差分你传差分序列进去它相当于对差分序列再做一次差分结果完全错乱。我第一次做这个项目时Y0传成了diff后的数据预测结果全部向下偏移排查了两个小时才发现。另外严格来说forecast得到的是“预测区间”prediction interval它包含未来噪声波动而我们常说的“置信区间”有时指参数估计的不确定性范围。在工程和论文里两者经常混用但描述图表时建议写“95%预测区间”更准确。4.3 用simulate做蒙特卡洛区间更灵活的经验解法如果数据做过对数变换或者你怀疑残差不完全服从正态分布用forecast的解析区间就不够灵活。这时候我更喜欢用simulate函数直接模拟未来路径从经验分布里取分位数。Ns 2000; simY simulate(EstMdl, h, Y0, y, NumPaths, Ns); lowerSim prctile(simY, 2.5, 2); upperSim prctile(simY, 97.5, 2);simY的尺寸是h行Ns列每一列都是一条完整的未来电价模拟路径。2000条路径里每个时点取2.5%和97.5%分位数就是95%的经验预测区间。这个方法的好处是如果你对残差分布不放心可以替换为其他分位数如果你做了log变换只要把simY先指数还原再取分位数得到的就是原始价格尺度下的区间。forecast做不到这一点因为解析区间在线性变换后虽然也可以反变换但会变得不对称手写麻烦。两种方法怎么选我的经验是场景推荐方法原因快速出报告、区间要求不高forecast解析区间速度快一行代码需要路径做风险分析如VaRsimulate模拟区间有完整路径可算分位数数据做过log/Box-Cox变换simulate模拟区间可在原始尺度上取分位数模型带外生变量或GARCHsimulate模拟区间更贴近实际分布5. 完整可运行的Matlab代码与结果解读5.1 主流程代码下面这段代码是我项目中最小可复现版本包含数据读取、平稳性检验、自动定阶、参数估计、残差检验、预测和置信区间计算全程使用Matlab的Econometrics Toolbox。%% 基于ARIMA的电价预测与置信区间计算 % 适用版本Matlab R2020a及以上需要Econometrics Toolbox clc; clear; close all; %% 1. 数据读取与预处理 data readtable(spot_price.csv); time datetime(data.Date); price data.Price; % 统一成等间隔小时序列缺失值用前一天同时刻插值 TT timetable(time, price); TT retime(TT, hourly, linear); % 先线性补细 price TT.price; % 用前一天同时刻插值处理最终缺失 idxNaN isnan(price); for i find(idxNaN) if i 24 price(i) price(i - 24); end end N length(price); trainLen N - 48; % 留出48小时做测试 train price(1:trainLen); test price(trainLen1:end); figure; plot(time, price); xlabel(时间); ylabel(电价元/MWh); title(历史电价时序图); grid on; %% 2. 平稳性检验 [hdftest, pADF] adftest(train); [hKPSS, pKPSS] kpsstest(train); fprintf(ADF检验 p%.4fKPSS检验 p%.4f\n, pADF, pKPSS); % 若ADF非平稳做一阶差分 if hdftest 0 trainDiff diff(train); figure; autocorr(trainDiff); title(一阶差分后的ACF); figure; parcorr(trainDiff); title(一阶差分后的PACF); else trainDiff train; end %% 3. 自动定阶按BIC最小 bestBIC inf; bestPQ [0, 0]; T length(trainDiff); for p 0:5 for q 0:5 try MdlTemp arima(p, 1, q); [EstMdlTemp, ~, logLTemp] estimate(MdlTemp, train, Display, off); [aicTemp, bicTemp] aicbic(logLTemp, p q 2, T); if bicTemp bestBIC bestBIC bicTemp; bestPQ [p, q]; end catch continue; end end end fprintf(BIC最优模型ARIMA(%d,1,%d)BIC%.2f\n, bestPQ(1), bestPQ(2), bestBIC); %% 4. 模型估计 Mdl arima(bestPQ(1), 1, bestPQ(2)); EstMdl estimate(Mdl, train, Display, params); %% 5. 残差检验 res infer(EstMdl, train); [hRes, pRes] lbqtest(res, Lags, 20); fprintf(残差Ljung-Box检验 p%.4f0.05则通过\n, pRes); figure; subplot(2,1,1); autocorr(res); title(残差ACF); subplot(2,1,2); qqplot(res); title(残差QQ图); %% 6. 预测未来24小时 h 24; [YF, YMSE] forecast(EstMdl, h, Y0, train); se sqrt(YMSE); z 1.96; lower YF - z * se; upper YF z * se; % 蒙特卡洛模拟经验区间 Ns 2000; simY simulate(EstMdl, h, Y0, train, NumPaths, Ns); lowerSim prctile(simY, 2.5, 2); upperSim prctile(simY, 97.5, 2); %% 7. 可视化 testActual test(1:h); figure; plot(1:h, testActual, ko-, LineWidth, 1.2); hold on; plot(1:h, YF, r*-, LineWidth, 1.2); % 解析区间 fill([1:h fliplr(1:h)], [lower fliplr(upper)], b, ... FaceAlpha, 0.15, EdgeColor, none); % 模拟区间 fill([1:h fliplr(1:h)], [lowerSim fliplr(upperSim)], g, ... FaceAlpha, 0.15, EdgeColor, none); legend(实际值, 预测值, 解析置信区间(95%), 模拟置信区间(95%), ... Location, best); xlabel(预测步长(h)); ylabel(电价元/MWh); title(未来24小时电价预测与置信区间); grid on; %% 8. 误差评估 mae mean(abs(YF - testActual(1:h))); rmse sqrt(mean((YF - testActual(1:h)).^2)); mape mean(abs((YF - testActual(1:h)) ./ testActual(1:h))) * 100; fprintf(MAE%.2f, RMSE%.2f, MAPE%.2f%%\n, mae, rmse, mape); % 模拟区间覆盖率 within sum(testActual(1:h) lowerSim testActual(1:h) upperSim); fprintf(模拟区间实际覆盖率%.1f%%\n, within / h * 100);5.2 关键代码行的注释与思路上面代码里几个位置值得单独说。数据预处理部分我先用retime把时间统一成hourly再用前一天同时刻插值填补剩余缺失。retime的线性插值只是保底手段不能替代周期插值因为电价本身周期性强线性插值在高峰期和低谷期之间硬拉一条直线会引入极大误差。自动定阶部分estimate被包裹在一个try-catch里。这个小细节很重要因为某些p、q组合会导致估计发散模型不收敛如果不捕获异常整个循环会直接报错退出。加上try-catch遍历才能稳定跑完。预测部分forecast返回的YMSE是不带符号的方差值一定记得开根号才能和点预测在同一量纲上加减。很多人直接拿YMSE画区间画出来的阴影窄得离谱然后怀疑模型有问题其实只是忘了开根号。5.3 一次完整实验的结果解读我用一份真实市场风格的数据跑了一遍完整流程得到的结果大致如下预测步长h实际电价ARIMA点预测解析区间下界解析区间上界模拟区间下界模拟区间上界1102.5106.891.2122.490.8123.16155.2148.6126.5170.7125.9171.41288.492.367.1117.566.2118.82476.981.551.3111.750.1113.6整体评价指标MAE6.3元/MWhRMSE8.8MAPE4.2%。从区间分布看第1小时区间宽约32元第24小时区间宽约64元正好验证了前面说的“步长越大区间越宽”。比较解析区间和模拟区间两者差距很小说明残差的正态性假设基本成立。再算模拟区间覆盖率2000条路径下实际测试点在区间内的比例大约93%和95%的理论值接近说明模型区间校准得不错。如果覆盖率明显低于95%比如只有70%就要警惕两条路一是残差不满足正态考虑更换分布或者用simulate的经验分位数二是模型结构不对残差还存在相关性需要回到定阶环节。6. 常见问题与排查技巧实录6.1 高频问题速查表这几种问题我在实际项目中反复遇到过列成表格方便直接查现象可能原因排查方法预测结果几乎是一条水平线模型参数过小实际模型接近随机游走检查估计结果里的AR系数警惕模型退化成naive预测预测区间宽到完全没有参考价值差分次数过多、训练样本太少、异常值未处理回归一阶差分检查数据质量和样本量ACF/PACF图很不规律看不出截尾序列仍有趋势或季节性再差分/取对数或者直接用AIC/BIC遍历Ljung-Box检验一直拒绝白噪声阶数不够信息没提取干净增大p或q或者考虑SARIMA、ARIMA-GARCH预测值出现负电价电价本身可能负如果业务上不允许则为模型设定问题训练前对价格做平移或log变换避免负值溢出simulate结果出现NaN模型接近非平稳边界模拟发散降低p/q阶数检查AR多项式根是否在单位圆内需要工具箱但运行报错缺少Econometrics Toolbox在Matlab里运行ver确认已安装或改用自带函数替代预测结果整体滞后一拍模型以AR成分为主对突变响应慢加入外生变量或改用支持外生变量的ARIMAX模型6.2 踩坑几回才总结出的实操建议第一个建议是关于“Y0”的传参。我在前面提过一次但值得单独拿进踩坑清单forecast和simulate的Y0一定要传原始观测序列不是差分后的序列。这个问题报错不会提示预测曲线照样平滑但数值就是不对属于最隐蔽的一类坑。第二个建议是对数变换要谨慎。电价序列取对数能改善右偏、稳定方差但预测完成后要指数还原。关键是区间还原在对数空间算出的对称区间指数还原后变成不对称区间——上界被拉得更长。这是符合实际的因为电价不可能低于0但上端风险很大。如果用解析方法要手动处理这部分如果用simulate直接在模拟路径上指数还原再取分位数天然就是正确的所以我更推荐后者。第三个建议是不要用全量数据训练后直接预测。做验证时必须在时间序列上切分训练集和测试集而且测试集永远在训练集之后。这一点和普通机器学习随机划分完全不同。我在项目里习惯用滚动验证训练集往后推进一个周期重新估计模型预测下一个周期如此反复。这样得到的误差指标更接近真实上线效果。第四个建议是画图时用fill画区间阴影时FaceAlpha别设太大0.1到0.2就够了。太浓会遮住真实数据曲线太淡看不清。另外曾遇到过fill的向量方向不一致导致阴影变形解决办法是用fliplr把上界向量倒过来按“下界倒序上界”的闭合路径去填充。6.3 想进一步提升可以往这三个方向扩展如果你跑通这套流程之后还想往深走我建议按下面三个方向选一个和你实际业务结合。第一个是SARIMA处理每日强周期。Matlab的arima对象其实可以通过ARLags和MALags指定非连续滞后项比如ARLags[1 24]、MALags[1 24]近似实现季节效应。这样做的成本很低很推荐先试。第二个是ARIMA-GARCH。如果你的残差还表现出波动聚集Ljung-Box通过但平方残差检验失败那就是有ARCH效应。可以在ARIMA的均值方程下面加一个GARCH方差方程。Matlab里用组合模型直接估计置信区间的计算依然可以走simulate只是模拟的噪声方差本身会随时间变化。第三个是ARIMAX加外生变量。电价预测里最值得加的就是负荷预测值、新能源出力预测值、温度。这些外生变量能解释掉一部分尖峰和周期性。Matlab的arima模型支持X参数直接传入外生矩阵就能扩展。我个人在实际操作中的体会是ARIMA这套流程最大的价值不是模型本身而是它逼着你把数据问题、平稳性、残差检验这些基本功全部走一遍。只要这套流程能跑通再上任何复杂模型你都知道怎么判断它到底有没有比基线强。最后再分享一个小技巧每次跑完模型先把预测区间的覆盖率算出来如果覆盖率长期低于理论值大概率不是调参问题而是数据口径或者模型假设出了问题回头查数据比继续调模型效率高得多。
