我们已经从理论和数学上了解了潜在结果框架的核心思想,现将其与Python和金融数据结合起来,通过具体示例直观地理解因果推断的基本原理。以下将使用金融时间序列来演示。使用AKshare获取沪深300指数(HS300)和300ETF期权波动率指数(VIX)。这两个序列是典型的负相关关系,经常用作因果分析的案例。import numpy as npimport pandas as pdimport akshare as ak# 获取数据HS_300 = ak.stock_zh_index_daily(symbol='sh000300')VIX = ak.index_option_300etf_qvix()# 合并为一个DataFramedata = pd.merge(HS_300, VIX, on='date').dropna()data = data[['date', 'close_x', 'close_y']]data.columns = ['date', 'HS300', 'VIX']data['HS300_ret'] = data['HS300'].pct_change()data["VIX"] = pd.to_numeric(data["VIX"], errors="coerce")data["VIX"] = data["VIX"].ffill()data['VIX_ret'] = data['VIX'].pct_change()data = data.dropna().reset_index()
我们此时拥有了两个时间序列。在因果推断的语境下,可以设想VIX的急剧上升(干预)是否对沪深300指数的回报率产生了因果影响?当然,这只是一个示例,实际上二者是同时决定的。我们这里只是想用真实的数据演示因果推断的概念。为了能在真实数据中演示“潜在结果”,我们人为创建一个“伪干预”。模拟一个随机化实验,使得我们可以直接观测到因果效应。我们随机选择一些交易日作为“干预日”(D=1),另一些作为"控制日"(D=2)。然后观察"干预日"的回报率VS“控制日”的回报率。由于是随机分配,理论上两组干预前的所有特征都应该是平衡的——这正是随机化实验的核心。随机化意味着干预分配独立于潜在结果。np.random.seed(99)N = len(data)# 随机生成干预指示变量(50%的概率)data['D'] = np.random.binomial(1, 0.5, N)# 定义结果变量Y:当天的HS300收益率data['Y'] = data['HS300_ret']# 检查随机化是否成功(干预组与控制组在协变量上是否平衡)balance_check = data.groupby('D')[['HS300_ret', 'VIX_ret', 'VIX']].mean()print('随机化平衡性检查:')print(balance_check)
随机化平衡性检查: HS300_ret VIX_ret VIXD 0 -0.000004 0.001214 19.6622071 0.000341 0.004305 19.779037
如果随机化有效,两组干预前的VIX水平、VIX变化率等协变量上应该没有显著差异。我们还可以做t-检验:from scipy.stats import ttest_indfor var in ['HS300_ret', 'VIX_ret', 'VIX']: t, p = ttest_ind(data[data['D'] == 0][var], data[data['D'] == 1][var]) print(f'{var}: t={t:.3f}, p={p:.3f}')
HS300_ret: t=-0.572, p=0.568VIX_ret: t=-0.772, p=0.440VIX: t=-0.526, p=0.599
p值均大于0.05,说明随机化成功,两组在可观测特征上平衡。在随机化实验中,观测到的均值之差就是平均因果效应,因为选择偏差为0。ate_obs = data[data['D']==1]['Y'].mean() - data[data['D']==0]['Y'].mean()print(f"观测到的干预组-控制组均值差(ATE)={ate_obs:.6f}")
观测到的干预组-控制组均值差(ATE)=0.000345
由于我们是用随机生成的D(与真实回报率无关),理论上ATE应该是0。观测到的值应该在0附近波动。这验证了随机化实验可以给出无偏的因果效应估计。我们可以在代码中显式定义“潜在结果”,即设想每个交易日存在两种状态下的回报率。但是现实我们只能观测到一种。# 定义两个潜在结果(现实中不可同时观测)# 假设在没有干预(D=0)时的回报率就是实际的HS300_ret# 而如果有干预,回报率会受到一个固定处理效应tau的影响tau_true = 0.001 # 假设干预使得回报率每天增加0.1%# 潜在结果data['Y0'] = data['HS300_ret']data['Y1'] = data['Y0'] + tau_true# 观测结果由分配机制决定data['Y_obs'] = np.where(data['D'] == 1, data['Y1'], data['Y0'])# 平均因果效应(真实值)ate_true = data['Y1'].mean() - data['Y0'].mean()print(f"真实的ATE={ate_true:.6f}")# 观测到的均值差(估计值)ate_est = data[data['D']==1]['Y_obs'].mean() - data[data['D']==0]['Y_obs'].mean()print(f"估计的ATE={ate_est:.6f}")
真实的ATE=0.001000估计的ATE=0.001345
由于随机化,估计值围绕真实值波动。此处我们显式地构造了潜在结果,演示了潜在结果框架的核心公式下面我们故意制造一个“非随机”的干预,引入一个混淆变量——VIX的变化。假设高波动率时期(VIX升高)沪深300回报率通常为负。如果我们“干预”(例如将高VIX日子定位D=1),就会产生选择偏差。# 制造非随机干预:当VIX变化大于0时,D=1(高波动)data['VIX_change'] = data['VIX'].diff()data['D_nonrandom'] = np.where(data['VIX_change']> 0, 1, 0)data = data.dropna()# 干预组treated = data[data['D_nonrandom'] == 1]# 控制组control = data[data['D_nonrandom'] == 0]# 观测到的均差值obs_diff = treated['HS300_ret'].mean() - control['HS300_ret'].mean()print(f"观测到的均差值(非随机干预)={obs_diff:.6f}")
在我们的构造中,因为高 VIX 日即使没有“干预”,收益率本来就更低,所以选择偏差不为零。我们可以近似估计选择偏差:假设“干预”本身没有任何因果效应(即 Y1=Y0),那么观测到的全部差异就是选择偏差。# 假设真实的因果效应为0(干预只是标签,无实际影响)# 那么观测到的差异完全来自选择偏差selection_bias = obs_diffprint(f"选择偏差(近似): {selection_bias:.6f}")