4、Python中的ADF检验实战:使用statsmodels进行ADF检验、结果解读、代码示例

聊到协整关系,就绕不开一个核心问题——时间序列到底平不平稳

我个人习惯,拿到任何金融时间序列数据,第一件事就是做ADF检验。为什么?因为后续的建模、回归、协整检验,全都建立在“序列平稳”这个前提上。如果序列不平稳,你算出来的相关系数、回归系数,很可能都是假的——这就是所谓的“伪回归”。

好,咱们直接上干货。这一节,我带你手把手跑一遍ADF检验,用Python的statsmodels库。代码不多,但每一步都有坑,我会把我在项目中踩过的雷都告诉你。

4.1 什么是ADF检验?一句话讲清楚

ADF检验,全称Augmented Dickey-Fuller检验。说白了,就是检验你的时间序列是否存在“单位根”。

有单位根 = 不平稳 = 随机游走 = 没法直接做回归。

没有单位根 = 平稳 = 均值回归 = 可以继续往下玩。

你想想看,如果一只股票的价格是随机游走的,那它今天涨了明天可能跌,没有任何规律可循。但如果你发现两只股票的价格差是平稳的,那就说明它们之间存在协整关系——这就是配对交易的基础。

核心结论: ADF检验的零假设是“序列存在单位根(不平稳)”。p值小于0.05,拒绝零假设,序列平稳。

4.2 安装与导入:别漏了关键包

嗯,这里要注意。很多人只装了statsmodels,但忘了装pandasnumpy。虽然它们不是ADF检验的直接依赖,但数据处理少不了它们。

# 安装(如果还没装的话)
# pip install statsmodels pandas numpy

import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import adfuller
import warnings
warnings.filterwarnings('ignore')  # 忽略一些烦人的警告

我个人习惯把adfuller函数单独拎出来,因为后面要反复调用。你如果只做一次检验,直接写statsmodels.tsa.stattools.adfuller()也行,但代码会显得啰嗦。

4.3 生成模拟数据:先造个“不平稳”的序列

为了让你看得更清楚,我先生成两组数据:一组是随机游走(不平稳),一组是白噪声(平稳)。

# 设置随机种子,保证结果可复现
np.random.seed(42)

# 生成100个时间点
n = 100
t = np.arange(n)

# 1. 随机游走(不平稳)
random_walk = np.cumsum(np.random.randn(n))

# 2. 白噪声(平稳)
white_noise = np.random.randn(n)

# 3. 带趋势的平稳序列(实际中常见)
trend_stationary = 0.5 * t + np.random.randn(n)

# 把它们放进DataFrame方便查看
data = pd.DataFrame({
    'random_walk': random_walk,
    'white_noise': white_noise,
    'trend_stationary': trend_stationary
})

print(data.head())

你看,random_walk是累加生成的,所以它没有均值回归的特性。而white_noise是纯随机,均值始终在0附近晃悠。

4.4 执行ADF检验:一行代码搞定

好,检验来了。用adfuller函数,传入一个序列就行。

# 对随机游走做ADF检验
result_rw = adfuller(data['random_walk'])
print("=== 随机游走(不平稳)===")
print(f"ADF统计量: {result_rw[0]:.4f}")
print(f"p值: {result_rw[1]:.4f}")
print(f"临界值: {result_rw[4]}")

print("\n")

# 对白噪声做ADF检验
result_wn = adfuller(data['white_noise'])
print("=== 白噪声(平稳)===")
print(f"ADF统计量: {result_wn[0]:.4f}")
print(f"p值: {result_wn[1]:.4f}")
print(f"临界值: {result_wn[4]}")

输出结果大概长这样:

=== 随机游走(不平稳)===
ADF统计量: -1.2345
p值: 0.6543
临界值: {'1%': -3.498, '5%': -2.891, '10%': -2.582}

=== 白噪声(平稳)===
ADF统计量: -9.8765
p值: 0.0000
临界值: {'1%': -3.498, '5%': -2.891, '10%': -2.582}

看到了吗?随机游走的p值0.65,远大于0.05,不能拒绝零假设——不平稳。白噪声的p值接近0,拒绝零假设——平稳。

小技巧: 我一般不看p值,直接看ADF统计量是否小于1%的临界值。如果小于,那基本稳了。p值有时候会因为样本量小而不准确,但统计量比较靠谱。

4.5 结果解读:别只看p值

adfuller返回的是一个元组,包含6个元素。很多人只取p值,其实浪费了信息。我列个表给你看:

索引 字段 说明
0 ADF统计量 值越小越平稳,通常与临界值比较
1 p值 小于0.05拒绝零假设
2 滞后阶数 自动选择的滞后阶数,用于消除自相关
3 观测样本数 实际用于检验的样本量
4 临界值 字典,包含1%、5%、10%三个水平的临界值
5 最大信息准则值 用于选择滞后阶数的参考值,一般用不到

我在项目中遇到过这样的情况:p值显示0.04,刚好小于0.05,但ADF统计量只比5%临界值小一点点。这时候我会多留个心眼——可能序列只是“边缘平稳”。建议你再做一次KPSS检验交叉验证。

4.6 封装成函数:以后直接复用

你想想看,如果每次都要手动写adfuller、手动打印结果,多麻烦。我习惯封装成一个函数,顺便把结果格式化成DataFrame,方便批量处理。

def adf_test(series, title=''):
    """
    对时间序列执行ADF检验,并打印详细结果
    """
    result = adfuller(series, autolag='AIC')
    
    print(f'=== ADF检验结果: {title} ===')
    print(f'ADF统计量: {result[0]:.6f}')
    print(f'p值: {result[1]:.6f}')
    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('-' * 40)
    
    return result

# 测试
adf_test(data['random_walk'], '随机游走')
adf_test(data['white_noise'], '白噪声')
adf_test(data['trend_stationary'], '带趋势的平稳序列')
注意: 我曾经在分析日频数据时,直接用默认参数跑ADF检验,结果发现p值总是很大。后来才意识到——日频数据往往有很强的自相关性,默认的滞后阶数可能不够。建议设置maxlag参数,比如adfuller(series, maxlag=20),让模型多考虑一些滞后项。

4.7 实战案例:检验两只股票价差的平稳性

好,咱们来点真实的。假设你找到了两只股票,觉得它们可能有协整关系。第一步,就是检验它们的价差是否平稳。

# 模拟两只股票的价格序列
np.random.seed(123)
n = 200

# 股票A:随机游走
stock_a = np.cumsum(np.random.randn(n)) + 100

# 股票B:与A协整(价差平稳)
spread = np.random.randn(n) * 0.5  # 平稳的价差
stock_b = stock_a + spread

# 计算价差
price_spread = stock_a - stock_b

# 对价差做ADF检验
adf_test(price_spread, '股票A与股票B的价差')

如果价差的p值小于0.05,恭喜你,找到了一个潜在的配对交易机会。接下来就可以做协整检验、计算半衰期、设计交易信号了——这些是后面章节的内容。

4.8 常见坑与避坑指南

  • 数据量太少: ADF检验对样本量敏感。我建议至少用50个以上的数据点,100个以上更稳妥。少于30个,检验结果基本不可信。
  • 忽略结构性断点: 如果序列在某个时间点发生了突变(比如政策变化、金融危机),ADF检验可能会误判。这时候可以考虑Zivot-Andrews检验,它允许存在一个结构性断点。
  • 过度依赖p值: 我见过有人p值0.049就说平稳,0.051就说不平稳。别这么干。结合统计量和临界值一起看,更靠谱。
  • 忘记去趋势: 如果序列有明显的上升或下降趋势,ADF检验可能会把“趋势平稳”误判为“不平稳”。这时候可以设置regression='ct'参数,让模型包含常数项和趋势项。
# 带趋势项的ADF检验示例
result_trend = adfuller(data['trend_stationary'], regression='ct')
print(f"带趋势项的ADF检验p值: {result_trend[1]:.4f}")

4.9 知识体系流程图

下面这张图,帮你理清ADF检验在整个协整分析中的位置:

ADF检验在协整分析中的位置 获取时间序列数据 ADF单位根检验 p < 0.05? 序列平稳 ✓ 序列不平稳 ✗ 进行协整检验 差分处理后再检验 ADF检验是协整分析的第一步,决定了后续建模方向

这张图你看懂了吗?ADF检验就像一道分水岭。平稳了,往前走做协整;不平稳,先差分再说。别跳过这一步,否则后面全是白费功夫。


好了,ADF检验的实战部分就到这里。代码不多,但每个参数、每个输出都有它的意义。我建议你拿真实数据跑一跑,比如沪深300和上证50的日收益率,看看它们是否平稳。实践出真知。

一句话总结: ADF检验是协整分析的“体检报告”。序列不平稳,一切免谈。序列平稳,恭喜你,可以进入下一关。

专注资料整理