如果你用ARMA模型预测过真实世界里的流量数据,大概率体会过那种“所有检验都通过,预测结果却完全不能看”的挫败感。我在做视频流量预测项目时也中过招:直接对原始序列建模,残差始终带着明显的周期性波动,白噪声检验做一次挂一次,模型定阶怎么调都像在猜。后来把问题拆开看,才意识到症结不在ARMA本身,而在输入序列不满足平稳前提。这篇博文要分享的,就是我当时用的一套组合拳——先用小波分解把非平稳序列拆成低频趋势和高频细节,再对低频分量建立ARMA模型预测,最后把各部分预测结果重构回去。整个过程全部在Matlab里完成,代码我会贴完整出来,初学者照着跑就能复现,有经验的人看思路也能直接套用到自己的数据上。
1. 为什么非平稳序列预测要先做小波分解
1.1 直接对流量序列跑ARMA为什么会翻车
ARMA模型的理论前提是序列满足弱平稳:均值、方差不随时间漂移,协方差只与时间间隔有关。但真实世界的流量数据、金融时序、用户消费序列,几乎没有一个满足这个前提——它们有长期趋势,有以天或周为周期的波动,偶尔还有突发毛刺。
在这种数据上直接跑ARMA,最直接的表现就是残差完全不是白噪声。我当初用adftest检验原始视频流量序列,p值高达0.6,说明非平稳极其显著。但当时没当回事,硬着头皮用AIC定了个ARMA(2,1),画残差自相关图,前十几阶的自相关系数全在置信区间外面,这意味着模型该提取的信息根本没提取干净,预测出来的东西自然没有参考价值。
把ARMA当成“在平静湖面上划船”的工具就很好理解了:湖面平稳,船走直线大概率可预测;暴风下的海面满是浪涌和长波,船的轨迹被环境主导,这时候再精密的船桨也白搭。小波分解起的作用,就是把这片狂暴海面先拆成“洋流大趋势 + 一波一波的主要浪涌 + 细碎浪花”,让ARMA只去处理其中最乖巧的那一部分。
1.2 小波分解为什么能帮ARMA一把
小波分解的本质,是把信号投影到一组不同尺度的子空间里。实现上通过Mallat算法逐层进行:先用低通滤波器得到近似系数(低频趋势),再用高通滤波器得到细节系数(高频波动),下一层继续对低频部分重复这个过程。以三层分解为例,原始序列被拆成A3 + D3 + D2 + D1四个分量,其中A3是最平滑的趋势主体,D3是较慢的波动,D1是最高频的细碎噪声。
它和FFT最大的区别在于时间定位能力。FFT能告诉你信号里有哪些频率成分,但它说不出来这些频率是出现在前五分钟还是后五分钟;小波分解则是一种“带着放大镜看频谱”的方式,既保留了频率信息,又保留了随时间变化的位置信息。对预测来说这一点很重要:流量序列里的“周期性波动”并不是严格的三角函数,它会什么时候开始、什么时候结束、幅度是否衰减,这些都需要时间定位能力。
类比一下混音:一首歌里有人声、鼓点、环境噪声,想把音量推均匀,直接对整个音频文件做均衡器调整并不好用,而把各轨分出来单独处理再混回,才能精准控制每一个成分。小波分解对时间序列做的就是这个事——分轨、分别建模、再叠加。
1.3 小波基函数与分解层数选择的两个关键点
第一是小波基选择。实际做预测时,正交小波基是首选,因为正交性保证了分解与重构之间没有信息冗余和损失。Daubechies系列(dbN)和Symlets系列(symN)是出镜率最高的两类,我一般从db4或sym8起步。消失矩N决定了小波对多项式趋势的抑制能力,N越大对平滑趋势的压缩效果越好,但支撑长度也越长,边界失真范围会扩大,所以N不是越大越好,常用3到6之间。
第二是分解层数。层数太少,低频趋势提取不干净,ARMA面对的序列还是非平稳的;层数太多,低频分量变得很短,边界效应在预测起步阶段会被严重放大。Matlab里有个wmaxlev函数可以根据序列长度给出最大分解层数建议,但实际经验是三层到五层最常用。我习惯的辅助判断方法是:分解后把低频分量wrcoef重构出来,和原始序列画在同一张图上,如果趋势部分已经能把主要走势都包住,说明层数够了。
2. Matlab环境下的小波分解实现细节
2.1 需要哪些工具箱和核心函数
以下函数是小波分解+ARMA预测这条链路里最核心的部分:
| 函数 | 所属工具箱 | 作用 |
|---|---|---|
wavedec | Wavelet Toolbox | 多层小波分解,返回系数向量C和长度记录L |
wrcoef | Wavelet Toolbox | 从分解系数中重构某一层(低频或高频)信号 |
dwtmode | Wavelet Toolbox | 设置边界延拓模式,直接影响边界重构精度 |
wmaxlev | Wavelet Toolbox | 根据数据长度建议最大分解层数 |
adftest | Econometrics Toolbox | ADF平稳性检验 |
arima/estimate/forecast | Econometrics Toolbox | ARMA模型的构造、估计、预测 |
aicbic | Econometrics Toolbox | 计算信息准则,用于定阶 |
如果你只有Wavelet Toolbox而没有Econometrics Toolbox,别急着放弃,后文有一小节专门讲替代路径。
2.2 十行代码完成多尺度分解
先给出一段最基础的分解代码,假设数据已经存在向量data里:
level = 3; dwtmode('sym'); % 对称延拓,边界重构误差比默认的零延拓小很多 [C, L] = wavedec(data, level, 'db4'); A3 = wrcoef('a', C, L, 'db4', 3); % 第三层低频 D3 = wrcoef('d', C, L, 'db4', 3); % 第三层高频 D2 = wrcoef('d', C, L, 'db4', 2); % 第二层高频 D1 = wrcoef('d', C, L, 'db4', 1); % 第一层高频wavedec返回的C是把所有层的系数按顺序拼在一起的向量,L记录每一段的长度。wrcoef的工作方式是:把想要的那一层系数保留,其余层系数置零,然后通过逆小波变换重构出该层对应的时域信号。所以无论分解多少层,每个分量的长度都和原始data相等,这个特性为后面的逐分量预测和叠加重构提供了很大方便。
这里特别提醒一句:dwtmode('sym')这行一定要在wavedec之前执行。默认的'zpd'零填充模式在序列两端会人为制造突变,重构出来的低频分量在首尾两端经常出现异常的“翘尾巴”,预测前几步误差直接起飞。
2.3 低频和高频分量分别怎么处理
分解完之后,每个分量的行为模式差异很大,处理策略也必须分开。
低频A3是整个序列的骨架,体现长期趋势和主要周期。一般对它先做ADF检验,如果还不平稳,需要再做一阶差分再套ARMA。有人会觉得奇怪:都做小波分解了,低频序列怎么还能非平稳?答案是,很多真实数据里的趋势本身就不是缓慢漂移,而是类似“斜坡”的持续性变化,小波分解只是把趋势剥离出来,并没有改变它的非平稳属性。
高频D1到D3的处理策略,取决于它们的能量占比。可以先算一下每个分量相对于原始信号的方差占比:
energy_total = sum(data.^2); energy_D1 = sum(D1.^2); ratio_D1 = energy_D1 / energy_total;如果某个高频分量的占比不到5%,它本质上接近于随机噪声,预测时可以当它不存在——用零均值去代替即可;如果占比在5%到15%之间,可以考虑对每一层分别建一个低阶AR(1)模型,追一下短时相关;如果占比特别大且表现出周期性,再考虑ARMA(1,1)。基本原则是:高频分量能少建模型就少建,宁可预测成均值,也不要让一个不稳定的模型把误差放大。
3. ARMA模型的定阶、估计与预测
3.1 先把ARMA的数学形式写清楚
ARMA(p,q) 的标准形式是:
x_t = c + φ1·x_{t-1} + ... + φp·x_{t-p} + ε_t + θ1·ε_{t-1} + ... + θq·ε_{t-q}
其中p是自回归阶数,q是移动平均阶数,ε_t是白噪声过程。AR部分刻画“当前值受过去值影响”,MA部分刻画“当前值受过去随机冲击影响”。两个部分加起来,能在参数尽可能少的情况下拟合一类较宽范围的平稳序列。
在实际工程里,如果低频分量一阶差分后才平稳,那对应的模型就是ARIMA(p,1,q),等于“先差分,再对差分序列建ARMA”。我用adftest判断是否需要差分,一行代码:
h = adftest(A3);h=1表示序列平稳,可以直接建ARMA;h=0表示不平稳,需要先diff(A3)再建模,预测完再把差分还原。
3.2 用Econometrics Toolbox完成估计和预测
Matlab的Econometrics Toolbox把ARMA建模封装得非常顺手。假设已经确定p=2, q=1,核心代码是:
mdl = arima(2, 0, 1); % p=2, d=0, q=1,即ARMA(2,1) mdl = estimate(mdl, A3); % 极大似然估计 yhat = forecast(mdl, h, 'Y0', A3); % 预测未来h步forecast函数里的'Y0'参数非常重要,它指定预测起点之前的历史观测值,让模型内部的状态初始化正确。如果不给Y0,Matlab默认从序列开头进行状态初始化,算出来的多步预测基本是废的。
3.3 定阶不要靠猜,给一个网格搜索模板
定阶是ARMA建模里最容易翻车的一环。理论上的方法是看ACF和PACF:AR(p)的PACF在p阶后截尾,MA(q)的ACF在q阶后截尾,这个规则在教科书上很干净,但真实数据里经常两边都拖尾,根本看不明朗。我的做法是直接网格搜索p和q,用BIC选最小:
logL = zeros(5, 5); numParams = zeros(5, 5); for p = 0:4 for q = 0:4 mdl = arima(p, 0, q); [~, ~, logL(p+1, q+1)] = estimate(mdl, A3, 'display', 'off'); numParams(p+1, q+1) = p + q; end end [~, bic] = aicbic(logL, numParams, length(A3)); [minVal, idx] = min(bic(:)); [pBest, qBest] = ind2sub(size(bic), idx);注意p=0且q=0时模型退化为纯白噪声,网格搜索里它有时会因为BIC最低胜出,这时候要结合数据实际判断幅值是否有预测价值,避免选出一个“全预测成均值”的空模型。还有一点,p和q不要贪大,时间序列预测界有句话叫“高阶数等于高过拟合”,训练集上误差再小,测试集上都会加倍还回来。
3.4 没有Econometrics工具箱时的替代路径
不是所有人的Matlab都装了Econometrics Toolbox,备选方案有两套。
如果装了System Identification Toolbox,可以用armax函数:
data_id = iddata(A3(:), [], 1); model = armax(data_id, [p q]); % 结构是 [na nb nk],na对应AR阶数,nb对应MA阶数 yp = predict(model, data_id, h); % 得到h步预测这个方案虽然不如Econometrics Toolbox顺手,但估计和预测都可用。
如果两个工具箱都没有,可以考虑Durbin两步法:先用aryule估计高阶AR模型,得到残差序列,再对残差序列估计MA参数。这一步近似能拼出一个ARMA模型,但参数可靠性差一些。更务实的做法是:当数据量足够时,直接用一个阶数较高的AR模型(比如AR(10))替代ARMA,因为MA部分本质上可以用高阶AR逼近,代价是参数多一些,需要用AIC约束阶数。工程里这招救过我好几次。
4. 完整预测流程:分解-建模-重构-评估
4.1 整体流程与数据准备
完整链路一共五步:
- 数据清洗与标准化:剔除异常值,必要时对数据进行z-score标准化,避免不同量纲影响阈值判断。
- 小波分解:用
dwtmode('sym')延拓,选db4或sym8,分解3到5层。 - 对每个分量分别建模预测:低频分量经过ADF检验后建模ARMA,高频分量视能量占比选择AR(1)、均值或忽略。
- 重构:把所有分量的预测结果直接相加,得到最终预测序列。
- 评估:用均方根误差RMSE、平均绝对误差MAE、平均绝对百分比误差MAPE对比基线模型。
数据划分上,我习惯取前80%做训练,后20%做测试。预测步长h根据业务需求定,视频流量预测通常预测6到24个半小时粒度的点,金融时序则可能是未来1到10个交易日。
4.2 可直接运行的完整Matlab代码
下面这段代码我把整个链路整合成一个函数,注释写全,直接替换数据就能跑:
function results = wavelet_arma_forecast(data, h, level, wname) % data: 列向量时间序列 % h: 预测步数 % level: 小波分解层数,推荐3 % wname: 小波基,推荐'db4'或'sym8' if nargin < 4, wname = 'db4'; end if nargin < 3, level = 3; end dwtmode('sym'); % 对称延拓 % 1. 小波分解 [C, L] = wavedec(data, level, wname); % 2. 依次重构各层信号 A = wrcoef('a', C, L, wname, level); % 低频 D = zeros(length(data), level); for k = 1:level D(:, k) = wrcoef('d', C, L, wname, k); end % 3. 对低频分量判断平稳性并建模 if adftest(A) == 0 dA = diff(A); diff_flag = 1; else dA = A; diff_flag = 0; end % 网格定阶 logL = zeros(4, 4); nP = zeros(4, 4); for p = 0:3 for q = 0:3 mdl = arima(p, 0, q); [~, ~, logL(p+1, q+1)] = estimate(mdl, dA, 'display', 'off'); nP(p+1, q+1) = p + q; end end [~, bic] = aicbic(logL, nP, length(dA)); [~, idx] = min(bic(:)); [pBest, qBest] = ind2sub(size(bic), idx); % 低频建模预测 mdlA = arima(pBest, 0, qBest); mdlA = estimate(mdlA, dA); forecastA = forecast(mdlA, h, 'Y0', dA); % 如果有差分,还原水平值 if diff_flag == 1 forecastA = forecastA + A(end); end % 4. 对高频分量低阶AR或均值预测 forecastD = zeros(h, level); for k = 1:level energyRatio = sum(D(:, k).^2) / sum(data.^2); if energyRatio > 0.05 mdlD = arima(1, 0, 0); % AR(1) mdlD = estimate(mdlD, D(:, k), 'display', 'off'); forecastD(:, k) = forecast(mdlD, h, 'Y0', D(:, k)); else forecastD(:, k) = mean(D(:, k)); % 均值代替 end end % 5. 重构最终预测 forecastTotal = forecastA + sum(forecastD, 2); results.forecast = forecastTotal; results.pBest = pBest; results.qBest = qBest; results.components = struct('A', forecastA, 'D', forecastD); end这段代码的实用性在于把“先检验、再定阶、后预测”的流程固化下来了。唯一的性能瓶颈是estimate在display关闭状态下循环跑20多次,数据量几千点以内完全没压力,不用担心。
4.3 结果重构与评估指标
小波分解的一大优点是重构就是简单的加法:
预测总序列 = 低频预测序列 + 各层高频预测序列
这个性质让“分而治之”的思路特别干净。评估阶段我固定用三个指标:
- RMSE = sqrt(mean((actual - forecast).^2)),放大大误差的惩罚
- MAE = mean(abs(actual - forecast)),直观反映平均偏差
- MAPE = mean(abs((actual - forecast) ./ actual)) * 100,无量纲,方便跨数据集对比
我拿600个训练点、120个测试点的视频流量数据跑过一次对比:直接对原始序列建模的ARMA(2,1),MAPE是23.7%;用小波分解+ARMA的组合,MAPE降到11.2%。差距主要出现在趋势拐点和周期峰值附近——直接ARMA在趋势段明显慢半拍,因为模型被高频噪声干扰,参数估计被带偏了。
5. 实测中的坑与优化建议
5.1 边界延拓:最隐蔽的坑
小波分解本质是卷积滤波,卷积在序列边界必然会截断,产生边界失真。实际表现是:重构出来的分量在序列末尾出现非正常的起伏,尤其是最高频的D1,失真幅度能比真实信号大好几倍。
如果预测起点正好落在失真区,那预测的前几步完全不可信。我的解决办法有两个:一是前面提到的dwtmode('sym'),把边界向外对称延拓,这个改动立竿见影;二是更狠的,在分解前先把序列向前多延展一段,比如预测h步,就向前多拼2*h个点(用最后一个周期的均值或直接镜像法),预测完之后把延展段产生的预测值裁掉。第二种方法在h比较大(超过10步)时非常管用,虽然多花了点计算量,但多步预测的稳定性明显提升。
5.2 高频分量到底该不该建模
高频分量的处理,很多人容易走极端:要么全部忽略,要么每个分量都堆ARMA高阶模型。两种我都试过,都是坑。
全部忽略的问题在于,如果D1的能量占比达到8%以上,那它承载的信息里包含短期的自相关性,全部置零会让预测结果太“光滑”,在真实数据曲线上就是丢失了所有细节波动。
每个分量都堆ARMA高阶模型的问题在于过拟合。高频分量本身就有点像随机过程,ARMA(2,1)这类模型能在训练集上拟合得很漂亮,但它学的是训练集的噪声,预测时输出基本退化成一个常数。我后面学乖了,对高频分量统一按“能量占比”决策:5%以下用均值,5%到15%用AR(1),超过15%才考虑ARMA(1,1)。这个规则虽然简单,但实测下来比“全建模”稳定得多。
5.3 多步预测的滚动更新策略
上面的forecast(mdl, h, 'Y0', series)是一次性告诉模型“从current状态往前推h步”。这种做法的缺点是:第2步的预测是建立在第1步预测值的基础上,误差会逐层累积。
如果你预测的是小时粒度以外的中长周期数据,建议用滚动预测:每推进一步,就把新得到的真实观测值(测试集里是已知的)或上一步预测值塞回模型的Y0里,再预测下一步。伪代码如下:
yhat = zeros(h, 1); y0 = dA; for t = 1:h yhat(t) = forecast(mdl, 1, 'Y0', y0); y0 = [y0(2:end); yhat(t)]; % 丢弃最老的值,把新预测推进窗口 end注意滚动窗口的长度不要一下用全部历史,取最近50到100个点足够,既保留了模型状态,又避免了过老的观测拖慢对新趋势的适应。
5.4 小波基与层数的敏感性测试
做过一次小规模的参数敏感性实验,用db2、db4、db6、sym8四种小波基,分别做2层、3层、4层分解,在同一个数据集上对比RMSE。结果是:RMSE的波动范围在3%以内,db4和sym8整体略微好一点。真正影响大的是延拓模式和分解层数对边界的影响——用默认零延拓时,4层分解的边界误差能把整体RMSE推高15%。
这说明一个道理:小波+ARMA这套方案,稳定性的核心是边界处理和分量策略,而不是小波基的细微差别。选定db4或sym8、3层分解、sym延拓,就已经是一个省心且可靠的默认配置,不用再花大量时间做参数搜索。
6. 案例:视频流量预测的实际效果
6.1 数据与模型配置
去年我做的一个CDN节点在线视频播放流量预测项目,数据粒度是半小时一次,一共720个点。前600点做训练,后120点做测试。这个序列的特点很典型:有明显的早中晚周期性,周末比工作日整体高三成,偶尔出现几分钟的突发峰值。
预处理阶段,我把连续观看数小于10的异常点直接replace成前后半小时的均值,防止极端值干扰小波分解的尺度判断。模型配置上用了db4、3层分解、sym延拓,低频分量ADF检验不平稳,先一阶差分,定阶后选用ARMA(2,1);D1到D3的能量占比分别是6.8%、3.1%、1.5%,所以D1用AR(1),D2和D3用均值代替。预测步长是24步,也就是往后看12小时。
6.2 预测结果与误差分析
三组对比模型的误差如下:
| 模型 | RMSE | MAE | MAPE |
|---|---|---|---|
| 直接ARMA(2,1) | 318 | 235 | 23.7% |
| 小波分解+ARMA(无高频处理) | 176 | 132 | 14.9% |
| 小波分解+ARMA(完整策略) | 142 | 96 | 11.2% |
完整策略比直接ARMA的RMSE下降了55%,这个幅度在我的预期内。预测曲线和真实曲线叠在一起看,低频趋势部分贴得很好,24步内的整体走向基本一致;高峰时段略有滞后,大约滞后一到两个采样点。这是因为小波滤波器本身有相位延迟,wrcoef重构出来的分量不是严格零相位的,属于方法固有的现象,可通过预测前延展数据来缓解,但无法完全消除。
6.3 项目落地时的几条经验
最后说几条掏心窝的经验。这套方案的定位是在“纯统计模型”和“深度学习模型”之间的一个均衡点:比直接用ARMA强很多,训练时间又远低于LSTM,而且每个分量都能解释清楚,出问题时知道去哪排查。如果你手里的数据有明显趋势加周期性,几百到几千个点,用它当基线非常合适;如果数据样本少于200个点,小波分解后的低频序列太短,ARMA估计会很不稳定,这种情况我更建议直接上指数平滑类的简单模型。
另外,预测之前的数据清洗优先级高于一切。小波分解对异常值相当敏感,一个比周围大20倍的尖峰,会通过滤波器扩散到多个尺度分量里,污染一整片区域的预测结果。先把数据修干净,再让模型去发挥,是我做这个项目学到的最大一课。