电力现货市场价格预测:ARIMA模型与置信区间实战详解
电力现货市场的价格预测这几年在售电、调度、策略分析这些岗位上几乎是绕不开的课题。我大概在一年多前拿到一组模拟项目里的电价数据需求方明确提出要做两件事一是基于ARIMA给出未来一段时间的电价预测值二是把预测结果的置信区间也算出来。刚开始我还觉得加区间只是顺手的事真正做到后面才发现业务方真正拿去落地的往往是预测线旁边那两条上下界而不是中间那条均值线。这个项目最终用Matlab完整跑通了一套流程从数据清洗、平稳性检验、定价定阶、残差诊断到滚动预测和区间绘制每一步都有对应的代码和踩坑记录。这篇文章就把这套流程完整梳理一遍适合对时间序列已有基础、想在实际数据上落地的人参考。数据本身我用的是某个区域电力现货市场的逐日平均结算价样本六百天左右不算大但完全够把ARIMA的完整建模过程跑透。1. 项目背景为什么电价预测非要带置信区间电力市场的价格预测和一般时序预测有一个很大的不同电价序列的波动幅度远超普通经济指标而且尖峰频发、周内季节性明显。这种数据特性带来的结果是单点预测的参考价值很有限。比如预测明天电价是420块/MWh如果实际价格在极端行情下冲到800那这条均值预测对决策层来说几乎起不到风控作用。反倒是预测区间能够告诉用户“正常情况大概落在哪个范围极端情况可能到哪里”这才跟业务决策真正挂钩。售电公司要用预测结果做现货报价区间调度部门要根据价格边界评估供需风险策略团队要拿区间设置止盈止损阈值——大家关心的都不是中心值而是上下边界。所以在这套ARIMA预测流程里我把置信区间和预测值放在同等重要的位置而不是最后补一张图就算完事。整个项目的设计目标就是让输出既包含点预测也包含能够直接用于定价和风控的预测区间。选择ARIMA这个模型理由也很实际。电价序列中存在明显的自相关结构昨天的价格对今天、甚至后几天都有持续影响ARIMA恰好就是围绕这种自相关结构设计的经典模型。相比于深度学习方案ARIMA参数少、可解释性强、训练速度快而且预测区间有完整的统计理论支撑不需要依赖大规模重采样。先跑通一套简单可靠的基线再考虑要不要换更复杂的模型这是最稳妥的路径。2. 整体设计思路从模型选型到区间怎么算2.1 为什么选ARIMA而不是机器学习模型每次聊到这个项目都会有人问我同一个问题现在深度学习做时序预测这么流行为什么还在用ARIMA这种“老模型”我的看法是ARIMA对于电价预测依然是一个性价比极高的起点。电力价格不是纯随机序列它有明显的惯性今天的价格受到昨天、前天的价格影响这种影响随着时间推移逐步衰减。ARIMA中的自回归部分就是专门捕捉这种惯性关系的用少量系数就能把序列的动态结构刻画出来。反观一般的机器学习模型树模型和神经网络更擅长处理高维特征映射关系但对于强自相关的时间序列如果不把滞后特征显式构造出来模型很难自动学到这种时间依赖。即便做了特征工程机器学习模型在预测区间上也相当麻烦点预测也许能做得很准但严格意义上的预测区间需要专门构造比如分位数回归、集成重采样、贝叶斯方法等开发验证成本都不小。从工程角度讲先用ARIMA把“点预测区间预测”的完整链路跑通之后再逐步替换或增强是一条非常务实的路线。项目里我用ARIMA建模从识别p、d、q三个参数开始到预测误差方差的解析计算整个过程对每个系数都有明确的统计含义出了问题也好排查。2.2 置信区间到底是怎么推出来的要理解置信区间的来源先从ARIMA的线性表示说起。任何一个平稳的ARMA过程都可以改写成白噪声的线性滤波形式Y_t μ ε_t ψ₁ε_{t-1} ψ₂ε_{t-2} ...这里的ψ权重由AR系数和MA系数共同决定。如果模型是ARIMA(p,d,q)那其实就是对差分后的序列做上述展开预测的时候再叠加回差分还原。从第t期出发预测未来第h期真实值中包含的未来噪声项是ε_{t1}到ε_{th}而模型可用的信息只到t期为止这部分未来噪声就是预测误差的主要来源。预测误差的方差可以写成Var(e_t(h)) σ²(1 ψ₁² ψ₂² ... ψ_{h-1}²)其中σ²是模型残差的方差估计。这个公式非常直观预测步长h越大累加的ψ平方项越多预测方差就越大置信区间逐渐变宽。95%的置信区间就是预测值加减1.96倍的标准差取z分数为1.96。这一步看着简单但前提是残差必须是白噪声如果模型没定好残差里还残留结构这个方差分解就不成立区间也不可信。在Matlab的forecast函数里第二个返回值是预测均方误差MSE这就是预测误差方差的具体估计值。用这个值开方再乘上对应的z分数就能画出置信区间。后面的代码部分我会详细演示这个计算过程。3. 数据准备与预处理要点3.1 读取数据与时间轴对齐拿到数据第一件事不是建模而是把数据格式完全理顺。我用的CSV文件包含两列一列是日期时间一列是价格用readtable读入后要先确认时间列已经解析成datetime对象、价格列是double类型再检查序列是否有序。data readtable(elec_price.csv, VariableNamingRule, preserve); price data.Price; dt data.DateTime;这里要强调一下readtable默认会把某些列猜成cell或string类型如果价格列带着千分位逗号或者币种符号直接算数值会出错需要先用str2double转换。时间列如果没有被正确识别也该用datetime函数重新构造。建模前我用sortrows按时间升序排了一遍确认没有乱序和重复时间戳。这个步骤看似基础实际项目里很多意外错误最后都溯源到这里。时间对齐的另一个要点是频率连续。如果一个日度序列中间缺了几天直接用差值会让模型误以为时间间隔是均匀的滞后结构就被扭曲了。所以我在补齐缺失值之前先对时间轴做了一次检查确保每一天都有对应的观测缺失的日期用上一日观测做前向填充或者用前后两天的线性插值补上。3.2 缺失值和异常尖峰的处理原则电价序列最让人头疼的是尖峰。现货市场在供需紧张的时段价格可以跳到正常水平的五到十倍。这种跳变对建模来说不一定是“脏数据”它可能是真实的市场交易结果。我的处理原则是分两类采集环节的噪声比如明显违反价格上下限逻辑的孤点用前后两天中位数替换真实价格尖峰则完整保留在训练样本里。这个分类看起来简单但一开始实际操作时我吃过亏。当时把所有尖峰一律当异常值剔除结果模型学到的方差严重偏小置信区间窄得离谱业务方一眼就看出不对劲。后来改成保留真实尖峰区间覆盖率立刻恢复正常。这个教训让我意识到做电力时序数据真实波动和噪声必须分开对待不能为了追求数据“干净”而破坏序列本身的市场行为特征。缺失值处理我优先用fillmissing加线性插值对连续三个以内的缺失点效果很好。如果一段数据连续缺失十几天我宁可把这一段从训练窗口里切掉再连接也不强行插值。强行插值制造的连续平台会误导模型让它以为价格长期保持不变这比缺失更糟糕。4. 建模与参数选择4.1 平稳性检验与差分阶数ARIMA建模的顺序固定为先定差分阶数d再定p和q。d的作用是让序列平稳平稳之后自回归和移动平均部分才能真正使用。我习惯用ADF检验判断平稳性adftest返回的逻辑值h为1表示拒绝单位根原假设序列平稳。[h0, p0] adftest(price); if h0 0 priceDiff diff(price); [h1, p1] adftest(priceDiff); if h1 1 d 1; else d 2; end else d 0; end实测下来逐日平均电价在原始序列上的ADF检验p值基本都大于0.05无法拒绝单位根。做一阶差分后p值通常降到0.01以下所以日度电价数据d取1就够。有一点要提醒不要一看到不平稳就继续做二阶差分。过度差分会让序列方差变大ACF在低阶出现接近-1的异常表现这跟经济序列的缓慢均值回归完全是两回事。实际数据里需要d2才能平稳的情况极少如果遇到了优先检查数据是否存在结构性断裂或长段缺失。4.2 从ACF和PACF初判p、q确定d之后用差分序列的自相关图和偏自相关图对p、q做初步判断。如果ACF拖尾衰减缓慢而PACF在某阶后截断说明主要是自回归结构p取截断点对应的滞后阶数反过来ACF截断、PACF拖尾则是移动平均结构q取截断点。if d 0 plotSeries diff(price, d); else plotSeries price; end autocorr(plotSeries, NumLags, 30); parcorr(plotSeries, NumLags, 30);教科书上的ACF/PACF模式很清晰但日度电价序列几乎不会这么标准。我看到的结果是一阶差分后的序列在滞后1期有显著自相关衰减曲线略带拖尾滞后4到7偶尔出现显著小尖峰这跟一周内的工作日和非工作日节奏有关。这种边界模糊的情况看图只能作为初筛真正定参还得靠信息准则网格搜索。只看图硬挑参数很容易陷入主观判断不同人给出不同答案。4.3 网格搜索与AIC/BIC定参网格搜索的思路很简单让p从0到4、q从0到4的所有组合过一遍每组都用estimate拟合计算信息准则选最小的那个。pRange 0:4; qRange 0:4; bestAIC Inf; bestMdl []; bestPQ []; for p pRange for q qRange if p 0 q 0 continue; end try Mdl arima(p, d, q); [estMdl, ~, logL] estimate(Mdl, price, Display, off); [aic, ~] aicbic(logL, p q 1, length(price)); if aic bestAIC bestAIC aic; bestMdl estMdl; bestPQ [p, q]; end catch continue; end end end if isempty(bestPQ) error(模型估计失败请检查数据或缩小搜索范围); end fprintf(最优p%d, q%d, AIC%.4f\n, bestPQ(1), bestPQ(2), bestAIC);我用的是AIC优先因为业务上对预测精度的敏感度高于对参数数量的敏感度。BIC对模型复杂度惩罚更强适合样本量很大、偏重解释性的场景AIC在预测场景里往往更实用。这套数据上跑出来的结果最优组合是ARIMA(2,1,1)次优是ARIMA(1,1,1)两者的AIC差距不到2说明模型落在了同一水平线上没必要为了零点几的差值纠结。真正决定取舍的是残差检验。4.4 残差白噪声检验模型拟合之后用infer函数取出残差再跑Ljung-Box检验。检验原假设是残差序列没有自相关也就是纯白噪声。如果p值小于0.05说明残差里还有可利用的时间结构当前模型阶数不够需要调高p或q。res infer(bestMdl, price); [hLB, pLB] lbqtest(res, Lags, [10, 20], DoF, bestPQ(1) bestPQ(2)); fprintf(Ljung-Box p值: lag10%.4f, lag20%.4f\n, pLB(1), pLB(2));我这组数据里ARIMA(1,1,1)在滞后10处的Ljung-Box p值略低于0.05意味着残差还有自相关残留。改成ARIMA(2,1,1)之后所有滞后阶的p值都明显大于0.05残差干净了这才最终定了(2,1,1)。这一步绝对不能跳因为置信区间的方差计算公式建立在残差白噪声的假设上。如果模型结构不对残差非白噪声预测区间算出来也是不成立的。5. Matlab实现从代码到置信区间可视化5.1 主流程代码现在把整个流程串起来给出可以直接跑的完整代码骨架。数据读取、预处理、定阶、建模、预测都在里面核心逻辑和项目实际使用的基本一致。%% 读取和清理 data readtable(elec_price.csv, VariableNamingRule, preserve); price data.Price; price fillmissing(price, linear); %% 平稳性判断 [h0, p0] adftest(price); if h0 d 0; else priceDiff diff(price); [h1, p1] adftest(priceDiff); if h1 d 1; else d 2; end end %% 差分序列的ACF/PACF if d 0 plotSeries diff(price, d); else plotSeries price; end figure; autocorr(plotSeries, NumLags, 30); figure; parcorr(plotSeries, NumLags, 30); %% 网格搜索p和q pRange 0:4; qRange 0:4; bestAIC Inf; bestMdl []; bestPQ []; for p pRange for q qRange if p 0 q 0 continue; end try Mdl arima(p, d, q); [estMdl, ~, logL] estimate(Mdl, price, Display, off); [aic, ~] aicbic(logL, p q 1, length(price)); if aic bestAIC bestAIC aic; bestMdl estMdl; bestPQ [p, q]; end catch continue; end end end fprintf(最优p%d, q%d, AIC%.4f\n, bestPQ(1), bestPQ(2), bestAIC); %% 残差白噪声检验 res infer(bestMdl, price); [hLB, pLB] lbqtest(res, Lags, [10, 20], DoF, bestPQ(1) bestPQ(2)); fprintf(Ljung-Box p值: lag10%.4f, lag20%.4f\n, pLB(1), pLB(2)); %% 预测未来30个点并计算95%置信区间 h 30; [forecastPrice, forecastMSE] forecast(bestMdl, h, Y0, price); zScore 1.96; lowerBound forecastPrice - zScore * sqrt(forecastMSE); upperBound forecastPrice zScore * sqrt(forecastMSE);这段代码里有一个关键参数要特别说明forecast函数的Y0必须传入训练序列的完整历史值。很多人在这一步犯错不写Y0或者只传最后几个观测导致预测起点错位出来的预测曲线明显飘偏。Y0的作用是让模型知道当期状态向量到底在哪里它是ARIMA预测能否衔接历史的关键。5.2 置信区间可视化算出上下界之后用Matlab的plot和fill把历史数据、预测均值和置信带画在同一张图上。figure; plot(price, k, LineWidth, 1); hold on; idxPred length(price) : length(price) h - 1; plot(idxPred, forecastPrice, b-, LineWidth, 1.6); fill([idxPred, fliplr(idxPred)], [lowerBound, fliplr(upperBound)], ... b, FaceAlpha, 0.2, EdgeColor, none); legend({历史电价, ARIMA预测, 95%置信区间}, Location, Best); grid on;fill函数是这条可视化链路的核心。x轴上是预测起始点到结束点的下标序列y轴上半部分从下界画到上界用fliplr保证闭合区域的顶点顺序正确。FaceAlpha设为0.2让半透明区间不遮挡预测线和历史曲线。区间越靠近预测后期越宽这是累计预测方差的直观呈现。如果你绘制出来的置信带宽呈现两头宽中间窄或者忽宽忽窄先检查forecastMSE是不是被按步长重新排序了。5.3 滚动预测与样本外评估定参阶段的AIC对比是在全样本上完成的这只能说明模型对历史数据的拟合程度不能代表真实预测能力。要评估模型的实际可用性必须做滚动预测。思路是把样本切成长度固定的训练窗口每次只预测下一个点然后窗口前进一个单位反复进行。trainLen 400; testLen length(price) - trainLen; forecasts zeros(testLen, 1); for i 1:testLen trIdx i : i trainLen - 1; Ytr price(trIdx); Mdl arima(bestPQ(1), d, bestPQ(2)); Est estimate(Mdl, Ytr, Display, off); forecasts(i) forecast(Est, 1, Y0, Ytr); end rmse sqrt(mean((price(trainLen1:end) - forecasts).^2)); mae mean(abs(price(trainLen1:end) - forecasts)); fprintf(RMSE%.4f, MAE%.4f\n, rmse, mae);滚动预测得到的RMSE和MAE才是模型真实误差的近似。我的经验是滚动误差通常比样本内拟合误差大百分之三五十这是正常的。如果差距异常大要检查测试期是否跨越了极端行情或者结构性变化比如某几天价格出现几倍的异常跳涨。这种情况下纯ARIMA很难给出理想预测要么引入外生变量要么考虑分段建模。6. 常见问题与排查方法6.1 ADF检验不通过连续差分还是不平稳怎么办先别急着提高差分阶数。差分次数越多序列方差被放得越大ARIMA对原始电价的还原能力就越差这是方向性错误。遇到连续差分还不平稳的情况优先回去检查数据是否存在长段缺失、时间序列是否乱序、有没有明显的结构性断裂。把断裂段切出来分开处理之后一般再做ADF检验就能通过。真正一阶差分后还完全不平稳的日度电价数据很少见如果确实遇到了说明这个序列不适合纯ARIMA建模。6.2 ACF/PACF图看起来都不截断这是日度电价序列的常态因为真实数据的动态结构远比教科书复杂很少有纯净的AR或MA特征。这时候不要看图硬猜了直接交给网格搜索和AIC去做决定。只需要保证p、q搜索范围覆盖足够宽比如0到5搜索结果大概率会落在合理区间。还有一种常见情况是ACF出现明显周期性的衰减模式说明存在季节性因素这时纯ARIMA表现会比较勉强可以考虑SARIMA或者加一周内的星期虚拟变量。6.3 estimate时报错或者出现NaN系数最常见的诱因有三个数据里有未清理的NaN、序列长度太短、某些p、q组合对应的参数方差阵退化。网格搜索代码里用try-catch包住估计过程就是干这个用的异常组合直接跳过。如果所有组合都报错先检查数据质量和样本长度。另一个容易被忽略的问题是极端尖峰对高斯最大似然估计的影响尖峰存在会让系数估计很不稳定可以考虑先对价格序列取对数或者对极端值做温和收缩处理。6.4 置信区间太窄覆盖不住实际价格这是最容易被业务方反馈的问题。要解释清楚这里计算的是点预测的置信区间反映的是模型估计的未来波动范围不等于电价所有可能值的范围。电价分布有厚尾特性极端尖峰远超正态假设模型对这类事件的方差估计天然偏低。如果区间用于风险监测有两个实用手段一是把z分数从1.96提高到2.33也就是99%区间二是在置信区间基础上再叠加滚动历史残差的极值包络作为辅助警戒线。统计工具本身没有好坏关键是怎么结合业务场景去使用。6.5 预测曲线太平滑捕捉不到周内节奏纯ARIMA对电价日度序列的拟合经常是平滑的因为模型捕捉的是短期自相关而不是周期结构。遇到周内明显差异的数据两个方向可以改进一是提高p的阶数让更多滞后项参与拟合模型可能间接吸收部分周期性二是在模型里加入外生变量用ARIMAX建模。具体操作是把星期几转成虚拟变量作为一个或多个外生回归项放进arima模型用X参数传入。这种做法的效果好得多因为电力需求跟人们的一周活动节奏高度相关工作日和周末的价格结构差异是真实存在的。7. 项目落地的一些体会这套ARIMA加置信区间的流程实际项目中跑下来最大的价值不是预测曲线多好看而是把“预测和不确定性”的框架完整落地了。做电力价格预测最怕的就是模型指标在纸面上很完美业务方却不知道输出该拿去怎么用。加上置信区间之后业务人员对最坏情况有了一个明确的量级讨论决策时也有了统一的参考坐标系。我自己在项目里总结了几条经验可能比模型参数本身更重要。第一数据清洗阶段一定要把真实价格尖峰和采集噪声分开处理一刀切是最大的坑。第二模型定参必须在滚动验证下确认不能只盯着样本内效果否则上线后大概率被打脸。第三置信区间不是越窄越好关键是覆盖率和业务可用度这个标准要提前跟需求方对齐否则你辛辛苦苦优化的区间指标在业务眼里可能毫无意义。按照这个思路ARIMA这套经典方法放在电价预测场景下仍然是一个足够稳定、能够解释得通的解法。如果后续想在这个项目上继续扩展我建议走两个方向。一个是在模型端加星期和节假日的虚拟变量把周内节奏显式建模升级成ARIMAX另一个是在波动率端引入GARCH类型的模型让置信区间的宽度能跟着近期行情的剧烈程度自适应变化。两个方向都能在现有代码基础上低成本迭代不用推翻整套框架。我从实际操作中体会到先把一个简单可解释的基线完整跑通再逐步叠加复杂度比一开始就上高级模型靠谱得多。