电价预测在电力市场里是个经久不衰的课题。想把它做好,第一步往往不是急着选模型,而是把手里的历史数据吃透。我在实际做这个方向时,ARIMA模型是绕不开的基础工具——它不像LSTM那样需要大量样本和复杂调参,也不像XGBoost那样需要精心构造一大堆特征,它只靠电价序列自身的历史信息就能提取趋势和惯性结构,而Matlab里的计量经济学工具箱让整个流程落地非常快。
但今天想重点聊的不只是把ARIMA跑出来。很多入门分享只讲点预测,拟合一条曲线就说“看,预测得不错”,这在真实场景里远远不够。电价预测最终要服务决策,比如购电计划、售电竞价、风险对冲,这些决策都需要知道预测值的不确定范围,也就是说你得把置信区间一起算出来。这个置信区间不是后期拍脑袋加上的,而是ARIMA模型自身的理论输出,Matlab里几行代码就能拿到。
这篇文章会以Matlab代码实现为主线,把ARIMA电价预测的完整链路拆开讲:从数据预处理、平稳性检验、定阶、参数估计到预测,再到置信区间计算与可视化。每一步为什么这样做、哪里容易踩坑,我会结合自己实际跑过的场景来聊。内容既适合刚接触时间序列建模的读者,也适合已经用上机器学习模型、想补一个经典基准模型的同行参考。
1. 为什么电价预测需要ARIMA建模
1.1 电价数据的典型特征与建模难点
电价序列与普通金融序列相比有几类显著特征。首先,它有强周期性:一天之内,早高峰和晚高峰的价格上抬明显,凌晨低谷几乎贴近地板价;一周之内,工作日的价格水平和周末有明显差异;如果数据跨度足够长,还能看到季节性的冷暖负荷影响。其次,电价序列经常有尖峰和跳跃,比如极端天气、机组非计划停机、负荷突增,都会让价格瞬间冲到上限。这些特征意味着序列不平稳、分布厚尾、波动聚集,直接套用标准模型很容易出问题。
我在处理数据时的一个习惯是,先画出完整的价格曲线,不急着做任何变换。这个动作非常关键,因为肉眼能看出很多统计检验未必能马上暴露的问题:有没有明显的缺失段、有没有超出正常范围好几倍的异常值、有没有结构性突变。比如某一天的现货价格突然录到正常水平十倍,这时候如果直接拿去建模,后面所有估计都会被这个点带偏;先把它识别出来做合理修正,比任何高级算法都重要。
1.2 ARIMA的建模逻辑与适用边界
ARIMA模型的本质,是用序列自身的历史值、历史预测误差以及白噪声的线性组合来描述当前值。它把“趋势”和“随机波动”分开处理:d阶差分消去趋势,自回归部分捕捉序列自身的惯性,滑动平均部分捕捉不可直接观测的冲击效应。说白了,ARIMA认为电价今天的值,跟过去若干个时刻的值有关,也跟过去若干个时刻预测不准的地方有关。
但ARIMA也不是万能的。它假设线性关系、假设残差是白噪声、对突发尖峰和结构性突变应对能力有限。那为什么还要用?因为对大多数常规时段的电价数据,ARIMA已经能给出足够坦率的基准结果,而且计算成本低、可解释性强、结果稳定性好。我们实验室内部做模型对比时,ARIMA始终是那个“保底基准”——不管新模型多花哨,如果打不过ARIMA,那这个新模型就得好好复盘了。
为了把这个问题说透,我常给合作者看一张简单的对比表:
| 特性 | ARIMA | LSTM | XGBoost |
|---|---|---|---|
| 数据量要求 | 数百至数千点即可 | 通常需要上万点 | 数千至数万点 |
| 特征工程 | 几乎不需要 | 需要特征构造 | 高度依赖特征工程 |
| 可解释性 | 高 | 低 | 中 |
| 置信区间 | 模型自带解析结果 | 需额外模拟或集成 | 需分位数回归等 |
| 计算开销 | 低 | 高 | 中 |
从这张表能看出,ARIMA在“基准预测+不确定性量化”这个组合任务上,性价比其实很高。
1.3 置信区间在电价预测里的实际价值
点预测给出的是一个数值,比如“明天下午四点电价是320元/MWh”。但实际决策不能只靠这一个数。假设你是售电公司,要决策要不要提前锁定一部分购电合约,你真正关心的不只是点预测,而是“价格超过某阈值的概率有多大”。置信区间刚好回答这个问题:如果95%的置信区间是[280, 360],你心里对波动范围大概有个底;如果区间是[200, 440],点预测虽然相同,但决策风险完全不同。
在Matlab里,ARIMA的forecast函数可以直接输出预测区间,不需要额外手写复杂的统计公式。这一点是ARIMA在这个场景下比很多黑盒模型强的地方:它给出了有统计依据的不确定性度量,而不是靠交叉验证凑出来的近似置信带。对电力市场分析人员来说,这种“白盒式”置信区间在报告和决策会议上也更容易解释。
2. 前期处理:从原始电价到可建模序列
2.1 数据清洗与异常值修正
我拿到的模拟数据是某区域电力市场连续半年的每小时现货电价,一共4320个点。第一步先检查缺失值——电力市场的数据偶尔会有通信中断或者记录缺失,如果缺失比例低于1%,直接用线性插值补齐基本没有副作用。这里不推荐用“前一天同时刻值”来代替,因为这会引入不必要的自相关污染,影响后面模型的残差判断。
异常值处理要讲策略。电价异常分两种:一种是真实的天价尖峰,比如供需极度紧张导致的超高价,这类是电力市场的真实信号,不能随便删,删了会影响后续预测对极端情况的判断;另一种是明显的数据错误,比如价格录成负数、突然跳变到十倍正常水平随后又回落。第二种才需要修正,常用方法是中位数滤波或者按“偏离滑动中位数超过若干倍MAD(中位数绝对偏差)”来识别,然后替换为滑动中位数附近的值。这个处理顺序一定要放在平稳性检验之前。
注意:不要一上来就删尖峰。先判断尖峰是市场真实行为还是数据错误,区分标准可以设定为一个可解释的物理阈值,比如超过历史日均价5倍且持续不到1小时的可疑值,再结合当日供需背景人工确认。
2.2 平稳性检验的两种思路
ARIMA建模有一个硬前提——序列必须平稳。所谓平稳,直观理解就是统计性质不随时间变化:均值不漂移、方差不明显变化、自相关结构相对稳定。电价序列原始数据通常不平稳,因为均值会随季节和负荷变化而上下起伏。
检验平稳性常用两类方法。一类是单位根检验,Matlab里的adftest和kpsstest就是干这个的,ADF检验的原假设是“存在单位根、序列不平稳”,如果p值小于0.05就拒绝原假设,认为序列平稳;另一类是通过自相关图辅助判断,如果自相关系数衰减得非常慢、接了十几阶还明显不为零,基本可以断定不平稳。实际操作中我都是两个都看,统计检验为主,自相关图做交叉验证。
% 假设price是原始电价序列 [h_adf, p_adf] = adftest(price); disp(['ADF检验p值: ', num2str(p_adf)]); % h_adf = 1 表示拒绝单位根原假设,序列平稳 % h_adf = 0 表示不能拒绝原假设,序列不平稳,需要差分如果p值大于0.05,就需要做差分。差分阶数d该怎么选?原则是“能用一阶就不用二阶”——因为过差分会引入额外的移动平均分量,反而导致模型变复杂。观察差分后序列是否围绕零均值波动,再跑一次adftest,如果显著平稳就可以定d=1了。
2.3 周期项问题的处理方案
电价序列还有一个ARIMA直接处理不了的问题:24小时周期。ARIMA只能处理趋势性非平稳,对周期性非平稳通常需要先做处理。这里有两个常见思路。
第一个思路是直接对每个小时分别建ARIMA模型。24小时一个周期,就把数据按小时切分成24条子序列,每天0点的价格单独建模,每天1点的价格单独建模,以此类推。这个思路实现简单,每个子序列的均值水平和波动特征一致,ARIMA建模假设更容易满足,缺点是每个模型样本量变成原来的1/24,长周期信息利用不充分。
第二个思路是差分预处理,对序列做周期差分。如果周期是24小时,则从当前值减去24步前的值,从而将日内周期成分移除,接着再对残差序列做常规ARIMA建模。这个方法保留了全部样本,但需要注意差分后的序列可能引入额外的负自相关,建模时需要适当提高MA阶数来适配。
就我自己测试过的效果看,如果数据量超过一年、主要目标是日内预报,第一种方法通常整体误差更小;如果数据只有一两个月,第二种方法更稳,因为样本没有萎缩。很多论文里直接对原始电价套一个ARIMA(p,d,q)也能跑,但残差诊断时往往有很强的周期自相关,滚动预测效果会打折扣。至于进一步引申出的季节ARIMA模型(SARIMA)就是另一套话题了,这篇文章先把ARIMA的核心实现讲透。
2.4 训练集与测试集的划分原则
时间序列的划分跟普通机器学习不一样,不能随机打乱,只能按时间顺序切。我的习惯是留出最后7到14天做测试集,前面的做训练集。测试集只用来做最终评估,不能参与定阶调参,否则会严重高估模型表现。
trainLen = length(price) - 24 * 14; % 留出14天做测试 trainData = price(1:trainLen); testData = price(trainLen + 1:end);这里的核心原则是:一切参数选择只基于训练集。你可以用训练集做滚动重估来模拟真实预测节奏,但绝对不要碰测试集,直到全部确定好、做最终预测那一刻。
3. Matlab中ARIMA定阶与参数估计
3.1 看图定阶:自相关图与偏自相关图
确定d之后,接下来是定p和q。经典的定阶方法是看差分后序列的自相关函数(ACF)和偏自相关函数(PACF)图。对应关系如下:如果ACF在滞后q阶后截尾,而PACF逐步衰减,倾向于MA(q);如果PACF在滞后p阶后截尾而ACF拖尾,倾向于AR(p);如果两者都拖尾,则需要用ARMA模型。
Matlab里直接两条命令:
d1 = diff(trainData); % 一阶差分 autocorr(d1, 'NumLags', 36); parcorr(d1, 'NumLags', 36);我观察电价序列差分后,ACF和PACF通常都没有非常明显的截尾现象,而是以较慢的拖尾形式存在,这意味着纯AR或纯MA都不够,需要ARMA。这时候看图只能起到初步框定范围的作用,比如“p大概在1到3之间,q大概率不超过2”,细选就要靠信息准则。
3.2 信息准则与网格搜索
信息准则的本质是兼顾拟合优度和复杂度,防止过拟合。AIC和BIC是常用选择,两者的差异在于对参数个数的惩罚力度,BIC对复杂模型的惩罚更重。在Matlab中可以非常方便地遍历不同的(p,q)组合,逐个估计模型、计算信息准则值,最后选最小的那组。
best_aic = inf; for p = 0:4 for q = 0:4 mdl = arima(p, d, q); [est, estCov, logL] = estimate(mdl, d1, 'Display', 'off'); aicVal = aicbic(logL, p + q + 1); if aicVal < best_aic best_aic = aicVal; best_p = p; best_q = q; end end end fprintf('最优阶数: p=%d, d=%d, q=%d\n', best_p, d, best_q);需要强调几点。第一,这里遍历的搜索空间不宜过大,p和q都放到5以内已经覆盖绝大多数典型场景,阶数过高反而容易让参数估计不稳定、信息准则优势也不明显。第二,aicbic函数需要传入估计的对数似然值logL和有效参数个数,如果差分后序列均值接近零,可以考虑去掉常数项,此时参数个数取p+q;如果保留常数项,则取p+q+1。第三,网格搜索是定阶的起点,不是终点,选出的最优阶数还需要配合残差诊断来验证——有些时候AIC最小的模型残差仍混有周期成分,那就要人工干预。
3.3 参数估计与残差诊断
阶数定好之后,直接用estimate函数拟合:
estMdl = estimate(arima(best_p, d, best_q), d1, 'Display', 'on');拟合完之后必须做残差诊断。最基础的三件事:看残差是否还有显著自相关、残差是否近似正态、残差方差是否稳定。Matlab里可以用infer函数拿到残差,然后跑Ljung-Box检验:
res = infer(estMdl, d1); [h_lb, p_lb] = lbqtest(res, 'Lags', [10, 20, 30]);如果Ljung-Box检验的p值大部分大于0.05,说明残差没有显著自相关,模型对数据的时序信息利用得比较充分;如果p值很小,说明还有结构没提出来,需要回去重新定阶或者处理周期项。这一步很多人跳过,但恰恰是判断模型能不能用的核心标准——AIC再低、拟合图再好看,残差带着自相关,滚动预测也会翻车。
4. 预测与置信区间的完整Matlab实现
4.1 forecast 函数的关键用法
Matlab的forecast函数是做ARIMA预测的核心入口。基本用法是:
horizon = 24 * 7; % 预测未来7天,共168小时 [YF, YMSE, V] = forecast(estMdl, horizon, 'Y0', d1);这里有个容易忽略的细节:Y0传入的是用于条件预测的历史序列。如果用差分后的序列做预测,Y0要传差分后的序列,不是原始电价序列;同理,预测出来的YF也是差分尺度上的值,最终展示时需要通过累加差分反向变换回原始价格尺度。不少新手在这里直接传原始序列,结果模型内部把差分逻辑再算一遍,输出对不上。
再补充一个细节:forecast默认给出的预测区间是95%置信水平,想改成90%或者99%可以设置参数:
[YF, YMSE, V] = forecast(estMdl, horizon, 'Y0', d1, 'ConfidenceLevel', 0.90);V矩阵是horizon×2的数组,第一列是下界,第二列是上界。注意这个上下界也是基于Y0条件的,所以跟点预测一样在差分尺度上,展示前要一起反向变换。YMSE是各预测时点的均方误差,也就是预测方差,把它开根号得到的标准误差配合正态分布分位数,实际上就是置信区间的基本构成,但Matlab封装好了,直接用V即可。
差分序列的反向变换可以用cumsum实现:
base = trainData(end); % 差分前的最后一个原始值 YF_original = base + cumsum(YF); lower_original = base + cumsum(V(:, 1)); upper_original = base + cumsum(V(:, 2));由于差分是线性操作,置信区间同样可以通过相同变换映射回原始尺度,区间覆盖率在线性变换下保持不变。
4.2 置信区间的原理与结果解读
ARIMA的预测区间本质上是基于预测误差分布推导出来的。以AR(1)模型为例,一步预测误差的方差就等于白噪声方差;两步预测误差的方差则等于噪声方差乘以一个含自回归系数的系数;步数越大,预测方差积累越多,区间就越宽;当预测步数趋向无穷大时,区间宽度会收敛到一个与模型无条件方差有关的常数上。Matlab在内部把这条计算链用解析式算好了,不需要你手工推导。
实际打印出的95%置信区间,宽度通常呈现三段特征:近端较窄、中段逐渐变宽、远端渐趋稳定。也就是说,ARIMA对近期的把握是相当有底气的,越往远期越只能给一个“大致的波动范围”,但不会再无限恶化。这个特性对电价预测特别有意义:电价预测往往最关心未来一小时到未来一天的短期走势,而ARIMA恰好在这个范围内置信度较高。
但也要说清楚,这个置信区间是纯模型理论意义上的,它假设模型参数已知、误差服从正态分布。实际建模中参数是估计出来的,估计本身有不确定性,而且电价残差厚尾明显,所以真实的不确定性通常比理论区间提示的要大。做严谨的实战评估时,我会用滚动窗口的方式反复计算区间覆盖率,如果覆盖率长期低于名义水平,就调整展示区间的置信度,而不是硬套95%。
4.3 预测可视化与效果评估
预测结果不能只给一张表,画图是必须的。绘图代码的骨架如下:
t = 1:length(price); figure; hold on; plot(t(1:trainLen), trainData, 'Color', [0.2 0.4 0.6]); plot(t(trainLen+1:end), testData, 'Color', [0.8 0.3 0.3]); tPred = trainLen + (1:horizon); plot(tPred, YF_original, '--r', 'LineWidth', 1.5); % 置信区间填充 fill([tPred fliplr(tPred)], ... [lower_original' fliplr(upper_original')], ... [0.9 0.95 1.0], 'EdgeColor', 'none', 'FaceAlpha', 0.4); legend({'训练数据', '测试数据', '预测值', '95%置信区间'});画出来的图里,最重要的观察点是:真实测试曲线落在置信区间内的比例有多少,这就是区间覆盖率。经验上,95%置信区间对电价这类厚尾数据,实际覆盖率往往会低于理论值——因为电价尖峰太多,误差分布并不完全正态。如果覆盖率明显偏低,比如不到70%,就说明模型对不确定性的估计偏乐观,这时候可以改用99%的区间来展示,或者考虑引入波动率模型对残差方差做进一步建模。
评估指标的程序化计算:
actual = testData(1:horizon); mae = mean(abs(YF_original - actual)); rmse = sqrt(mean((YF_original - actual).^2)); inside = mean(actual >= lower_original & actual <= upper_original);MAE、RMSE是对点预测的评估,覆盖率是对区间预测的评估。两者结合才是一个完整的预测评价链。很多学术文章只报RMSE不说覆盖率,这对实际工程应用是不够的,因为对决策者来说,“未来这周价格落在什么范围内”往往比“平均偏差多少元”更有参考价值。
5. 实测经验与常见障碍排查
5.1 预测值发散或者漂移严重
ARIMA做多步预测时,预测值会逐渐收敛到序列的长期均值,这个过程是正常的。但如果预测值在几步之内就出现严重漂移,一般意味着模型参数出了问题。我遇到过两类常见情况。一类是过差分,d取大了,模型从平稳序列里提炼出多余成分,预测曲线会表现出异常的上升或下降趋势;解决办法是减小d,回到一阶差分,必要时直接处理趋势项。另一类是MA参数估计接近某个临界值,模型出现近不可逆性,预测误差方差不稳定,预测值发散;这时需要回看参数估计的标准误差,如果参数估计的置信区间包含了临界值,就要考虑降低q阶数或者使用约束估计。
5.2 置信区间过宽或者过窄
置信区间宽度直接取决于残差方差。如果区间宽得离谱,比如上下界相差上千元,首先检查训练数据是否包含少量极端异常值——这些点会把残差方差撑大好几倍,把异常值修正后区间马上收紧。区间窄得不像话的情况也有,常见原因是训练集数据过于“平静”,残差方差估计偏低,模型没有见过真实的尖峰波动;解决方案一是扩大训练窗口,把更多覆盖不同电价场景的历史数据纳入进来,二是直接使用更高的置信水平来适应实际决策需求。
5.3 残差仍有周期自相关
电价数据如果没做周期处理,残差自相关图在滞后24、48阶的地方会明显冒出高峰,这就是常说的周期残留。碰到这种情况别急着加p、q阶数,通常怎么加都压不下去,正确做法是回到数据预处理阶段,把24小时周期先行消解,比如使用2.3节中的按小时分组或周期差分策略。我见过不少项目在这个问题上浪费大量调参时间,原因就是没意识到问题根源在预处理而不在模型阶数。
5.4 模型参数不稳定
对于滚动预测场景,每隔一段时间就要用最新数据重新估计模型参数。如果每次重估出来的p、q阶数都不同,不必太惊讶——信息准则在一个平坦的最小值附近经常换选择,这很正常。但如果你发现估计的参数值本身剧烈抖动,比如AR系数从一个安全值跳到另一个方向,那多半是训练数据里最近一段时间有异常结构,比如发生了持续一周的特殊事件。这时候我会选择把异常段从训练数据中剔除,而不是让模型去强行适配它。
5.5 快速排查速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 预测曲线发散 | 过差分或参数接近临界值 | 降低d或q,检查参数估计 |
| 置信区间过宽 | 训练集含异常尖峰 | 修正异常值后重新估计 |
| 残差有24阶自相关 | 周期项未处理 | 按小时分组或周期差分 |
| 覆盖率偏低 | 厚尾导致正态假设失效 | 提高置信水平或换波动率模型 |
| 参数每次重估变化大 | 近期数据存在结构突变 | 剔除异常段或缩短窗口 |
这里再补充一点个人经验:我始终建议在项目里保留一个“简单基线模型”作为对照。无论你最后用的是什么复杂模型,ARIMA这个基线永远值得保留。它训练快、结果稳定、自带置信区间,而且能在模型出问题的时候第一时间暴露异常。很多时候团队讨论半夜都说不清的预测问题,拿ARIMA一跑,问题到底出在数据还是模型,立刻就有线索了。做电价预测这个行当,能把基准线画准,比追求高精度的花活重要得多。
最后再分享一个我自己常用的判断技巧:拿到一组新数据,先用ARIMA跑一个24小时预测,画出置信区间,观察真实曲线的落点是否大致符合区间分布。如果偏差不大,说明数据结构比较健康;如果真实值经常冲出区间,那后面不管上什么高级模型都要先回去清洗数据。这套习惯我保持了很久,帮我避开了不少坑。