☰
R语言平稳序列建模全流程:平稳性检验、差分、ARIMA与滚动回测
2026/10/26 11:35:39 网站建设 项目流程

做时间序列这行时间久了会发现一个现象:多数人拿到一份带时间戳的数据,第一反应是直接上 ARIMA,第二反应是被一条缓慢爬升的曲线或者一条规律性起伏的曲线教育一顿,然后才回头补“平稳性”这一课。平稳序列建模在 R 语言里其实是一套非常成熟的流水线,工具早就备齐了,难的从来不是写代码,而是判断——判断这份数据到底要不要差分、差几分、差分完之后是不是把有用的信息也一起差没了。这篇内容想把这条流水线完整走一遍,从数据进 R 到残差检验到滚动回测,把每一步的“为什么”讲清楚,也把那些文档里不写、只有踩过才知道的细节摊开讲。不管你是刚接触时间序列的学生,还是已经用过auto.arima()但说不清它到底在选什么的一线分析人员,应该都能从中捞到点东西。

1. 平稳性这道坎到底卡在哪儿

1.1 平稳性的两种含义,别混着用

很多人一上来就背“均值方差自协方差不随时间变化”,背完还是不知道该怎么判断。我更愿意把它拆成两层来看:严平稳要求整个联合分布对时间平移不变,这个条件在实际数据里几乎不可能验证;弱平稳只要求均值是常数、方差是有限常数、自协方差只跟时间间隔有关、跟绝对时间点无关。我们建模时说的平稳,九成九指的是弱平稳。这个区分很重要,因为后面所有检验手段——ADF、KPSS、PP——针对的都是弱平稳的某几个侧面,没有哪一个检验能给你一个“完全平稳”的盖章认证。

理解这一点之后,你再看那些检验结果就不会那么焦虑了。一个序列 ADF 检验 p 值 0.06,不代表它“不能建模”,只代表在当前样本量下没有足够证据拒绝单位根。样本 30 个点和 3000 个点,同样的数据生成过程,结论可能完全相反。检验的结论永远要和样本量、图形形态、业务背景一起看,单看一个 p 值下判断,是新手最容易犯的错。

1.2 不平稳的代价:伪回归比想象中更阴险

有人会想,不平稳又怎样,我照样拟合,R² 照样很高。这正是问题所在。两条各自独立随机游走的序列放在一起做回归,R² 经常能干到 0.8 以上,t 值漂亮得不像话,但残差根本不是白噪声,这就是经典的伪回归。它的危险之处在于“看起来太正常了”,如果你不做残差诊断,很容易就把一个毫无意义的系数写进报告里。

放到预测场景,后果更直接。非平稳序列的方差随时间发散,你算出来的预测区间是建立在一个错误假设上的,名义上是 95% 区间,实际覆盖可能只有 60%。业务方拿着这个区间去做库存安全垫,出了事只会说“模型不准”,没人会回头查是不是差分次数搞错了。我在实际项目里见过一次,某个日度指标的预测区间窄得离谱,最后追下去发现是季节差分漏了一阶,残差里还留着明显的周期结构,模型把季节波动当成随机噪声压缩掉了。

1.3 整条建模链路的设计逻辑

我习惯把整个流程想成一条有明确检查点的流水线:可视化 → 平稳性判定 → 必要的变换与差分 → 模型识别定阶 → 参数估计 → 残差诊断 → 回测评估 → 上线监控。每一环都可能把你打回去重来,这不是流程设计得不好,而是时间序列建模本来就带反馈环。

这里有个取舍要提前想清楚。差分是把双刃剑:差一分能去掉随机趋势,但同时会放大高频噪声,还会让序列丢失长期水平信息。如果业务方关心的是“下个月的绝对量是多少”,那过度差分会让你在还原尺度时误差被放大。所以我一般的原则是差分次数能少不多,能用“去趋势 + 残差建模”解决的,不一定非要硬差。对于带明显季节周期的数据,季节差分和季节虚拟变量、傅里叶项也是可以互相替代的,选哪个取决于周期是否稳定、样本是否够长。

2. 环境和数据进 R 的正确姿势

2.1 包选型:够用就好,别堆

R 生态里做时间序列的包多到让人选择困难,我的习惯是分层使用:

用途首选包说明
基础时间序列对象stats::ts()内置,无依赖,月度/季度数据首选
高频与不规则时间戳xts/zoo日频、日内、缺失值容忍度高
单位根检验tseries、urcaadf.test快速版,ur.df精细控制
KPSS 检验tseries::kpss.test原假设与 ADF 相反,互补用
建模与自动定阶forecastauto.arima、tsCV、checkresiduals
残差诊断可视化forecast+ 自写 ggplotcheckresiduals出图快,细节定制靠自己
install.packages(c("forecast", "tseries", "urca", "xts", "ggplot2")) library(forecast); library(tseries); library(urca)

提示:forecast与较新版本的fable/fabletools是两套体系,别在一个脚本里混着用,函数名冲突会让你找半天 bug。团队协作时把依赖写进renv.lock,比在群里喊“你装一下 forecast”靠谱得多。

2.2 时间索引错位是最高频的低级错误

ts()的start和frequency两个参数,写错了不会有任何报错,只会让你的季节性完全错乱。月度数据frequency = 12、start = c(2019, 3)表示从 2019 年 3 月开始;周度数据不要用frequency = 52,一年不是整齐的 52 周,用frequency = 365.25/7会引入伪周期,日频转周频我更推荐先聚合再建模。

# 月度示例:2019年3月起,共 60 期 y <- ts(raw_vec, start = c(2019, 3), frequency = 12) # 日频数据建议先转成 xts,再按需聚合 library(xts) x <- xts(raw_df$value, order.by = as.Date(raw_df$date)) weekly <- apply.weekly(x, sum)

还有一个隐蔽的坑:时间戳重复。系统导出数据时偶尔会出现同一天两条记录,ts()不会管你,直接按顺序排下去,后面的周期全部错位一格。进 R 的第一件事应该是anyDuplicated(index(x)),返回非零就先聚合成唯一时间点。

2.3 三种图先画出来再谈建模

在动任何模型之前,我一定会画三张图,成本极低但收益巨大:

  • 时序图:看趋势形态、看有没有结构性断点、看方差是否随时间放大。方差明显放大的,先取对数再谈平稳性。
  • 季节图(ggseasonplot):把每年同一月份拉成一条线,看季节形态是否稳定。如果 2020 年那一年的线整体位移了,说明有外部冲击,可能需要加虚拟变量或者分段建模。
  • 分解图(stl或decompose):加法还是乘法分解,直接决定你要不要先做对数变换。
library(ggplot2) autoplot(y) + labs(title = "原始序列", y = "指标值") ggseasonplot(y, year.labels = TRUE) plot(stl(y, s.window = "periodic"))

注意:stl的s.window选"periodic"意味着季节成分在各年间不变,如果业务上季节形态本身在演化(比如促销节奏逐年变化),用固定窗口会把真实变化吸收进趋势项,反而误导判断。

3. 平稳性检验的三种手段与判读方法

3.1 ACF 图:肉眼能看出的信息比你想象的多

自相关图是最便宜的诊断工具。平稳序列的 ACF 会较快衰减到零附近并在置信带内震荡;非平稳序列(尤其是带趋势的)ACF 衰减极慢,几个滞后期都还在 0.8 以上,画出来是一条“缓坡”。带季节性的序列会在季节周期整数倍的位置出现明显尖峰,比如月度数据在第 12、24 滞后上翘起来。

但 ACF 有个陷阱:它分不清“趋势导致的慢衰减”和“长记忆过程”。一个 AR(1) 系数 0.95 的平稳过程,ACF 也会衰减得很慢,看着跟单位根过程差不多。这时候就必须借助正式检验,光靠看图,误判率不低。

Acf(y, lag.max = 36, main = "自相关图") Pacf(y, lag.max = 36, main = "偏自相关图")

3.2 单位根检验:原假设不同,结论含义完全不同

这是最容易被误解的一块。三个常用检验的原假设方向不一样,混着用会得出自相矛盾的结论还浑然不觉:

检验原假设拒绝的含义常见函数
ADF存在单位根(非平稳)支持平稳或趋势平稳tseries::adf.test
PP存在单位根(非平稳)支持平稳,对异方差更宽容tseries::pp.test
KPSS序列平稳(水平或趋势)支持非平稳tseries::kpss.test

正确用法是成对使用:ADF 拒绝 + KPSS 不拒绝 = 平稳,结论干净;ADF 不拒绝 + KPSS 拒绝 = 非平稳,也干净;两个都拒绝或者都不拒绝,说明样本信息不足或存在结构性断点,这时候要靠图形和业务判断兜底,别硬套结论。

adf.test(y) # 含常数项,不含趋势 kpss.test(y, null = "Level") # 水平平稳 kpss.test(y, null = "Trend") # 趋势平稳

urca::ur.df给了更细的控制,type = "none"无常数无趋势,"drift"含常数,"trend"含常数和趋势,lags可以按 AIC 或 BIC 选滞后阶:

library(urca) summary(ur.df(y, type = "trend", selectlags = "BIC"))

3.3 检验结论冲突时的处理思路

冲突场景一:ADF 和 KPSS 都说非平稳,但差分之后 KPSS 还是拒绝。这时候先别急着差第二次,回头看看是不是有结构性断点。一次均值位移会让 ADF 严重偏向“不拒绝单位根”,实际上序列在断点之后是平稳的。处理方式有二:一是业务上确认断点原因,加一个虚拟变量;二是分段建模。硬差两次,等于把一个阶跃变成了一个脉冲,模型会很难看。

冲突场景二:检验说平稳,但 ACF 衰减极慢。优先相信图形,考虑是不是样本太短导致检验功效不足。这时候可以做个稳健性检查:把前 20% 样本砍掉再测一次,结论反转就说明前段有异常。

冲突场景三:季节性问题被 ADF 误判。ADF 不处理季节性,一个强季节序列经常表现为“不拒绝单位根”。我的做法是先做季节差分再测普通单位根,顺序上先季节后趋势,因为季节差分会同时削弱趋势信息,反过来做容易过度差分。

3.4 差分、去趋势与变换的实操取舍

差分的阶数选择,别只依赖ndiffs():

ndiffs(y) # 建议的普通差分阶数 nsdiffs(y) # 建议的季节差分阶数 yd <- diff(diff(y, lag = 12)) # 先季节差分再普通差分

ndiffs默认用 KPSS,也可以指定test = "adf"或"pp",不同检验给出的阶数有时不一致。我的一般策略:以ndiffs结果作为上限,再往下一阶试,看哪个差分后序列的 ACF 最干净、残差检验最容易通过。差一阶就够用的,绝不差两阶。

去趋势的替代方案是拟合一个确定性趋势项(tslm(y ~ trend))然后对残差建模,这在“趋势稳定、样本较短”的场景下比差分更省信息。取对数变换则是为了稳定方差,判断标准很简单:如果时序图的波动幅度随水平值上升而放大,就取对数;如果波动幅度与水平值无关,取对数反而会把原本干净的结构搅乱。

实操心得:对数变换要在差分之前做,顺序反了就没意义了。另外,如果序列里有零值,log()会给你-Inf,先检查min(y) > 0,必要时用log1p。

4. 模型识别与定阶的实际操作

4.1 ACF 与 PACF 的读图规则与局限

教科书给的规则是:ACF 拖尾、PACF 在 p 阶截尾 → AR(p);ACF 在 q 阶截尾、PACF 拖尾 → MA(q);两个都拖尾 → ARMA。这套规则在小样本、接近单位根、或季节性叠加的情况下经常失效,图上根本看不出“截尾”,全是慢慢往下溜的柱子。

我的做法是把它当参考不当结论:先用图形框定一个候选范围(比如 p ∈ 0:3,q ∈ 0:3),再用信息准则在范围内挑,两个方法互相印证。完全相信图形定阶,最后模型经常过拟合;完全交给自动定阶,有时候会选出一个参数不显著的模型。

4.2 信息准则:AIC、AICc 与 BIC 的取舍

三个准则的计算逻辑分别是:

  • AIC = −2lnL + 2k
  • AICc = AIC + 2k(k+1)/(n−k−1)
  • BIC = −2lnL + k·ln(n)

其中 k 是参数个数(含方差参数),n 是有效样本量。AIC 偏向选更复杂的模型,BIC 惩罚更重、偏向简洁,AICc 在小样本(n/k < 40)时更可靠。时间序列的样本量通常不大,我默认用 AICc。

fit <- auto.arima(y, d = NA, D = NA, seasonal = TRUE, stepwise = TRUE, approximation = FALSE, ic = "aicc", max.p = 5, max.q = 5, max.P = 2, max.Q = 2) summary(fit)

stepwise = FALSE会做全网格搜索,慢但更彻底;approximation = FALSE关掉近似似然,结果更准。数据量不大时我建议都关掉,跑一次几十秒的事,别为了省时间拿近似结果糊弄自己。

注意:auto.arima默认会自带漂移项(allowdrift),差分后的序列如果带非零均值,漂移项是有业务含义的(长期平均增长率),别看到就删。

4.3 参数估计与显著性的正确读法

summary(fit)出来的系数表,看两样东西:系数值和标准误。AR 系数接近 1(比如 0.98)要警惕,可能还是欠差分;MA 系数接近 −1 是典型的“过度差分”信号,说明你把可逆性边界踩到了。这两种情况都建议回头重看差分阶数。

显著性方面,个别参数 t 值偏低(|t| < 1.96)并不必然要删,因为参数之间是联动的,删掉一个可能导致整体拟合明显变差。我的处理方式是:先看整体 AICc 有没有改善,再决定删不删。如果删掉之后 AICc 上升超过 2,就保留。

# 手动比较两个候选模型 fit2 <- Arima(y, order = c(1,1,1), seasonal = c(0,1,1)) c(AICc = fit$aicc, AICc2 = fit2$aicc)

5. 残差诊断:模型是否成立看这一步

5.1 白噪声检验的滞后阶怎么定

残差诊断的核心问题是:模型有没有把可解释的结构抽干净。判断工具是 Ljung-Box 检验:

checkresiduals(fit) Box.test(residuals(fit), lag = 24, type = "Ljung-Box", fitdf = 3)

fitdf必须设成模型参数个数,否则自由度算错,p 值会系统性偏大,让你误以为残差很干净。滞后阶lag的选择有个经验值:月度数据取 24,季度数据取 8 到 12,日度数据取 10 到 20,或者取min(2*frequency, n/5)。阶数太低测不出长滞后上的结构,太高则检验功效下降。

p 值大于 0.05 只是“没有足够证据拒绝白噪声”,不等于“残差一定是白噪声”。我一般会同时看残差 ACF 图,只要在季节周期位置上有接近或超出置信带的尖峰,即使 Ljung-Box 通过,也要怀疑模型漏了季节结构。

5.2 残差图的三种典型异常形态

  • 残差呈喇叭口(方差随时间放大):异方差,考虑对数变换或 ARCH 类模型。
  • 残差在某一点突然跳变后持续偏移:结构性断点,加虚拟变量或分段。
  • 残差 ACF 在滞后 12、24 处反复出现尖峰:季节项没处理干净,回去加季节差分或季节 AR/MA 项。

这三类问题在看数值检验结果时经常被掩盖,画图反而一目了然。checkresiduals()一张图里同时给了残差时序、ACF 和直方图,我基本是每次必看。

5.3 参数冗余与过拟合的识别

模型不是越复杂越好。判断标准有三条:一是参数个数超过样本量的 1/10,就要警惕;二是两个模型 AICc 差不到 2,选简单的那个;三是某个高阶 AR 或 MA 系数绝对值很小但显著,多半是在拟合噪声。

还有个隐性过拟合信号:样本内拟合极好,样本外一塌糊涂。这种情况在auto.arima的max.p设得太大时特别常见。我通常把上限压得比较保守(p ≤ 3,q ≤ 3,季节项 ≤ 2),宁可欠拟合一点,也不要让模型去背噪声。

6. 预测与滚动回测的落地细节

6.1 用 tsCV 做真正的样本外评估

样本内指标(RMSE、MAPE)在时间序列里参考价值有限,因为模型见过那些点。真正的评估要靠滚动起点回测:

rmse_fn <- function(y, x) sqrt(mean((y - x)^2, na.rm = TRUE)) e <- tsCV(y, forecastfunction = function(x, h) { forecast(Arima(x, order = c(1,1,1), seasonal = c(0,1,1)), h = h) }, h = 12) sqrt(mean(e^2, na.rm = TRUE))

tsCV的forecastfunction必须在每次调用时重新拟合,这是个常见错误——有人把已经训练好的模型塞进去,结果回测结果跟样本内一样好,纯属自欺欺人。

回测窗口的设置也有讲究。数据量允许的话,window参数设成固定长度(比如总是用最近 60 期),这样更贴近“上线后模型滚动更新”的真实场景;如果业务是“一次性训练长期使用”,那就用扩展窗口。两种设定出来的误差经常差一截,选错了评估结论就是错的。

6.2 预测区间还原到原始尺度

对数变换 + 差分的模型,预测值不能简单取指数还原——exp(E[log y])不等于E[y],会有系统性低估。严格做法要加方差修正项:

# 对数模型的无偏还原近似 fc <- forecast(fit, h = 12) point <- exp(fc$mean) * exp(fc$upper[,1] * 0 + 0.5 * fc$model$sigma2)

更省事的办法是直接用forecast包对带lambda的模型自动处理,或者干脆在原始尺度上做残差自助法(bootstrap)生成预测分布。我倾向后者,因为区间形态不受正态假设约束,对偏态数据更友好。

sim <- replicate(1000, { path <- simulate(fit, nsim = 12, future = TRUE) sum(exp(log_tail + path)) # 累加还原到水平值 }) quantile(sim, c(0.05, 0.5, 0.95))

差分的还原要累加:一阶差分还原是cumsum,季节差分要先在季节维度上累加再在普通维度上累加,顺序不能颠倒。手动还原时建议拿最后几期已知数据做校验,还原出来的历史值和原始值对不上,说明索引对齐出问题了。

7. 常见问题速查与踩坑记录

7.1 问题速查表

现象可能原因处理方式
ADF 不拒绝但图形明显平稳样本短、存在断点补 KPSS 交叉验证,检查断点
差分后 ACF 全为负值过度差分减少差分阶数
MA 系数接近 −1过度差分同上,重估阶数
Ljung-Box 显著但残差图正常fitdf未设或滞后阶过低修正自由度,提高滞后阶
预测值明显偏低对数还原未加方差修正用 bootstrap 或修正项
回测误差与样本内差异巨大回测函数里复用了训练好的模型确保每次重新拟合
季节图每年形态不一致季节结构演化或有外部冲击加虚拟变量或缩短建模窗口
ts()后周期错位时间戳重复或start写错去重、核对起始期

7.2 几条不会写进文档的经验

第一,先把数据画出来,再决定要不要建模。有些序列根本就不适合 ARIMA 类模型,比如大量零值的计数序列、有明显多峰分布的序列、被人工干预截断的序列。硬套模型,残差诊断永远过不了,浪费的是自己的时间。

第二,建模窗口的选择比模型阶数更重要。我有一次做日度指标预测,用全部三年数据训练,效果一般;改成只用最近一年,同样的模型结构,回测误差降了接近三成。原因是两年前的一次业务规则调整让早期数据的生成机制变了,模型把两个不同机制混在一起学,谁都学不好。

第三,残差里藏着的往往不是统计问题,是业务问题。某次残差在特定月份系统性为负,查下去发现那几个月有一项固定的费用扣减没有体现在特征里。加了一个虚拟变量之后,模型立刻干净了。纯靠调阶数是调不出来的。

第四,别迷信自动定阶。auto.arima是个很好的起点,但它不知道你的业务约束。比如它可能选出一个带漂移项的模型,而业务上这个指标根本没有长期增长逻辑,那这个漂移项就是在拟合噪声。定阶结果一定要过一遍业务合理性。

第五,检验结论要留档。每隔一段时间重跑一次平稳性检验和残差诊断,把 p 值、AICc、回测误差记在一张表里。模型漂移是渐进的,单次结果看不出问题,拉长时间线一眼就能看出哪个月开始变差。

提示:如果你要复现别人的结果,set.seed()一定要设,auto.arima在approximation = TRUE时用到的随机搜索、bootstrap 区间都会受随机数影响,不设种子,同一份数据两次跑出来的区间可能不一样。

再补一个我自己的习惯:每建完一个模型,我都会留一个“最简基线”——朴素法或者季节朴素法。最终模型如果在回测上打不过基线,那不管 AICc 多漂亮、残差多干净,我都不会用。这条规矩帮我避开过好几次“统计上很优雅、业务上一无是处”的坑。模型这东西,能稳定跑赢最笨的方法,才算真的有用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询