机器学习与因果推断

第三讲:有向无环图(Directed Acyclic Graph, DAG)——用图形化方法思考因果

陈志远

中国人民大学商学院

2026-03-16

上节课回顾

核心概念

  • 概率与统计基础:随机变量、分布、条件期望
  • 回归的本质E[Y|X] 是条件期望,不只是拟合线
  • 预测 vs 因果:预测关注相关性,因果需要识别策略

本节课目标

  1. 理解有向无环图(DAG)的基本符号
  2. 掌握后门准则(Backdoor Criterion)
  3. 识别碰撞节点(Collider)及其偏误
  4. 学会用DAG指导研究设计

为什么需要DAG?

从混淆变量到图形化思考

之前的问题

  • 混淆变量(Confounder)导致选择偏误
  • 遗漏变量(Omitted Variable)使估计有偏
  • 如何判断需要那些控制变量(Control Variables)

关键问题

控制更多变量总是更好吗?

DAG的解决方案

  • 用图形明确表示变量关系
  • 系统化识别因果路径
  • 指导需要控制哪些变量

核心优势

DAG让因果假设可视化可讨论可检验

真实案例:教育回报研究

研究问题 上大学对收入的因果效应

传统方法:控制所有可观测变量?

  • 年龄、性别、种族
  • 父母教育、家庭收入
  • 高中成绩、能力测试
  • 职业选择、婚姻状况

潜在陷阱 控制“职业选择”或“婚姻状况”可能引入碰撞偏误

  • 这些变量可能是教育收入的共同结果
  • 控制它们会打开虚假路径

DAG基础符号

DAG的基本要素

有向无环图(Directed Acyclic Graph, DAG)

  • 节点(Nodes):表示随机变量
  • 有向边(Directed Edges):表示直接的因果效应
  • 无环(Acyclic):因果只沿时间向前,不能形成循环

简单DAG示例

  • D \longrightarrow Y: 教育直接影响收入
  • D \longrightarrow X \longrightarrow Y:教育通过中介变量职业技能(或能力)影响收入
  • D \longleftarrow X \longrightarrow Y:能力同时影响教育和收入, 混淆因果关系!

链式结构:中介变量

链式路径:D \rightarrow Z \rightarrow Y

  • D:处理变量(如教育)
  • Z:中介变量(如职业技能)
  • Y:结果变量(如收入)

控制Z阻断DY的真实因果效应

教育回报的例子

上大学(D) → 获得技能(Z) → 更高收入(Y)

  • 总效应 = 直接效应 + 间接效应(通过技能)
  • 如果控制“职业技能”(Z),会低估教育总回报
  • 若想估计直接效应,才需要控制中介变量

分叉结构:混淆变量

分叉路径:D \leftarrow Z \rightarrow Y

  • ZDY共同原因
  • 造成DY的统计相关,但非因果
  • 必须控制Z才能识别因果效应

教育回报中的混淆因素

能力(Z) → 上大学(D)

能力(Z) → 高收入(Y)

  • 能力强的人更可能上大学收入更高
  • 如果不控制能力,会高估教育效应
  • 这就是遗漏变量偏误的DAG表示

后门准则

后门路径(Backdoor Path)

后门路径(Backdoor Path)

  • 因果效应D\longrightarrow Y
  • 后门路径D \longleftarrow X \longrightarrow Y
  • X是混淆变量,造成DY虚假相关性(Spurious Correlation)

定义后门路径

后门路径(Backdoor Path)

从处理变量D到结果变量Y非因果路径,满足:

  1. 以指向D的箭头开始(“进入后门”):D \leftarrow \dots
  2. 路径最终到达Y
  3. 包含任何方向的边(\rightarrow\leftarrow

Z满足后门准则,则: P(Y|do(D)) = \sum_z P(Y|D,Z)P(Z)

后门路径的类型举例

不可观测的混淆变量I

  • U不可观测
  • 后门路径U \rightarrow D \rightarrow Y
  • 称后门路径是开放的 (Open Backdoor Path)

不可观测的混淆变量II

  • Becker 人力资本模型(Becker, 1964): 教育投资(D)受家庭收入I)、父母教育水平(E)和其他不可观测因素B,e.g.,能力,基因,家庭氛围等)影响
  • 教育水平与家庭收入共同作用于收入(Y)。
  • B 是混淆变量
  • 思考: B会直接影响Y吗?

碰撞结构

  • 后门路径:D \leftarrow X \rightarrow Y
  • X碰撞节点: 当有两个变量共同影响一个结果变量时,称该变量是碰撞节点(Collider)
  • 当控制碰撞节点X时,DY会变得相关,即使它们原本没有任何关系!

练习 在人力资本模型中,针对教育回报的DAG,所有的后门路径有哪些?

提示:后门路径以指向D的箭头开始

答案

序号 路径 类型
1 D \rightarrow Y ✓ 因果路径(我们要估计的)
2 D \leftarrow I \rightarrow Y ✗ 后门路径(需阻断)
3 D \leftarrow PE \rightarrow I \rightarrow Y ✗ 后门路径(需阻断)
4 D \leftarrow B \rightarrow PE \rightarrow I \rightarrow Y ✗ 后门路径(需阻断)

发现:所有后门路径都经过I → 控制I即可满足后门准则!

后门准则的直观理解

为什么叫”后门”?

想象你要从D走到Y

  • 前门是因果路径:D \rightarrow Y(你想走的正路)
  • 后门是混淆路径:D \leftarrow \dots \rightarrow Y(偷偷绕进来的偏误)

关键洞察

后门路径造成DY虚假相关(spurious correlation)

  • 我们看到DY相关
  • 但这可能是通过后门路径的相关,而非真正的因果
  • 控制适当的变量可以“关闭”后门,只留下因果路径

回归视角:如何实现后门准则

人力资本模型的回归实现

Becker模型中,识别教育(D)对收入(Y)的因果效应:

朴素回归(有偏误) Y_i = \alpha + \delta D_i + \varepsilon_i \widehat{\delta} 包含教育效应 + 所有后门路径的偏误

重要

满足后门准则的回归 Y_i = \alpha + \delta D_i + \beta_1 I_i + \varepsilon_i

通过控制家庭收入I,我们阻断了所有以I为节点的后门路径,\widehat{\delta} 成为因果效应的无偏估计。

回归如何“阻断”后门路径?

直观理解:比较 vs 对比

不控制I时:

  • 比较”上大学者”和”没上大学者”的收入
  • 两组在”家庭收入”上分布不同
  • 观察到的差异 = 教育效应 + 家庭背景差异

控制I时:

  • 在相同家庭收入I水平内比较
  • 剥离IDY的共同影响
  • 只剩教育→收入的因果路径

数学机制

回归Y_i = \alpha + \delta D_i + \beta I_i + \varepsilon_i 中:

  1. \beta I_i 吸收IY的直接影响
  2. 残差\varepsilon_iI无关
  3. D的变异分为两部分:
    • I相关的部分(后门路径)→ 被控制
    • I无关的部分(因果路径)→ 估计\delta

结果 \widehat{\delta} 反映I无法解释的那部分DY的影响

人力资本模型:完整DAG分析

变量说明D=大学教育, Y=收入, PE=父母教育, I=家庭收入, B=不可观测背景因素

人力资本模型:识别所有路径

DY的四条路径

路径 类型 状态
D \rightarrow Y 因果路径 ✓ 我们想要估计的
D \leftarrow I \rightarrow Y 后门路径 ✗ 需要通过控制关闭
D \leftarrow PE \rightarrow I \rightarrow Y 后门路径 ✗ 需要通过控制关闭
D \leftarrow B \rightarrow PE \rightarrow I \rightarrow Y 后门路径 ✗ 需要通过控制关闭

关键发现

所有后门路径都经过家庭收入I

因此,仅控制I就足以满足后门准则

这是最小充分调整策略

回归实现:逐步展示

步骤1:朴素回归(有偏)

Y_i = \alpha + \delta D_i + \varepsilon_i

估计结果:

  • \widehat{\delta}_{\text{ols}} = 教育相关效应
  • 包含真实效应 + 后门路径偏误
  • 可能高估或低估真实效应

步骤2:控制家庭收入(满足后门准则)

Y_i = \alpha + \delta D_i + \beta I_i + \varepsilon_i

估计结果:

  • \widehat{\delta}_{\text{causal}} = 因果效应
  • 后门路径被阻断
  • 无偏估计(假设无其他开放后门)

如何“阻断”后门路径?

控制I相当于:

  • I的每个水平内比较DY的影响
  • 剥离I造成的DY的相关性
  • 只剩下D \rightarrow Y的因果路径

回归的魔法 通过将I包含在模型中,我们实现了对I的条件化,从而阻断了所有经过I的后门路径。

为什么控制I就够了?

后门路径的阻断原理

后门路径必须通过某个共同原因连接DY

  • 路径1:D \leftarrow I \rightarrow Y(经过I
  • 路径2:D \leftarrow PE \rightarrow I \rightarrow Y(经过I
  • 路径3:D \leftarrow B \rightarrow PE \rightarrow I \rightarrow Y(经过I

控制I的效果

  • 阻断所有经过I的路径
  • 不满足后门准则的路径被关闭
  • 只剩下因果路径开放

控制I后,所有后门路径被阻断

反例:当控制策略失效时

修改后的DAG:B直接影响Y

如果在DAG中加入 B \rightarrow Y

  • 新后门路径:D \leftarrow B \rightarrow Y
  • 这条路径不经过I
  • 仅控制I 无法阻断这条路径
  • 估计仍然有偏!

回归中的体现

Y_i = \alpha + \delta D_i + \beta I_i + \varepsilon_i

即使控制了I,如果B直接影响Y

  • \varepsilon_i 包含B的影响
  • B同时影响DY
  • D\varepsilon_i相关
  • 内生性偏误仍然存在

人力资本模型:控制策略总结

控制变量 阻断的后门路径 是否满足后门准则 备注
存在遗漏变量偏误
I(家庭收入) 所有经过I的路径 最小充分集
I + PE 所有经过IPE的路径 充分但非最小
PE D \leftarrow PE \rightarrow I \rightarrow Y 仍有D \leftarrow I \rightarrow Y开放
B(若可观测) 所有路径 理想但通常不可行

实践启示

  • 找到最小充分调整集是关键
  • 控制不必要的变量会降低统计功效
  • 遗漏关键混淆变量会导致偏误
  • DAG帮助我们系统化地识别关键变量

应用:教育回报的DAG

后门路径识别

从“教育”到“收入”:

  • 后门路径1:教育 ← 能力 → 收入
  • 后门路径2:教育 ← 家庭背景 → 收入
  • 必须控制: 能力、家庭背景

不能控制的变量

  • 职业选择(碰撞节点)
  • 婚姻状况(可能是结果)
  • 工作经验(部分中介)

碰撞节点与碰撞偏误

什么是碰撞节点?

碰撞结构:D \rightarrow X \leftarrow Y

  • X是碰撞节点(Collider)
  • DY原本独立(无因果路径)
  • 但一旦控制XDY会虚假相关!

悖论:控制碰撞节点会创造虚假相关

名人效应:解释碰撞偏误

电影明星案例

成为电影明星(X)需要:

  • 颜值(D)
  • 演技(Y)

如果已有颜值,演技可以差一些

如果已有演技,颜值可以差一些

注记

在明星群体中:颜值与演技负相关, 但这只是选择效应!

颜值与演技的“权衡”

核心洞察:在明星样本中,颜值和演技看似负相关,

但这只是碰撞偏误——两者都是成为明星的原因!

更多碰撞偏误的例子

案例1: 研究幸福感对收入的影响

  • 控制”婚姻状态”会引入偏误
  • 因为幸福感和收入都影响婚姻

案例2: 研究教育对健康的效应

  • 控制”就业状态”需谨慎
  • 可能是教育和健康的共同结果

真实应用:歧视与碰撞偏误

性别工资差距的争论

常见观点

“控制职业后,性别工资差距消失或减小”

  • 这是否证明不存在歧视?
  • 还是职业本身就是歧视的结果?

DAG视角

职业可能是碰撞节点:

  • 性别 → 职业选择
  • 能力/偏好 → 职业选择
  • 歧视 → 职业选择

控制职业可能掩盖歧视的真实效应!

样本选择作为碰撞偏误

样本选择偏误

当样本本身是基于碰撞节点选择时,即使不”控制”任何变量,偏误也已存在。

例子 只在明星样本中研究颜值与演技的关系

研究建议

使用DAG指导样本选择:

  1. 明确目标总体
  2. 识别选择机制
  3. 评估选择是否基于碰撞节点
  4. 必要时使用选择模型纠正

两种因果框架的比较

潜在结果框架 vs DAG框架

潜在结果框架

Rubin因果模型

核心概念

  • 每个个体的Y(0)Y(1)
  • 因果效应:\tau_i = Y_i(1) - Y_i(0)
  • 识别假设:无混淆、正向性、SUTVA

优势

  • 精确的数学表达
  • 清晰的识别条件
  • 估计方法丰富

DAG框架

Pearl因果图

核心概念

  • 节点和有向边
  • 路径:链式、分叉、碰撞
  • 后门准则、do-演算

优势

  • 可视化因果关系
  • 系统化识别偏误来源
  • 指导变量选择

两种框架的互补性

维度 潜在结果框架 DAG框架
表达形式 数学公式 图形结构
核心问题 平均处理效应是多少? 哪些路径造成偏误?
识别条件 条件独立假设(CIA) 后门准则、d-分离
变量选择 基于理论判断 基于路径分析
最佳应用 估计和推断 研究设计和假设

重要

本课程的观点:两种框架互补

  • 设计阶段:用DAG理清因果结构
  • 分析阶段:用潜在结果框架进行估计

同一问题的两种视角

混淆偏误

潜在结果框架E[Y(0)|D=1] \neq E[Y(0)|D=0]

处理组和对照组的反事实不同

DAG框架

存在从DY的开放后门路径

提示

需要通过控制变量阻断

碰撞偏误

潜在结果框架

条件化导致样本选择,破坏可比性

DAG框架

控制碰撞节点打开虚假路径

提示

即使该变量”看起来”相关,也不能控制

如何用DAG指导研究设计

DAG分析的五步法

步骤1:绘制DAG

列出所有相关变量及其因果关系:

  • 基于理论和先验知识
  • 包含可观测和不可观测变量
  • 讨论完善

步骤2:识别目标路径

从处理变量到结果变量的直接因果路径:

  • 想估计的效应
  • 不要阻断这条路径!

步骤3:识别后门路径

找出所有非因果路径:

  • 以后门箭头开始
  • 标记开放的后门路径

DAG分析的五步法(续)

步骤4:确定控制策略

找出满足后门准则的变量集合:

  • 阻断所有后门路径
  • 不包含碰撞节点
  • 不包含中介变量(除非估计直接效应)

步骤5:敏感性分析

考虑不可观测混淆因:

  • 如果有未观测的混淆变量怎么办?
  • 效应估计对遗漏变量有多敏感?

案例:评估培训项目

研究设计

某公司想评估新员工培训的效果:

变量列表:

  • 处理变量D:参加培训
  • 结果变量Y:工作绩效
  • 可能的混杂变量C:能力、动机、经验
  • 可能的中介变量M:技能掌握
  • 可能的碰撞变量L:留任状态

DAG指导

应该控制:

  • ✓ 入职前能力测试
  • ✓ 工作经验
  • ✓ 教育背景

不应该控制:

  • ✗ 技能掌握(中介)
  • ✗ 留任状态(碰撞)
  • ✗ 部门分配(可能是结果)

实践:模拟数据分析

import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch
import warnings
warnings.filterwarnings('ignore')

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['Source Han Sans SC', 'Noto Sans CJK SC',
                                    'WenQuanYi Micro Hei', 'SimHei', 'Arial Unicode MS']
plt.rcParams['axes.unicode_minus'] = False

np.random.seed(42)
n = 1000
true_effect = 2.0

# 生成变量:C=能力, D=培训, M=技能, Y=绩效, L=留任
C = np.random.normal(5, 1.5, n)
D = (np.random.uniform(0, 1, n) < 1/(1+np.exp(-(C-5)/2))).astype(int)
M = 0.7*D + 0.3*C + np.random.normal(0, 0.5, n)
Y = true_effect*D + 1.5*C + 0.5*M + np.random.normal(0, 1, n)
L = (np.random.uniform(0, 1, n) < 1/(1+np.exp(-(0.3*Y+0.5*D-3)))).astype(int)

data = pd.DataFrame({'training': D, 'performance': Y, 'ability': C,
                     'skill': M, 'retention': L})

print(f"样本量: {n} | 真实效应: {true_effect}")
print(data.head())
样本量: 1000 | 真实效应: 2.0
   training  performance   ability     skill  retention
0         1    10.982069  5.745071  2.268917          1
1         1     8.773640  4.792604  1.761703          0
2         0     9.476702  5.971533  1.951047          1
3         1    14.886038  7.284545  3.555589          1
4         1    10.146707  4.648770  1.157045          0

绘制 DAG 结构

Python 绘图代码

# 绘制 DAG 结构
fig, ax = plt.subplots(figsize=(6, 6))
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.axis('off')

# 节点位置
positions = {
    'C': (2, 8),    # 能力
    'D': (2, 5),    # 培训
    'M': (5, 5),    # 技能
    'Y': (8, 5),    # 绩效
    'L': (5, 2)     # 留任
}

# 节点颜色配置
colors = {
    'C': '#FF6B6B',  # 红色 - 混淆变量
    'D': '#4ECDC4',  # 青色 - 处理变量
    'M': '#95E1D3',  # 浅绿 - 中介变量
    'Y': '#F38181',  # 粉色 - 结果变量
    'L': '#FFA07A'   # 橙色 - 碰撞节点
}

labels = {
    'C': '能力(C)\n混杂变量',
    'D': '培训(D)\n处理变量',
    'M': '技能(M)\n中介变量',
    'Y': '绩效(Y)\n结果变量',
    'L': '留任(L)\n碰撞节点'
}

# 绘制节点
for node, (x, y) in positions.items():
    circle = plt.Circle((x, y), 0.6, color=colors[node],
                        ec='black', linewidth=2, zorder=3)
    ax.add_patch(circle)
    ax.text(x, y, labels[node], ha='center', va='center',
            fontsize=9, fontweight='bold', zorder=4)

# 绘制箭头
arrows = [('C','D'), ('C','Y'), ('D','M'), ('M','Y'),
          ('D','Y'), ('D','L'), ('Y','L')]

for start, end in arrows:
    x1, y1 = positions[start]
    x2, y2 = positions[end]
    dx, dy = x2 - x1, y2 - y1
    dist = np.sqrt(dx**2 + dy**2)
    ax.annotate('', xy=(x2-0.6*dx/dist, y2-0.6*dy/dist),
                xytext=(x1+0.6*dx/dist, y1+0.6*dy/dist),
                arrowprops=dict(arrowstyle='->', lw=2))

ax.set_title('DAG Structure', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()

生成的 DAG 图

关键洞察:

  • 能力(C)是混淆变量:影响培训和绩效
  • 技能(M)是中介变量:培训→技能→绩效
  • 留任(L)是碰撞节点:受绩效和培训共同影响

使用 CausalGraph 包绘制 DAG

import networkx as nx
import matplotlib.pyplot as plt

# 创建有向图
G = nx.DiGraph()
nodes = [
    ('C', {'label': '能力(C)', 'type': 'confounder', 'color': '#FF6B6B'}),
    ('D', {'label': '培训(D)', 'type': 'treatment', 'color': '#4ECDC4'}),
    ('M', {'label': '技能(M)', 'type': 'mediator', 'color': '#95E1D3'}),
    ('Y', {'label': '绩效(Y)', 'type': 'outcome', 'color': '#F38181'}),
    ('L', {'label': '留任(L)', 'type': 'collider', 'color': '#FFA07A'})
]
G.add_nodes_from([(n, attr) for n, attr in nodes])

edges = [('C','D'), ('C','Y'), ('D','M'), ('M','Y'), ('D','Y'), ('D','L'), ('Y','L')]
for u, v in edges:
    G.add_edge(u, v)

fig, ax = plt.subplots(figsize=(7, 4.5))
pos = {'C': (0, 2), 'D': (-1.5, 1), 'M': (0, 1.2), 'Y': (1.5, 1), 'L': (0, 0)}
node_colors = [G.nodes[n]['color'] for n in G.nodes()]

nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=2000,
                       edgecolors='black', linewidths=2, ax=ax)
nx.draw_networkx_edges(G, pos, edge_color='#333333', width=2.5, arrows=True,
                       arrowstyle='-|>', arrowsize=28, ax=ax,
                       connectionstyle='arc3,rad=0.10')
labels = {n: G.nodes[n]['label'] for n in G.nodes()}
nx.draw_networkx_labels(G, pos, labels, font_size=9, font_weight='bold', ax=ax)

ax.set_title('NetworkX/CausalGraph 绘制 DAG', fontsize=12, fontweight='bold')
ax.axis('off')
ax.set_xlim(-2.5, 2.5)
ax.set_ylim(-0.5, 2.5)

# 图例(右下角)
legend_text = '节点类型:红色=混淆(C) 青色=处理(D) 浅绿=中介(M) 粉色=结果(Y) 橙色=碰撞(L)'
ax.text(2.3, -0.3, legend_text, ha='right', va='bottom', fontsize=7,
        bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.7))

plt.tight_layout()
plt.show()

print(f"DAG统计: {G.number_of_nodes()}节点, {G.number_of_edges()}边, 2条后门路径")
print("CausalGraph特点: 基于networkx.MultiDiGraph | 支持时序因果 | 兼容ggdag")

DAG统计: 5节点, 7边, 2条后门路径
CausalGraph特点: 基于networkx.MultiDiGraph | 支持时序因果 | 兼容ggdag

回归分析:不同控制策略比较

# 5种控制策略比较
models = {
    '模型1(不控制)': sm.OLS(data['performance'], sm.add_constant(data['training'])).fit(),
    '模型2(控制C)✓': sm.OLS(data['performance'], sm.add_constant(data[['training','ability']])).fit(),
    '模型3(控制M)✗': sm.OLS(data['performance'], sm.add_constant(data[['training','skill']])).fit(),
    '模型4(控制C+M)': sm.OLS(data['performance'], sm.add_constant(data[['training','ability','skill']])).fit(),
    '模型5(控制L)✗': sm.OLS(data['performance'], sm.add_constant(data[['training','retention']])).fit()
}

print(f"真实效应: {true_effect:.3f}")
print("=" * 62)

results = []
for name, model in models.items():
    coef = model.params['training']
    results.append({
        '模型': name,
        '估计值': f"{coef:.3f}",
        '偏差': f"{coef-true_effect:+.3f}",
        'R²': f"{model.rsquared:.3f}"
    })

results_df = pd.DataFrame(results)
print(results_df.to_string(index=False))
真实效应: 2.000
==============================================================
        模型   估计值     偏差    R²
  模型1(不控制) 3.924 +1.924 0.371
 模型2(控制C)✓ 2.330 +0.330 0.889
 模型3(控制M)✗ 1.295 -0.705 0.675
模型4(控制C+M) 1.953 -0.047 0.896
 模型5(控制L)✗ 3.299 +1.299 0.419

结果解释

模型评估

模型 评估 问题
模型1(不控制) ✗ 有偏 包含能力(C)混淆效应
模型2(控制C) 正确 阻断后门路径
模型3(控制M) ✗ 低估 阻断间接效应
模型4(控制C+M) △部分正确 估计直接效应
模型5(控制L) ✗✗ 严重错误 碰撞偏误

DAG 指导原则

  • 控制混淆变量:阻断后门路径,获得无偏估计
  • 不要控制中介变量:除非只想估计直接效应
  • ✗✗ 绝对不能控制碰撞变量:会引入新的偏误

提示

DAG 是选择控制变量的科学依据!

总结

本讲要点

  1. DAG基础
    • 节点表示变量,有向边表示因果
    • 三种路径结构:链式、分叉、碰撞
  2. 后门准则
    • 识别需要阻断的后门路径
    • 指导控制变量选择
  3. 碰撞偏误
    • 控制碰撞节点创造虚假相关
    • 样本选择可能引入偏误
  4. 框架比较
    • DAG用于研究设计
    • 潜在结果用于估计

实践建议

使用DAG的检查清单

感谢聆听!

欢迎提问与讨论!

chenzhiyuan@rmbs.ruc.edu.cn