第四节:单位根检验(上)——DF检验原理与Python实现

各位同学,今天我们进入协整分析的核心环节——单位根检验。

说实话,我刚入行做量化的时候,对单位根检验的理解就是“跑个ADF,看p值”。直到有一次,我在做国债期货的跨期套利策略时,发现价差序列明明通过了ADF检验,但实盘跑起来却频繁止损。后来复盘才发现,是我对DF检验的底层逻辑理解不够深。

嗯,今天我们就从最基础的DF检验讲起。把地基打牢,后面ADF、PP检验才能用得顺手。

4.1 为什么需要单位根检验?

先问大家一个问题:两个时间序列做回归,R²很高,就说明它们有关系吗?

答案是否定的。我见过太多新手直接拿两个价格序列跑OLS,看到R²=0.95就兴奋得不行。其实这很可能是“伪回归”——两个毫无经济关系的随机游走序列,回归结果也可能显著。

举个例子:

# 生成两个独立的随机游走
import numpy as np
import pandas as pd
import statsmodels.api as sm

np.random.seed(42)
n = 1000
x = np.cumsum(np.random.randn(n))
y = np.cumsum(np.random.randn(n))

# 跑回归
X = sm.add_constant(x)
model = sm.OLS(y, X).fit()
print(model.summary())  # 你会发现R²可能很高!

为什么会这样?因为两个非平稳序列的残差也是非平稳的,t统计量不再服从标准分布。所以,检验序列的平稳性,是协整分析的第一步。

4.2 DF检验的核心思想

DF检验(Dickey-Fuller Test)要回答的问题很简单:

这个序列是随机游走吗?

它的基本模型是这样的:

假设我们有一个时间序列 y_t,考虑一阶自回归模型:

y_t = ρ * y_{t-1} + ε_t

如果 ρ = 1,序列就是随机游走(非平稳)。
如果 |ρ| < 1,序列是平稳的。

但直接检验 ρ = 1 有个问题——在单位根下,OLS估计量的分布不是标准t分布。所以Dickey和Fuller推导了专门的临界值表。

实际操作中,我们通常把模型改写为:

Δy_t = δ * y_{t-1} + ε_t

其中 δ = ρ - 1。原假设 H₀: δ = 0(有单位根),备择假设 H₁: δ < 0(平稳)。

关键点:DF检验是左单侧检验。统计量越负,越倾向于拒绝单位根假设。

4.3 DF检验的三种形式

我个人习惯把DF检验分为三种情况,你在实际项目中要根据数据特征选择:

模型形式 回归方程 适用场景
无截距无趋势 Δy_t = δ * y_{t-1} + ε_t 序列均值为0,无趋势
有截距无趋势 Δy_t = α + δ * y_{t-1} + ε_t 序列均值非零,无趋势
有截距有趋势 Δy_t = α + β*t + δ * y_{t-1} + ε_t 序列有明显时间趋势

我记得有一次做商品期货的价差分析,发现价差序列的DF检验结果不显著。后来仔细一看,价差其实有缓慢的均值回归趋势,但我用了无截距模型。换成有截距模型后,结果就显著了。

我的建议:如果不确定数据特征,优先用有截距有趋势的模型。虽然检验功效会降低一点,但能避免模型设定错误。

4.4 Python实现DF检验

好,我们来看代码。Python里实现DF检验,最常用的是statsmodels库。

先手动实现一遍,理解原理:

import numpy as np
import statsmodels.api as sm

def manual_df_test(y, trend='c'):
    """
    手动实现DF检验
    trend: 'n'无截距趋势, 'c'有截距, 'ct'有截距和趋势
    """
    n = len(y)
    dy = np.diff(y)  # Δy_t
    y_lag = y[:-1]   # y_{t-1}
    
    if trend == 'n':
        X = y_lag.reshape(-1, 1)
    elif trend == 'c':
        X = sm.add_constant(y_lag)
    elif trend == 'ct':
        t = np.arange(1, n).reshape(-1, 1)
        X = np.column_stack([np.ones(n-1), t, y_lag])
    
    model = sm.OLS(dy, X).fit()
    # δ的t统计量
    t_stat = model.tvalues[-1]
    
    return t_stat

# 测试
np.random.seed(123)
random_walk = np.cumsum(np.random.randn(200))
t_stat = manual_df_test(random_walk, trend='c')
print(f'DF统计量: {t_stat:.4f}')

当然,实际工作中我们直接用statsmodels封装好的函数:

from statsmodels.tsa.stattools import adfuller

# 注意:adfuller默认包含常数项,regression参数控制趋势项
result = adfuller(random_walk, regression='c')
print(f'ADF统计量: {result[0]:.4f}')
print(f'p值: {result[1]:.4f}')
print(f'临界值: {result[4]}')

注意:statsmodels里的adfuller函数实际上是ADF检验(Augmented DF),它包含了滞后项来处理自相关。但当我们设置maxlag=0时,它就退化为DF检验。

4.5 避坑指南

我曾经在回测一个股指期货的期现套利策略时,遇到过一个坑:

我用日线数据做DF检验,p值显示价差序列是平稳的。但换成5分钟高频数据后,同样的检验却显示非平稳。为什么?

原因在于:采样频率会影响检验结果。高频数据往往包含更多噪声,单位根检验的功效会下降。

我的经验是:

  • 日线数据:DF检验效果较好
  • 高频数据(分钟级):考虑用方差比检验作为补充
  • 低频数据(周线/月线):样本量太少,检验功效不足

另外,还有一个常见问题:结构性突变。如果序列在某个时间点发生了均值或趋势的突变,标准DF检验会倾向于不拒绝单位根。这时候要用Zivot-Andrews检验,我们后面会讲到。

4.6 本章知识体系

为了让大家更直观地理解DF检验在整个单位根检验中的位置,我画了一张流程图:

单位根检验知识体系 时间序列平稳性检验 DF检验(本章核心) 无截距无趋势 有截距无趋势 有截距有趋势 ADF检验(滞后项) PP检验(非参数) KPSS检验(反向) 协整分析 → 价差交易策略

从这张图可以看出,DF检验是整个单位根检验体系的基石。理解了DF检验,后面ADF、PP检验就只是锦上添花的改进。

4.7 小结

今天的内容就到这里。总结几个要点:

  • DF检验的核心是检验自回归系数是否为1
  • 三种模型形式要根据数据特征选择
  • 统计量是左单侧检验,越负越显著
  • Python实现用statsmodels的adfuller函数,设置maxlag=0

下一节我们会讲ADF检验——如何通过加入滞后项来处理自相关。到时候我会分享一个我踩过的坑:滞后阶数选错了,检验结果完全相反。

好,今天就到这里。大家回去可以拿自己的数据跑一跑DF检验,看看哪些序列是平稳的,哪些不是。有问题随时交流。


公众号:蓝海数据掘金营,微信deep3321