写时间序列分析的人,十有八九都栽过这个跟头:拿到数据,不管三七二十一,直接丢进模型里跑回归,结果R方高得吓人、系数显著得一塌糊涂,兴冲冲拿去给业务方看,结果模型一上新数据就崩得不成样子。问题出在哪里?多半是你手里的序列不平稳,跑出了一堆“伪回归”。所谓伪回归,就是两个本身就带趋势的序列,哪怕毫无因果关系,回归结果也会显得高度相关。
所以做时间序列建模之前,预处理环节里的平稳性检验,是绕不过去的一道门槛。这篇继续聊R语言里的实现,侧重点放在操作细节和判断逻辑上,帮你在建模前把把关,省得后面返工。
1. 平稳性检验的应用场景与核心思路
先捋清楚一个概念:平稳性到底是什么。严格来说,平稳分强平稳和弱平稳。实际建模用到的、也是各种检验函数默认考察的,基本都是弱平稳,也就是序列的均值、方差在整个时间范围内保持恒定,而任意两个时点之间的协方差只依赖于时间间隔,不依赖于具体位置。用人话说,这组数据的统计性质不随时间的推移发生变化。
为什么建模前必须先过这一关?因为绝大多数经典的时间序列模型,比如ARMA、ARIMA,甚至向量自回归VAR,都建立在“序列平稳”这个前提之上。如果直接用非平稳序列去建模,参数估计的统计性质会出问题,t检验、F检验的结果都不再可靠。更现实一点说,模型预测能力会很差,样本外的表现往往惨不忍睹。
检验平稳性的方法大概分三类:一类是看时序图和自相关图,做个直观判断;一类是单位根检验,是目前的主流;还有一类是其他辅助性检验方法,比如方差比检验、KPSS检验等。单位根检验里最常用的又是ADF检验、PP检验、DF-GLS检验这几种。
我在实际工作中常用的是组合策略:先用ADF检验判断是否有单位根,再用KPSS检验从另一个方向验证,两个结果互相印证。因为ADF的原假设是“序列存在单位根”,即非平稳;而KPSS的原假设是“序列平稳”。这两个检验的原假设正好相反,如果ADF拒绝了原假设、同时KPSS不能拒绝原假设,那基本可以放心。如果两个检验结果矛盾,就得再仔细检查模型设定和数据处理环节是不是出了问题。
这一篇的侧重点是R语言里怎么把这些检验跑起来、怎么解读输出结果、遇到不一致怎么办。理论部分只做必要铺垫,重心放在能直接上手的代码和判断经验上。
2. 检验环境准备与工具链选型
R里做平稳性检验,最常用的两个包是tseries和urca。两者各有侧重,我一般配合着用。
tseries包用起来最简单,函数数量少,参数也不复杂。核心是两个函数:adf.test()做ADF检验,kpss.test()做KPSS检验。对入门或者快速验证来说,这两个函数就够了,几行代码出结果,解读也直白。
但urca包的功能要细致得多。它提供的ur.df()、ur.kpss()、ur.pp()函数可选参数更灵活。比如ur.df()可以让你手动指定是否带截距、是否带趋势、滞后阶数怎么选,输出里同时给出检验统计量、临界值和p值,能更好地适应不同形态的数据。调模型的时候,我更喜欢用urca包,信息量大,输出的细节多。
安装和加载很简单:
install.packages("tseries") install.packages("urca") library(tseries) library(urca)两个包的函数对应关系大致如下:
| 检验类型 | tseries实现 | urca实现 |
|---|---|---|
| ADF检验 | adf.test() | ur.df() |
| KPSS检验 | kpss.test() | ur.kpss() |
| PP检验 | 无 | ur.pp() |
tseries的adf.test内部基于urca的adf.test实现,输出的周界设置略有不同。有一点必须提醒:adf.test默认会包含常数项和趋势项,如果你的序列是纯随机游走或者只有漂移项而没有什么趋势,用了默认参数可能会导致检验功效下降。后面实操环节会专门展开。
另外一个值得做的事,是把需要的包全部装在统一的代码片段里。我一般在项目开头把这些库都加载好,后面写起来不用反复library()。
加载完包之后,建议先对数据的整体情况做一个基本检查,包括时间频率、缺失值、样本长度。样本量太小(比如少于50个观测值),无论哪种单位根检验,功效都有限。样本量至少要上百,检验结果才有参考价值。
3. 实战:完整检验流程演示
下面用模拟数据走一遍完整流程。虽然用的是模拟数据,但步骤可以完全照搬到真实业务数据上。
3.1 构建演示数据集
先生成两组数据:一组是随机游走序列,本质非平稳;一组是带趋势的平稳序列,经过确定性趋势的剥离后是平稳的。这样方便对比不同检验在不同类型序列上的表现。
set.seed(20240601) # 随机游走序列:y_t = y_(t-1) + eps_t n <- 200 rw <- cumsum(rnorm(n)) # 带趋势平稳序列:x_t = 0.2 * t + epsilon_t t <- 1:n trend_stationary <- 0.2 * t + rnorm(n) ts.plot(rw, col = "steelblue", main = "随机游走") ts.plot(trend_stationary, col = "darkred", main = "趋势平稳序列")从时序图上看,随机游走序列的均值会漂来漂去,没有一个固定的中心;而趋势平稳序列围绕着一条上升直线上下波动。这两种序列用肉眼就能看出区别,但肉眼判断只能作为初步参考,最终还是要靠检验定量给出结论。
顺便说一句,画ACF图也很直观。随机游走的自相关函数衰减得非常慢,拖着长长的尾巴;而平稳序列的自相关函数通常会在几阶之后迅速衰减到置信区间内。我习惯在检验之前,先画出ACF图来做一个预判。
acf(rw, lag.max = 30, main = "随机游走的ACF") acf(trend_stationary, lag.max = 30, main = "趋势平稳序列的ACF")3.2 ADF检验实操
ADF检验是扩展的Dickey-Fuller检验,核心思路是考察序列的一阶差分是否可以用滞后项来解释。简单说,就是构造一个回归模型,检验滞后项的系数是否为0。如果系数显著为0,说明序列有一个单位根,非平稳。
tseries包的adf.test()用法很直接:
adf.test(rw) adf.test(trend_stationary)输出的结果包括Dickey-Fuller统计量、滞后阶数和p值。对于rw序列,p值通常会远大于0.05,说明没有充足理由拒绝“存在单位根”的原假设,也就是序列非平稳。对于trend_stationary序列,p值通常会小于0.05,拒绝原假设,判断为平稳。
这里有个细节:adf.test()默认的滞后阶数是由内部截断规则决定的,基本等于长度的三分之一取整附近。这个默认值在部分情况下会选得偏大或偏小。偏大会降低检验功效,偏小则可能漏掉更高阶的滞后项。
所以如果用ur.df()来做,可以更精细地控制参数:
adf_rw <- ur.df(rw, type = "none", selectlags = "AIC") summary(adf_rw) adf_trend <- ur.df(trend_stationary, type = "trend", selectlags = "AIC") summary(adf_trend)ur.df()里type参数可以选"none"(无截距无趋势)、"drift"(只带截距)、"trend"(带截距和趋势)。选哪个取决于序列的特征。如果序列在0附近波动且没有明显趋势,用"none";如果围绕非零均值波动且没有趋势,用"drift";如果有明显的线性趋势,用"trend"。
selectlags参数可以选"AIC"或"BIC",也可以直接指定一个数字作为固定滞后阶数。我个人偏好用AIC,因为它能更充分地捕捉自相关结构。
ur.df()的summary输出里面,ts值对应的是ADF检验统计量,下面会列出1%、5%、10%三个显著性水平的临界值。判断规则很简单:如果检验统计量小于临界值,拒绝原假设,认为序列平稳;反之则不能拒绝原假设。注意,因为检验是左尾的,所以“小于”指的是往负方向走得更远。
3.3 KPSS检验实操
KPSS检验的原假设是序列平稳。这跟ADF正好相反。
tseries包的kpss.test()用法:
kpss.test(rw) kpss.test(trend_stationary)这里有一个需要注意的地方:kpss.test的默认参数里,trend参数默认是NULL,函数会自动判断是否有趋势。输出的LM统计量如果较大,超出临界值,就拒绝“平稳”的原假设。对于rw序列,kpss.test通常会给出很小的p值,拒绝平稳。对于trend_stationary序列,如果函数没有正确识别趋势,有可能会误判成非平稳。这就是为什么我建议用ur.kpss():
kpss_rw <- ur.kpss(rw, type = "tau") summary(kpss_rw) kpss_trend <- ur.kpss(trend_stationary, type = "tau") summary(kpss_trend)ur.kpss()的type参数有两个选择:"mu"表示只检验平稳性(检验类型是均值的稳定性),"tau"表示带趋势的平稳性。如果序列有明显趋势,设置成"tau"更合适;如果没有趋势,用"mu"。
同样地,判断规则是:如果检验统计量大于临界值,拒绝原假设,认为序列非平稳;如果小于临界值,不能拒绝原假设,有证据表明序列可能是平稳的。
把两个检验结果放在一起:
| 检验 | 原假设 | 适用范围 |
|---|---|---|
| ADF | 存在单位根(非平稳) | 判断是否差分 |
| KPSS | 平稳 | 验证是否不需要差分 |
当ADF说“非平稳”、KPSS说“平稳”,两个结论恰好相反,就需要特别注意,后面专门写一节处理这种矛盾。
3.4 PP检验与综合判定
PP检验,即Phillips-Perron检验,也是一种常用的单位根检验。它跟ADF检验的不同在于对序列相关和异方差的处理方式。ADF通过增加滞后差分项来修正残差自相关,而PP直接对检验统计量进行修正,不需要指定滞后阶数。说白了,PP对序列相关更稳健一些。
urca包里的ur.pp()用法:
pp_rw <- ur.pp(rw, type = "Z(t_alpha)", model = "constant") summary(pp_rw)type参数一般保留默认的"Z(t_alpha)",model参数可选"constant"(带常数项)、"trend"(带趋势项)或"none"(不带)。具体选择参考序列形态。
PP检验的结果解读方式跟ADF一致:检验统计量小于临界值则拒绝单位根原假设。
实际操作里,我会把ADF、KPSS、PP三个检验的结果列在一张表里综合判断。比如ADF说平稳、PP也说平稳、KPSS也支持平稳,那这个序列可以比较放心地作为平稳序列处理。如果ADF和PP都说非平稳、KPSS也说非平稳,那基本可以确定这个序列差分后再建模。
还有一种常用辅助判断:检查序列对一阶差分后的ACF图和PACF图。如果差分后ACF快速截尾或拖尾衰减,也侧面验证了差分后的序列是平稳的。
4. 参数选择与细节陷阱
工具会用了,接下来这些细节才是真正区分新手和老手的环节。同一个序列,参数设定不同,检验结论可能天差地别。
4.1 滞后阶数到底怎么选
滞后阶数对ADF检验影响最大。选太少了,残差里可能还残留自相关,检验统计量有偏;选太多了,又浪费自由度,小样本情况下功效降低。
实际操作中掌握两个原则:一是让模型残差尽可能接近白噪声;二是用信息准则辅助选择。AIC和BIC都是可用的,我个人倾向于用AIC,因为它在预测精度方面常常优于BIC,尤其在样本量不够大的时候。
如果想手动检查滞后阶数是否足够,可以对回归残差做Ljung-Box检验。如果残差还有序列相关,就需要增加滞后阶数:
# 用ur.df获得lm模型对象 adf_model <- ur.df(rw, type = "none", selectlags = 6) # 残差白噪声检验 Box.test(adf_model@testreg$residuals, lag = 12, type = "Ljung-Box")如果检验结果p值小于0.05,说明残差中还有自相关结构,当前的滞后阶数不够,需要加阶数。
4.2 是否带趋势项与截距项
前面提到过,ADF回归里的额外项会改变检验统计量的分布,所以必须根据序列形态来选择。
判断方法很简单:看时序图。如果序列围绕0上下波动,选"none"或"drift";如果围绕一个非零常数波动,选"drift";如果呈现明显的上升或下降趋势,选"trend"。
如果选了"trend"但序列其实没有趋势,检验功效下降;反过来如果该带趋势却不带,可能把趋势平稳序列误判成单位根过程。这个坑我栽过好几次。有一次处理一个增长率数据,增速明显围绕某个均值波动,没有趋势。我下意识用了"trend",结果ADF检验跑出来说非平稳,数据其实稳定得很。后来改成"drift",才得到正确的判断。
所以,用错模型设定,比用错检验方法更容易造成误判。
4.3 样本量的影响
单位根检验在小样本下的检验功效普遍较低。也就是说,序列本来平稳,但因为样本太少,检验没有足够证据拒绝原假设,造成误判为“非平稳”。
比如只有30个观测值的数据,不管用什么检验,结论都很难让人放心。这种情况下,更好的做法是画图观察,或者使用更适用于小样本的DF-GLS检验,即Elliott-Rothenberg-Stock检验。这个检验通过对数据做广义最小二乘变换,在小样本下功效比ADF更高,urca包里没有现成函数,可以使用urca包里的dfgls相关实现,或者用urca里的ur.ers()函数:
ers_test <- ur.ers(trend_stationary, type = "DF-GLS", model = "trend") summary(ers_test)ur.ers()的实际可用性取决于R版本和包更新情况。如果不可用,也可以换用tseries或者其他包里的实现。总之,遇到小样本数据,不要只依赖ADF一个结果。
5. 常见问题与排查心得
下面集中整理几个高频问题,都是实际操作中经常遇到的。
5.1 ADF和KPSS结论不一致怎么办
这种矛盾最常出现。比如ADF检验不拒绝单位根原假设,判定非平稳;但KPSS检验也不拒绝平稳原假设,判定可能是平稳。听起来两头都占,实际上信息是矛盾的。
我遇到这种情况的常见原因有几种。一是序列有结构突变,比如政策调整、突发事件导致均值发生跳变,两种检验对突变点的处理不同,结果就会打架。二是序列其实是一个长记忆过程,自相关衰减得很慢,ADF可能漏判,KPSS也可能出偏差。三是确定性趋势被误设定,前面提到的trend和drift用错,也会导致不一致。
处理方式分几步:先画图确认有没有结构突变,有突变就用带结构断点的单位根检验,比如urca的ur.za()函数,或者把突变点前后的数据分段检验。然后用差分或者分解方法提取确定性趋势后重新检验。如果还是不一致,就保守起见,按非平稳处理,做差分后再检验。
一条经验是:当检验结果互相矛盾时,宁可差分后建模,也不要冒险用非平稳数据直接建模。差分虽然损失一点信息,但至少模型结论是可靠的。
5.2 过度差分问题
一阶差分后序列平稳,就此打住。很多人为了更平稳,会对差分后的序列再做差分,结果引入过度差分的问题——序列的方差会增大,模型反而更难估计。
判断是否过度差分的一个简单办法:比较一阶差分序列和二阶差分序列的方差。如果二阶差分后的方差明显大于一阶差分后,大概率是差分过头了。另一种方法是用信息准则比较不同差分阶数下的ARIMA模型拟合结果,选AIC或BIC最小那个阶数。
实际操作中,我通常最多做到二阶差分。如果一个序列需要二阶差分才能平稳,我会先看看是不是数据本身有指数增长趋势,比如GDP、人口这类数据,更适合用对数变换或者增长率序列来做分析。
5.3 季节性序列的检验误判
月度或者季度数据经常有季节性。如果直接做ADF或者KPSS检验,季节性特征会被当成自相关结构,影响滞后阶数的选择,可能导致误判。
处理方式:先做季节分解,把趋势项、季节项、随机项分开,再对季节调整后的序列做平稳性检验。R里用stl()函数做LOESS分解,或者用decompose()做经典分解。
monthly_data_ts <- ts(some_data, frequency = 12) decomposed <- stl(monthly_data_ts, s.window = "periodic") seasonally_adj <- monthly_data_ts - decomposed$time.series[, "seasonal"] adf.test(seasonally_adj)如果检验季节调整后的序列是平稳的,那么说明原始数据的非平稳性有一部分是季节性造成的,建模时用加法或者乘法分解的方式把季节项单独处理即可。
5.4 缺失值和异常值的干扰
缺失值不会直接导致误判,但会通过影响滞后阶数选择和回归样本量,间接影响检验结果。异常值则更麻烦,尤其是那种异常的大跳变,会让序列的方差变大,均值漂移,ADF检验可能漏掉单位根,或者正好相反。
所以我建议在平稳性检验之前,先做数据清洗:插补缺失值、处理明显异常值。插补方法可以是线性插值、移动平均或者更复杂的季节插值。如果用tseries里的na.interp()函数,处理起来比较方便。
异常值是否值得处理,要看它属于真实业务事件还是数据采集错误。比如电商平台的促销日销量暴增,这是有业务含义的,就不建议直接抹掉,可以考虑加入干预变量或者使用robust方法。
6. 一些实用建议与踩坑记录
最后分享几点个人经验,谈不上什么理论创新,但都是实打实用时间换来的。
第一个建议是:检验之前先弄清楚数据生成过程的大致类型。是流量数据、财务数据还是传感器数据?它们的基本形态完全不同。流量数据可能带有强季节性,财务数据往往有波动聚集效应,传感器数据可能带有缓慢漂移。先有个直觉判断,再决定用哪组参数,会省很多事。
第二个建议是:把检验结果做成一套固定模板。每次拿到新数据,我都按固定顺序跑一遍:时序图、ACF图、ADF、KPSS、一阶差分后的ACF和PACF。这套流程跑下来,数据的大致脾气基本摸清了。模板形式可以是R脚本,也可以是R Markdown,都可以。
第三点,检验时机也很重要。在做时间序列建模时,不要只在建模前做一次平稳性检验。滚动窗口的平稳性检验,或者分样本期的稳定性检验,在实际业务中更有参考价值。因为很多数据在长时间段内并不稳定,前期平稳、后期出现趋势或者突变的情况很常见。用滚动窗口观察平稳性随时间的演化,能提前预警模型失效的风险。
第四,R的输出结果一定要学会看细节再下结论。比如ur.df()的summary输出里,除了检验统计量、临界值,还有回归系数、残差标准误等。有时候回归系数本身就能告诉你很多信息,比如趋势项是否显著、常数项是否显著。这些信息对进一步建模都有价值。
最后一个提醒:任何单位根检验都不是万能的,不要以一个检验结果的p值来定生死。数据形态复杂的时候,多画图、多角度验证,比死磕检验输出更有用。时间序列分析这个领域,经验积累比背公式更重要,各种数据见多了,自然就总结出自己的一套判断逻辑。
这一期把平稳性检验的R语言实现讲到这里。下一期可以聊聊差分阶数的选择、ARIMA模型的定阶,以及模型诊断里面那些容易被忽略的细节。如果对你有帮助,欢迎在评论区留言,我尽量回复。