Skip to content

13.5 因果图与结构因果模型

"Causal inference is not a statistical problem—it is a problem of translating causal assumptions into statistical estimands.""因果推断不是统计问题——它是将因果假设转化为统计估计量的问题。"— Judea Pearl, 2011 Turing Award Winner (2011年图灵奖得主)

Judea Pearl的因果革命:从相关到因果的认知飞跃

难度方法应用


本节目标

  • 理解有向无环图(DAG)与因果关系
  • 掌握do-算子、后门准则、前门准则
  • 学习中介分析(Mediation Analysis)
  • 用图形理解IV、DID等方法
  • 使用DoWhy库进行因果推断
  • 理解Pearl vs Rubin的统一框架

引言:因果推断的三个层次

Judea Pearl的因果阶梯

《The Book of Why》(2018) 提出人类认知的三个层次:

Level 1: 关联(Association) - "看到"

  • 问题: "如果我看到,关于我能推断什么?"
  • 数学: - 条件概率
  • 工具: 统计学、回归分析
  • 例子: "吸烟者的肺癌率更高"

Level 2: 干预(Intervention) - "做"

  • 问题: "如果我,会发生什么?"
  • 数学: - 干预分布
  • 工具: RCT、IV、DID、RDD
  • 例子: "如果强制戒烟,肺癌率会如何变化?"

Level 3: 反事实(Counterfactual) - "如果当初"

  • 问题: "如果我当初做了,会怎样?"
  • 数学: - 反事实概率
  • 工具: 结构因果模型(SCM)
  • 例子: "如果这个已患癌的吸烟者当初没吸烟,他还会得癌症吗?"

关键差异:

直觉:

  • : 被动观察
  • : 主动干预
  • : 时光倒流

有向无环图(DAG)

定义

DAG (Directed Acyclic Graph):

  • 节点(Node): 变量
  • 有向边(Directed Edge): 因果关系
  • 无环(Acyclic): 没有这样的循环

例子:

简单DAG:
    X → Y

混淆DAG:
      Z
     / \
    ↓   ↓
    X → Y

中介DAG:
    X → M → Y

对撞DAG:
    X → Z ← Y

图形元素

1. 路径(Path): 节点序列,忽略箭头方向

2. 有向路径(Directed Path): 沿箭头方向的路径

3. 父节点(Parent): ,则的父节点

4. 子节点(Child): 的子节点

5. 祖先(Ancestor): 沿有向路径可达

6. 后代(Descendant): 反向

三种基本结构

(Chain):

  • 中介变量
  • 控制 → 阻断路径

分叉(Fork):

  • 混淆变量
  • 控制 → 阻断路径

对撞(Collider):

  • 对撞变量
  • 不控制 → 路径阻断
  • 控制 → 路径打开(️ 危险!)

d-分离与独立性

d-分离定义

d-separation (d-分离):

给定条件集,如果之间的所有路径都被"阻断",则称 d-分离

阻断规则:

路径被阻断,当且仅当路径上存在节点满足:

  1. 链或分叉: 在条件集

    X → M → Y   (控制M → 阻断)
    X ← M → Y   (控制M → 阻断)
  2. 对撞: 及其所有后代都不在

    X → M ← Y   (不控制M → 阻断)
    X → M ← Y   (控制M → 打开!)

d-分离的含义:

直觉: 图结构蕴含统计独立性!

例子:对撞偏误

场景: 招聘

DAG:
    才能 → 录用 ← 关系

数据: 只观察被录用的人

问题: 在被录用者中,"才能"和"关系"负相关!

原因: 控制"对撞变量"(录用)打开了路径

教训: 永远不要无意中控制对撞变量!


do-算子:干预的数学化

do-算子定义

Pearl (2000) 的核心贡献:

含义: 如果我们强制取值为,切断所有指向的箭头,Y的分布是什么?

vs 条件概率:

例子: 吸烟与肺癌

DAG:
         基因
        /    \
       ↓      ↓
    吸烟 →  肺癌

观察相关:

可能部分由基因混淆。

因果效应:

通过干预(强制吸烟),切断"基因→吸烟"箭头,得到纯因果效应。

do-算子的图操作

算法:

  1. 在DAG中,删除所有指向的箭头
  2. 在修改后的DAG(记为)中计算

公式:

中。


后门准则(Backdoor Criterion)

定义

后门准则 (Pearl 1993):

给定DAG ,变量集满足后门准则相对于,如果:

条件1: 不包含的任何后代

条件2: 阻断之间的所有后门路径

后门路径: 从的路径,箭头指向

后门调整公式

如果满足后门准则:

含义: 通过条件调整,可以识别因果效应!

这就是传统回归的理论基础!

例子

DAG:
      Z
     / \
    ↓   ↓
    X → Y

后门路径:

满足后门准则:

因果效应:

Python:

python
# 通过分层估计因果效应
ate = 0
for z in z_values:
    ate += P_Y_given_X_Z(Y, X, z) * P_Z(z)

前门准则(Frontdoor Criterion)

定义

前门准则: 当存在中介变量时的识别策略

条件:

条件1: 截获所有从的有向路径

条件2: 之间没有后门路径

条件3: 所有的后门路径被阻断

前门调整公式

如果满足前门准则:

经典例子:吸烟与肺癌

DAG:
         基因(U,不可观测)
        /              \
       ↓                ↓
    吸烟(X) → 焦油(M) → 肺癌(Y)

问题: 基因不可观测,无法用后门准则!

解决: 焦油满足前门准则!

步骤:

1. 估计:

2. 估计:

3. 边际化:

意义: 即使有不可观测混淆,仍可识别因果效应!


中介分析(Mediation Analysis)

直接效应vs间接效应

设定:

    X → M → Y
     \     /
      ↓   ↓

总效应(Total Effect):

直接效应(Direct Effect):

固定中介,X对Y的效应。

间接效应(Indirect Effect):

通过中介的效应。

自然直接/间接效应

Baron & Kenny (1986) 的经典框架:

自然直接效应(NDE):

自然间接效应(NIE):

其中是当的值。

分解:

识别条件

需要假设:

1. 无混淆: 外生

2. 无中介-结果混淆: 没有影响的共同原因

3. 无处理-中介混淆

Python实现:中介分析

python
import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression

def mediation_analysis(X, M, Y):
    """
    中介分析

    参数:
    ------
    X: (N,) 处理
    M: (N,) 中介变量
    Y: (N,) 结果

    返回:
    ------
    effects: dict, 包含总效应、直接效应、间接效应
    """
    N = len(X)

    # 步骤1:总效应 (Y ~ X)
    model_total = LinearRegression()
    model_total.fit(X.reshape(-1, 1), Y)
    total_effect = model_total.coef_[0]

    # 步骤2:X对M的效应 (M ~ X)
    model_mediator = LinearRegression()
    model_mediator.fit(X.reshape(-1, 1), M)
    a = model_mediator.coef_[0]  # X → M

    # 步骤3:控制M后,X对Y的直接效应 (Y ~ X + M)
    model_direct = LinearRegression()
    model_direct.fit(np.column_stack([X, M]), Y)
    c_prime = model_direct.coef_[0]  # 直接效应
    b = model_direct.coef_[1]  # M → Y

    # 间接效应
    indirect_effect = a * b

    # Sobel检验
    se_indirect = np.sqrt(b**2 * np.var(M) + a**2 * np.var(Y))
    z_score = indirect_effect / se_indirect
    p_value = 2 * (1 - stats.norm.cdf(abs(z_score)))

    results = {
        'Total Effect': total_effect,
        'Direct Effect': c_prime,
        'Indirect Effect': indirect_effect,
        'Proportion Mediated': indirect_effect / total_effect if total_effect != 0 else 0,
        'Sobel z': z_score,
        'p-value': p_value
    }

    return results

# 示例
np.random.seed(42)
N = 1000
X = np.random.binomial(1, 0.5, N)
M = 0.5*X + np.random.normal(0, 0.5, N)  # X → M
Y = 0.3*X + 0.4*M + np.random.normal(0, 0.5, N)  # X → Y, M → Y

results = mediation_analysis(X, M, Y)
for key, value in results.items():
    print(f"{key}: {value:.4f}")

工具变量(IV)的图形理解

IV的DAG表示

传统IV设定:

    Z → D → Y
         ↑   ↑
         └─U─┘
  • : 工具变量
  • : 内生处理
  • : 结果
  • : 不可观测混淆

IV假设的图形表述:

1. 相关性: (有边)

2. 排除性约束: 的唯一路径是

3. 外生性: 之间没有路径

用后门准则理解IV

问题: 估计

直接:后门路径无法阻断(不可观测)

间接: 利用!

步骤:

  1. 可识别(无后门路径)
  2. 可识别(无后门路径)
  3. IV估计:

️ DoWhy库:因果推断统一框架

DoWhy的四步框架

Microsoft开发的DoWhy统一了Pearl和Rubin:

步骤1: 建模(Model) - 构造因果图

步骤2: 识别(Identify) - 使用图准则识别因果效应

步骤3: 估计(Estimate) - 选择估计方法

步骤4: 反驳(Refute) - 敏感性分析

完整示例

python
import numpy as np
import pandas as pd
import dowhy
from dowhy import CausalModel
import matplotlib.pyplot as plt

plt.rcParams['font.sans-serif'] = ['Arial Unicode MS']

# =================================================================
# 第1步:生成数据
# =================================================================

np.random.seed(123)
N = 2000

# 混淆变量
Z1 = np.random.normal(0, 1, N)
Z2 = np.random.binomial(1, 0.5, N)

# 处理(受Z影响)
logit_D = -0.5 + 0.8*Z1 + 0.5*Z2
p_D = 1 / (1 + np.exp(-logit_D))
D = np.random.binomial(1, p_D, N)

# 结果(受D和Z影响)
Y = 2*D + 1.5*Z1 + 0.8*Z2 + np.random.normal(0, 1, N)

# 创建DataFrame
data = pd.DataFrame({
    'D': D,
    'Y': Y,
    'Z1': Z1,
    'Z2': Z2
})

print(f"数据概览:")
print(data.describe())
print(f"\n简单均值差(有偏): {data[data['D']==1]['Y'].mean() - data[data['D']==0]['Y'].mean():.3f}")
print(f"真实ATE: 2.0")

# =================================================================
# 第2步:构造因果模型
# =================================================================

# 方法1:指定DAG
model = CausalModel(
    data=data,
    treatment='D',
    outcome='Y',
    graph="""
    digraph {
        Z1 -> D;
        Z2 -> D;
        Z1 -> Y;
        Z2 -> Y;
        D -> Y;
    }
    """
)

# 可视化DAG
model.view_model()
plt.title('因果图', fontsize=14, fontweight='bold')
plt.show()

# 方法2:自动识别混淆
model_auto = CausalModel(
    data=data,
    treatment='D',
    outcome='Y',
    common_causes=['Z1', 'Z2']
)

# =================================================================
# 第3步:识别因果效应
# =================================================================

identified_estimand = model.identify_effect(proceed_when_unidentifiable=True)
print("\n识别的因果效应:")
print(identified_estimand)

# =================================================================
# 第4步:估计因果效应(多种方法)
# =================================================================

# 4a. 后门调整(线性回归)
estimate_backdoor = model.estimate_effect(
    identified_estimand,
    method_name="backdoor.linear_regression"
)
print(f"\n后门调整(回归): ATE = {estimate_backdoor.value:.3f}")

# 4b. 倾向得分匹配
estimate_psm = model.estimate_effect(
    identified_estimand,
    method_name="backdoor.propensity_score_matching"
)
print(f"倾向得分匹配: ATE = {estimate_psm.value:.3f}")

# 4c. 倾向得分加权
estimate_ipw = model.estimate_effect(
    identified_estimand,
    method_name="backdoor.propensity_score_weighting"
)
print(f"倾向得分加权: ATE = {estimate_ipw.value:.3f}")

# 4d. 工具变量(如果有)
# estimate_iv = model.estimate_effect(
#     identified_estimand,
#     method_name="iv.instrumental_variable"
# )

# =================================================================
# 第5步:反驳/稳健性检验
# =================================================================

print("\n稳健性检验:")

# 5a. 随机共同原因
refute_random = model.refute_estimate(
    identified_estimand,
    estimate_backdoor,
    method_name="random_common_cause"
)
print(f"随机共同原因检验: p = {refute_random.refutation_result['p_value']:.3f}")

# 5b. 安慰剂处理
refute_placebo = model.refute_estimate(
    identified_estimand,
    estimate_backdoor,
    method_name="placebo_treatment_refuter"
)
print(f"安慰剂处理检验: 新估计 = {refute_placebo.estimated_effect:.3f}")

# 5c. 数据子集验证
refute_subset = model.refute_estimate(
    identified_estimand,
    estimate_backdoor,
    method_name="data_subset_refuter",
    subset_fraction=0.8
)
print(f"子集验证: 新估计 = {refute_subset.estimated_effect:.3f}")

# =================================================================
# 第6步:CATE估计(可选)
# =================================================================

from dowhy.causal_estimators.econml import Econml

# 使用EconML的DML估计CATE
estimate_dml = model.estimate_effect(
    identified_estimand,
    method_name="backdoor.econml.dml.DML",
    method_params={
        "init_params": {
            'model_y': GradientBoostingRegressor(),
            'model_t': GradientBoostingRegressor()
        },
        "fit_params": {}
    }
)
print(f"\nDML估计: ATE = {estimate_dml.value:.3f}")

# 异质性分析
if hasattr(estimate_dml, 'estimator'):
    cate = estimate_dml.estimator.effect(data[['Z1', 'Z2']].values)
    print(f"CATE范围: [{cate.min():.3f}, {cate.max():.3f}]")

# =================================================================
# 第7步:可视化
# =================================================================

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 子图1:不同方法的估计
methods = ['后门调整', 'PSM', 'IPW', 'DML']
estimates = [
    estimate_backdoor.value,
    estimate_psm.value,
    estimate_ipw.value,
    estimate_dml.value
]

ax1 = axes[0]
ax1.barh(methods, estimates, color='steelblue', alpha=0.7)
ax1.axvline(x=2.0, color='red', linestyle='--', linewidth=2, label='真实ATE')
ax1.set_xlabel('估计的ATE', fontsize=12)
ax1.set_title('不同方法比较', fontsize=14, fontweight='bold')
ax1.legend()
ax1.grid(axis='x', alpha=0.3)

# 子图2:CATE分布
if 'cate' in locals():
    ax2 = axes[1]
    ax2.hist(cate, bins=50, alpha=0.7, color='green', edgecolor='black')
    ax2.axvline(x=cate.mean(), color='red', linestyle='--', linewidth=2,
                label=f'平均CATE = {cate.mean():.3f}')
    ax2.set_xlabel('CATE', fontsize=12)
    ax2.set_ylabel('频数', fontsize=12)
    ax2.set_title('异质性处理效应分布', fontsize=14, fontweight='bold')
    ax2.legend()
    ax2.grid(axis='y', alpha=0.3)

plt.tight_layout()
plt.show()

print("\nDoWhy分析完成!")

DoWhy的优势

1. 统一框架: 融合Pearl(图)和Rubin(潜在结果)

2. 明确假设: 通过DAG清晰展示识别假设

3. 多种方法: 支持后门、前门、IV等

4. 自动化: 自动识别调整集

5. 稳健性: 内置多种反驳检验


DAG vs 潜在结果框架

两大流派对比

维度Pearl的DAGRubin的潜在结果
核心工具图、do-算子
识别图准则(后门/前门)假设(CIA, SUTVA)
优势直观、非参数统计推断严谨
劣势图可能错误假设难验证
适用复杂因果结构简单处理效应

统一

Imbens (2020): "The Two Approaches to Causal Inference: Structural and Reduced Form"

核心: 两者等价!

例子:

DAG: 满足后门准则相对于

Rubin:

都导出:


️ 常见陷阱

陷阱1:错误的DAG

问题: 如果DAG画错,所有推断都错!

缓解:

  • 领域知识
  • 敏感性分析
  • 比较多个DAG

陷阱2:控制对撞变量

经典错误: 无意中控制了对撞变量

例子: Berkson's Paradox

病因A → 住院 ← 病因B

在住院患者中,A和B负相关(虽然本无关)!

陷阱3:忽略时间顺序

问题: DAG必须反映时间因果顺序

错误:

收入 → 教育  (不合理!)

正确:

教育 → 收入

本节小结

核心要点

1. 因果阶梯:

  • Level 1: 关联
  • Level 2: 干预
  • Level 3: 反事实

2. DAG的三大工具:

  • d-分离: 识别独立性
  • 后门准则: 通过调整识别因果效应
  • 前门准则: 利用中介识别

3. do-算子: 形式化干预

4. 中介分析: 分解总效应 = 直接效应 + 间接效应

5. DoWhy: 统一Pearl和Rubin的实用工具

关键公式

do-算子:

后门调整:

前门调整:

中介分解:

Python工具

  • dowhy: 因果推断统一框架
  • causalgraphicalmodels: DAG操作
  • networkx: 图论算法
  • pgmpy: 概率图模型

下一节: 13.6 前沿专题与综合应用 - Staggered DID与综合案例


因果图:从图形到因果的认知桥梁!

基于 MIT 许可证发布。内容版权归作者所有。