第八讲:单位根检验实战——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 本章小结:一张图说清楚
说了这么多,咱们用一张流程图把单位根检验的实战流程串起来:
这张图把整个流程串起来了。你从数据准备开始,判断序列特征,选择合适的检验版本,执行ADF和KPSS双重检验,最后综合判断。每一步都有坑,但只要你按这个流程走,基本不会出大问题。
好了,这一讲就到这里。代码我都贴出来了,建议你打开Jupyter Notebook,把代码一行一行跑一遍。光看不练,永远学不会。下一讲我们会讨论结构突变下的单位根检验,那个更有意思。