因果推断后门调整实战:Python 代码 3 步实现混杂因子控制与 ATE 估计

发布时间:2026/7/6 21:41:43
因果推断后门调整实战:Python 代码 3 步实现混杂因子控制与 ATE 估计 因果推断后门调整实战Python代码3步实现混杂因子控制与ATE估计在数据科学和机器学习领域我们经常需要回答如果...那么...这类因果问题。传统的统计方法只能揭示变量间的相关性而因果推断则能帮助我们理解变量间的真实因果关系。后门调整作为因果推断中的核心方法之一能够有效控制混杂因子准确估计处理效应。1. 后门调整原理与准备工作1.1 什么是后门调整后门调整(Backdoor Adjustment)是因果推断中控制混杂变量的核心方法。它基于Judea Pearl提出的因果图模型通过阻断处理变量和结果变量之间的后门路径来消除混杂偏差。关键概念解释混杂因子(Confounder)同时影响处理变量和结果变量的变量后门路径处理变量和结果变量之间非因果的关联路径后门准则一组变量Z满足(1)阻断所有X和Y之间的后门路径(2)不包含X的任何后代节点1.2 环境配置与数据模拟在开始编码前我们需要准备Python环境和模拟数据。这里使用Pandas进行数据处理Statsmodels进行统计分析NetworkX绘制因果图。# 导入必要库 import pandas as pd import numpy as np import statsmodels.api as sm import statsmodels.formula.api as smf import networkx as nx import matplotlib.pyplot as plt from sklearn.linear_model import LogisticRegression # 设置随机种子保证结果可复现 np.random.seed(2023) # 模拟数据生成函数 def generate_data(n1000): # 混杂因子Z年龄(20-60岁) Z np.random.uniform(20, 60, sizen) # 处理变量X是否接受治疗(受Z影响) X_prob 1 / (1 np.exp(-(Z - 40)/10)) # 年龄越大接受治疗概率越高 X np.random.binomial(1, X_prob) # 结果变量Y健康评分(受X和Z影响) Y 50 5*X - 0.5*Z np.random.normal(0, 5, sizen) # 构建DataFrame data pd.DataFrame({Treatment: X, Outcome: Y, Age: Z}) return data # 生成数据 sim_data generate_data(1000)1.3 因果图可视化理解变量间的因果关系对正确应用后门调整至关重要。我们使用NetworkX绘制简单的因果图def plot_causal_graph(): G nx.DiGraph() # 添加节点和边 G.add_edges_from([ (Age, Treatment), # 年龄影响治疗选择 (Age, Outcome), # 年龄直接影响健康评分 (Treatment, Outcome) # 治疗影响健康评分 ]) # 绘制图形 pos nx.spring_layout(G) nx.draw(G, pos, with_labelsTrue, node_size2000, node_colorskyblue, font_size16, arrowsize20) plt.title(因果图示例: 年龄是混杂因子, fontsize18) plt.show() plot_causal_graph()2. 后门调整的Python实现2.1 步骤一识别混杂因子在我们的模拟数据中年龄(Age)同时影响治疗选择(Treatment)和健康评分(Outcome)因此是需要控制的混杂因子。这一步通常需要领域知识或因果发现算法来确定。混杂因子检查清单与处理变量相关与结果变量相关(在未处理时)不在处理变量到结果变量的因果路径上# 检查Age是否与Treatment相关 corr_treatment_age sim_data[[Treatment, Age]].corr().iloc[0,1] print(fTreatment与Age的相关系数: {corr_treatment_age:.3f}) # 检查Age是否与Outcome相关 corr_outcome_age sim_data[[Outcome, Age]].corr().iloc[0,1] print(fOutcome与Age的相关系数: {corr_outcome_age:.3f})2.2 步骤二分层分析与调整后门调整的核心公式为 [ P(Y|do(Xx)) \sum_z P(Y|Xx, Zz)P(Zz) ]Python实现主要有三种方式方法1回归调整# 使用线性回归进行后门调整 model smf.ols(Outcome ~ Treatment Age, datasim_data).fit() ate_reg model.params[Treatment] print(f回归调整估计的ATE: {ate_reg:.3f})方法2倾向得分加权# 计算倾向得分(PS) ps_model LogisticRegression() ps_model.fit(sim_data[[Age]], sim_data[Treatment]) sim_data[PS] ps_model.predict_proba(sim_data[[Age]])[:,1] # 计算逆概率权重(IPTW) sim_data[weight] np.where(sim_data[Treatment] 1, 1/sim_data[PS], 1/(1-sim_data[PS])) # 加权最小二乘估计ATE weighted_model smf.wls(Outcome ~ Treatment, datasim_data, weightssim_data[weight]).fit() ate_iptw weighted_model.params[Treatment] print(f逆概率加权估计的ATE: {ate_iptw:.3f})方法3匹配法from sklearn.neighbors import NearestNeighbors # 对每个处理组样本寻找最近邻的对照组样本 treated sim_data[sim_data[Treatment] 1] control sim_data[sim_data[Treatment] 0] nbrs NearestNeighbors(n_neighbors1).fit(control[[Age]]) distances, indices nbrs.kneighbors(treated[[Age]]) # 计算匹配后的ATE matched_control control.iloc[indices.flatten()] ate_match (treated[Outcome].mean() - matched_control[Outcome].mean()) print(f匹配法估计的ATE: {ate_match:.3f})2.3 步骤三结果验证与敏感性分析为了验证我们的调整是否有效可以进行以下检查# 检查调整后处理组和对照组的Age分布是否平衡 print(\n调整前后Age分布比较:) print(原始数据:) print(sim_data.groupby(Treatment)[Age].describe()) print(\n匹配后数据:) print(pd.concat([treated[Age], matched_control[Age]], keys[Treated, Matched Control]).groupby(level0).describe()) # 敏感性分析假设未观测混杂因子的影响 def sensitivity_analysis(data, gamma): Rosenbaum敏感性分析 odds_ratio np.exp(gamma) print(f\n当未观测混杂因子的最大OR{odds_ratio:.1f}时:) # 这里简化实现实际应使用专用包如rbounds p_values [] for _ in range(1000): # 添加随机扰动模拟未观测混杂 perturb np.random.choice([-gamma/2, gamma/2], sizelen(data)) data[perturbed_PS] 1/(1np.exp(-(data[Age] perturb -40)/10)) weights np.where(data[Treatment] 1, 1/data[perturbed_PS], 1/(1-data[perturbed_PS])) model smf.wls(Outcome ~ Treatment, datadata, weightsweights).fit() p_values.append(model.pvalues[1]) sig_rate np.mean(np.array(p_values) 0.05) print(f显著性比例: {sig_rate*100:.1f}%) sensitivity_analysis(sim_data, gamma0.5)3. 实际应用中的关键问题3.1 如何选择合适的调整方法不同调整方法各有优劣下面是主要方法的比较方法优点缺点适用场景回归调整实现简单计算高效依赖模型设定正确连续型混杂变量较多时倾向得分加权不指定结果模型极端PS导致权重不稳定二元处理变量样本量大时匹配法直观保留原始数据可能丢弃大量样本需要可视化展示平衡性时提示在实践中建议尝试多种方法并比较结果的一致性。如果结果差异大可能表明模型设定有问题或存在未观测混杂。3.2 常见陷阱与解决方案过度调整问题调整了中介变量会屏蔽处理效应解决方案绘制因果图确认只调整混杂因子未观测混杂敏感性分析评估结论稳健性# 使用E-value评估未观测混杂需要多强才能解释结果 def calculate_evalue(estimate, se): 计算E-value null_effect 0 ci_lower estimate - 1.96*se rr np.exp(estimate) rr_ci np.exp(ci_lower) evalue rr np.sqrt(rr*(rr - rr_ci)) return evalue evalue calculate_evalue(ate_reg, model.bse[Treatment]) print(f\nE-value: {evalue:.2f})模型误设检查非线性关系和交互项# 检查Age和Treatment的交互作用 model_interaction smf.ols(Outcome ~ Treatment * Age, datasim_data).fit() print(model_interaction.summary().tables[1])3.3 扩展应用连续处理变量当处理变量不是二元而是连续时后门调整仍然适用# 生成连续处理变量数据 def generate_continuous_data(n1000): Z np.random.normal(0, 1, sizen) X Z np.random.normal(0, 0.5, sizen) Y 2*X - 1*Z np.random.normal(0, 0.5, sizen) return pd.DataFrame({X: X, Y: Y, Z: Z}) cont_data generate_continuous_data() # 连续处理变量的后门调整 cont_model smf.ols(Y ~ X Z, datacont_data).fit() print(\n连续处理变量的因果效应估计:) print(cont_model.summary().tables[1])4. 案例研究医疗数据因果分析让我们通过一个真实场景的模拟案例来巩固所学内容。假设我们有一组医疗数据研究某种药物对血压的影响年龄和吸烟状况是潜在的混杂因子。# 模拟医疗数据 def generate_medical_data(n1000): age np.random.normal(50, 10, n).clip(20, 80) smoke_prob 1/(1np.exp(-(age-50)/10)) smoking np.random.binomial(1, smoke_prob) # 药物处方受年龄和吸烟影响 drug_prob 0.3 0.4*(age 60) 0.2*smoking drug np.random.binomial(1, drug_prob.clip(0,1)) # 血压结果 bp 120 5*drug - 0.3*age 2*smoking np.random.normal(0, 5, n) return pd.DataFrame({ Age: age, Smoking: smoking, Drug: drug, BloodPressure: bp }) med_data generate_medical_data() # 因果分析 print(\n 医疗数据分析 ) print(1. 未调整分析:) naive_model smf.ols(BloodPressure ~ Drug, datamed_data).fit() print(naive_model.summary().tables[1]) print(\n2. 后门调整分析(控制Age和Smoking):) adjusted_model smf.ols(BloodPressure ~ Drug Age Smoking, datamed_data).fit() print(adjusted_model.summary().tables[1]) # 检查平衡性 print(\n平衡性检查:) print(原始数据中Drug组的Age差异:, med_data[med_data[Drug]1][Age].mean() - med_data[med_data[Drug]0][Age].mean()) # 计算标准化差异 def std_diff(treated, control): return (treated.mean() - control.mean())/np.sqrt((treated.var() control.var())/2) print(Age的标准化差异:, std_diff(med_data[med_data[Drug]1][Age], med_data[med_data[Drug]0][Age])) # 使用所有混杂因子计算倾向得分 ps_model_full LogisticRegression() ps_model_full.fit(med_data[[Age, Smoking]], med_data[Drug]) med_data[PS_full] ps_model_full.predict_proba(med_data[[Age, Smoking]])[:,1] print(\n倾向得分重叠检查:) print(处理组PS范围:, med_data[med_data[Drug]1][PS_full].agg([min, max])) print(对照组PS范围:, med_data[med_data[Drug]0][PS_full].agg([min, max]))