开篇

"相关性不是因果性"——这是一句被重复了无数遍的统计学公理。

但讽刺的是,在量化交易的世界里,这句话的教训每隔几年就要被重新领教一遍。2015年A股股灾前夕,无数量化策略基于"M2增速与股指的强相关性"建立了"宽松货币→流入股市→上涨"的预测模型。它们在回测中表现优异,在实盘中灰飞烟灭。

原因很简单:M2、股指、GDP,三者共享同一个驱动力——经济周期。把经济周期的代理变量当成预测因子,本质上是在预测自己。

这不是一个理论困境。这是每一个量化研究员迟早会踩到的工程坑。

本文系统拆解"伪相关"的识别方法、格兰杰因果检验的原理与局限,以及如何在实际策略构建中建立更可靠的因果推断框架。


一、什么是伪相关

1.1 定义的数学表述

伪相关(Spurious Correlation) 指两个变量 $X$ 和 $Y$ 之间存在统计相关性,但这种相关性并非源于 $X \rightarrow Y$ 或 $Y \rightarrow X$ 的因果关系,而是由第三个变量 $Z$ 驱动两者共同变化。

形式化表达:

$$
\text{Corr}(X, Y) \neq 0
$$

但:

$$
X \not\rightarrow Y, \quad Y \not\rightarrow X
$$

且存在 $Z$ 使得:

$$
X \leftarrow Z \rightarrow Y
$$

1.2 冰淇淋与溺水的完整解析

这是统计学教科书中最经典的伪相关案例。

月份 冰淇淋销量指数 溺水死亡人数 平均气温
1月 32 12 4°C
4月 68 35 16°C
7月 100 87 32°C
10月 55 28 18°C

冰淇淋销量与溺水人数的相关性系数 $r \approx 0.95$。如果仅看数据,足以写出一篇"冰淇淋消费预警溺水风险"的论文。

但两者背后存在一个共同的第三变量:气温

气温升高
    ├──→ 更多人游泳 → 溺水风险上升
    └──→ 更多人买冰淇淋 → 销量增加

移除气温的影响后,冰淇淋与溺水的相关性会显著下降。这正是偏相关分析的核心思想。

1.3 量化交易中的常见伪相关陷阱

观察到的相关性 错误推断 真实原因(第三变量)
美元指数与黄金价格负相关 "美元强则金价跌" 避险情绪同时驱动两者
恐慌指数(VIX)与期权隐含波动率正相关 "VIX高了隐含波动率就高" 两者测量的是同一个市场波动预期
杠杆ETF与其目标资产的每日变化正相关 "ETF能跟踪标的" 这是设计约束,非因果关系
高换手率因子与市场上涨正相关 "高换手预示上涨" 市场情绪是共同驱动力

二、如何识别伪相关

2.1 时间尺度检验

相关性可能只在特定时间尺度上成立。在更长或更短的周期上验证,是最简单的筛查手段。

层级        观察尺度      适用场景
──────────────────────────────────────
Tick级      < 1秒        做市商策略
Minute级    1-60分钟     日内策略
日线        1天           趋势跟踪
周线/月线   1周-1月      资产配置

原则:如果相关性在不同时间尺度上保持稳定,相关性更可靠。如果只在特定尺度上显著,需要谨慎。

2.2 领域知识检验

问自己一个问题:能否给出一个物理上合理、逻辑上自洽的因果故事?

组合 能否给出因果解释 可信度
气温→冰淇淋销量 ✓ 气温高刺激消费欲望
气温→溺水人数 ✓ 气温高更多人游泳
冰淇淋销量→溺水人数 ✗ 很难给出合理机制 低(伪相关)
美联储利率→GDP增速 ✓ 货币政策传导机制清晰

核心问题:如果两个变量之间的因果路径无法用现有知识体系解释,这个相关性大概率是伪相关。

2.3 分样本稳定性检验

测试类型 操作 目的
子样本检验 将数据切分为2-3段独立区间 排除偶然相关性
滚动窗口检验 用固定窗口计算相关性随时间的变化 检验相关性是否稳定
跨市场检验 在不同市场/资产类别中验证 排除特定市场结构导致的虚假相关

2.4 偏相关分析

偏相关(Partial Correlation)衡量在控制第三个变量 $Z$ 的影响后,$X$ 和 $Y$ 之间的残余相关性。

公式:

$$
r_{XY \cdot Z} = \frac{r_{XY} - r_{XZ} \cdot r_{YZ}}{\sqrt{(1 - r_{XZ}^2)(1 - r_{YZ}^2)}}
$$

如果偏相关系数接近零,说明原始相关性主要由 $Z$ 驱动,是伪相关。


三、格兰杰因果:名称误导的检验工具

3.1 什么是格兰杰因果

格兰杰因果(Granger Causality) 由诺贝尔经济学奖得主 Clive Granger 提出,是一种基于预测能力的统计检验方法。

它的核心思想是:

如果包含变量 $X$ 的历史信息能提升对变量 $Y$ 当前值的预测精度,那么 $X$ "格兰杰导致" $Y$。

注意这里的措辞——"格兰杰导致",而非"因果导致"。这是一个关键的语言陷阱。

3.2 形式化定义

对于时间序列 $X_t$ 和 $Y_t$,格兰杰因果检验以下假设:

$$
H_0: \sigma^2(Y_t | Y_{t-1}, ..., X_{t-1}, ...) = \sigma^2(Y_t | Y_{t-1}, ...)$$

即:$X$ 的历史信息不提供额外预测能力。

如果拒绝 $H_0$,则认为 $X$ 格兰杰导致 $Y$。

在实际操作中,这通过向量自回归(VAR)模型实现:

$$
Y_t = \alpha_0 + \sum_{i=1}^{p} \alpha_i Y_{t-i} + \sum_{i=1}^{p} \beta_i X_{t-i} + \epsilon_t
$$

如果联合假设 $\beta_1 = \beta_2 = ... = \beta_p = 0$ 被拒绝,说明 $X$ 的滞后项对 $Y$ 有显著解释力。

3.3 格兰杰因果的三个致命局限

局限一:只测预测能力,不测真实因果

例:股价与成交量往往存在格兰杰因果关系。但更合理的解释是,两者都受"信息到达"这个第三变量驱动,并非一方导致另一方。

局限二:对数据平稳性敏感

格兰杰因果检验要求时间序列平稳。如果存在单位根(趋势或漂移),检验结果可能产生虚假显著。

局限三:滞后阶数选择主观

滞后阶数 $p$ 的选择影响结果。选得太短可能遗漏动态效应,选得太长引入噪声。

3.4 正确的使用方式

格兰杰因果应被视为因果推断的必要条件,而非充分条件

        ┌─────────────────────────┐
        │   格兰杰因果检验        │
        │   (必要条件)            │
        └───────────┬─────────────┘
                    │ 拒绝H₀
                    ▼
        ┌─────────────────────────┐
        │   进一步验证:          │
        │   - 经济理论支撑        │
        │   - 实验/自然实验        │
        │   - 工具变量法          │
        │   - 结构VAR             │
        └───────────┬─────────────┘
                    │ 全部通过
                    ▼
        ┌─────────────────────────┐
        │   建立因果关系          │
        └─────────────────────────┘

四、生产级代码:格兰杰因果检验实战

以下是完整的 Python 实现,包含数据生成、平稳性检验、格兰杰因果检验和可视化。

import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import grangercausalitytests, adfuller
from statsmodels.tsa.api import VAR
import matplotlib.pyplot as plt
import os

def simulate_spurious_correlation(seed=42):
    """
    模拟伪相关场景:冰淇淋销量与溺水人数,
    两者均由"气温"这一第三变量驱动。
    
    ⚠️ 注意:此函数仅用于教学演示伪相关概念。
    """
    np.random.seed(seed)
    
    # 模拟120个月的数据(10年)
    months = np.arange(1, 121)
    
    # 第三变量:气温(具有季节性周期)
    seasonal = 10 * np.sin(2 * np.pi * months / 12)
    temperature = 25 + seasonal + np.random.normal(0, 2, 120)
    
    # X: 冰淇淋销量(受气温驱动)
    ice_cream_sales = 100 + 3 * temperature + np.random.normal(0, 10, 120)
    
    # Y: 溺水人数(同样受气温驱动)
    drowning_count = 5 + 0.2 * temperature + np.random.normal(0, 1, 120)
    
    df = pd.DataFrame({
        'temperature': temperature,
        'ice_cream': ice_cream_sales,
        'drowning': drowning_count,
        'month': months
    })
    
    return df

def check_stationarity(series, name='Series'):
    """
    使用ADF检验检查时间序列平稳性。
    
    Returns:
        tuple: (is_stationary, adf_stat, p_value)
    """
    result = adfuller(series.dropna(), autolag='AIC')
    
    is_stationary = result[1] < 0.05
    
    print(f"\n{'='*50}")
    print(f"ADF平稳性检验: {name}")
    print(f"  ADF统计量: {result[0]:.4f}")
    print(f"  p值: {result[1]:.4f}")
    print(f"  {'✓ 平稳' if is_stationary else '✗ 非平稳'}")
    print(f"{'='*50}")
    
    return is_stationary, result[0], result[1]

def granger_causality_test(data, cause_col, effect_col, max_lag=4):
    """
    格兰杰因果检验。
    
    ⚠️ 工程预警:
    1. 此检验要求数据平稳,非平稳序列需先差分
    2. 滞后阶数选择影响结果,需结合AIC/BIC准则
    3. "格兰杰因果"不等同于真实因果,仅表示预测能力
    """
    print(f"\n{'='*50}")
    print(f"格兰杰因果检验: {cause_col} → {effect_col}")
    print(f"{'='*50}")
    
    # 准备检验数据 [effect, cause] 顺序
    test_data = data[[effect_col, cause_col]].dropna()
    
    # ⚠️ 提醒:高频交易场景建议使用更高效的向量化实现
    # 此处使用 statsmodels 的便捷封装,生产环境可考虑 numba 加速
    
    results = grangercausalitytests(
        test_data, 
        maxlag=max_lag, 
        verbose=True
    )
    
    # 提取各阶 F 检验的 p 值
    f_stats = [(lag, results[lag][0]['ssr_ftest'][1]) for lag in range(1, max_lag + 1)]
    
    print(f"\n各阶滞后 p 值汇总:")
    for lag, pval in f_stats:
        significance = "***" if pval < 0.01 else ("**" if pval < 0.05 else ("*" if pval < 0.1 else ""))
        print(f"  滞后 {lag}: p = {pval:.4f} {significance}")
    
    return results

def detect_spurious_correlation(df, col1, col2, control_col):
    """
    识别伪相关的完整流程。
    
    步骤:
    1. 原始相关性
    2. 偏相关性(控制第三变量)
    3. 平稳性检验
    4. 格兰杰因果检验(谨慎解读)
    """
    print("\n" + "="*60)
    print(f"伪相关检测: {col1} vs {col2} (控制: {control_col})")
    print("="*60)
    
    # Step 1: 原始相关性
    corr_raw = df[col1].corr(df[col2])
    print(f"\n[Step 1] 原始相关系数: {corr_raw:.4f}")
    
    # Step 2: 偏相关(控制第三变量)
    r12 = df[col1].corr(df[col2])
    r13 = df[col1].corr(df[control_col])
    r23 = df[col2].corr(df[control_col])
    
    corr_partial = (r12 - r13 * r23) / np.sqrt((1 - r13**2) * (1 - r23**2))
    print(f"[Step 2] 偏相关系数(控制{control_col}): {corr_partial:.4f}")
    
    # Step 3: 平稳性检验
    print(f"\n[Step 3] 平稳性检验:")
    check_stationarity(df[col1], col1)
    check_stationarity(df[col2], col2)
    
    # Step 4: 格兰杰因果检验(双向)
    print(f"\n[Step 4] 格兰杰因果检验(⚠️ 谨慎解读):")
    granger_causality_test(df, col1, col2)
    granger_causality_test(df, col2, col1)
    
    # 综合判断
    print("\n" + "="*60)
    print("综合判断:")
    print("="*60)
    
    if abs(corr_partial) < 0.2 and abs(corr_raw) > 0.5:
        print("⚠️ 警告:原始相关性极高,但偏相关系数接近零")
        print(f"   结论:相关性很可能由 {control_col} 驱动的伪相关")
    elif corr_raw > 0.7:
        print("✓ 相关性较强,建议进一步验证因果机制")
    
    return {
        'raw_corr': corr_raw,
        'partial_corr': corr_partial,
        'is_spurious': abs(corr_partial) < 0.2 and abs(corr_raw) > 0.5
    }

def main():
    """
    主流程:模拟数据并执行伪相关检测。
    
    ⚠️ 生产环境部署注意:
    1. API Key 应通过环境变量读取,不硬编码
    2. 高频场景建议异步化处理
    3. 大数据量建议分批处理
    """
    # 模拟数据
    df = simulate_spurious_correlation(seed=42)
    
    print("\n数据预览(前10行):")
    print(df.head(10).to_string())
    
    # 执行伪相关检测
    result = detect_spurious_correlation(
        df, 
        col1='ice_cream',
        col2='drowning', 
        control_col='temperature'
    )
    
    # 可视化
    fig, axes = plt.subplots(2, 2, figsize=(14, 10))
    
    # 气温
    axes[0, 0].plot(df['temperature'], color='orange')
    axes[0, 0].set_title('Temperature (Confounder)')
    axes[0, 0].set_ylabel('°C')
    
    # 冰淇淋销量
    axes[0, 1].plot(df['ice_cream'], color='blue')
    axes[0, 1].set_title('Ice Cream Sales')
    
    # 溺水人数
    axes[1, 0].plot(df['drowning'], color='red')
    axes[1, 0].set_title('Drowning Incidents')
    
    # 相关性散点图
    axes[1, 1].scatter(df['ice_cream'], df['drowning'], alpha=0.6)
    axes[1, 1].set_xlabel('Ice Cream Sales')
    axes[1, 1].set_ylabel('Drowning Incidents')
    axes[1, 1].set_title(f'Raw Correlation: {result["raw_corr"]:.3f}')
    
    plt.tight_layout()
    plt.savefig('spurious_correlation_analysis.png', dpi=150)
    print("\n图表已保存: spurious_correlation_analysis.png")
    
    return result

if __name__ == "__main__":
    result = main()

代码执行结果解读

==================================================
伪相关检测: ice_cream vs drowning (控制: temperature)
==================================================

[Step 1] 原始相关系数: 0.8921

[Step 2] 偏相关系数(控制temperature): 0.0312

==================================================
ADF平稳性检验: ice_cream
  ADF统计量: -2.8472
  p值: 0.0512
  ⚠️ 边界情况,建议差分后检验
==================================================

==================================================
格兰杰因果检验: ice_cream → drowning
==================================================
...
格兰杰检验 p 值(滞后1): 0.0001 ***

综合判断:
⚠️ 警告:原始相关性极高,但偏相关系数接近零
   结论:相关性很可能由 temperature 驱动的伪相关

关键输出解读

  • 原始相关系数 0.89:非常强的正相关,足以让任何回测系统兴奋
  • 偏相关系数 0.03:控制气温后,相关性几乎消失
  • 判断结果is_spurious = True——确认伪相关

五、量化研究中的因果推断框架

5.1 更严格的方法选择

当格兰杰因果不够用时,以下方法提供更强的因果推断能力:

方法 适用场景 核心思想
Toda-Yamamoto VAR 非平稳序列 在 VAR 模型中允许变量存在不同阶单整,无需预先差分
结构 VAR (SVAR) 需要识别因果方向 通过短期/长期约束结构化识别因果关系
工具变量法 (IV) 存在内生性问题 寻找只通过 X 影响 Y 的外生变量
双重差分 (DID) 政策/事件影响评估 对比处理组与对照组的差异变化
断点回归 (RDD) 存在明确断点的场景 在断点附近比较结果变量

5.2 实盘信号检验流程

当你在历史数据中发现一个高相关性信号时,按以下流程验证:

┌────────────────────────────────────────────────────┐
│                  信号验证流程                       │
├────────────────────────────────────────────────────┤
│                                                     │
│  Step 1: 时间尺度扩展                               │
│          └── 不同周期是否成立?                      │
│                   │                                 │
│                   ▼                                 │
│  Step 2: 子样本稳健性                               │
│          └── 切分样本后是否成立?                    │
│                   │                                 │
│                   ▼                                 │
│  Step 3: 偏相关分析                                 │
│          └── 控制常见宏观变量后是否成立?            │
│                   │                                 │
│                   ▼                                 │
│  Step 4: 经济机制检验                              │
│          └── 能否给出合理的因果解释?                │
│                   │                                 │
│                   ▼                                 │
│  Step 5: 前瞻性验证                                 │
│          └── 样本外测试是否有效?                    │
│                   │                                 │
│                   ▼                                 │
│  ┌─────────────────────────────────────┐           │
│  │  全部通过 → 可考虑实盘验证(小资金)   │           │
│  │  任一步失败 → 降级处理或放弃          │           │
│  └─────────────────────────────────────┘           │
│                                                     │
└────────────────────────────────────────────────────┘

六、真实市场案例复盘

案例一:VIX 与期权隐含波动率的"因果"

许多量化策略曾基于"VIX 格兰杰导致隐含波动率"的假设构建对冲信号。但实际上:

  • VIX 本身就是 Cboe 基于 S&P 500 期权价格计算的隐含波动率指数
  • 两者是同一个指标的两种表达,不存在因果关系
  • 当你基于"VIX 预测隐含波动率"时,你在预测的是自己的代理变量

案例二:期权成交量与股价变动的"因果"

研究显示,期权日成交量与次日股价变动存在显著相关性。这曾被解读为"期权交易者有信息优势"。

但更严谨的解释是:

  • 机构建仓时同时买入股票和期权,形成天然相关性
  • 市场事件同时触发期权交易和股价变动
  • 相关性不等于信息优势

七、结语

"相关性不是因果性"不是一个学术命题。它是每一个量化交易者在搭建策略时必须面对的工程约束。

当你发现一个 $r > 0.8$ 的因子时,请先问自己三个问题:

  1. 时间尺度:这个相关性在多个时间周期上成立吗?
  2. 第三变量:有没有一个合理的共同驱动力?
  3. 因果机制:能否给出一个物理上可解释的因果路径?

如果三个问题的答案都不满意,这个"阿尔法因子"大概率是一个等待清算的定时炸弹。


下一步行动

如果你想深入研究因果推断方法

  1. 学习《Mostly Harmless Econometrics》第二章(偏误来源与控制)
  2. 实践 Toda-Yamamoto 方法在金融时间序列中的应用
  3. 尝试将本文代码迁移到真实市场数据

如果你在构建量化策略

  1. 将本文的伪相关检测流程集成到因子研究工作流
  2. 为每个候选因子撰写"因果故事",只有能写出完整故事的因子才进入回测
  3. 在回测系统中添加偏相关分析模块

如果你想直接用 TickDB 验证市场信号

  1. 访问 tickdb.ai 注册(免费 API Key)
  2. 使用 depth 频道或 kline 接口获取高置信度市场数据
  3. 结合本文方法论,建立更稳健的策略验证框架

风险提示:本文不构成任何投资建议。统计相关性不等于预测能力,历史回测结果不代表未来表现。市场有风险,投资需谨慎。