news 2026/10/14 15:51:46

R语言平稳性检验实战:ADF、KPSS、PP组合判读与陷阱详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
R语言平稳性检验实战:ADF、KPSS、PP组合判读与陷阱详解

做时间序列建模的人,几乎都遇到过一个让人头大的场景:同样的数据,ADF检验显示p值小于0.05,说明序列平稳;可换成KPSS检验,p值又小于0.05,反而说明序列不平稳。两个都是权威检验,结论却完全相反,该信哪个?这个困惑我太熟悉了。如果刚接触这个方法系列,建议先翻翻第一篇里关于时序图和自相关图的初步判断;直接看这篇也不影响,关键结论我会带着重新说明。这篇重点落在R语言实现上,把三种最常用的平稳性检验——ADF、KPSS、PP——逐一拆开,讲清楚参数怎么设、结果怎么读、结论怎么综合,以及操作里那些文档从来不写、但实际总会踩的坑。内容适合正在做时间序列预处理、准备进入ARIMA建模阶段的朋友,也适合那些已经调用过adf.test但不太确定结果含义的学习者。

1. 为什么平稳性检验要有"一套组合拳"而不是一个函数搞定

先回到一个基础问题:平稳性检验到底在检验什么?大多数教材给出的定义是"序列的统计性质不随时间变化",但落到统计检验里,不同方法对"不平稳"的刻画方式完全不同。ADF检验把序列看成带漂移的随机游走,原假设是"存在单位根",也就是默认序列非平稳;KPSS检验反过来,原假设是"序列围绕某个确定性水平或确定性趋势是平稳的",也就是默认序列平稳。PP检验的原假设和ADF一样,但对误差项序列相关性的处理方式不一样。

这里面的关键点在于:ADF和KPSS的原假设是相反的。很多初学者没有意识到这一点,直接把两个检验的p值朝同一个方向解读,自然就会得出矛盾的结论。用日常场景来类比:ADF像是"默认你携带违禁品,除非有证据证明清白",KPSS则是"默认你没问题,除非搜出违禁品"。同一个被检查者,在两种默认规则下得到不同判定,本来就很正常。

既然单个检验各有偏向,专业做法就是同时跑多个检验,再根据它们的共识做判断。两个检验都说平稳,结论可信度高;两个都说非平稳,差分处理没跑;一个说平稳一个说不平稳,就要往下查模型设定是不是写错了。这种交叉验证的思路,跟用多个模型投票做预测是一个道理。

三种检验的具体分工如下表:

检验方法原假设拒绝原假设的含义R函数
ADF序列存在单位根,非平稳序列平稳tseries::adf.test、urca::ur.df
KPSS序列围绕水平或趋势平稳序列非平稳tseries::kpss.test、urca::ur.kpss
PP序列存在单位根,非平稳序列平稳tseries::pp.test

从这个表能看出来,ADF和PP是一组,KPSS是一组。实操中推荐"ADF + KPSS"组合,一个负责证明平稳,一个负责证明不平稳,两边都不越界的时候,结论才站得住脚。PP检验更适合在数据量较大、且明显存在序列相关的时候作为补充验证。

2. R语言三大检验的逐个拆解:ADF、KPSS、PP的参数与输出解读

2.1 ADF检验:滞后阶数怎么选才不冤枉数据

ADF检验在R里的常用入口有两个:tseries包的adf.test()和urca包的ur.df()。前者用起来简单,一行代码出结果,但很多东西被隐藏了;后者需要手动指定模型类型和滞后阶数,写起来麻烦一些,却给了我们更多的控制权。

先说默认参数。adf.test(x, alternative = "stationary")会自动选择滞后阶数k,默认值是trunc((length(x)-1)^(1/3)),这是经验公式,样本量100时大约选4阶。这个默认值在多数场景够用,但如果序列本身存在较强的短期相关或者样本量很大,建议手动增大k值再跑一次,看看结论是否稳健。

演示之前先明确一个原则:检验结论的可信度,一半取决于检验方法是否正确使用。为了更直观,我模拟一段带有明显趋势和周期性的数据,假设它是某门店的日客流量。随机种子固定在2024是为了让读者重跑代码时得到一模一样的结果;样本量选120,接近实际业务中一年日数据量的规模,既有代表性,又不至于小到检验失效。

library(tseries) library(urca) set.seed(2024) n <- 120 t <- 1:n ts_data <- ts(1.2 * t + 8 * sin(2 * pi * t / 7) + rnorm(n, 0, 5), frequency = 7) # 方式一:tseries包,直接跑 adf.test(ts_data, alternative = "stationary")

这段代码在我这里跑出来的结果是p值0.4421,远大于0.05,无法拒绝"存在单位根"的原假设,判定为非平稳——这符合预期,因为序列确实带趋势。接着用urca包跑同一个序列,但把模型设定为带漂移项和趋势项:

summary(ur.df(ts_data, type = "trend", lags = 4, selectlags = "AIC"))

ur.df的type有三个选项:none表示既无常数项也无趋势项,drift表示含常数项(漂移项),trend表示既含常数项也含确定性趋势项。选择原则很简单:序列有明显上升或下降趋势用trend;围绕某个非零均值波动用drift;围绕零波动用none。模型设定和实际数据不匹配时,检验功效会明显下降,这是很多人跑出奇怪结果的第一大原因。

输出结果里最需要关注的是最后一张表的"Value of test-statistic"和t值临界值。ADF检验的统计量要跟临界值比:统计量比临界值更负(绝对值更大),才说明显著,可以拒绝单位根原假设。另外还有phi1、phi2、phi3这几个联合检验统计量,分别对应"漂移项和单位根是否同时存在"这类组合假设,操作上通常重点看tau3(trend类型下的单位根检验)就够了。

2.2 KPSS检验:原假设一颠倒,结论跟着反转

KPSS检验在tseries包里的函数是kpss.test(x, null = "Level"),null参数可以选"Level"或"Trend"。Level表示检验序列是否围绕一个常数水平平稳,Trend表示检验是否围绕一条确定性趋势线平稳。urca包对应的是ur.kpss(x, type = "mu", lags = "short"),type参数"mu"对应Level,"tau"对应Trend。

继续用上面那段带趋势的序列:

kpss.test(ts_data, null = "Trend")

对带趋势的序列,如果错误地用null = "Level",几乎必然得到非平稳结论,因为序列的均值一直在变,当然不围绕常数水平波动。正确的做法是:先判断数据的宏观形态,有趋势就用Trend,没有趋势再用Level。这与ur.df里选type参数的逻辑是同一个道理——模型设定必须匹配数据的实际形态。

KPSS输出的解读方向跟ADF完全相反。kpss.test的p值如果大于0.05,说明不能拒绝"平稳"的原假设,倾向认为序列平稳;p值小于0.05,说明拒绝了平稳性,序列被判为非平稳。记住这个方向,后面章节做组合判读才不会错。

还有个小细节:kpss.test默认的lshort = TRUE,使用较短滞后数。对于样本量超过200的序列,建议设置lshort = FALSE让检验使用更长的滞后截断,以减轻序列相关对统计量的污染。这个参数平时没人提,但遇到长序列时差别很明显。

2.3 PP检验:序列相关下的稳健备选

PP检验(Phillips-Perron)解决的问题是:当误差项存在序列相关时,ADF检验的临界值会失真。PP检验通过非参数方法对检验统计量做修正,所以不需要像ADF那样显式设定滞后阶数。它适合数据量较大、且你知道序列有明显相关结构但不想花精力逐个选滞后阶数的场景。

pp.test(ts_data, alternative = "stationary")

输出结构跟adf.test很类似,同样看p值是否小于0.05。不过pp.test在tseries包里没有提供带宽选择的参数,如果你需要精细控制,可以用urca包的ur.pp()。从我的使用体验看,PP检验对小样本的表现不如ADF稳健,样本太小时结果容易不稳定,所以更推荐把它作为辅助验证,而不是唯一依据。

3. 检验结果互相打脸时怎么办:多指标交叉判读的实操套路

3.1 四种组合对应的结论矩阵

把ADF和KPSS的结论组合起来,一共有四种情况。下表是我在项目里常用的判读规则:

ADF结论KPSS结论综合判断建议动作
平稳平稳证据一致,确认平稳直接进入模型识别阶段
非平稳非平稳证据一致,确认非平稳差分后重新检验
平稳非平稳矛盾,警惕设定问题检查模型类型、滞后阶数、是否存在结构突变
非平稳平稳模糊区,证据偏弱结合ACF图判断,或增加样本量重试

需要再次提醒的是,"平稳"和"非平稳"在这个表格里是指检验结论,不是p值方向。ADF的p值小于0.05得出"平稳"结论,KPSS的p值大于0.05才得出"平稳"结论,方向完全不同,别记混了。

第3种情况尤其值得展开。ADF说平稳,KPSS说非平稳,通常是数据存在确定性趋势但ADF的trend设定没有跟上,或者序列中间出现了断点,导致两个检验各自只看到数据的一部分。遇到这种情况,我的第一反应不是急着差分,而是回头画时序图,看趋势是否分段、方差是否随时间变化,排查清楚再下结论。

3.2 从检验结果到差分决策的完整链路

当综合判断指向非平稳时,下一步就是差分。差分阶数不一定要靠肉眼猜,R的forecast包提供了ndiffs()函数,可以直接给出建议:

library(forecast) ndiffs(ts_data, test = "kpss") # 默认方法 ndiffs(ts_data, test = "adf") ndiffs(ts_data, test = "pp")

ndiffs会在0到2之间搜索,返回建议的差分阶数。它的内部逻辑并不复杂:对原始序列做检验,若判定非平稳,差分一次再检验,直到检验通过或达到上限。实际操作中我习惯把kpss和adf两种test结果都跑一遍,如果两者一致,就按建议操作;如果不一致,再手动确认。

差分后记得立刻用三件套重新检验一次,确认差分后的序列是否已平稳:

diff_ts <- diff(ts_data) adf.test(diff_ts, alternative = "stationary") kpss.test(diff_ts, null = "Level") pp.test(diff_ts, alternative = "stationary")

确认差分后序列平稳,再进入自相关函数和偏自相关函数的阶数识别阶段。这里有个反直觉的经验:不要盲目追求差分到完全平稳再停。若差分后的序列在ACF图上出现明显的滞后一阶负相关,很可能说明差分过头了,引入了多余的移动平均成分。金融场景里的经典做法是只对价格取对数差分一次得到收益率,很少见到需要二阶差分的序列。

3.3 一个函数同时输出三种检验的结果

每次手动敲三个检验函数很啰嗦,我在项目里习惯封装一个小小的报告函数,把三个检验的统计量、p值和解读一次性输出:

stationarity_report <- function(x, alpha = 0.05) { library(tseries) adf_out <- adf.test(x, alternative = "stationary") kpss_out <- kpss.test(x, null = "Level") pp_out <- pp.test(x, alternative = "stationary") data.frame( test = c("ADF", "KPSS", "PP"), statistic = round(c(adf_out$statistic, kpss_out$statistic, pp_out$statistic), 3), p_value = round(c(adf_out$p.value, kpss_out$p.value, pp_out$p.value), 4), conclusion = c( ifelse(adf_out$p.value < alpha, "stationary", "non-stationary"), ifelse(kpss_out$p.value < alpha, "non-stationary", "stationary"), ifelse(pp_out$p.value < alpha, "stationary", "non-stationary") ) ) } stationarity_report(ts_data)

注意这个函数对KPSS的conclusion字段做了方向反转,因为KPSS的p值大才代表平稳。用的时候只需要记住"看结果里每个检验给出的conclusion列"即可。如果需要处理带趋势数据,把kpss.test的null参数改为"Trend"再跑一次,两个设定都看看,信息更全。

4. 平稳性检验最容易翻车的五个操作细节

4.1 样本量太小,检验功效形同虚设

ADF检验在小样本下的拒绝能力很弱。所谓功效低,通俗讲就是:序列其实不平稳,但检验统计量就是达不到临界值,系统反而给出"不能拒绝单位根"的结论。样本量低于30时,即使序列明显是随机游走,ADF也常常一脸无辜地报告p值大于0.05。反过来,样本量极大时,很小的偏离也会被判为显著,实际经济含义可能微乎其微。

我的实操底线是:做单个序列的平稳性检验,样本尽量不低于50个观测,理想情况是100以上。样本不够时,至少要把检验结论和ACF图交叉对照,千万不能只凭一个p值就拍板。

4.2 缺失值让检验直接罢工

adf.test、kpss.test、pp.test遇到NA会直接报错,不会自动处理。更隐蔽的问题是:你用了某种填充方式把缺失值补上,但填充方法本身会影响序列的动态结构,进而污染检验结果。比如用线性插值填充一段数据,会让序列显得比实际更平滑,ADF可能因此给出偏平稳的结论。

正确处理顺序是:先做缺失值诊断,判断缺失机制是随机缺失还是有系统性原因;再用对数据结构影响最小的方式处理(常见选择是线性插值或简单移动平均补偿);最后才做平稳性检验。项目中我还会顺手把缺失比例记录下来,后面建模时如果结果异常,能多一个回溯线索。

4.3 趋势与季节效应混在一起时的误判

遇到过很多次这样的情况:一个季度销售序列,用ADF跑出来p值接近1,判定为高度非平稳;但细心看数据,序列在每年同期表现出强烈且稳定的周期性。这种序列严格说属于"季节性平稳",并不是随机游走式的非平稳。处理的关键是先分解再检验:把趋势项、季节项、不规则项拆开,对剔除季节成分或经过季节差分后的序列再做平稳性检验。

# 分解后再检验 library(forecast) stl_fit <- stl(ts_data, s.window = "periodic") remainder <- stl_fit$time.series[, "remainder"] adf.test(remainder, alternative = "stationary")

或者用nsdiffs()判断是否需要季节差分:

nsdiffs(ts_data, m = 12) # m=12表示月度数据

如果nsdiffs返回1,说明需要做一次季节差分,之后再结合ndiffs的结果做整体差分。

4.4 结构突变点会让两类检验集体失灵

序列在某一个时间点突然发生了水平跳变,比如外部冲击或口径调整,ADF和KPSS都可能给出错误的结论。原因在于这两种检验都假设整个序列服从同样的生成机制,一旦数据里藏着断点,检验统计量被前后两段的不同均值拉扯,很可能落在临界区域附近,怎么判都别扭。

遇到可疑断点,常规做法是把样本按断点切分,分段做平稳性检验,看各段结论是否一致;或者先做结构突变检验(R里可以用strucchange包),确认突变位置后,再决定是分段建模还是引入干预变量。这部分展开讲又是一篇文章,这里先提醒一句:当ADF和KPSS结论矛盾且数据有明显跳变时,别急着怀疑代码,先怀疑结构。

4.5 批量序列检验的"假平稳"积累

在批量场景里,比如要为几十上百个序列统一做预处理,每个序列单独在5%显著性水平下检验,多重比较问题就来了。假设所有序列真的平稳,每个序列误判为非平稳的概率约5%;反过来,如果存在某个序列本来不平稳但恰好被误判为平稳,概率虽然低,但序列一多,总有撞上的。更麻烦的是,批量场景里不同序列的样本量、波动特征差异很大,统一阈值对短序列特别苛刻,对长序列又过于宽松。

我的建议是:批量检验时不要只看每个序列的p值,而是把p值的分布打印出来,观察整体形态;如果多数序列p值聚集在0.9以上,说明整体非平稳,但其中夹杂少量p值恰好落在0.01附近的序列,要特别警惕那些孤立的显著值是否是数据质量问题导致的。

5. 预处理动作的落地顺序:从检验结果到可建模数据

5.1 先做方差稳定还是先做差分

实际数据里,非平稳常常是均值问题和方差问题同时存在。均值随时间上升,同时波动幅度也跟着变大。如果直接差分,方差的不稳定仍然会影响后续模型拟合和预测区间。标准的处理顺序是:先通过变换稳定方差,再做差分稳定均值。

最常用的变换是对数变换,适合数据恒为正(比如销量、客流、价格)。判据很简单:序列的波动幅度和水平成正比时,log变换通常有效。Box-Cox变换是更一般的做法,R里可以用forecast包的BoxCox.lambda()自动搜索变换参数:

lambda <- BoxCox.lambda(ts_data) ts_transformed <- BoxCox(ts_data, lambda)

变换完后,再用平稳性检验确认是否需要差分。顺序不能反过来,因为先差分再变换的序列可能包含负值,对数就没法用了。

5.2 一阶差分后仍然不平稳的进阶方案

一阶差分后ADF依然无法拒绝单位根,先别急着上二阶差分。按下面顺序排查:

  1. 先看差分序列的ACF图,如果衰减是渐进型的,可能还有残余趋势;
  2. 再看nsdiffs()的结果,确认是否存在季节性差分需求;
  3. 检查是否有离群值,尤其是单点巨大的冲击,会把差分序列的整体均值拉偏;
  4. 最后才考虑二阶差分。

如果真的到了二阶差分,建模时要注意:二阶差分等价于对一阶差分再做一阶差分,这会显著放大高频噪声,模型往往需要额外引入MA成分来吸收。所以能一次差分解决的问题,绝不要为了"保险"多做一次。

5.3 建模前最后一道确认程序

平稳性确认之后,做ARIMA等模型之前,还有一个常被跳过的步骤:白噪声检验。如果差分后的序列本身就是白噪声,那说明序列里没有可建模的自相关结构,继续建模等于对噪音拟合。

# 对差分后的序列做Ljung-Box白噪声检验 Box.test(diff_ts, lag = 10, type = "Ljung-Box")

Ljung-Box检验的原假设是"序列无自相关",p值小于0.05说明存在显著的自相关结构,值得继续建模。反过来,p值很大,最好停一停想一想。记住这个套路:检验平稳性、确认差分阶数、检查白噪声,三步走完,数据才算真正达到了建模标准。

这一篇的实操内容差不多就到这里。我在实际项目里最深的体会是,平稳性检验从来不是一个"跑完函数就结束"的动作,它更像一个和数据的对话过程:先听它告诉你趋势和季节性,再让检验工具确认你的判断,如果工具和直觉打架,一定是某个环节出了问题。建议你把文中的stationarity_report函数保存下来,以后面对新数据直接调用,然后结合ACF图一起看。如果后续在实际操作中碰到AIC选择滞后阶数不理想、或者KPSS检验在长样本下一直拒绝平稳之类的问题,欢迎继续留言交流,这些都是非常典型的实战细节。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 15:51:35

书霸AI:从一张数据表走到期刊论文

手里有一张调查数据表&#xff0c;论文的结果部分却迟迟写不出来&#xff1a;不知道先分析什么&#xff0c;也不知道哪些数字值得放进正文。这类卡点往往出现在数据与研究问题之间。使用书霸AI辅助期刊论文写作时&#xff0c;可以先准备一份清楚的研究说明&#xff0c;再逐步组…

作者头像 李华
网站建设 2026/10/14 15:51:22

插件机制深度拆解:从plugins概念到加载失败排查

经常在命令行、IDE 或应用设置里看到plugins这个词&#xff0c;但问身边的人&#xff0c;十个人里九个说不清它到底在干吗。更别提那种直接甩你一脸failed to load plugins web boot: 2 entries did not activate的报错&#xff0c;英文都看得懂&#xff0c;意思完全搞不懂。这…

作者头像 李华
网站建设 2026/10/14 15:51:10

近端梯度下降:高效处理L1正则与稀疏模型优化

1. 为什么梯度下降在最常见的稀疏模型上突然失效了先聊个非常实际的场景。你在做机器学习模型&#xff0c;目标函数是损失项加正则项&#xff0c;其中正则项用了L1范数。L1正则的好处大家都知道&#xff0c;能压出稀疏解&#xff0c;特征选择利器。我们习惯性掏出梯度下降就开始…

作者头像 李华
网站建设 2026/10/14 15:51:06

算法训练营DAY1:数组特性、二分查找与双指针全解析

算法训练营迈进DAY1&#xff0c;今天的题单是数组四连击&#xff1a;数组的特性、二分查找、移除元素、有序数组的平方。光看名字全是最基础的入门内容&#xff0c;但真正自己动手写一遍才发现&#xff0c;每一道题都不是在考“你会不会写循环”&#xff0c;而是在逼你回答同一…

作者头像 李华
网站建设 2026/10/14 15:50:38

CentOS 7部署Oracle 11g:从静默安装到内核调优与运维实战

1. 项目背景与整体思路拆解1.1 为什么到今天还有人折腾 CentOS 7 上的 Oracle 11g先说个现实&#xff1a;CentOS 7 早已进入生命周期尾期&#xff0c;Oracle 11g 也已经是“爷爷辈”的数据库版本&#xff0c;但这两样组合起来&#xff0c;仍然是很多生产环境里跑得最稳的底座。…

作者头像 李华