第八讲:单位根检验实战——Python代码实现与结果解读

好,咱们直接进入正题。前面几讲把单位根的理论讲得差不多了,什么DF检验、ADF检验、PP检验,还有那个让人头疼的滞后阶数选择。但说实话,光看公式推导,你很难真正理解这些东西在实际数据里长什么样。

我个人习惯是:理论看三遍,不如跑一遍代码。这一讲,我们就用Python把单位根检验完整走一遍。我会带着你从数据导入开始,到结果解读结束,中间穿插一些我在实际项目中踩过的坑。

8.1 准备工作:环境与数据

先说说环境。我建议你用Jupyter Notebook或者VS Code,配合Python 3.8以上版本。核心库就这几个:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import statsmodels.api as sm
from statsmodels.tsa.stattools import adfuller, kpss
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
import warnings
warnings.filterwarnings('ignore')

# 设置中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

嗯,这里要注意。statsmodels库的版本不同,有些函数的参数名会变。我目前用的是0.14.0版本,如果你用的是老版本,比如0.12.x,有些参数可能不兼容。建议统一升级一下。

数据方面,我准备了三组典型的时间序列:

  • 平稳序列:白噪声 + 一个常数均值
  • 随机游走:纯单位根过程
  • 带漂移的随机游走:单位根 + 线性趋势

咱们先生成这些数据,看看它们长什么样。

np.random.seed(42)
n = 200

# 1. 平稳序列:AR(1) 系数0.6
epsilon = np.random.randn(n)
y_stationary = np.zeros(n)
y_stationary[0] = epsilon[0]
for t in range(1, n):
    y_stationary[t] = 0.6 * y_stationary[t-1] + epsilon[t]

# 2. 随机游走:单位根
y_random_walk = np.cumsum(np.random.randn(n))

# 3. 带漂移的随机游走
drift = 0.1
y_drift = drift * np.arange(n) + np.cumsum(np.random.randn(n))

# 画图看看
fig, axes = plt.subplots(3, 1, figsize=(10, 8))
axes[0].plot(y_stationary)
axes[0].set_title('平稳序列 (AR(1), phi=0.6)')
axes[1].plot(y_random_walk)
axes[1].set_title('随机游走 (单位根)')
axes[2].plot(y_drift)
axes[2].set_title('带漂移的随机游走')
plt.tight_layout()
plt.show()

你跑一下这段代码,会看到三个完全不同的走势。平稳序列围绕均值上下波动,随机游走到处乱窜,带漂移的那个更是直线往上冲。这就是为什么我们做单位根检验之前,一定要先看图——视觉判断往往能给你第一印象。

8.2 ADF检验:最常用的单位根检验

ADF检验是单位根检验的标配。statsmodels里直接封装好了,一行代码就能跑。但问题在于——参数怎么设?

我见过太多人直接写 adfuller(data),然后看p值。这样其实很危险。为什么?因为ADF检验有三个版本:

  • 无截距无趋势:适用于均值为0的序列
  • 只有截距:适用于均值非零的序列
  • 截距+趋势:适用于有线性趋势的序列

选错了,检验功效会大打折扣。我曾经在做一个宏观经济项目时,就因为选了「只有截距」去检验一个明显有趋势的GDP数据,结果p值显示平稳,差点把整个模型带偏。

来看看代码怎么写:

def adf_test(series, title=''):
    """
    执行ADF检验,自动选择合适版本
    """
    # 先看数据特征
    mean_val = np.mean(series)
    # 简单判断:如果序列有明显趋势,用带趋势的版本
    # 这里用线性回归的斜率做粗略判断
    from scipy import stats
    slope, _, _, _, _ = stats.linregress(range(len(series)), series)
    
    if abs(slope) > 0.01:  # 有明显趋势
        regression = 'ct'   # 截距+趋势
        desc = '带截距和趋势'
    elif abs(mean_val) > 0.5:  # 均值明显非零
        regression = 'c'    # 只有截距
        desc = '只有截距'
    else:
        regression = 'nc'   # 无截距无趋势
        desc = '无截距无趋势'
    
    result = adfuller(series, regression=regression, autolag='AIC')
    
    print(f'===== ADF检验: {title} =====')
    print(f'检验版本: {desc}')
    print(f'ADF统计量: {result[0]:.4f}')
    print(f'p值: {result[1]:.4f}')
    print(f'临界值:')
    for key, value in result[4].items():
        print(f'  {key}: {value:.4f}')
    print(f'滞后阶数: {result[2]}')
    print(f'样本量: {result[3]}')
    
    if result[1] < 0.05:
        print('结论: 拒绝单位根假设,序列平稳')
    else:
        print('结论: 无法拒绝单位根假设,序列非平稳')
    print('')
    
    return result

# 对三个序列分别检验
adf_test(y_stationary, '平稳序列')
adf_test(y_random_walk, '随机游走')
adf_test(y_drift, '带漂移的随机游走')

跑完这段代码,你会看到:平稳序列的p值远小于0.05,拒绝单位根;随机游走的p值很大,无法拒绝;带漂移的那个,如果选了「截距+趋势」版本,p值也会很小——因为趋势项被纳入回归后,剩下的部分其实是平稳的。

关键点:带漂移的随机游走,在「截距+趋势」版本下,检验的是「去趋势后的序列是否有单位根」。如果趋势被正确建模,检验结果应该是平稳的。但这不代表原始数据是平稳的——它只是趋势平稳。

8.3 滞后阶数怎么选?autolag参数详解

刚才代码里有个参数 autolag='AIC',这个很关键。ADF检验需要选择滞后阶数,用来消除残差的自相关。选少了,残差还有自相关,检验结果不可靠;选多了,自由度损失大,检验功效下降。

statsmodels提供了四种选择:

参数值 含义 我什么时候用
'AIC' 用AIC准则自动选择 默认选项,大部分情况够用
'BIC' 用BIC准则自动选择 样本量较大时,BIC更保守
't-stat' 基于t统计量显著性选择 我很少用,容易选太少
None 使用maxlag参数指定的值 做敏感性分析时手动指定

我个人习惯先用AIC,然后手动检查一下残差的自相关。如果残差还有明显的自相关,就手动增加maxlag。

# 手动指定滞后阶数,做敏感性分析
maxlags = [4, 8, 12, 16]
for lag in maxlags:
    result = adfuller(y_random_walk, maxlag=lag, autolag=None)
    print(f'滞后阶数={lag}, p值={result[1]:.4f}')

你试试看,不同滞后阶数下p值可能会有波动。如果p值在0.05附近摇摆不定,那就要小心了——这说明数据可能处于「边界情况」,需要结合其他检验方法综合判断。

8.4 KPSS检验:从另一个角度验证

ADF检验的零假设是「存在单位根」,KPSS检验正好反过来——零假设是「序列平稳」。两者结合使用,能给你更全面的判断。

为什么需要两个检验?因为ADF检验有个弱点:当序列接近单位根(比如系数0.95)时,它很难区分是单位根还是强自相关。KPSS检验可以帮你做交叉验证。

def kpss_test(series, title=''):
    """
    KPSS检验,零假设:序列平稳
    """
    # KPSS也需要选择版本
    # 同样用斜率判断
    from scipy import stats
    slope, _, _, _, _ = stats.linregress(range(len(series)), series)
    
    if abs(slope) > 0.01:
        regression = 'ct'  # 带趋势
        desc = '带趋势'
    else:
        regression = 'c'   # 只有常数
        desc = '只有常数'
    
    result = kpss(series, regression=regression, nlags='auto')
    
    print(f'===== KPSS检验: {title} =====')
    print(f'检验版本: {desc}')
    print(f'KPSS统计量: {result[0]:.4f}')
    print(f'p值: {result[1]:.4f}')
    print(f'临界值:')
    for key, value in result[3].items():
        print(f'  {key}: {value:.4f}')
    
    if result[1] < 0.05:
        print('结论: 拒绝平稳假设,序列非平稳')
    else:
        print('结论: 无法拒绝平稳假设,序列平稳')
    print('')
    
    return result

kpss_test(y_stationary, '平稳序列')
kpss_test(y_random_walk, '随机游走')
kpss_test(y_drift, '带漂移的随机游走')

对比一下ADF和KPSS的结果,你会发现一个有趣的现象:对于平稳序列,ADF说平稳,KPSS也说平稳;对于随机游走,ADF说非平稳,KPSS也说非平稳。两者结论一致时,你可以比较放心。

实战技巧:当ADF和KPSS结论不一致时(比如ADF说平稳,KPSS说非平稳),我通常会怀疑数据存在结构突变或者非线性趋势。这时候需要进一步做Zivot-Andrews检验或者考虑分段检验。

8.5 结果解读:一个完整的实战案例

好了,理论讲完了,咱们来一个真实案例。假设你拿到了某只股票的日收益率数据,想检验它是否平稳。

# 模拟股票收益率数据
np.random.seed(123)
returns = np.random.randn(500) * 0.02  # 日收益率,标准差2%

# 加入一些波动率聚集效应(GARCH效应)
volatility = np.zeros(500)
volatility[0] = 0.02
for t in range(1, 500):
    volatility[t] = np.sqrt(0.05 + 0.9 * volatility[t-1]**2 + 0.05 * returns[t-1]**2)
    returns[t] = returns[t] * volatility[t] / 0.02

# 画ACF和PACF
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes[0, 0].plot(returns)
axes[0, 0].set_title('股票日收益率')
axes[0, 1].hist(returns, bins=50, density=True)
axes[0, 1].set_title('收益率分布')
plot_acf(returns, lags=30, ax=axes[1, 0])
plot_pacf(returns, lags=30, ax=axes[1, 1])
plt.tight_layout()
plt.show()

# 执行检验
adf_test(returns, '股票日收益率')
kpss_test(returns, '股票日收益率')

你猜结果会怎样?金融收益率数据通常都是平稳的——ACF和PACF在滞后1阶之后迅速衰减到0附近,ADF检验p值远小于0.05,KPSS检验p值大于0.05。这说明收益率序列是平稳的,可以直接用于建模。

但这里有个坑:虽然收益率是平稳的,但它的波动率(方差)可能不是。这就是为什么金融时间序列要用GARCH模型——均值方程用ARMA,方差方程用GARCH。嗯,这个我们后面会讲到。

8.6 常见错误与避坑指南

做单位根检验这么多年,我踩过的坑可以写一本书了。挑几个最常见的说说:

错误1:不检查数据就做检验

我曾经接过一个项目,对方直接跑ADF检验,结果显示平稳。我一看数据,发现他把缺失值用线性插值填充了,而且填充后的序列看起来特别平滑。实际上原始数据有大量缺失,插值过程引入了虚假的平稳性。正确的做法是先可视化数据,检查缺失值,必要时用更稳健的方法处理。

错误2:忽略结构突变

2008年金融危机前后的GDP数据,如果你直接跑ADF检验,大概率会说非平稳。但如果你加入一个虚拟变量表示危机前后的结构突变,结果可能完全不同。Zivot-Andrews检验就是专门处理这个问题的。

错误3:过度依赖p值

p值0.049和0.051真的有本质区别吗?没有。我建议你同时看统计量和临界值的对比,以及ACF/PACF图。如果统计量刚好在临界值附近,最好多试几种检验方法,或者增加样本量。

8.7 本章小结:一张图说清楚

说了这么多,咱们用一张流程图把单位根检验的实战流程串起来:

单位根检验实战流程 1. 数据准备与可视化 2. 判断序列特征(趋势/均值) 3. 选择ADF检验版本 无截距无趋势 只有截距 截距+趋势 4. 执行ADF + KPSS检验 5. 综合判断与结论

这张图把整个流程串起来了。你从数据准备开始,判断序列特征,选择合适的检验版本,执行ADF和KPSS双重检验,最后综合判断。每一步都有坑,但只要你按这个流程走,基本不会出大问题。

好了,这一讲就到这里。代码我都贴出来了,建议你打开Jupyter Notebook,把代码一行一行跑一遍。光看不练,永远学不会。下一讲我们会讨论结构突变下的单位根检验,那个更有意思。


交易系统化学习资料 微信Strategy888888