
拿到“时间序列学习4平稳性检验单位根检验、ADF检验”这个标题懂行的朋友应该能猜到这已经是这个系列走到中段了。前几篇如果聊的是时序基础、可视化、自相关那些内容那从这一篇开始才算真正碰触到时间序列建模的“硬门槛”。我刚开始接触时序分析时最直观的感受就是数据明明摆在那可不管你画图、看统计量还是跑模型结果总是不对劲后来才发现问题多半出在“数据压根不平稳”这一步。很多朋友一上来就急着拟合ARIMA、上LSTM做预测结果要么预测值离谱要么模型训练半天收敛不了根源往往就是没做平稳性检验。这篇文章我把单位根检验和ADF检验掰开揉碎讲清楚包括它们到底在干什么、原理是什么、Python里怎么一行行实现、结果怎么解读以及我实际跑数据时踩过的那些坑争取让你看完就能直接上手用。1. 先从“平稳性”说起为什么这个东西卡住了这么多人1.1 平稳性到底是什么先别急着碰单位根和ADF我们得先搞清楚一个基础问题什么样的时间序列叫做“平稳”用一句大白话说如果一个时间序列的统计性质不随时间变化而变化那它就是平稳的。这里说的统计性质最常见的就是均值和方差。你拿一段数据从第1天看到第100天它的均值不会忽高忽低波动幅度也不会一会儿大一会儿小这就是平稳序列最核心的特征。更学术一点的说法是严平稳和宽平稳。严平稳要求序列的联合分布随时间平移而不变这个条件非常严格实际数据几乎不可能满足。所以我们日常说的平稳99%的情况下指的是宽平稳也叫弱平稳宽平稳只需要满足三个条件均值恒定不随时间变化方差恒定不随时间变化任意两个时刻之间的协方差只依赖于时间间隔不依赖于具体时刻你可以把平稳序列理解成一个“老实人”它有自己的脾气均值和冲劲方差但是不管你怎么观察它它的脾气和冲劲都是稳定发挥不会突然暴走也不会突然躺平。相反非平稳序列就像坐过山车均值在飘、方差在变你根本没法用一个统一的规律去描述它更别提建模预测了。为什么建模非要强调平稳性因为绝大多数时间序列模型比如ARIMA本质上是在利用历史数据中的“规律”去外推未来。如果序列本身都不稳定均值方差一直在变那你学到的规律换个时间窗口就失效了模型自然也就废了。这也是为什么很多做深度学习时间序列预测的人直接拿原始序列丢给LSTM训练效果却惨不忍睹。LSTM虽然理论上能拟合非线性关系但面对强非平稳序列训练过程极其痛苦收敛慢、泛化差倒不如先把序列处理平稳了再喂进去。1.2 非平稳序列的两种典型模样实际业务里的时间序列非平稳的情况大致可以归成两类。第一类是趋势性非平稳。序列随着时间有明显的上升或下降趋势比如一个产品的月销量在持续增长或者一个城市的日均气温在逐年变暖。这种序列的均值不是恒定的换个时间段均值完全不一样。第二类是季节性/周期性非平稳。比如电商销售额每年双十一都冲高比如用电量冬夏高春秋低这时候序列的规律是存在的但这个规律是周期的、跟具体时间点相关而不是全局稳定的。当然现实数据往往是趋势加季节再加随机噪声混在一起。所以你拿到任何一条时间序列第一步永远应该是画折线图肉眼看它是平稳的还是非平稳的是带趋势还是带季节性做到心中有数然后再用统计检验去验证你的判断。2. 单位根到底是什么揭开非平稳的内核2.1 从AR(1)模型讲起前面说了一堆平稳性的定义但还没回答一个关键问题单位根为什么能代表非平稳这里需要引入自回归模型最常见的是AR(1)模型形式长这样y_t ρ * y_{t-1} ε_t其中ε_t是白噪声ρ是自回归系数。这个式子很直观当前时刻的值等于上一时刻的值乘以一个系数再加上一点随机扰动。此时ρ的取值决定了序列的一切。如果|ρ| 1那序列是平稳的因为过去的影响会随时间衰减离得越远的历史信息对现在的影响越小比如ρ0.5时往前推10步的影响是0.5的10次方约等于0.001基本可以忽略。如果|ρ| 1序列会爆炸式增长这种情况现实中比较少见但如果出现了基本可以断定数据有问题。最关键的临界情况是ρ 1此时序列变成了y_t y_{t-1} ε_t这就是大名鼎鼎的随机游走。随机游走序列的当前值等于上一期的值加上一个随机冲击而且这个冲击会永远留在序列里不会消失。你今天受到的冲击明天、后天、大后天都在序列的均值会随着时间漂移方差也会越来越大。这就是非平稳序列最典型的微观机制。单位根的名字就是从这里来的。对AR(1)模型做特征方程变换1 - ρ*B 0B是滞后算子解得根为B 1/ρ。当ρ 1时根等于1所以叫“单位根”。所以单位根检验的本质就是检验ρ是不是等于1。2.2 为什么随机游走这么可怕我见过很多新手对随机游走没概念觉得不就是y_t y_{t-1} ε嘛有啥大不了的。但随机游走有两个致命特性。第一它的方差是随时间线性增长的。你可以推导一下假设y_0 0那么y_t ε_1 ε_2 ... ε_t它的方差是t倍的ε方差。这意味着你观测的时间越长序列的波动范围就越大均值窗口换一个时间段就完全不一样。第二随机游走中任何一次冲击的影响都是永久性的。比如一只股票的价格今天因为某个消息涨了10%这个影响会永远留存在价格序列里不会自动消失。而平稳序列中的冲击只是暂时的过几天就会被“吸收”掉序列会回到它的均值附近。这就是为什么金融资产价格几乎都是非平稳的也是为什么金融领域的建模往往需要对收益率而不是原始价格进行建模。收益率的本质是价格的一阶差分而差分后的序列通常就平稳了。2.3 单位根检验和ADF检验的关系单位根检验是一类检验方法的总称它要回答的问题就是这个序列是否存在单位根如果存在序列非平稳如果不存在序列平稳。而DF检验Dickey-Fuller test是其中最经典的一种它的核心思路是把AR(1)模型做一个变形得到Δy_t δ * y_{t-1} ε_t其中δ ρ - 1Δy_t y_t - y_{t-1}。此时原假设是δ 0也就是ρ 1备择假设是δ 0也就是ρ 1。如果检验结果显示显著拒绝原假设说明δ显著小于0即ρ显著小于1序列平稳如果不能拒绝原假设说明ρ 1不能排除序列存在单位根非平稳。ADF检验Augmented Dickey-Fuller test是DF检验的增强版。DF检验假设ε_t是白噪声但真实数据的误差项往往是自相关的。ADF通过在回归方程中加上Δy_t的滞后项来解决这个问题Δy_t α β*t δ * y_{t-1} Σ γ_i * Δy_{t-i} ε_t其中加上了截距项α、时间趋势项β*t以及Δy的滞后项。滞后阶数的选择一般根据AIC或BIC来确定。ADF的检验统计量和临界值也有自己的一套分布因为传统的t分布在这里不适用。3. Python实现ADF检验动手跑一遍才是真的会了3.1 准备环境和数据纸上谈兵没意思我们直接上Python操作。我用的是最常用的几个库pandas处理数据numpy生成模拟数据statsmodels做ADF检验。import numpy as np import pandas as pd import matplotlib.pyplot as plt from statsmodels.tsa.stattools import adfuller为了演示效果我们手动构造两组数据一组是平稳序列一组是带随机游走的非平稳序列。这样我们心里有底再去看检验结果就能直观感受到ADF检验到底在干什么。np.random.seed(42) n 500 # 平稳序列均值0方差恒定的白噪声 stationary np.random.normal(0, 1, n) # 非平稳序列随机游走 random_walk np.cumsum(np.random.normal(0, 1, n)) # 带趋势的非平稳序列 trend_data np.linspace(0, 5, n) np.random.normal(0, 1, n) fig, axes plt.subplots(3, 1, figsize(12, 8)) axes[0].plot(stationary) axes[0].set_title(Stationary Series) axes[1].plot(random_walk) axes[1].set_title(Random Walk) axes[2].plot(trend_data) axes[2].set_title(Trend Noise) plt.tight_layout() plt.show()运行这段代码你会看到第一条序列在0附近均匀波动第二条序列明显“漂移”了第三条序列整体向上。画图这一步一定不能省后面检验结果出来了你可以拿结果和你的视觉判断做交叉验证能帮你快速建立对“平稳vs非平稳”的直觉。3.2 核心代码一行adfuller搞定statsmodels里的adfuller函数用法非常简单def adf_test(series, title): 封装ADF检验输出关键结果 result adfuller(series, autolagAIC) print(f {title} ) print(fADF统计量: {result[0]:.4f}) print(fp值: {result[1]:.4f}) print(f滞后阶数: {result[2]}) print(f样本量: {result[3]}) print(临界值:) for key, value in result[4].items(): print(f {key}: {value:.4f}) if result[1] 0.05: print(结论: 拒绝原假设序列平稳) else: print(结论: 不能拒绝原假设序列非平稳) print() return result adf_test(stationary, 白噪声平稳序列) adf_test(random_walk, 随机游走非平稳序列) adf_test(trend_data, 带趋势非平稳序列)跑出来的结果大概是这样的形态具体数值会因随机种子不同而略有差异对于白噪声平稳序列ADF统计量会是一个很大的负数比如-20左右p值远小于0.05结论显著拒绝原假设序列平稳。对于随机游走p值通常在0.5左右甚至更高远远无法拒绝原假设序列非平稳。特别有意思的是带趋势的那条序列它看起来有一个线性上升趋势但ADF检验通常会告诉你它也是非平稳的因为趋势项的存在让均值不恒定。这里有人会疑惑我的数据明明有趋势ADF说非平稳那是不是差分一次就好了答案是肯定的这个我们后面会说到。3.3 解读结果统计量、p值、临界值到底看哪个很多初学者拿到ADF输出会懵因为结果太多了。这里我按优先级帮你理一理看结果的顺序。先看p值。这是最直观的指标p值小于0.05如果你取95%置信水平就拒绝原假设认为序列平稳。p值大于0.05则没法拒绝原假设序列存在单位根非平稳。这里建议记住一句口诀p小平稳p大非稳。再看ADF统计量和临界值的对比。统计量越负越好。如果ADF统计量小于1%临界值说明在99%的置信水平下可以拒绝原假设这是最强的平稳性证据如果只小于5%临界值说明在95%置信水平下拒绝如果连10%临界值都大于那这个序列基本是铁板钉钉的非平稳。千万记住一个常见误区ADF统计量和t检验里的t值不是一回事不能拿常规t分布的临界值去卡它。ADF统计量服从的是Dickey-Fuller分布这也是为什么statsmodels会单独给出1%、5%、10%三档临界值你只能拿它跟这三档比不能用±1.96这种常规区间去判断。最后可以看看滞后阶数。ADF回归里加入了Δy的滞后项来处理自相关这个阶数通常由autolagAIC自动选择一般不作为主要判断依据但如果你在做对比分析可以通过它了解数据自相关的大致强弱。4. 处理非平稳序列的核心操作差分到底怎么差4.1 一阶差分和二阶差分的区别如果ADF检验告诉你序列非平稳那最常用的处理手段就是差分。差分的本质是用“变化量”来代替“水平值”把趋势和随机游走中的永久性冲击消掉。一阶差分公式是Δy_t y_t - y_{t-1}。你用今天的值减去昨天的值得到的是今天相对昨天的变化。随机游走序列差完会得到白噪声带趋势的序列差完会得到一个围绕常数波动的平稳序列。二阶差分就是对一阶差分后的序列再做一次差分公式是Δ²y_t Δy_t - Δy_{t-1}。什么时候需要二阶差分一阶差分之后ADF检验仍然显示非平稳的时候。我自己的经验是实际业务数据基本一阶差分就能搞定需要二阶差分的场景相对少见如果你发现自己要差分三次以上那大概率不是数据的问题而是你选择的模型方向可能有问题。差分之后记得再做一次ADF检验确认序列已经平稳了。这一步很多人会偷懒跳过然后直接拿去拟合模型最后模型诊断的时候才发现残差还有明显的自相关回头再去差分白白浪费时间。4.2 差分操作的代码示例# 对随机游走做一阶差分 random_walk_diff np.diff(random_walk) adf_test(random_walk_diff, 随机游走一阶差分) # 对带趋势序列做一阶差分 trend_data_diff np.diff(trend_data) adf_test(trend_data_diff, 带趋势序列一阶差分)跑完代码你会发现随机游走的一阶差分序列p值直接掉到0.01以下非常显著的平稳。带趋势序列的一阶差分也平稳了因为差分把那个线性趋势直接消掉了。4.3 差分会损失信息吗这是一个很多新手会问的问题。差分确实会“损失”一部分信息最直接的就是样本量减少1个而且水平值的信息被转化成了变化量你做预测时预测的是未来变化量要还原成真实值需要累加回去。但是这并不意味着差分是坏事。真实业务中绝大多数非平稳序列的“水平值”信息并不稳定你强行去建模水平值反而会被趋势和漂移带偏。差分后的序列虽然信息形式变了但它的统计性质稳定模型学到的规律才真正具有泛化能力。另外如果你的数据有明显的季节性比如周期是12个月那么单纯的一阶差分可能还不够需要做季节差分也就是y_t - y_{t-12}同时可能还要再做一阶差分这就是ARIMA模型里SARIMA的由来。判断方法也很简单画出自相关图如果滞后12阶的自相关系数显著非零大概率需要季节差分。5. ADF检验之外的平稳性检验工具多一层交叉验证5.1 KPSS检验和ADF反过来光用ADF检验其实有点单薄更稳妥的做法是同时用KPSS检验做交叉验证。KPSS检验的原假设和ADF恰好相反。ADF的原假设是“序列存在单位根非平稳”而KPSS的原假设是“序列平稳”。所以这两个检验的结论组合起来看才有意思。理想情况下ADF拒绝原假设平稳、KPSS不拒绝原假设平稳那么两个检验都指向“序列平稳”这是最完美的情况。反过来ADF不拒绝非平稳、KPSS拒绝非平稳两个检验都指向“序列非平稳”结论也很明确。最怕的是结果矛盾ADF说平稳KPSS说非平稳。这种情况往往意味着序列处于临界状态比如有弱趋势或者长记忆效应。我处理这种矛盾时的经验是结合ACF图一起看如果ACF衰减很慢那大概率还是非平稳宁可多差一阶。statsmodels里KPSS检验的调用方式和ADF类似from statsmodels.tsa.stattools import kpss def kpss_test(series, title): result kpss(series, regressionc, nlagsauto) print(f KPSS: {title} ) print(fKPSS统计量: {result[0]:.4f}) print(fp值: {result[1]:.4f}) for key, value in result[3].items(): print(f {key}: {value:.4f}) print()注意kpss函数的回归选项regressionc表示只带常数项regressionct表示带常数项和趋势项。如果你怀疑数据有趋势用ct会更合适。5.2 自相关图ACF辅助判断ACF图也是判断平稳性的一个重要工具。平稳序列的ACF通常会在几阶之后迅速衰减到0附近非平稳序列的ACF衰减非常缓慢呈“拖尾”状态很多阶都显著非零。画ACF图的代码很简单from statsmodels.graphics.tsaplots import plot_acf fig, axes plt.subplots(2, 1, figsize(12, 6)) plot_acf(random_walk, axaxes[0], lags40) axes[0].set_title(ACF of Random Walk) plot_acf(random_walk_diff, axaxes[1], lags40) axes[1].set_title(ACF of Differenced Random Walk) plt.tight_layout() plt.show()你会看到随机游走原始序列的ACF衰减极慢很多阶的相关系数都非常高。而一阶差分后的序列ACF基本只有滞后0阶显著其他都在置信区间内看上去清爽很多。这才是平稳序列该有的样子。6. 我踩过的那些坑ADF检验实操避坑指南6.1 坑一忘记看趋势项导致误判我第一次拿真实业务数据做ADF检验时犯了个错默认用adfuller的默认参数结果p值非常大判定序列非平稳。我心想这数据画图看着还好啊怎么就不平稳呢后来才发现数据里有一个缓慢上升的长期趋势而我的ADF回归里没带趋势项。adfuller函数有个参数regression可以设置为c仅常数项、ct常数和趋势、ctt常数、趋势和二次趋势、n无常数无趋势。默认是c。如果你的序列看起来有明显的上升或下降趋势请用ct再测一次。这可能直接影响结论。6.2 坑二样本量太小检验功效不足ADF检验在样本量很小的时候检验功效很低也就是说它可能把一个非平稳序列误判为平稳第二类错误或者反过来。我个人的经验是少于30个数据点基本别指望ADF给你一个靠谱的结论少于50个点也要谨慎。如果手头数据实在少可以多依赖ACF图、目视检查以及对业务逻辑的判断。数据量少的时候任何统计检验都不如你对业务的理解靠谱。6.3 坑三把p值的“不显著”当成“存在单位根”严格来说p值大于0.05只能说“无法拒绝原假设”不能说“确认存在单位根”。这两者是有区别的但在实际报告中基本可以等价对待。不过你要清楚统计检验永远无法100%确认一个事情尤其是ADF这种对样本量敏感的检验。所以做时序分析永远要把统计检验和业务判断结合起来别做一个纯“跑代码看p值”的工具人。6.4 坑四差分之后无脑套ARIMA差分只是让序列平稳的手段不是终点。差分完之后你还要做模型识别比如看ACF和PACF图确定AR和MA的阶数。有些人差分完直接套auto_arima跑出一个模型连诊断都不看结果残差还是不满足白噪声假设。永远记住ARIMA建模最后的落脚点是残差应当是白噪声如果残差还有自相关说明模型没有捕捉完信息需要重新设定阶数或者加入季节项。7. ADF检验在真实场景中的一些应用细节7.1 金融数据对价格还是收益率建模金融领域是单位根检验应用最密集的地方。股票价格、指数点位这些原始序列几乎无一例外是非平稳的直接建模没有意义。但收益率价格的对数差分通常就是平稳序列了所以业内默认对收益率建模。你去跑任何金融时序模型第一步永远是对价格序列做ADF检验确认有单位根之后转到收益率继续分析。7.2 宏观经济数据趋势与周期的分离宏观数据比如GDP、CPI大多带明显的趋势。有些场景下研究者用ADF检验判断序列的单整阶数进而决定是否适合做协整分析。协整的核心思想是两条非平稳序列虽然各自有单位根但它们的某种线性组合可能是平稳的这说明它们之间存在长期均衡关系。ADF检验在这里是协整检验的前置步骤重要性不言而喻。7.3 深度学习时间序列预测预处理同样重要现在的热门话题是LSTM、Transformer做时序预测很多人以为深度学习可以自动学习趋势和季节不需要做平稳性检验。这个想法其实很危险。虽然深度网络拟合能力确实强但强非平稳序列的训练难度会显著上升容易出现梯度爆炸、loss不收敛等问题。我的建议是即便是用深度学习也至少要做差分或归一化预处理把强趋势和异常波动先压下来。稳妥路线是先ADF检验再差分/去趋势再归一化最后再进模型。这套流程做完模型的收敛速度和预测效果都会提升一个档次。7.4 pandas操作中容易忽略的一步做时间序列分析很多人的数据是从Excel或者数据库导出来的第一步就该把时间列转成datetime类型并设为索引。很多人跳过这步直接画图结果x轴标签乱七八糟差分、滞后操作也没法按照时间对齐。基础性工作但只要做过一次就再也不会忘df[date] pd.to_datetime(df[date]) df.set_index(date, inplaceTrue) df df.sort_index()如果原始数据有时间戳重复的还需要聚合处理。用pandas做时间序列处理时这些前置操作没做好后面全是一团乱麻。8. 关于平稳性检验最后想再叮嘱几句ADF检验是时间序列分析里绕不开的一道工序但千万别把它当成形式主义。我见过不少项目建模前象征性跑一下检验流程然后看一眼p值也不管结论是啥直接就跳到下一步了。这样做的结果往往是模型效果不稳定换个时间段重新训练就拉胯。我自己实际操作中的体会是平稳性检验最有价值的地方不是那个最终的“平稳”或“非平稳”结论而是它在强迫你深入理解数据的生成机制。你在做ADF检验时会不自觉地思考序列有没有趋势、有没有季节性、是不是随机游走、误差项自相关情况如何这些恰恰是对模型效果影响最大的几个因素。随口说一个小技巧做ADF检验之前先把序列画出来然后自己在脑子里做个预判再去跑检验看二者是否一致。不一致的时候往往就是你对数据理解不到位的地方。这个习惯坚持下来比看一百篇教程都有用。时间序列学习的路还长平稳性检验只是其中一块基石但它真的值得花时间彻底搞懂。