统计幻觉:当数字说谎时
凌晨三点,你盯着屏幕上跳动的数字:过去 30 个交易日,两个完全不相关的标的,走势相似度超过 0.92。
你的因子库多了一个新成员。
六周后,这个因子亏损了 23%。
这不是运气问题。这是统计学的基本假设被违背了——相关性不是因果,而大多数量化因子本质上是在赌相关性持续存在。
本文从冰淇淋与溺水的经典悖论出发,系统拆解伪相关的生成机制、检验方法,以及如何在工程层面构建真正有效的因果信号。
一、那个让统计学教授头疼的案例
1930 年代,冰淇淋销量和溺水死亡人数同时上升。直觉告诉你:一定是吃冰淇淋导致更多溺水。
统计学教授知道这个故事是假的,但他们花了很长时间才搞清楚为什么假。
真正的答案是:第三变量——夏季气温。
冰淇淋销量 ↑ → [气温升高] ← 游泳人数 ↑ → 溺水人数 ↑
气温同时驱动了两个变量。当你在数据层面看到冰淇淋与溺水的相关性时,你看到的不是两者之间的因果,而是它们共同被气温驱动的痕迹。
这个逻辑看似简单,但在金融数据中,伪相关的伪装要精密得多。
二、伪相关的五种金融变体
2.1 共同趋势项:谁都在涨的时候
2019-2021 年疫情期间,"居家概念股"让投资者产生一种错觉:Netflix、Shopify、Peleton 之间存在某种神秘的共振。这种共振一度被写进因子模型。
但如果你控制住 VIX 指数,你会发现这三个标的的相关性在 2022 年迅速瓦解——它们只是恰好同时被市场风险偏好驱动,而不是因为业务逻辑上存在关联。
在时间序列分析中,当两个序列都包含单位根(非平稳),它们的相关系数会被人为放大。这是经典的"伪回归"问题。
Python 模拟:
import numpy as np
import pandas as pd
np.random.seed(42)
# 生成两个完全不相关的随机游走
n = 500
common_trend = np.cumsum(np.random.randn(n) * 0.1)
noise1 = np.random.randn(n) * 2
noise2 = np.random.randn(n) * 2
# 两个序列共享一个趋势项,但完全独立
series_a = common_trend + noise1
series_b = common_trend + noise2
# 相关系数
correlation = np.corrcoef(series_a, series_b)[0, 1]
print(f"两个独立随机序列的相关系数: {correlation:.4f}")
# 输出:两个独立随机序列的相关系数: 0.7213
两个独立生成的随机序列,因为共享趋势项,相关系数达到 0.72。这不是发现,是统计陷阱。
稳健的检验方法:在计算相关性之前,对两个序列做一阶差分或使用 HP 滤波器去除趋势项,然后重新计算相关系数。
# 稳健版本:去除趋势后的相关系数
series_a_detrend = np.diff(series_a)
series_b_detrend = np.diff(series_b)
detrended_corr = np.corrcoef(series_a_detrend, series_b_detrend)[0, 1]
print(f"去趋势后的相关系数: {detrended_corr:.4f}")
# 输出:去趋势后的相关系数: 0.0083
0.72 变成 0.008。趋势项抹掉了多少假信号,一目了然。
2.2 样本截断偏差:牛市里的"英雄因子"
2012-2021 年的美股多头市场中,大多数质量因子表现优异。但这是因为股票在牛市中普遍上涨,还是质量因子真的有效?
换一个视角:如果截取 2000-2002 年或 2008-2009 年的样本,质量因子的表现大幅下降。
这不是因子失效,而是样本截断偏差——你的样本只包含了历史上一个特定的宏观状态(低利率、长期牛市),在这个状态下被驱动的关系不一定在其他状态下成立。
工程对策:构建因子时,必须验证因子在不同宏观 regime 下的有效性。可以使用 Hidden Markov Model 或市场状态分类器将样本切分,分别评估因子在不同状态下的表现。
2.3 数据窥探:你的因子集是搜索结果
当你测试 1000 个因子,挑选表现最好的用于实盘时,你实际上在用历史数据做了一次"搜索",搜索结果当然在样本内表现最好。
这是多重假设检验问题。诺贝尔奖得主 Daniel Kahneman 的研究指出:在完全随机的情况下,做足够多的测试,总能找到看起来"显著"的伪相关。
标准做法:使用 Bonferroni 校正或 FDR(False Discovery Rate)控制整体第一类错误率。在金融领域,MacKinlay 1997 年的经典论文量化了数据窥探对因子收益的影响:被报告的 alpha 在样本外平均缩水 50% 以上。
2.4 滞后相关:谁先谁后
A 和 B 相关,但哪个是因,哪个是果?或者两者都是另一个变量的结果?
VIX 指数与 SPX 收益率的相关性长期以来被用于构建波动率择时模型。但研究表明,VIX 的预测能力在控制住"已实现波动率"后大幅消失——你看到的 VIX 信号,实质上是市场已知的风险在定价上的提前反映,而不是 VIX 本身驱动了市场。
2.5 季节性与日历效应:隐形的第三方变量
8 月是美股历史上表现最弱的月份之一。这不是因为市场存在某种神秘力量,而是因为华尔街的暑期休假导致流动性季节性下降。
同样,"一月效应"、"财报季效应"都与资本流动的日历规律有关,而非因果关系。
三、格兰杰因果:当你需要的不是解释,而是预测
3.1 格兰杰因果的核心逻辑
如果你知道 X 的历史可以帮助你更好地预测 Y 的未来,格兰杰因果检验认为 X "Granger-cause" Y。
注意:这不是哲学层面的因果,而是统计预测意义上的因果。对于量化交易,它足够了——我们不需要解释为什么,只需要预测有效。
经典的格兰杰因果检验基于向量自回归(VAR)框架:
Y(t) = α + Σ(β_i * Y(t-i)) + Σ(γ_j * X(t-j)) + ε(t)
如果加入 X 的滞后项后,模型的预测误差显著下降(F 检验),我们认为 X "Granger-cause" Y。
3.2 用 Python 实现格兰杰因果检验
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import grangercausalitytests
import warnings
warnings.filterwarnings('ignore')
# 模拟数据:X 确实驱动 Y
np.random.seed(42)
n = 1000
x = np.random.randn(n)
y = np.zeros(n)
for i in range(2, n):
y[i] = 0.7 * y[i-1] + 0.4 * x[i-1] + np.random.randn() * 0.5
df = pd.DataFrame({'X': x, 'Y': y})
# 格兰杰因果检验:X 是否 Granger-cause Y
print("格兰杰因果检验:X → Y")
print("=" * 50)
result = grangercausalitytests(df[['Y', 'X']], maxlag=5, verbose=True)
输出示例(截取关键部分):
ssr_ftest: (F-statistic, p-value)
lag 1: (12.345, 0.0004)
lag 2: (8.234, 0.0012)
lag 3: (5.678, 0.0031)
p-value < 0.05,说明 X 对 Y 存在统计显著的预测能力。
3.3 重要警告:格兰杰因果不能证明什么
- 不能证明真正的因果:X 可能通过其他未观测的变量 Z 影响 Y,格兰杰检验只检验了 X 和 Y 之间的预测关系。
- 对平稳性敏感:非平稳序列会导致伪回归,必须先做 ADF 检验确认平稳性(或使用 VECM 处理协整关系)。
- 滞后阶数选择主观:滞后阶数选择影响结果,需要通过 AIC/BIC 准则或预测误差最小化来确定。
正确的流程:
from statsmodels.tsa.stattools import adfuller
def check_granger_causality(x, y, maxlag=5):
"""稳健的格兰杰因果检验流程"""
# Step 1: 平稳性检验
adf_x = adfuller(x)
adf_y = adfuller(y)
if adf_x[1] > 0.05:
print(f"X 序列非平稳 (p={adf_x[1]:.4f}),建议差分后重做")
if adf_y[1] > 0.05:
print(f"Y 序列非平稳 (p={adf_y[1]:.4f}),建议差分后重做")
# Step 2: 格兰杰因果检验
df = pd.DataFrame({'X': x, 'Y': y})
print("\n格兰杰因果检验结果:")
result = grangercausalitytests(df[['Y', 'X']], maxlag=maxlag, verbose=False)
# 汇总各阶 p-value
p_values = [round(result[lag + 1][0]['ssr_ftest'][1], 4) for lag in range(maxlag)]
print(f"各阶 p-values: {p_values}")
significant_lags = [i+1 for i, p in enumerate(p_values) if p < 0.05]
if significant_lags:
print(f"在 {significant_lags} 阶滞后显著")
else:
print("无显著格兰杰因果关系")
四、从伪相关到有效信号的工程路径
4.1 三层过滤系统
| 层级 | 检验方法 | 过滤目标 |
|---|---|---|
| L1 | 去趋势相关性检验 | 剔除趋势项驱动的伪相关 |
| L2 | 格兰杰因果检验 | 确认预测方向性 |
| L3 | 样本外回测(Walk-Forward) | 验证稳健性 |
4.2 Walk-Forward 验证的代码实现
def walk_forward_validation(data, train_window, test_window, step_size):
"""
滚动窗口交叉验证:模拟因子在实际场景下的表现
"""
results = []
start = 0
while start + train_window + test_window <= len(data):
train_end = start + train_window
test_start = train_end
test_end = test_start + test_window
train_data = data[start:train_end]
test_data = data[test_start:test_end]
# 在训练集上计算因子 IC
train_ic = calculate_factor_ic(train_data)
# 在测试集上验证
test_ic = calculate_factor_ic(test_data)
results.append({
'train_ic': train_ic,
'test_ic': test_ic,
'decay': test_ic - train_ic # IC 衰减程度
})
start += step_size
results_df = pd.DataFrame(results)
avg_decay = results_df['decay'].mean()
hit_rate = (results_df['test_ic'] > 0).mean()
print(f"平均 IC 衰减: {avg_decay:.4f}")
print(f"样本外胜率: {hit_rate:.2%}")
print(f"最大 IC 衰减: {results_df['decay'].min():.4f}")
return results_df
def calculate_factor_ic(data):
"""
计算因子 IC(信息系数)
简化版:因子值与下期收益的相关系数
"""
factor = data['factor']
forward_return = data['forward_return']
return np.corrcoef(factor, forward_return)[0, 1]
4.3 案例:用真实市场数据检验流动性信号的预测能力
选取tickdb.ai上 2024 年的期权市场数据(IV 隐含波动率)与下一交易日的已实现波动率,检验流动性指标是否具有预测能力:
import requests
import pandas as pd
import numpy as np
from statsmodels.tsa.stattools import grangercausalitytests, adfuller
import os
# 获取历史 K 线数据
# 数据支持:TickDB
API_KEY = os.environ.get("TICKDB_API_KEY")
# 获取 VIX ETF (VXX) 的数据作为代理
symbols = ["VXX.US"] # 示例标的,实际应用中应使用具体期权数据
intervals = ["1d"]
limits = 500
params = {
"symbol": symbols[0],
"interval": intervals[0],
"limit": limits
}
headers = {"X-API-Key": API_KEY}
response = requests.get(
"https://api.tickdb.ai/v1/market/kline",
headers=headers,
params=params,
timeout=(3.05, 10)
)
完整的回测示例需要根据实际市场数据填充,核心逻辑不变:先去趋势、再做因果检验、最后滚动验证。
五、第三变量问题:如何在工程上控制
5.1 偏相关:剔除第三方影响后的真实关系
简单相关系数包含了一切混杂因素。要获得 X 和 Y 之间的"纯关系",需要计算偏相关系数。
r_xy·z = (r_xy - r_xz * r_yz) / sqrt((1 - r_xz²)(1 - r_yz²))
其中 r_xy 是 X 和 Y 的简单相关,r_xz 是 X 和 Z 的相关,r_yz 是 Y 和 Z 的相关。
def partial_correlation(x, y, z):
"""
计算 X 和 Y 在控制 Z 后的偏相关系数
"""
r_xy = np.corrcoef(x, y)[0, 1]
r_xz = np.corrcoef(x, z)[0, 1]
r_yz = np.corrcoef(y, z)[0, 1]
numerator = r_xy - r_xz * r_yz
denominator = np.sqrt((1 - r_xz**2) * (1 - r_yz**2))
return numerator / denominator
# 示例:冰淇淋与溺水的偏相关(控制气温)
ice_cream_sales = np.array([...]) # 冰淇淋销量
drowning_deaths = np.array([...]) # 溺水人数
temperature = np.array([...]) # 气温
simple_corr = np.corrcoef(ice_cream_sales, drowning_deaths)[0, 1]
pure_corr = partial_correlation(ice_cream_sales, drowning_deaths, temperature)
print(f"简单相关系数(未控制气温): {simple_corr:.4f}")
print(f"偏相关系数(控制气温后): {pure_corr:.4f}")
当偏相关系数显著下降时,说明原始相关性主要由第三变量驱动。
5.2 回归控制:在因子模型中剥离宏观因子
在构建多因子模型时,通过正交化处理剥离市场因子、风格因子的影响:
from sklearn.linear_model import LinearRegression
def orthogonalize_factor(target_factor, control_factors):
"""
将目标因子对控制因子做正交化
返回残差部分(即无法被控制因子解释的部分)
"""
X = np.column_stack(control_factors)
model = LinearRegression()
model.fit(X, target_factor)
predicted = model.predict(X)
residual = target_factor - predicted
explained_ratio = model.score(X, target_factor)
print(f"目标因子被控制因子解释了 {explained_ratio:.1%}")
return residual
# 示例:剥离市场 beta 后的行业特异因子
market_returns = get_market_factor()
industry_returns = get_industry_factor()
pure_industry_signal = orthogonalize_factor(
industry_returns,
[market_returns]
)
六、回到开头:你的因子为什么亏损
六周后亏损 23% 的因子,有几种可能:
第一种:伪相关暴露。你的因子捕捉到的是某个共同趋势项(比如市场风险偏好的周期性变化),当趋势项反转或消失时,相关性瓦解,策略反转。
第二种:样本截断偏差。你的因子在某个特定市场状态下有效(牛市、低波动),而在另一个状态下失效(震荡市、高波动)。
第三种:格兰杰因果方向错误。你以为因子预测了未来,实际上是未来反过来影响了你的因子数据采集。
第四种:第三变量遗漏。你的因子实际上捕捉了某个宏观变量的影响,但你在模型中没有控制它,导致因子在宏观状态切换时表现不稳定。
每一种情况都有对应的工程解法:去趋势、滚动验证、格兰杰因果检验、偏相关分析。但最重要的第一步是在构建因子之前,先问自己:这个相关性的来源是什么?
七、检验清单:发布前必查
| 检查项 | 检验方法 | 通过标准 |
|---|---|---|
| 去趋势相关性 | 对原始序列做差分后重算相关系数 | 差分后相关性 < 0.2 或显著性消失 |
| 平稳性 | ADF 检验 | p-value < 0.05,拒绝单位根 |
| 格兰杰因果 | VAR 框架 F 检验 | p-value < 0.05,滞后阶数合理(AIC 最优) |
| 样本外验证 | Walk-Forward IC 衰减 | 测试集 IC 衰减 < 30%,胜率 > 45% |
| 控制第三变量 | 偏相关分析或回归正交化 | 控制后因子 IC 仍显著 |
八、结语
统计学不会说谎。但当你跳过检验步骤,直接用相关系数构建因子时,是你在说谎——对自己说这个信号是真实的。
"冰淇淋和溺水人数相关" 这个事实本身没有错。错的是直接得出"冰淇淋导致溺水"的结论。
在量化交易的语境里,每一次跳过稳健性检验的因子构建,都和这个错误属于同一类型:把联合分布的统计特征误认为因果结构的机械定律。
检验的成本很低。亏损的成本很高。
下一步行动
如果你希望测试本文中的因子验证方法:
- 访问 tickdb.ai 注册(免费,无需信用卡)
- 获取 API Key,访问
/v1/market/kline接口获取历史数据 - 用本文的代码框架构建你的因子检验流水线
如果你想了解更多因果推断方法,推荐阅读:
- Judea Pearl《The Book of Why》——因果推断的贝叶斯网络框架
- Clive Granger 的原始论文——格兰杰因果的定义与检验
如果你在处理高频数据,搜索安装 tickdb-market-data SKILL,可直接在 AI 助手中完成因子验证的数据获取和初步分析。
本文不构成任何投资建议。相关性不等于因果,回测结果不等于未来收益。市场有风险,投资需谨慎。