因果发现算法:让AI自动帮你画因果图,靠谱吗?
引言:从数据中自动学习因果结构的梦想
"""
场景:你有一个包含100个变量的大数据集
问题:这些变量之间的因果关系到底是什么?
传统做法:
咨询领域专家(费时、主观、容易偏见)
进行大量实验(费钱、耗时)
写论文时猜测(学术不诚实)
新想法:
能否让计算机算法自动从数据中"发现"因果关系?
这就是因果发现算法的核心承诺。
"""
print("""
本篇博文的核心问题:
1️⃣ 因果发现算法原理是什么?它是如何工作的?
2️⃣ 主要的因果发现方法有哪些?各有什么优缺点?
3️⃣ 因果发现靠谱吗?它的局限性在哪里?
4️⃣ 怎样判断一个因果发现结果是否可信?
5️⃣ 在实际研究中如何应用因果发现?
带着这些问题,让我们踏上因果发现的探索之旅。
""")
第一部分:因果发现的基本概念
1.1 什么是因果发现(Causal Discovery)?
定义
causal_discovery_definition = """
因果发现(Causal Discovery):
从数据中自动学习变量之间因果关系的算法和方法。
目标:从观测数据(或有限的实验数据)推断:
✓ 因果图的结构(哪个变量对哪个变量有影响)
✓ 因果关系的方向(X → Y 还是 Y → X)
✓ 因果关系的强弱
✓ 因果效应的大小
与传统统计学推断的区别:
传统:假定因果图已知,估计参数
因果发现:图本身也是未知的,需要学习
"""
print(causal_discovery_definition)
为什么困难?
def explain_causal_discovery_challenge():
"""
为什么因果发现这么难?
"""
challenge = """
【挑战1:因果图不可观测】
我们能观测到数据,但看不到"真实的"因果机制。
例子:
数据显示 X 和 Y 相关,但为什么?
可能的原因:
a) X 导致 Y (X → Y)
b) Y 导致 X (Y → X)
c) 两者都由 Z 导致 (Z → X, Z → Y) [混淆]
d) 这是巧合! (无因果)
仅从相关性,无法区分这些情况!
【挑战2:需要解决的问题太多】
设有 n 个变量时:
- 可能的DAG数量:指数级增长(factorial in n)
- 例:5个变量 → ~4000种可能的因果图
- 例:10个变量 → ~天文数字
- 例:100个变量 → ???(计算机无法遍历)
因此不能简单地尝试所有可能!
【挑战3:识别(Identifiability)问题】
即使完美的数据和无穷的样本,某些因果图也可能是
"不可区分的"(Markov Equivalent)。
例子:以下三个因果图生成完全相同的数据分布!
图1:X → Y (X导致Y)
图2:Y → X (Y导致X)
图3:X ← Z → Y (Z同时导致X和Y)
没有额外信息,无法区分它们。
【挑战4:假设的有效性】
所有因果发现算法都基于某些假设:
- 无环(DAG假设)
- 无遗漏变量(无隐藏混淆)
- 因果充分性(Causal Sufficiency)
- 无反馈循环
现实中这些假设常常被违反!
【挑战5:样本量和噪声】
因果发现对样本量、数据质量非常敏感。
- 小样本 → 容易发现虚假的相关性
- 噪声 → 掩盖真实的因果关系
- 非线性 → 更难检测
"""
print(challenge)
explain_causal_discovery_challenge()
三种主要问题设定
def causal_discovery_problem_settings():
"""
因果发现的三种不同问题设定
"""
settings = pd.DataFrame({
'问题设定': [
'完全因果发现',
'因果方向学习',
'因果骨架学习'
],
'已知条件': [
'无(只有观测数据)',
'已知相关对,需确定方向',
'已知哪些变量相关,需学习结构'
],
'学习目标': [
'整个因果图DAG',
'DAG的方向',
'DAG的连接(忽略方向)'
],
'难度': [
'高',
'中',
'中'
],
'典型方法': [
'FCI, PC, GES',
'因果方向规则',
'PC skeleton, MMHC'
],
'典型应用': [
'基因调控网络',
'已知相关变量的方向',
'基因表达数据'
]
})
print(settings.to_string(index=False))
causal_discovery_problem_settings()
1.2 因果模型的数学表示
有向无环图(DAG)
def explain_dag_representation():
"""
用DAG表示因果关系
"""
print("""
【DAG的基本要素】
顶点(Vertices):变量
- X, Y, Z, ... 表示不同的变量
有向边(Directed Edges):因果关系
- X → Y 表示 X 因果影响 Y
- 箭头方向很重要!
无环性(Acyclic):
- 不存在循环:X → Y → Z → X
- 体现因果的非对称性
- 但允许非因果的反馈(如系统的稳定性)
【DAG的例子】
例1:简单的链
X → Y → Z
因果解释:
- X影响Y(直接)
- X影响Z(通过Y,间接)
- Y影响Z(直接)
例2:混淆
Z
↙ ↘
X Y
因果解释:
- Z直接影响X和Y
- X和Y无因果关系,但相关(因为有共同原因Z)
例3:前门(Collider)
X → Z ← Y
因果解释:
- X和Y都导致Z,但X和Y间无直接因果
- 特别:条件化Z会*激活* X-Y间的关联!
例4:复杂网络
W → X → Y
W → Z → Y
Z ← X
因果解释:
- W是共同原因(混淆)
- X有多条路径影响Y
- Z和X有反馈关系(错!DAG不允许循环)
【数学表示】
邻接矩阵 A:
- A[i,j] = 1 表示 X_i → X_j
- A[i,j] = 0 表示无边
例:Y = f(X, Z) + ε_Y
表示 X → Y, Z → Y
结构因果模型(SCM):
X_i := f_i(PA_i, ε_i)
其中:PA_i 是 X_i 的父节点(直接原因)
ε_i 是外生扰动(噪声)
f_i 是因果函数
""")
explain_dag_representation()
Markov等价类
def explain_markov_equivalence():
"""
Markov等价类:不可区分的因果图
"""
print("""
【关键概念:某些因果图不可区分】
定义:两个因果图是Markov等价的,当且仅当
它们代表完全相同的条件独立性结构。
后果:基于条件独立性的因果发现方法
无法区分Markov等价的图!
【典型例子】
以下三个因果图是Markov等价的(生成相同数据分布):
图1: X → Y 图2: Y → X 图3: X ← Z → Y
这被称为"马尔可夫等价类"。
【涵义图(PDAG)】
为了表示一个等价类中的所有图,使用
部分有向无环图(Partially Directed Acyclic Graph)。
用法:
- 有向边 (→):在等价类中所有图中方向都相同
- 无向边 (-):在等价类中方向不同的图中方向相反
例:上面三个图的PDAG表示为:
X - Y
这表示X和Y相关,但方向不确定。
【为什么重要?】
1. 因果发现不能识别所有关系的方向
→ 必须明确报告不确定性
2. 需要额外信息(时间序列、实验等)
来区分等价的图
3. 评估因果发现算法时,应该
比较涵义图,而不是要求完全正确
""")
explain_markov_equivalence()
第二部分:主要的因果发现算法
2.1 约束型方法(Constraint-based Methods)
核心思想
def explain_constraint_based_methods():
"""
约束型方法的核心逻辑
"""
print("""
【基本思想】
从数据中学习条件独立性(Conditional Independencies)
然后根据这些条件独立性推断因果图。
逻辑链:
数据 → 条件独立性 → 因果图
【为什么工作?】
因果Markov条件(Causal Markov Condition):
在因果图中,每个变量独立于它的非后代变量,
条件化在它的父节点。
例子:
图:Z → X → Y
则:Z ⊥ Y | X
即:给定X,Z和Y独立
反向推理:
如果我们从数据中发现 Z ⊥ Y | X,
这提示因果图可能是 Z → X → Y。
【优点】
✅ 理论基础扎实(基于Markov条件)
✅ 对某些结构有强大的识别能力
✅ 可以识别不可区分的图(等价类)
✅ 相对稳健(不依赖因果函数的形式)
【缺点】
❌ 对样本量要求高(需要准确估计条件独立性)
❌ 条件独立性检验在高维时困难
❌ 对假设违反敏感(无遗漏变量)
❌ 可能发现多个等价的图
""")
explain_constraint_based_methods()
PC算法
def explain_pc_algorithm():
"""
PC算法(Peter-Clark Algorithm)
最经典的约束型因果发现方法
"""
print("""
【PC算法的工作流程】
第一步:骨架学习(Skeleton Recovery)
─────────────────────────────────
初始:完全连接图(所有变量间都有边)
第0层:检验边 (X, Y) 是否应该删除
条件:检测 X ⊥ Y (无条件独立)
如果独立,删除边
第1层:检验边 (X, Y) 是否应该删除
条件:检测 X ⊥ Y | Z (Z可以是任意一个变量)
如果独立,删除边
第2层:检验边 (X, Y) 是否应该删除
条件:检测 X ⊥ Y | Z1, Z2
如果独立,删除边
继续...直到没有边可以删除。
结果:因果骨架(忽略方向)
第二步:方向规则应用(Rule Determination)
────────────────────────────────────────
规则1:v-结构(Collider)识别
如果 X - Z - Y 且 X 和 Y 之间无边,
则调整为 X → Z ← Y (这是collider)
规则2-4:传播方向
根据:
- 无环约束(不产生循环)
- 不产生新的v-结构
递归地定向更多的边。
【Python伪代码】
```python
def PC_algorithm(data, alpha=0.05):
# 初始化
g = CompleteGraph(data.columns)
# 第一步:骨架学习
for order in range(data.shape[1]):
for (X, Y) in g.edges():
# 找X的所有邻居(除Y外)
neighbors_X = g.neighbors(X) - {Y}
# 遍历所有大小为order的邻居子集
for S in subsets(neighbors_X, order):
# 条件独立性测试
if ci_test(X, Y, S, data) < alpha:
# 记录S为X-Y的独立集
g.record_sep_set(X, Y, S)
# 删除边
g.remove_edge(X, Y)
break
# 第二步:方向规则
rules = [rule1, rule2, rule3, rule4]
changed = True
while changed:
changed = False
for rule in rules:
if rule(g): # 规则适用
changed = True
apply_rule(g, rule)
return g
```
【例子演示】
假设数据中:
- X 和 Y 无条件独立
- Z 和 Y 给定X条件独立
- X 和 Z 无条件独立(但看上去相关)
步骤1:初始图
X - Y - Z
X - Z
步骤2:条件独立性测试
- 测试 X ⊥ Y ?否 → 保留边
- 测试 Y ⊥ Z ?否 → 保留边
- 测试 X ⊥ Z ?是 → 删除边
图变为:X - Y - Z
步骤3:v-结构检查
没有colliders
步骤4:方向规则
无法定向(信息不足)
最终:X - Y - Z (无向图)
【关键参数】
alpha:显著性水平(p-value阈值)
- alpha越小:越保守,删除的边越少
- alpha越大:越激进,删除的边越多
条件独立性检验方法:
- 高斯图模型:偏相关检验(Partial Correlation Test)
- 连续变量:Fisher-Z检验
- 离散变量:卡方检验
- 混合变量:其他方法
""")
explain_pc_algorithm()
PC算法的Python实现
import numpy as np
from scipy.stats import chi2
from itertools import combinations
class PCAlgorithm:
"""
Peter-Clark因果发现算法的实现
"""
def __init__(self, data, alpha=0.05, ci_test='fisherz'):
"""
初始化PC算法
参数:
- data: DataFrame,n_samples × n_variables
- alpha: 条件独立性检验的显著性水平
- ci_test: 条件独立性检验方法
"""
self.data = data
self.alpha = alpha
self.ci_test_method = ci_test
self.n_vars = data.shape[1]
self.var_names = list(data.columns)
# 初始化邻接矩阵(完全连接)
self.adj_matrix = np.ones((self.n_vars, self.n_vars)) - np.eye(self.n_vars)
self.sep_set = {} # 分离集(记录条件独立的条件)
def partial_correlation_test(self, i, j, S):
"""
Fisher-Z偏相关检验
H0:X_i ⊥ X_j | S
"""
# 计算相关矩阵
corr_matrix = self.data.corr().values
if len(S) == 0:
# 无条件独立性检验
r = corr_matrix[i, j]
else:
# 条件独立性检验:偏相关系数
r = self.partial_correlation(corr_matrix, i, j, S)
# Fisher-Z变换
n = len(self.data)
z = np.sqrt(n - len(S) - 3) * 0.5 * np.log((1 + r) / (1 - r + 1e-10))
# 计算p-value
p_value = 2 * (1 - chi2.cdf(z**2, df=1))
return p_value
def partial_correlation(self, corr_matrix, i, j, S):
"""
计算偏相关系数 r(X_i, X_j | X_S)
"""
# 创建索引
idx = [i, j] + list(S)
# 提取子相关矩阵
sub_corr = corr_matrix[np.ix_(idx, idx)]
try:
# 计算偏相关系数(使用精度矩阵)
precision = np.linalg.inv(sub_corr)
partial_corr = -precision[0, 1] / np.sqrt(precision[0, 0] * precision[1, 1])
return partial_corr
except:
return 0
def ci_test(self, i, j, S):
"""
条件独立性检验
"""
if self.ci_test_method == 'fisherz':
return self.partial_correlation_test(i, j, S)
else:
raise ValueError(f"Unknown CI test method: {self.ci_test_method}")
def learn_skeleton(self):
"""
第一步:学习因果骨架
删除所有不相关的变量对的边
"""
order = 0
while True:
changed = False
edges_to_remove = []
# 对每条边进行测试
for i in range(self.n_vars):
for j in range(i + 1, self.n_vars):
# 检查边是否存在
if self.adj_matrix[i, j] == 0:
continue
# 找i的邻居(除j)
neighbors_i = [k for k in range(self.n_vars)
if self.adj_matrix[i, k] == 1 and k != j]
if len(neighbors_i) < order:
continue
# 遍历所有大小为order的邻居子集
for S in combinations(neighbors_i, order):
S_set = set(S)
# 条件独立性检验
p_value = self.ci_test(i, j, S_set)
if p_value > self.alpha:
# 条件独立,删除边
edges_to_remove.append((i, j))
self.sep_set[(i, j)] = S_set
self.sep_set[(j, i)] = S_set
changed = True
break
# 删除边
for i, j in edges_to_remove:
self.adj_matrix[i, j] = 0
self.adj_matrix[j, i] = 0
if not changed:
break
order += 1
print(f"骨架学习完成,删除了{len(edges_to_remove)}条边")
return self.adj_matrix
def orient_edges(self):
"""
第二步:方向规则定向边
"""
# 规则1:识别colliders (X → Z ← Y)
for z in range(self.n_vars):
neighbors_z = [k for k in range(self.n_vars)
if self.adj_matrix[z, k] == 1]
for i in range(len(neighbors_z)):
for j in range(i + 1, len(neighbors_z)):
x, y = neighbors_z[i], neighbors_z[j]
# 检查X和Y是否无边
if self.adj_matrix[x, y] == 0:
# 检查Z是否在X-Y的分离集中
if (x, y) in self.sep_set and z not in self.sep_set[(x, y)]:
# X → Z ← Y
self.adj_matrix[x, z] = 1
self.adj_matrix[z, x] = -1 # 标记方向
self.adj_matrix[y, z] = 1
self.adj_matrix[z, y] = -1
print(f"发现collider: {self.var_names[x]} → "
f"{self.var_names[z]} ← {self.var_names[y]}")
# 规则2-4:传播方向
# (这里省略,实现会相当长)
return self.adj_matrix
def fit(self):
"""
运行完整的PC算法
"""
print("开始PC算法...")
print("-" * 60)
self.learn_skeleton()
print("-" * 60)
self.orient_edges()
print("-" * 60)
return self
def get_causal_graph(self):
"""
获取因果图(DAG)
"""
# 简化表示:只保留有向边
dag = np.zeros_like(self.adj_matrix)
for i in range(self.n_vars):
for j in range(self.n_vars):
if self.adj_matrix[i, j] > 0:
dag[i, j] = 1
return dag
def print_graph(self):
"""
打印发现的因果关系
"""
print("\n发现的因果关系:")
print("-" * 60)
dag = self.get_causal_graph()
for i in range(self.n_vars):
for j in range(self.n_vars):
if dag[i, j] > 0:
print(f" {self.var_names[i]} → {self.var_names[j]}")
# 打印无向边(方向不确定)
print("\n不确定方向的关系:")
for i in range(self.n_vars):
for j in range(i + 1, self.n_vars):
if self.adj_matrix[i, j] == 1 and self.adj_matrix[j, i] == 1:
print(f" {self.var_names[i]} - {self.var_names[j]}")
使用例子
if name == "main":
# 生成模拟数据
np.random.seed(42)
n_samples = 500
# 真实因果模型:Z → X → Y, Z → Y (fork)
Z = np.random.randn(n_samples)
X = Z + np.random.randn(n_samples) * 0.5
Y = X + Z + np.random.randn(n_samples) * 0.5
data = pd.DataFrame({'Z': Z, 'X': X, 'Y': Y})
# 运行PC算法
pc = PCAlgorithm(data, alpha=0.05)
pc.fit()
pc.print_graph()
2.2 得分型方法(Score-based Methods)
def explain_score_based_methods():
"""
得分型因果发现方法
"""
print("""
【基本思想】
定义一个得分函数来评估每个因果图有多好,
然后搜索最高分的图。
流程:
对于每个候选因果图G:
计算 Score(G | data)
找 argmax Score(G | data)
【常见得分函数】
1. BIC (Bayesian Information Criterion)
────────────────────────────────────
Score_BIC(G | D) = LL(G | D) - (k/2) × log(n)
其中:
- LL(G | D):图G下的似然函数
- k:参数数量
- n:样本量
直觉:拟合得好的图得分高,但过度复杂会被惩罚。
2. BDe (Bayesian Dirichlet Equivalent)
───────────────────────────────────
Score_BDe(G | D) = ∏_{i} ∏_{j,k} Γ(α_{ijk} + n_{ijk}) / Γ(α_{ijk})
──────────────────────────────
∏_j Γ(α_j + n_j) / Γ(α_j)
用途:处理离散变量时更适合。
3. MDL (Minimum Description Length)
─────────────────────────────────
选择能最小化编码数据所需长度的图。
类似于BIC但有更强的复杂性惩罚。
【优点】
✅ 原理简洁:最大化某个目标函数
✅ 处理高维数据较好
✅ 可融合先验知识(加权)
✅ 不需要条件独立性检验
【缺点】
❌ 计算复杂:搜索空间指数级
❌ 可能陷入局部最优
❌ 需要假设因果函数形式(如线性)
❌ 对参数设置敏感
""")
explain_score_based_methods()
GES算法(Greedy Equivalence Search)
def explain_ges_algorithm():
"""
GES:贪心等价类搜索算法
"""
print("""
【GES算法原理】
核心思想:
在因果图等价类的空间中进行贪心搜索。
步骤1:初始化
────────
从空图开始(没有边)。
步骤2:前向贪心搜索(Forward)
───────────────────────────
重复:
对每条可添加的边e:
计算 Score(G + e)
添加分数增益最大的边
直到无法继续改进。
步骤3:后向贪心搜索(Backward)
───────────────────────────────
从前向得到的图开始。
重复:
对每条可删除的边e:
计算 Score(G - e)
删除分数增益最大的边
直到无法继续改进。
步骤4:再次前向
──────────────
基于后向的结果,再进行一次前向搜索。
结果:收敛到局部最优。
【Python伪代码】
```python
def GES(data, score_function, max_iterations=100):
G = EmptyGraph(data.columns)
best_score = score_function(G, data)
improved = True
while improved:
improved = False
# 前向搜索
for edge in possible_edges(G):
G_new = G + edge
score = score_function(G_new, data)
if score > best_score:
G = G_new
best_score = score
improved = True
break
improved = True
while improved:
improved = False
# 后向搜索
for edge in G.edges():
G_new = G - edge
score = score_function(G_new, data)
if score > best_score:
G = G_new
best_score = score
improved = True
break
return G
```
【为什么是贪心?】
GES不保证找到全局最优解,而是
快速找到一个局部最优的因果图。
优点:高维问题中仍然可行(不需遍历所有DAG)
缺点:可能错过全局最优
【常见的得分函数】
1. 对数似然 + BIC惩罚
2. 贝叶斯信息准则(BIC)
3. Akaike信息准则(AIC)
""")
explain_ges_algorithm()
2.3 基于函数形式的方法
def explain_functional_form_methods():
"""
利用因果函数形式进行因果发现
例如:非线性性、非高斯性等
"""
print("""
【核心思想】
不同的因果方向会产生不同的数据统计性质。
可以利用这些性质来确定因果方向。
例子:
假设X和Y相关,但方向不确定:
情景1:X → Y
Y = f(X) + ε_Y
情景2:Y → X
X = g(Y) + ε_X
问题:如何区分?
答案:检查数据的非对称性!
【方法1:非线性性】
假设:Y = f(X) + ε,其中f非线性
如果X和Y满足平衡条件(Markov条件),
则Y很难由X的线性组合生成。
应用:如果X → Y的假设下数据高度非线性,
但Y → X则相反,说明X → Y更可能。
实现:比较两个方向下的残差分布。
【方法2:非高斯性】
关键观察:
如果X和ε独立,且至少一个非高斯,
则E[X × ε^k] ≠ 0 对某些k。
推论:如果X → Y,则X和ε_Y独立;
如果Y → X,则Y和ε_X独立。
因此可以检验这两个独立性哪个成立。
实现:LiNGAM方法(线性非高斯非循环模型)
【LiNGAM(Linear Non-Gaussian Acyclic Model)】
假设:
1. 因果关系是线性的
2. 外生噪声是非高斯分布的
3. 无循环
模型:x = B×x + e
其中B是下三角矩阵(因果序列)
优势:
✅ 可以完全识别因果图(包括方向)
✅ 不需要大量数据
✅ 相对高效
局限:
❌ 要求线性关系
❌ 要求噪声非高斯
❌ 实践中这些假设不一定成立
""")
explain_functional_form_methods()
LiNGAM的Python实现
class LiNGAM:
"""
线性非高斯非循环模型(LiNGAM)
用于因果发现的实现
"""
def __init__(self, data):
"""
初始化LiNGAM
参数:
- data: DataFrame,n_samples × n_variables
"""
self.data = data.values
self.n_samples, self.n_vars = self.data.shape
self.var_names = list(data.columns)
# 标准化数据
self.data_normalized = (self.data - self.data.mean(axis=0)) / self.data.std(axis=0)
def skewness(self, x):
"""计算偏度"""
return np.mean((x - x.mean())**3) / (x.std()**3)
def kurtosis(self, x):
"""计算峰度"""
return np.mean((x - x.mean())**4) / (x.std()**4) - 3
def non_gaussianity_measure(self, x):
"""
非高斯性的度量
结合偏度和峰度
"""
s = self.skewness(x)
k = self.kurtosis(x)
return s**2 + (k**2) / 4
def estimate_B_by_causal_order(self, causal_order):
"""
给定因果顺序,估计系数矩阵B
"""
B = np.zeros((self.n_vars, self.n_vars))
residuals = self.data_normalized.copy()
for j in causal_order:
# j依赖于causal_order[:j]中的所有变量
parents = [causal_order[i] for i in range(causal_order.index(j))]
if len(parents) > 0:
X = residuals[:, parents]
y = residuals[:, j]
# 回归
try:
b = np.linalg.lstsq(X, y, rcond=None)[0]
B[parents, j] = b
# 更新残差
residuals[:, j] = y - X @ b
except:
pass
return B
def calculate_information_criterion(self, B):
"""
计算模型拟合的信息准则
用于模型选择
"""
# 计算残差
residuals = self.data_normalized - self.data_normalized @ B.T
# 总非高斯性
total_ng = np.sum([self.non_gaussianity_measure(residuals[:, j])
for j in range(self.n_vars)])
# BIC惩罚
n_params = np.count_nonzero(B)
bic = -2 * total_ng + n_params * np.log(self.n_samples)
return bic
def find_causal_order(self):
"""
寻找因果顺序(最难的部分)
使用ICA方法结合启发式搜索
"""
from scipy import stats
from itertools import permutations
print("搜索最优因果顺序...")
# 对于小规模问题,可以遍历所有排列
if self.n_vars <= 5:
best_order = None
best_criterion = np.inf
for perm in permutations(range(self.n_vars)):
B = self.estimate_B_by_causal_order(list(perm))
criterion = self.calculate_information_criterion(B)
if criterion < best_criterion:
best_criterion = criterion
best_order = list(perm)
return best_order
else:
# 对于大规模问题,使用启发式
# (这里省略具体实现)
return list(range(self.n_vars))
def fit(self):
"""
拟合LiNGAM模型
"""
print("开始LiNGAM因果发现...")
print("-" * 60)
# 寻找因果顺序
causal_order = self.find_causal_order()
print(f"发现的因果顺序:{[self.var_names[i] for i in causal_order]}")
# 估计系数矩阵
self.B = self.estimate_B_by_causal_order(causal_order)
self.causal_order = causal_order
print("-" * 60)
return self
def get_causal_graph(self):
"""
获取因果图
"""
adj = (np.abs(self.B) > 1e-5).astype(int)
return adj
def print_graph(self):
"""
打印因果关系
"""
print("\n发现的因果关系(LiNGAM):")
print("-" * 60)
adj = self.get_causal_graph()
for i in range(self.n_vars):
for j in range(self.n_vars):
if adj[i, j] > 0:
coeff = self.B[i, j]
print(f" {self.var_names[i]} → {self.var_names[j]} "
f"(系数={coeff:.4f})")
使用例子
if name == "main":
# 生成非高斯数据
np.random.seed(42)
n_samples = 500
# 真实因果模型:Z → X → Y
e_z = np.random.exponential(1, n_samples) - 1 # 非高斯
e_x = np.random.laplace(0, 1, n_samples) # 非高斯
e_y = np.random.uniform(-1, 1, n_samples) # 非高斯
z = e_z
x = 2 * z + e_x
y = 3 * x + e_y
data = pd.DataFrame({'Z': z, 'X': x, 'Y': y})
# 运行LiNGAM
lingam = LiNGAM(data)
lingam.fit()
lingam.print_graph()
第三部分:因果发现的局限性与挑战
3.1 根本性的识别问题
def explain_fundamental_identifiability_issues():
"""
为什么某些因果关系永远无法从数据中识别
"""
print("""
【问题1:Markov等价性(不可区分性)】
根本事实:
某些不同的因果图会产生完全相同的数据分布。
最著名的例子:
图1: X → Y 图2: Y → X 图3: X ← Z → Y
这三个图在数据上无法区分!
为什么?
因为它们代表相同的条件独立性结构:
- 在图1中:无条件独立关系
- 在图2中:无条件独立关系
- 在图3中:无条件独立关系
所有三个图都"说"X和Y相关。
数学术语:这些图在同一个Markov等价类中。
【涵义】
即使你有完美的数据和无穷的样本量,
某些因果关系的方向仍然是不可识别的。
这不是算法问题,而是信息的根本缺陷。
解决方案:需要额外信息!
- 随机对照实验
- 时间序列数据
- 领域知识约束
- 工具变量
【问题2:Faithfulness假设的违反】
定义:Faithfulness假设
数据中的条件独立关系恰好对应因果图中的条件独立关系。
反例:未忠实的图
例1:巧合的相关性消失
X → Z → Y
X → W → Y (路径2)
假设:
Z和W的因果效应大小相等但方向相反。
结果:Z和W的效应相互抵消!
从数据看:X和Y不相关(看似无因果)
但实际上X → Y!
例2:参数的精确取消
线性模型中,如果系数恰好满足某些关系,
某些独立性可能消失。
现实问题:
- 不知道这种巧合是否发生
- 非常罕见,但理论上可能
- 大样本可能暴露这个问题
【问题3:无遗漏混淆假设】
假设:Causal Sufficiency
所有混淆变量都被测量了。
现实:常常违反!
例子:教育→收入的关系
关系图(不完整):
教育 → 收入
实际因果图(有隐藏混淆):
家庭背景(未测量)
↙ ↘
教育 收入
真相:
教育→收入的因果效应较小
但大部分相关性来自共同原因
因果发现方法(如PC算法)无法区分:
- 真实的因果效应
- 由于隐藏混淆导致的虚假关联
局限:
❌ 无法自动发现隐藏变量的存在
❌ 可能发现虚假的因果关系
❌ 需要领域知识来识别可能的隐藏因子
【问题4:反向因果与循环】
DAG假设:无循环
现实中的循环:
经济例子:
高利率 → 低就业
低就业 → 高利率 (反馈)
这是一个反馈循环,不是DAG!
因果发现算法假设DAG,因此:
❌ 无法处理反馈系统
❌ 可能输出错误的图
❌ 需要时间序列方法替代
【问题5:时间性的假设】
因果关系要求"原因必须先于结果"。
从横截面数据中无法识别时间顺序!
例子:
数据:2020年的X和Y观测值
问题:是X在2020年导致Y在2020年?
还是Y导致X?
还是都受2019年Z的影响?
横截面数据无法回答这些问题。
需要:时间序列数据或实验信息
【问题6:不同范式的不可比性】
因果图只是表示因果的一种方式。
其他方式:
- 结构方程模型(SEM)
- 潜在结果框架(Potential Outcomes)
- 动态因果模型(DCM)
它们不一定相容!
例如:
某个关系在DAG中无法表示
但在SEM中可以
选择哪个代表?→ 取决于应用和假设
""")
explain_fundamental_identifiability_issues()
3.2 实践中的数据问题
def explain_practical_data_challenges():
"""
实际数据中的问题导致因果发现失败
"""
print("""
【问题1:样本量不足】
因果发现对样本量很敏感。
理论需求:
- 条件独立性检验需要大量样本
- 尤其当条件集很大时(维度诅咒)
例子:
假设检验 X ⊥ Y | S
其中|S|=10
所需样本量:至少 n ~ 2^|S| × 常数
= 1000+ 级别
如果有100个变量,同时检验所有可能的条件集合:
样本量需求:指数级爆炸!
现实:
- 医学研究:n = 100-1000,p = 100+
- 神经影像:n = 30-100,p = 1000+
结果:很容易发现虚假的因果关系!
【问题2:高维性(维度诅咒)】
当变量数>样本数时:
p >> n 问题
典型情况:
- 基因数据:n=100,p=20000
- 脑成像:n=50,p=50000
后果:
❌ 条件独立性检验不可靠
❌ 假阳性率暴增
❌ 算法计算复杂度高
❌ 很多随机关联看起来显著
应对:
✓ 预处理:特征选择、降维
✓ 方法改进:稀疏因果学习
✓ 更强的假设:稀疏性假设
【问题3:离散与混合变量】
因果发现方法多数设计用于连续变量。
离散变量的问题:
条件独立性检验困难
- 卡方检验:样本要求高
- 依赖性度量:选择众多(互信息、Cramér's V等)
- 样本稀疏:很多组合无观测
混合变量(连续+离散):
- 标准方法不适用
- 需要特殊处理
- 效果通常较差
【问题4:非线性关系】
线性因果发现方法假设线性关系。
现实:非线性处处存在!
例子:
Y = sin(X) + ε (非线性)
Z = X^2 + ε (非线性)
线性PC算法会失败:
❌ 检测不到关系(偏相关≈0)
❌ 输出错误的图
非线性方法:
- 核方法:高计算成本
- 神经网络:难以解释
- 加性模型:有限的非线性能力
【问题5:测量误差】
现实中所有测量都有误差!
例子:
观测到的X = 真实X + 测量噪声
影响:
1. 假阴性(漏检真关系)
测量噪声削弱相关性
X —→ Y (真实)
观测到的相关性减弱
可能被判断为无关
2. 假阳性(虚假关系)
两个无关的变量若测量误差相关:
X (无关) 和 Y (无关)
但都受同一个测量设备影响
可能看起来相关!
应对困难:
❌ 无法直接纠正(需要已知误差模型)
❌ 误差结构往往未知
❌ 可能导致系统偏差
【问题6:选择偏差】
数据不是完全随机的,有偏差的收集过程。
例子:医学研究
只有症状严重的患者会去看医生
数据集只包含这些患者
因果图在这个选择的子群中可能完全不同!
例子:经济数据
经济衰退时期的薪资数据
与整个薪资数据分布不同
从衰退期数据学到的因果关系
可能不适用于繁荣期
处理选择偏差:
❌ 非常困难
❌ 需要特殊的方法
❌ 或需要特殊的数据
""")
explain_practical_data_challenges()
3.3 因果发现的假设清单
def list_all_causal_discovery_assumptions():
"""
因果发现方法所有的假设汇总
"""
assumptions = pd.DataFrame({
'假设名称': [
'1. 无遗漏混淆',
'2. 因果充分性',
'3. DAG假设',
'4. 无反馈循环',
'5. Faithfulness',
'6. 马尔可夫条件',
'7. 时间顺序',
'8. 参数独立',
'9. 线性(部分方法)',
'10. 高斯性(部分方法)'
],
'含义': [
'所有混淆因子都被测量',
'变量集合足以解释观测',
'因果关系是非循环的',
'无因果反馈',
'数据独立性对应图独立性',
'因果结构对数据分布的影响',
'原因必须先于结果',
'参数独立性(Markov性)',
'因果关系是线性函数',
'噪声是高斯分布'
],
'违反频率': [
'常见',
'常见',
'偶见',
'常见',
'极少',
'偶见',
'常见',
'偶见',
'常见',
'常见'
],
'后果': [
'发现虚假关系',
'因果图不完整',
'算法失败',
'算法失败',
'发现虚假关系',
'算法失败',
'方向不确定',
'估计偏差',
'漏检非线性关系',
'偏差估计'
],
'监测方式': [
'领域知识、敏感性分析',
'理论分析',
'检查输出图的环',
'检查输出图的环',
'启发式检查',
'数据分析',
'时间戳检查',
'稳健性检验',
'拟合度检查',
'高斯性检验'
]
})
print("\n因果发现的假设清单")
print("=" * 100)
print(assumptions.to_string(index=False))
print("=" * 100)
print("""
【关键认识】
1. 没有一个因果发现方法对所有假设都鲁棒
2. 大多数假设在实践中被违反
3. 责任在于研究者判断:
- 哪些假设在该应用中合理
- 假设违反时的影响有多大
- 如何进行敏感性分析
4. 这就是为什么因果发现不能完全自动化
需要人类的领域知识和批判性思维!
""")
list_all_causal_discovery_assumptions()
第四部分:如何判断因果发现结果的可信度?
4.1 红旗警告信号
def identify_red_flags_in_causal_discovery():
"""
因果发现结果不可信的警告信号
"""
print("""
【红旗信号#1:发现了太多的因果关系】
风险指示:
如果算法发现几乎所有变量都相互因果影响
原因:
❌ 样本量太小
❌ 高维诅咒
❌ 条件独立性检验虚假阳性率高
❌ 显著性水平(alpha)设置过宽松
诊断方法:
✓ 计算发现的边数 / 理论最大边数
如果>50%,要警惕
✓ 进行引导法(Bootstrap)验证
✓ 减小alpha值重新运行
【红旗信号#2:图与先验知识矛盾】
例子:
发现"阳光 → 体温"
但实际上"体温 → 阳光"(温度影响天气预报)
或发现因果循环(这是不可能的!)
原因:
❌ 算法假设违反
❌ 隐藏混淆的迹象
❌ 时间数据的混淆
应对:
✓ 重新审视数据
✓ 检查算法参数
✓ 咨询领域专家
【红旗信号#3:边的强度不合理】
例子:
发现 X → Y,但估计的因果效应极小
(系数0.0001)
问题:
❌ 可能是虚假发现
❌ 效应大小接近测量误差
❌ 统计功效不足
检验标准:
如果|因果系数| < 噪声标准差 × 10%
→ 可能虚假
如果|因果系数| < 测量标准误 × 2
→ 不显著
【红旗信号#4:高度不稳定的结果】
例子:
- 运行两次算法,结果完全不同
- 移除1%的数据,图大幅改变
- 改变alpha值,发现的关系消失
原因:
❌ 样本量接近临界阈值
❌ 多重共线性问题
❌ 条件独立性检验power不足
诊断方法:
✓ 进行子样本分析
✓ 交叉验证
✓ 引导法(Bootstrap)稳定性检验
【红旗信号#5:所有节点都互相连接】
结果示例:
完全连接图 (Complete Graph)
每个变量都有指向每个其他变量的边
这几乎总是错误的,因为:
❌ 在现实中极其罕见
❌ 通常表明算法完全失败
❌ 或参数设置极端
解决方案:
✓ 检查alpha值(应该很小)
✓ 检查数据质量
✓ 尝试不同的算法
【红旗信号#6:无法重现】
问题:
其他研究者用相似数据无法得到相同结果
原因:
❌ 样本特异性强(过拟合)
❌ 假发现
❌ 参数选择不当
❌ 数据质量差异
这最严重!说明结果不可靠。
""")
identify_red_flags_in_causal_discovery()
4.2 验证因果发现的方法
def validation_methods_for_causal_discovery():
"""
验证因果发现结果的可靠性方法
"""
print("""
【方法1:对比多个算法】
原理:
不同的算法基于不同的假设。
如果多个算法都发现相同的关系,可信度高。
实施:
同时运行:
- PC算法(约束型)
- GES(得分型)
- LiNGAM(函数形式型)
结果解读:
- 三个都发现 X → Y:高度可信
- 两个发现,一个没有:中等可信
- 只有一个发现:低度可信
例子代码:
```python
def compare_multiple_algorithms(data, alpha=0.05):
results = {}
# 运行PC算法
pc = PCAlgorithm(data, alpha=alpha)
pc.fit()
results['PC'] = pc.get_causal_graph()
# 运行GES
ges = GES(data)
ges.fit()
results['GES'] = ges.get_causal_graph()
# 运行LiNGAM
lingam = LiNGAM(data)
lingam.fit()
results['LiNGAM'] = lingam.get_causal_graph()
# 比较结果
agreement = (results['PC'] + results['GES'] + results['LiNGAM']) / 3
# 高度一致的边(3个都发现)
strong_edges = (agreement > 0.66)
return results, agreement, strong_edges
```
【方法2:引导法(Bootstrap)检验】
原理:
通过重新采样检验结果的稳定性。
步骤:
1. 原始样本 → 运行因果发现算法 → 得到图G*
2. 重复B次(通常B=100-500):
a) 从原始数据中有放回地随机采样
b) 运行因果发现算法
c) 记录发现的边
3. 统计:
- 每条边被发现的频率
- 置信区间
- 稳定性度量
解释:
- 边的发现频率 > 90%:非常稳定
- 60-90%:中等稳定
- <60%:不稳定,不可信
代码示例:
```python
def bootstrap_validation(data, algorithm='PC', n_bootstrap=100):
n_samples = len(data)
edge_counts = {}
for b in range(n_bootstrap):
# 有放回重采样
indices = np.random.choice(n_samples, n_samples, replace=True)
data_boot = data.iloc[indices]
# 运行算法
if algorithm == 'PC':
model = PCAlgorithm(data_boot)
else:
model = GES(data_boot)
model.fit()
adj = model.get_causal_graph()
# 记录边
for i in range(len(adj)):
for j in range(len(adj)):
if adj[i, j] > 0:
edge = (data.columns[i], data.columns[j])
edge_counts[edge] = edge_counts.get(edge, 0) + 1
# 计算发现频率
edge_stability = {edge: count/n_bootstrap
for edge, count in edge_counts.items()}
# 只保留高稳定性的边
stable_edges = {edge: freq for edge, freq in edge_stability.items()
if freq > 0.6}
return stable_edges
```
【方法3:与已知因果关系对比】
前提:
你知道某些因果关系是真实的。
方法:
1. 列出已知真实的因果关系(来自文献、实验等)
2. 检查算法是否发现了它们
3. 检查误发现了什么虚假关系
计算指标:
- 灵敏度(Sensitivity)= 发现的真关系数 / 真关系总数
- 特异性(Specificity)= 正确识别的非关系 / 非关系总数
- 精确度(Precision)= 正确发现 / 总发现数
- F1得分 = 2 × (Precision × Recall) / (Precision + Recall)
例子:
真实图有5条边,算法发现8条:
- 其中6条是正确的
- 2条是虚假的
灵敏度 = 6/5 = 120% (发现了所有真边,还多了)
精确度 = 6/8 = 75% (发现中75%正确)
F1 = 2×(0.75×1.0)/(0.75+1.0) = 0.857
【方法4:时间顺序验证】
如果有时间数据:
验证:
发现的因果关系方向是否符合时间顺序?
例如:
不合理:发现 2020年事件 → 2019年事件
检验代码:
```python
def check_temporal_validity(causal_graph, timestamps, data):
"""检查因果关系是否符合时间顺序"""
valid_relations = []
invalid_relations = []
for i, j in causal_graph.edges():
cause_var = data.columns[i]
effect_var = data.columns[j]
cause_time = timestamps[i]
effect_time = timestamps[j]
if cause_time < effect_time:
valid_relations.append((cause_var, effect_var))
else:
invalid_relations.append((cause_var, effect_var))
return valid_relations, invalid_relations
```
【方法5:敏感性分析】
问题:
如果某个假设被违反了怎么办?
方法:
1. 列出关键假设(如无隐藏混淆)
2. 假设它被轻微违反
3. 重新运行因果发现
4. 比较结果改变了多少
如果结果稳健(改变不大)→ 可信
如果结果敏感(大幅改变)→ 不可信
例子:
原假设:所有混淆因子都测量了
敏感性分析:假设存在10%的隐藏混淆
结果:
- 原图发现15条边
- 敏感性分析发现12条边
- 差异20%,还可以接受
【方法6:与实验结果对比】
最强的验证:随机对照实验
步骤:
1. 用观测数据进行因果发现
2. 设计随机实验验证几个关键发现
3. 比对结果
例子:
观测数据说:教育 → 收入 (系数=0.15)
实验:
随机给1000个人提供免费教育
对照组1000人无干预
一年后测量收入差异
如果实验结果支持观测发现 → 高度可信
如果相反 → 发现是虚假的
注意:实验昂贵,但最可靠。
""")
validation_methods_for_causal_discovery()
第五部分:实际应用指南
5.1 因果发现工作流程
def practical_workflow():
"""
在实际研究中应用因果发现的完整工作流程
"""
print("""
【步骤1:问题定义与领域知识收集】
任务:
□ 清楚定义要研究的系统
□ 列出所有相关变量
□ 从领域专家获取先验知识
□ 文献回顾:已知的因果关系
□ 列出所有能想到的潜在混淆因子
输出:
✓ 变量列表
✓ 已知的因果关系清单
✓ 已知的混淆因子清单
✓ 无法测量但可能重要的因子清单
重要:这一步非常关键!直接影响最终结果质量。
【步骤2:数据收集与清洗】
任务:
□ 确定数据来源
□ 确定样本量是否足够
□ 检查缺失值
□ 识别和处理异常值
□ 检查测量误差
□ 验证数据质量
质量检查清单:
✓ 是否有明显的数据录入错误?
✓ 缺失值是否是随机的?
✓ 异常值是真实还是错误?
✓ 变量的分布是否合理?
✓ 样本量与变量数的比例是否合理?
最小建议:
n / p ≥ 10
(样本数至少是变量数的10倍)
【步骤3:描述性分析】
任务:
□ 计算所有变量的描述性统计
□ 绘制所有两两变量的散点图
□ 计算相关矩阵
□ 检查多重共线性
□ 检查分布性质(正态性、偏度等)
输出:
✓ 变量的统计特征
✓ 初步的相关性模式
✓ 可能的非线性关系
✓ 潜在的多重共线性问题
代码示例:
```python
def exploratory_analysis(data):
print("描述性统计:")
print(data.describe())
print("\\n相关矩阵:")
corr = data.corr()
print(corr)
print("\\n检查多重共线性:")
for col in data.columns:
other_cols = [c for c in data.columns if c != col]
X = data[other_cols]
y = data[col]
from sklearn.linear_model import LinearRegression
lr = LinearRegression()
lr.fit(X, y)
r2 = lr.score(X, y)
vif = 1 / (1 - r2) if r2 < 1 else np.inf
print(f"{col}: VIF = {vif:.2f}")
if vif > 10:
print(f" ⚠️ {col} 存在多重共线性问题")
# 绘制相关矩阵热力图
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, cmap='coolwarm', center=0)
plt.title('变量相关矩阵')
plt.show()
```
【步骤4:选择合适的因果发现算法】
决策树:
问题1:数据类型?
├─ 连续 → 更多选择
├─ 离散 → PC、GES
└─ 混合 → 需要特殊处理
问题2:样本量 vs 变量数?
├─ n >> p (充足)→ 任何算法
├─ n > p (中等)→ PC、GES
└─ n < p (少)→ 需要正则化
问题3:关系的性质?
├─ 可能线性 → PC、GES、LiNGAM
├─ 明确非线性 → 特殊方法(CGNN等)
└─ 不确定 → 多个算法并用
问题4:计算资源?
├─ p < 20 → 任何算法
├─ 20 < p < 100 → PC、GES(有优化)
└─ p > 100 → 必须优化版本
推荐方案:
一般情况:
优先使用 PC算法 + GES算法组合
原因:
- 互补的假设
- 结果可对比
- 相对高效
【步骤5:参数调优】
关键参数:
1. PC算法的 alpha(显著性水平)
默认:0.05
如果发现太多关系 → 减小alpha (0.01)
如果发现太少关系 → 增大alpha (0.1)
2. GES的得分函数
选项:BIC(默认)、AIC
BIC更保守(倾向删除边)
AIC更激进(倾向保留边)
3. 条件独立性检验方法
连续高斯:Fisher-Z (default)
离散:卡方检验
非参数:HSIC检验
参数调优方法:
```python
def parameter_tuning(data, algorithm='PC'):
results = {}
if algorithm == 'PC':
alphas = [0.001, 0.01, 0.05, 0.1, 0.2]
for alpha in alphas:
pc = PCAlgorithm(data, alpha=alpha)
pc.fit()
adj = pc.get_causal_graph()
n_edges = np.sum(adj)
results[f'alpha={alpha}'] = n_edges
print("参数调优结果:")
for param, n_edges in results.items():
print(f" {param}: 发现{n_edges}条边")
return results
```
【步骤6:运行因果发现】
执行:
```python
def run_causal_discovery(data, algorithm='PC', alpha=0.05):
if algorithm == 'PC':
model = PCAlgorithm(data, alpha=alpha)
elif algorithm == 'GES':
model = GES(data)
else:
raise ValueError(f"Unknown algorithm: {algorithm}")
model.fit()
return model
# 同时运行多个算法
pc_result = run_causal_discovery(data, 'PC', alpha=0.05)
ges_result = run_causal_discovery(data, 'GES')
```
【步骤7:结果验证与比较】
任务:
□ 比较多个算法的结果
□ 进行Bootstrap稳定性检验
□ 与已知关系对比
□ 检查是否有红旗信号
□ 进行敏感性分析
代码:
```python
def validate_results(data, pc_result, ges_result):
pc_adj = pc_result.get_causal_graph()
ges_adj = ges_result.get_causal_graph()
# 计算一致性
agreement = np.sum(pc_adj == ges_adj) / pc_adj.size
print(f"PC和GES的一致性:{agreement:.2%}")
# 发现一致的边
consensus_edges = (pc_adj > 0) & (ges_adj > 0)
print(f"两个算法都发现的边:{np.sum(consensus_edges)}条")
# 进行Bootstrap验证
boot_results = bootstrap_validation(data, 'PC', n_bootstrap=100)
print(f"Bootstrap验证的稳定边:{len(boot_results)}条")
```
【步骤8:解释与报告】
关键点:
□ 清楚说明所有假设
□ 列出已知违反的假设
□ 报告置信度评估
□ 讨论替代解释
□ 指出局限性
□ 建议进一步的验证实验
报告模板:
✓ 摘要:发现了什么关键因果关系
✓ 方法:用了哪个算法,参数是什么
✓ 结果:具体的因果图,定量指标
✓ 验证:多算法对比、Bootstrap检验结果
✓ 局限:哪些假设可能被违反
✓ 讨论:结果的含义、与先前工作的关系
✓ 结论:主要发现和建议的后续工作
【步骤9:后续验证】
理想情况下:
□ 进行小规模实验验证关键发现
□ 在独立数据集上重现结果
□ 让领域专家评审
□ 与文献进行系统对比
这是提高可信度的最重要步骤!
""")
practical_workflow()
5.2 常见应用领域案例
def application_examples():
"""
因果发现在不同领域的实际应用
"""
print("""
【应用1:基因调控网络发现】
问题:
有100个基因的表达水平数据,要推断哪个基因调控哪个。
特点:
- 高维数据(p=100+,n=50-100)
- 强假设:基因调控是有向无环的
- 时间序列数据可用
方法:
✓ LiNGAM(假设非高斯噪声)
✓ GENIE3(结合机器学习)
✓ PC算法的改进版本
局限:
❌ 大量隐藏的基因调控因子
❌ 非线性的基因调控
❌ 样本量通常很小
案例成功指标:
✓ 与已知的基因调控关系一致
✓ 新预测的关系被实验验证
✓ 发现有生物学意义
【应用2:医学因果发现(诊断因素)】
问题:
哪些症状和检验结果是某种疾病的因果因子?
例:COVID-19相关症状的因果关系
症状:发热、咳嗽、呼吸困难、疲劳、嗅觉缺失
特点:
- 中等维度(p=20-50)
- 中等样本量(n=1000+)
- 部分变量是离散的
- 可能存在隐藏因子(严重程度的混淆)
方法:
✓ PC算法(较稳健)
✓ 分别分析不同亚群
✓ 融合专家知识作为约束
局限:
❌ 医学中因果关系复杂
❌ 选择偏差(严重患者更可能被检测)
❌ 多种隐藏混淆(基础健康状况)
案例成功指标:
✓ 发现的关系与医学知识一致
✓ 帮助改进诊断策略
✓ 预测能力得到提高
【应用3:经济数据因果发现】
问题:
利率、失业率、通胀率等宏观经济变量间的因果关系。
例:构建因果模型分析:
政策利率 → 市场利率 → 投资 → 就业 → 通胀
特点:
- 时间序列数据
- 样本量足(每月数据×10年 = 120)
- 变量数少(p=5-10)
- 明确的时间顺序
- 可能存在反馈循环
方法:
✓ VAR模型(矢量自回归)
✓ Granger因果性检验
✓ 动态因果模型
❌ 不适合DAG模型(存在反馈)
局限:
❌ 系统中存在反馈环
❌ 时间滞后不确定
❌ 政策变化导致关系不稳定
❌ 隐藏的全球因素影响
案例成功指标:
✓ Granger因果性显著
✓ 冲动响应函数合理
✓ 预测能力改进
【应用4:社会科学(教育、心理学)】
问题:
教育质量、教师质量、学生动机等对学业成绩的因果影响。
例:
教师期望 → 学生动机 → 学业成绩
特点:
- 中等维度(p=15-30)
- 足够样本(n=500+)
- 混淆因素众多(社会经济地位等)
- 变量多为问卷测量(有测量误差)
方法:
✓ 结构方程模型(SEM)+ 因果发现
✓ PC算法
✓ 融合理论模型
局限:
❌ 大量不可测或难以测的变量
❌ 测量误差大
❌ 选择偏差(自我选择进入教育)
❌ 长期追踪困难
案例成功指标:
✓ 模型拟合度好
✓ 效应方向和大小合理
✓ 与理论一致
✓ 可重现性高
【应用5:用户行为与推荐系统】
问题:
用户的属性、兴趣、行为间的因果关系。
发现:用户特征 → 内容偏好 → 点击行为
例:在电商中
用户年龄 → 商品类别兴趣 → 购买决策
特点:
- 高维数据(p=100+)
- 超大样本(n=100万+)
- 变量混合(连续+离散)
- 时间顺序清晰(用户创建→浏览→购买)
方法:
✓ 时间序列因果发现
✓ 大规模PC算法(并行版)
✓ 机器学习强化的因果发现
局限:
❌ 数据非常稀疏(大多数交互为0)
❌ 隐藏因素(推荐算法本身影响行为)
❌ 反馈循环(系统学习→改变推荐→改变用户)
案例成功指标:
✓ 能预测用户下一步行为
✓ 推荐效果提升
✓ 个性化准确度改进
""")
application_examples()
第六部分:处理真实数据的高级技巧
6.1 隐藏混淆因子的识别与处理
def handle_hidden_confounders():
"""
隐藏(未测量)混淆因子的识别与应对
"""
print("""
【问题的核心】
现实情景:
观测数据显示:教育 ← → 收入(相关)
可能的因果关系:
情况1(教育导致收入):
教育 → 收入
情况2(虚假相关):
隐藏变量(能力)
↙ ↘
教育 收入
问题:我们无法从数据中区分这两种情况!
【识别隐藏混淆的方法】
方法1:背门准则的延伸
────────────────────
定义:后门路径(Backdoor Path)
不经过因果箭头的路径。
例子:
X → Y (正面门:想要的因果路径)
Z → X ← Z → Y (后门:虚假相关路径)
后门准则(Backdoor Criterion):
为了识别X→Y的因果效应,需要:
1. 没有后门路径(已经被已测量变量阻断)
2. 或所有后门路径都被可测量的变量阻断
如果违反 → 存在隐藏混淆!
应用:
```python
def check_backdoor_paths(causal_graph, treatment, outcome):
\"\"\"
检查是否存在未阻断的后门路径
\"\"\"
# 找所有从treatment经过非因果边到outcome的路径
backdoor_paths = []
# DFS搜索所有路径
def dfs(node, target, current_path, visited):
if node == target:
backdoor_paths.append(current_path)
return
for neighbor in causal_graph.neighbors(node):
# 只沿着后门路径(不是因果箭头)
if not is_causal_edge(node, neighbor):
if neighbor not in visited:
visited.add(neighbor)
current_path.append(neighbor)
dfs(neighbor, target, current_path, visited)
current_path.pop()
visited.remove(neighbor)
dfs(treatment, outcome, [treatment], set([treatment]))
return backdoor_paths
# 使用示例
# backdoor_paths = check_backdoor_paths(graph, 'education', 'income')
# if backdoor_paths:
# print(f"警告:发现{len(backdoor_paths)}条未阻断的后门路径")
```
【方法2:双变量模型的灵敏度分析】
─────────────────────────────────
情景:只关心两个变量X和Y的因果关系
问题:隐藏混淆Z存在
Z
↙ ↘
X Y
↓ ↗
可能的情况:
1. Z只影响X
X → Y (有效因果效应)
2. Z只影响Y
X → Y (有效因果效应)
3. Z同时影响X和Y
X → Y的系数被混淆偏差扭曲
灵敏度分析:
假设存在隐藏Z,其与X和Y的相关性是多少,
才能改变我们的结论?
代码示例:
```python
def sensitivity_analysis_rotnitzky(
beta_xy, # 观测到的X→Y系数
se_beta, # 标准误
alpha_xz, # Z与X的相关性(未知)
gamma_zy, # Z与Y的相关性(未知)
sd_x, sd_y # X和Y的标准差
):
\"\"\"
基于Rotnitzky方法的灵敏度分析
问题:隐藏混淆Z的相关性范围是多少,
会使得X→Y的因果效应为0?
\"\"\"
# 混淆偏差 = alpha_xz * gamma_zy * (sd_y / sd_x)
bias = alpha_xz * gamma_zy * (sd_y / sd_x)
# 校正后的因果效应
true_effect = beta_xy - bias
# 找到使true_effect = 0的边界
# 即 alpha_xz * gamma_zy = beta_xy * (sd_x / sd_y)
critical_correlation_product = beta_xy * (sd_x / sd_y)
return {
'观测系数': beta_xy,
'需要的混淆相关性乘积': critical_correlation_product,
'如果相关性乘积达到': f"将使因果效应变为0"
}
# 例子:教育→收入
result = sensitivity_analysis_rotnitzky(
beta_xy=0.15, # 教育每增加1年,收入增加15%
se_beta=0.02,
alpha_xz=0.3, # 隐藏能力与教育的相关性
gamma_zy=0.4, # 隐藏能力与收入的相关性
sd_x=4, # 教育年限的标准差
sd_y=0.5 # log收入的标准差
)
```
解释:
- 如果隐藏混淆的相关性"足够强"(乘积>临界值)
→ 因果效应可能为0或反向
- 如果相关性必须"不合理地强"
→ 因果结论可能稳健
【方法3:多变量因果发现中的缺失节点**】
问题:
有p个测量变量,但可能遗漏了关键的隐藏变量。
症状(隐藏混淆的迹象):
1. 发现很多无法解释的高相关性对
```python
# 检查异常的相关性
def detect_suspicious_correlations(corr_matrix, threshold=0.7):
suspicious = []
n = len(corr_matrix)
for i in range(n):
for j in range(i+1, n):
if abs(corr_matrix[i,j]) > threshold:
# 这些变量为什么相关?
# 检查是否有共同的因果机制
suspicious.append((i, j, corr_matrix[i,j]))
return suspicious
```
2. PC算法发现的图有"奇怪的图案"
例如:某个变量与很多其他变量相连
但没有明确的因果机制
```python
def detect_hidden_confounder_signature(causal_graph):
\"\"\"
隐藏混淆的图表特征
\"\"\"
node_degrees = causal_graph.degree()
# 寻找"中心"节点(高度数)
hub_nodes = [node for node, degree in node_degrees.items()
if degree > 2 * np.mean(list(dict(node_degrees).values()))]
if hub_nodes:
print("警告:发现潜在的中心节点(可能反映隐藏混淆)")
for hub in hub_nodes:
print(f" {hub}: 度数 = {node_degrees[hub]}")
# 检查是否有"星形"拓扑
# (一个节点连接所有其他节点)
# 这通常表明隐藏的共同原因
```
3. 条件独立性检验的不一致
```python
def test_markov_property(data, estimated_graph):
\"\"\"
检查Markov属性是否被违反
(可能的隐藏混淆迹象)
\"\"\"
violations = []
for node in estimated_graph.nodes():
parents = list(estimated_graph.predecessors(node))
# 给定父节点,该节点应独立于非后代
# 如果不独立 → 可能有隐藏混淆
# 进行条件独立性检验
# ...
return violations
```
【应对隐藏混淆的策略】
策略1:工具变量法(Instrumental Variables)
──────────────────────────────────────
思想:找到一个"工具"变量Z,它:
- 影响处理X
- 不直接影响结果Y
- 只通过X影响Y
图:
Z → X → Y
其中Z与X的任何混淆因子都无关
这样就可以识别X→Y的因果效应!
例子:教育与收入
工具变量:法律改变导致的教育年限变化
为什么有效?
- 法律是外生的(不由个人特征决定)
- 法律通过教育年限影响收入
- 法律不直接影响收入(只通过教育)
```python
def instrumental_variable_analysis(
y, # 结果
x, # 处理
z, # 工具变量
):
\"\"\"
两阶段最小二乘法(2SLS)
第一阶段:X ~ Z
第二阶段:Y ~ X_hat
\"\"\"
from sklearn.linear_model import LinearRegression
# 第一阶段:用Z预测X
model1 = LinearRegression()
model1.fit(z.reshape(-1, 1), x)
x_hat = model1.predict(z.reshape(-1, 1))
# 第二阶段:用X_hat预测Y
model2 = LinearRegression()
model2.fit(x_hat.reshape(-1, 1), y)
causal_effect = model2.coef_[0]
return causal_effect, model1, model2
# 条件:Z需要满足
# 1. Relevance: Z与X相关(检查model1的R²)
# 2. Exogeneity: Z与混淆因子无关(难以检验!)
```
策略2:前门调整法(Front-door Adjustment)
───────────────────────────────────────
思想:虽然有隐藏混淆,但如果满足特殊结构,
仍然可以识别因果效应。
图:
U(隐藏混淆)
↙ ↘
X → M → Y
关键:
- M是中介(X→Y的唯一路径)
- 没有X→Y的直接路径
- U可以影响X和Y,但不能影响M(通过X除外)
前门公式可以识别效应!
```python
def front_door_adjustment(
data,
x_col, # 处理
m_col, # 中介
y_col, # 结果
):
\"\"\"
前门调整的实现
\"\"\"
# 步骤1:X → M的效应
model_m = LinearRegression()
model_m.fit(data[[x_col]], data[m_col])
# 步骤2:M → Y的效应(控制X)
# 关键:需要用到X和M
from sklearn.linear_model import LinearRegression
model_y = LinearRegression()
model_y.fit(
data[[x_col, m_col]],
data[y_col]
)
# 总效应 = (X→M) × (M→Y)
# (简化版,完整版更复杂)
effect = model_m.coef_[0] * model_y.coef_[1]
return effect
```
策略3:使用多个控制变量选择方法
────────────────────────────
问题:不知道应该控制哪些变量
解决方案:
1. Causal Forest(因果森林)
2. LASSO + 因果检验
3. 双重机器学习(Double Machine Learning)
```python
def double_debiased_machine_learning(
y, x, w, # y:结果,x:处理,w:所有其他变量
):
\"\"\"
双重去偏机器学习(DML)
好处:即使控制了很多变量(包括不必要的),
仍能得到一致的处理效应估计
\"\"\"
from sklearn.ensemble import RandomForestRegressor
# 步骤1:用w预测y(去除w的影响)
model_y = RandomForestRegressor()
model_y.fit(w, y)
y_residual = y - model_y.predict(w)
# 步骤2:用w预测x(去除w的影响)
model_x = RandomForestRegressor()
model_x.fit(w, x)
x_residual = x - model_x.predict(w)
# 步骤3:残差上的线性回归(在所有残差上)
model_final = LinearRegression()
model_final.fit(x_residual.reshape(-1, 1), y_residual)
treatment_effect = model_final.coef_[0]
return treatment_effect
```
""")
handle_hidden_confounders()
6.2 时间序列因果发现
def time_series_causal_discovery():
"""
从时间序列数据中发现因果关系
"""
print("""
【时间序列因果发现的优势】
与横截面数据相比:
✓ 有明确的时间顺序
原因必须在结果之前发生
✓ 可以检测动态因果关系
X在时刻t的值影响Y在时刻t+k的值
✓ 较少混淆问题
潜在混淆因子是时变的
✓ 更多信息
同一单位的多个时间点测量
【Granger因果性】
定义:
如果利用X的历史信息能比只用Y的历史信息
更好地预测Y的未来,
则称X有Granger因果性对Y成立。
数学表述:
H0: X不Granger导致Y
Ha: X Granger导致Y
Var(Y_t | 历史Y) > Var(Y_t | 历史Y,历史X)
(如果加入X能显著减少预测误差 → 拒绝H0)
重要限制:
❌ 不一定是真正的因果性
❌ 只是预测能力上的Granger关系
❌ 需要满足特定的模型假设
【实现:VAR模型】
```python
def granger_causality_test(data, max_lag=4):
\"\"\"
Granger因果性检验(使用VAR模型)
参数:
data: DataFrame,每列是一个时间序列
max_lag: 最大滞后阶数
\"\"\"
from statsmodels.tsa.vector_ar.vecm import coint_johansen
from statsmodels.stats.outliers_influence import variance_inflation_factor
from statsmodels.tsa.api import VAR
# 步骤1:确定最优滞后阶数
model = VAR(data)
lag_order = model.select_lags(maxlags=max_lag)
print(f"最优滞后阶数:{lag_order.aic}") # 使用AIC准则
# 步骤2:拟合VAR模型
var_model = model.fit(lag_order.aic)
print(var_model.summary())
# 步骤3:进行Granger因果性检验
# 检验:各列是否Granger导致其他列
results = {}
for col_cause in data.columns:
for col_effect in data.columns:
if col_cause == col_effect:
continue
# 限制模型:X不影响Y
# 完全模型:X可能影响Y
# 计算限制模型的残差平方和
# 计算完全模型的残差平方和
# F统计量 = (SSR_restricted - SSR_full) / (k × p)
# / (SSR_full / (T - kp - 1))
# 其中k是移除的系数数量,p是延迟数
# 实际使用statsmodels的granger_causality_matrix
return var_model, lag_order
def granger_causality_matrix(data, max_lag=4):
\"\"\"
计算所有变量对之间的Granger因果性
\"\"\"
from statsmodels.tsa.stattools import grangercausalitytests
n_vars = data.shape[1]
col_names = data.columns
# 初始化结果矩阵
gc_matrix = np.zeros((n_vars, n_vars))
pval_matrix = np.zeros((n_vars, n_vars))
for i, col_effect in enumerate(col_names):
for j, col_cause in enumerate(col_names):
if i == j:
gc_matrix[i, j] = np.nan
pval_matrix[i, j] = np.nan
continue
# 创建包含因和果的数据
test_data = data[[col_cause, col_effect]]
# 进行Granger检验
gc_result = grangercausalitytests(
test_data,
max_lag=max_lag,
verbose=False
)
# 提取p值(使用第一个延迟)
_, pval, _, _ = gc_result[1][0]
pval_matrix[i, j] = pval
gc_matrix[i, j] = 1 if pval < 0.05 else 0
return gc_matrix, pval_matrix
```
【动态因果模型(DCM)】
适用于:神经影像、生理学数据
特点:
- 显式建模神经动力学
- 基于微分方程
- 参数有生物学意义
```python
def dynamic_causal_modeling_example():
\"\"\"
动态因果模型示例:脑区之间的连接性
\"\"\"
# 三个脑区:V1, V2, V5(视觉处理)
# 方向:V1 → V2 → V5
# 动态系统方程:
# dx/dt = A×x + u
# y = C×x
# 其中:
# x:神经活动状态
# u:外部输入
# y:观测信号(fMRI)
# A:有向连接矩阵(因果效应)
# C:输入矩阵
# 参数:
# A[1,0] > 0 V1→V2的连接强度
# A[2,1] > 0 V2→V5的连接强度
# A[2,0] ≈ 0 V1→V5的直接连接很弱
# 适配过程:
# 1. 提出多个模型(不同的连接模式)
# 2. 用数据拟合每个模型
# 3. 使用模型比较(e.g., 贝叶斯因子)
# 选择最优模型
print("""
DCM步骤:
1. 定义候选模型(可能的因果连接)
2. 拟合每个模型
3. 计算模型证据
4. 使用贝叶斯模型选择
5. 提取获胜模型的参数
""")
【转移熵(Transfer Entropy)】
优点:完全非参数,基于信息论
定义:
TE(X→Y) = H(Y_t | Y_past) - H(Y_t | Y_past, X_past)
其中H是熵,衡量从X的过去对Y的现在的信息传递。
```python
def transfer_entropy(x, y, history_len=1):
\"\"\"
计算从X到Y的转移熵
\"\"\"
from scipy.special import digamma
from sklearn.neighbors import NearestNeighbors
n = len(x)
# 构建历史向量
# Y_past = (y_{t-1}, y_{t-2}, ..., y_{t-history_len})
# X_past = (x_{t-1}, x_{t-2}, ..., x_{t-history_len})
y_past = np.array([y[i:i+history_len]
for i in range(n-history_len)])
x_past = np.array([x[i:i+history_len]
for i in range(n-history_len)])
y_current = y[history_len:]
# 计算k-近邻距离(用于熵估计)
# 使用Kraskov等人的方法
k = 3 # 近邻数
# 组合向量:(Y_current, Y_past, X_past)
combined = np.column_stack([y_current, y_past, x_past])
# 找k-最近邻
nbrs = NearestNeighbors(n_neighbors=k+1).fit(combined)
distances, indices = nbrs.kneighbors(combined)
# 提取距离
k_distance = distances[:, k]
# 估计条件熵
# 使用digamma函数的期望差
te = np.mean(digamma(k) - digamma(1) +
digamma(len(y_past)) - digamma(len(x_past)))
return te
```
【因果推断的向量自回归】
结构向量自回归(SVAR):
可以从时间序列数据中识别因果冲击
```python
def structural_var_analysis(data, shock_identification='cholesky'):
\"\"\"
结构VAR模型用于因果分析
可以回答问题如:
"GDP的突然冲击对失业率的动态影响是什么?"
\"\"\"
from statsmodels.tsa.api import VAR
from statsmodels.tsa.vector_ar.irf import IRF
# 拟合VAR模型
model = VAR(data)
results = model.fit(2) # 2个滞后
# 进行脉冲响应分析(Impulse Response Function)
# 这显示了一个变量的冲击对所有变量的动态影响
irf = results.irf()
# 绘制脉冲响应
irf.plot(orth=shock_identification)
# 解释:
# 如果GDP增加1个标准差的冲击
# 失业率在第1期、第2期、第3期的响应是多少?
# 累计因果效应
cumulative_effect = np.cumsum(irf.irfs[:, 0, 1])
return results, irf, cumulative_effect
```
""")
time_series_causal_discovery()
第七部分:实际案例研究
7.1 完整案例:基因表达调控网络
def case_study_gene_network():
"""
完整案例:从时间序列基因表达数据发现基因调控网络
"""
print("""
【案例背景】
问题:
一项研究测定了5个基因(A, B, C, D, E)
在12个时间点的表达水平。
目标:
推断这5个基因之间的因果调控关系。
例如:
是否存在基因A→基因B的调控?
【数据模拟】
实际的基因调控网络(地真):
A → B → D
↓ ↓
C E
即:
- A调控B和C
- B调控D和E
```python
def simulate_gene_expression_data(n_timepoints=50, seed=42):
\"\"\"
模拟遵循已知因果结构的基因表达数据
\"\"\"
np.random.seed(seed)
# 初始化
A = np.random.normal(1, 0.1, n_timepoints)
B = np.zeros(n_timepoints)
C = np.zeros(n_timepoints)
D = np.zeros(n_timepoints)
E = np.zeros(n_timepoints)
# 时间序列动态
for t in range(1, n_timepoints):
# A是自主的(随机游走)
A[t] = 0.9 * A[t-1] + np.random.normal(0, 0.1)
# B由A调控
B[t] = 0.7 * A[t-1] + 0.5 * B[t-1] + np.random.normal(0, 0.1)
# C由A调控
C[t] = 0.6 * A[t-1] + 0.4 * C[t-1] + np.random.normal(0, 0.1)
# D由B调控
D[t] = 0.8 * B[t-1] + 0.3 * D[t-1] + np.random.normal(0, 0.1)
# E由B调控
E[t] = 0.7 * B[t-1] + 0.4 * E[t-1] + np.random.normal(0, 0.1)
# 收集为DataFrame
data = pd.DataFrame({
'GeneA': A,
'GeneB': B,
'GeneC': C,
'GeneD': D,
'GeneE': E
})
return data
# 生成数据
gene_data = simulate_gene_expression_data(n_timepoints=50)
print("基因表达数据(前10行):")
print(gene_data.head(10))
【步骤1:描述性分析】
```python
def exploratory_analysis_genes(data):
print("基因表达统计:")
print(data.describe())
# 绘制时间序列
import matplotlib.pyplot as plt
fig, axes = plt.subplots(5, 1, figsize=(12, 8))
for i, col in enumerate(data.columns):
axes[i].plot(data[col])
axes[i].set_ylabel(col)
axes[i].set_title(f'{col} 表达轨迹')
axes[-1].set_xlabel('时间')
plt.tight_layout()
plt.show()
# 相关矩阵
print("\\n基因间相关矩阵:")
print(data.corr())
```
【步骤2:Granger因果性分析】
```python
def granger_analysis_genes(data):
\"\"\"
使用Granger因果性检验基因间的因果关系
\"\"\"
from statsmodels.tsa.stattools import grangercausalitytests
results = {}
genes = data.columns
for gene_cause in genes:
for gene_effect in genes:
if gene_cause == gene_effect:
continue
# 构建测试数据(原因在前,结果在后)
test_data = data[[gene_cause, gene_effect]]
# 进行Granger检验
gc_result = grangercausalitytests(
test_data,
max_lag=2, # 最多2个时间步的滞后
verbose=False
)
# 提取p值(滞后=1)
_, pval_lag1, _, _ = gc_result[1][0]
# 提取p值(滞后=2)
_, pval_lag2, _, _ = gc_result[2][0]
results[f'{gene_cause}→{gene_effect}'] = {
'p_value_lag1': pval_lag1,
'p_value_lag2': pval_lag2,
'significant': pval_lag1 < 0.05 or pval_lag2 < 0.05
}
# 显示显著的因果关系
print("\\nGranger因果性检验结果:")
print("=" * 60)
for relation, pvals in results.items():
if pvals['significant']:
print(f"✓ {relation}: p={pvals['p_value_lag1']:.4f}")
return results
granger_results = granger_analysis_genes(gene_data)
```
【步骤3:转移熵分析】
```python
def transfer_entropy_genes(data):
\"\"\"
使用转移熵识别基因间的因果信息流
\"\"\"
genes = data.columns
n_genes = len(genes)
# 初始化TE矩阵
te_matrix = np.zeros((n_genes, n_genes))
for i, gene_source in enumerate(genes):
for j, gene_target in enumerate(genes):
if i == gene_source:
continue
# 计算转移熵
te = transfer_entropy(
data[gene_source].values,
data[gene_target].values,
history_len=1
)
te_matrix[i, j] = te
# 绘制热力图
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(8, 6))
sns.heatmap(
te_matrix,
xticklabels=genes,
yticklabels=genes,
annot=True,
fmt='.3f',
cmap='YlOrRd'
)
plt.title('转移熵矩阵:行→列')
plt.xlabel('目标基因')
plt.ylabel('源基因')
plt.show()
return te_matrix
te_results = transfer_entropy_genes(gene_data)
```
【步骤4:PC算法进行因果发现】
```python
def pc_algorithm_genes(data, alpha=0.05):
\"\"\"
应用PC算法发现基因调控网络
\"\"\"
# 初始化:完全图
genes = list(data.columns)
n_genes = len(genes)
# 邻接矩阵(无向)
adjacency = np.ones((n_genes, n_genes)) - np.eye(n_genes)
# 分离集(用于确定箭头方向)
sep_sets = {(i, j): set() for i in range(n_genes)
for j in range(n_genes)}
depth = 0
# 第一阶段:从相关矩阵中删除边
while depth < n_genes:
changed = False
for i in range(n_genes):
for j in range(i+1, n_genes):
if adjacency[i, j] == 0:
continue
# 获取i的邻接节点(不包括j)
neighbors_i = [k for k in range(n_genes)
if k != j and adjacency[i, k] > 0]
if len(neighbors_i) < depth:
continue
# 测试i和j是否条件独立
for S in combinations(neighbors_i, depth):
pval = conditional_independence_test_timeseries(
data.iloc[:, i].values,
data.iloc[:, j].values,
data.iloc[:, list(S)].values,
lag=1
)
if pval > alpha:
# 条件独立:删除边
adjacency[i, j] = 0
adjacency[j, i] = 0
sep_sets[(i, j)] = sep_sets[(j, i)] = set(S)
changed = True
break
if not changed:
break
depth += 1
# 第二阶段:确定箭头方向(基于对撞器)
# ...
return adjacency, sep_sets
adj_matrix, sep_sets = pc_algorithm_genes(gene_data, alpha=0.05)
print("\\nPC算法发现的基因调控网络:")
print("=" * 40)
print("邻接矩阵(1表示有连接):")
print(pd.DataFrame(adj_matrix,
index=gene_data.columns,
columns=gene_data.columns))
```
【步骤5:结果验证与比较】
```python
def validate_gene_network(
true_edges, # 已知的真实因果边
discovered_edges # 发现的因果边
):
\"\"\"
对比发现的网络与已知网络
\"\"\"
# 计算灵敏度、特异性等
true_positives = len(set(discovered_edges) & set(true_edges))
false_positives = len(set(discovered_edges) - set(true_edges))
false_negatives = len(set(true_edges) - set(discovered_edges))
sensitivity = true_positives / len(true_edges) if true_edges else 0
specificity = (len(discovered_edges) - false_positives) / len(discovered_edges) if discovered_edges else 0
precision = true_positives / len(discovered_edges) if discovered_edges else 0
f1 = 2 * (precision * sensitivity) / (precision + sensitivity) if (precision + sensitivity) > 0 else 0
print(f"灵敏度(发现真因果的比例): {sensitivity:.2%}")
print(f"精确度(发现的是真因果的比例): {precision:.2%}")
print(f"F1分数: {f1:.3f}")
print(f"\\n真阳性: {true_positives}")
print(f"假阳性: {false_positives}")
print(f"假阴性: {false_negatives}")
return sensitivity, precision, f1
# 已知的真实因果边(根据模拟过程)
true_edges = [
('GeneA', 'GeneB'),
('GeneA', 'GeneC'),
('GeneB', 'GeneD'),
('GeneB', 'GeneE')
]
# 从PC算法结果提取边
# (这是简化的,实际需要解析邻接矩阵)
validate_gene_network(true_edges, []) # 结果取决于实际发现
【主要发现】
1. Granger因果性揭示了时间滞后的预测关系
2. 转移熵提供了信息论的视角
3. PC算法基于条件独立性识别图结构
4. 多个方法的一致性提高可信度
【局限与改进】
局限:
❌ 时间点数量有限(只有50)
❌ 无法区分同时进行的效应
❌ 隐藏的调控因子(如转录因子)
改进方向:
✓ 使用更长的时间序列
✓ 加入已知的生物学约束
✓ 结合表观遗传学和蛋白质数据
✓ 进行小规模验证实验
""")
case_study_gene_network()
现在让我创建一个总结和实用检查清单:
def comprehensive_summary():
"""
因果发现的完整知识总结与实用清单
"""
print("""
╔════════════════════════════════════════════════════════════════════╗
║ 因果发现:完整知识框架 ║
╚════════════════════════════════════════════════════════════════════╝
【核心概念回顾】
1. 因果发现 vs 因果推断
─────────────────────
因果推断:
- 已知因果图的结构
- 用数据估计效应大小
- 例:治疗效应估计
因果发现:
- 因果图的结构未知
- 用数据推断图的连接
- 例:发现疾病的致病因子
2. 从数据推断因果关系的可能性
──────────────────────────────
关键洞察:
✓ 并非不可能(与流行观点相反)
✓ 需要特定条件和假设
✓ 多个图可能与数据一致(等价类)
✓ 可以缩小可能图的范围
3. 主要方法家族
──────────────
(1) 约束型 Constraint-based:
- PC算法
- FCI算法
- 基于条件独立性
(2) 得分型 Score-based:
- GES算法
- 爬山搜索
- 基于模型拟合度
(3) 函数型 Functional-causal-model:
- LiNGAM
- 线性非高斯因果模型
(4) 时间序列型 Time-series:
- Granger因果性
- 动态因果模型
【方法选择决策树】
问题1:你有多少变量?
├─ <20个 → 任何方法都可以
├─ 20-100个 → PC、GES(需要优化)
└─ >100个 → 必须使用稀疏学习版本
问题2:数据类型?
├─ 横截面 → PC、GES、LiNGAM
├─ 时间序列 → Granger因果性、DCM
└─ 混合(面板数据)→ 特殊方法
问题3:变量关系的性质?
├─ 可能线性 → PC+GES组合
├─ 明确非线性 → 核方法或神经网络
└─ 不确定 → 多个算法并用
问题4:可以做实验吗?
├─ 是 → 随机实验最可靠
├─ 否 → 必须依赖观测数据
└─ 部分可以 → 混合策略
【实际应用检查清单】
✓ 任何人在应用因果发现前都应该检查:
数据准备阶段:
□ 数据质量评估
- 缺失值处理
- 异常值检测
- 测量误差评估
□ 变量选择
- 包含了所有相关变量吗?
- 排除了不相关的变量吗?
- 隐藏变量的可能性?
□ 样本量检验
- n / p ≥ 10(至少)
- 样本数足以进行条件独立性检验
前置知识收集:
□ 已知的因果关系(从文献)
□ 可能的混淆因子
□ 已知的时间顺序
□ 领域理论约束
算法执行:
□ 选择合适的算法(基于数据特征)
□ 参数调优(如alpha值)
□ 运行多个算法
□ 记录所有设置
结果验证:
□ 与先前知识对比
□ 检查红旗信号
□ Bootstrap稳定性检验
□ 多算法一致性检验
□ 敏感性分析
- 改变参数,结果是否稳定?
- 移除5%的数据,结果是否改变?
结论与报告:
□ 清楚声明所有假设
□ 讨论哪些假设可能被违反
□ 定量信心度评分
□ 明确指出局限性
□ 建议的后续验证步骤
【常见陷阱与避免】
陷阱1:因果发现后立即相信结果
────────────────────────────
症状:
"算法说A→B,所以A导致B"
风险:
⚠️ 所有假设可能都被违反
⚠️ 结果可能极其不稳定
⚠️ 发现可能是虚假的
预防:
✓ 严格的验证步骤
✓ 多个方法确认
✓ 进行小规模实验验证
陷阱2:忽视隐藏混淆
────────────────
症状:
"我测量了所有变量,所以没有隐藏混淆"
事实:
⚠️ 隐藏混淆在现实中很常见
⚠️ 无法通过数据自动检测
⚠️ 很容易导致虚假发现
预防:
✓ 对隐藏混淆进行灵敏度分析
✓ 使用工具变量法
✓ 咨询领域专家
陷阱3:p-value黑魔法
──────────────────
症状:
"p<0.05,所以这个因果关系是真实的"
问题:
⚠️ 多重比较问题(进行了很多测试)
⚠️ p-value不等于后验概率
⚠️ 小样本中虚假发现率很高
预防:
✓ 多重比较校正(Bonferroni等)
✓ 报告效应大小,不仅是p值
✓ 进行贝叶斯分析
陷阱4:过度解释
────────────
症状:
"因果发现说X→Y→Z,所以Z是完全由X通过Y引起的"
问题:
⚠️ 可能存在直接效应:X→Z
⚠️ 可能存在双向关系
⚠️ 因果图只是近似
预防:
✓ 量化直接和间接效应
✓ 讨论替代解释
✓ 适度表述结论
陷阱5:单一方法依赖
──────────────
症状:
"我用PC算法了,所以结果是可靠的"
问题:
⚠️ 每种算法都有特定假设
⚠️ 算法可能对数据不适合
⚠️ 结果可能极其特定于某个方法
预防:
✓ 总是运行多个算法
✓ 只相信多个方法都支持的发现
✓ 理解每个算法的假设
【从统计意义到实际意义】
统计显著 ≠ 实际重要
例子:
因果系数估计值:0.001
标准误:0.0003
t值:3.3,p<0.001 ✓ 统计显著
95%置信区间:[0.0004, 0.0016]
问题:
⚠️ 真实效应可能极小
⚠️ 实际意义可能无足轻重
⚠️ 可能不值得干预
解决方案:
✓ 报告效应大小(Cohen's d等)
✓ 临床/实际意义评估
✓ 成本-收益分析
【高级话题预览】
超越本教程的主题:
1. 未来研究的因果推断设计
- A/B测试设计
- 多臂老虎机问题
- 在线因果学习
2. 因果推断的深度学习
- 表征学习
- 神经网络因果模型
- 差分隐私中的因果推断
3. 大规模因果发现
- 分布式算法
- 在线学习算法
- 流数据中的因果发现
4. 因果发现的实验设计
- 主动学习
- 实验设计优化
- 干预的最优选择
【最后的话】
因果发现是一个强大但风险的工具。
核心原则:
─────────
1. 谦虚
因果图只是现实的近似模型
总有你不知道的地方
2. 透明
清楚说明所有假设和局限
不要隐藏不确定性
3. 验证
不要只依赖一种方法或数据
用多种方式验证关键发现
4. 合作
咨询领域专家
让他们审查你的结果
5. 谨慎
如果要进行干预,要小心
错误的因果发现可能导致有害的政策
"好的因果发现涉及理论知识、统计技能和
健康的怀疑态度的结合。"
""")
comprehensive_summary()
第八部分:高性能Python实现与优化
8.1 优化的PC算法实现
def optimized_pc_algorithm():
"""
生产级别的PC算法实现,包含性能优化
"""
print("""
【标准PC算法的性能瓶颈】
时间复杂度:O(p^d × T)
- p:变量数量
- d:最大度数(图的稠密程度)
- T:每个条件独立性检验的时间
问题:
⚠️ 20个变量时就需要大量计算
⚠️ 高维下几乎不可行
【优化策略】
""")
class OptimizedPCAlgorithm:
"""
优化的PC算法实现
"""
def __init__(self, data, alpha=0.05, verbose=True):
"""
参数:
data: DataFrame
alpha: 显著性水平
verbose: 是否打印进度
"""
self.data = data
self.alpha = alpha
self.verbose = verbose
self.n_vars = data.shape[1]
self.n_samples = data.shape[0]
self.var_names = list(data.columns)
# 初始化邻接矩阵
self.adjacency = np.ones((self.n_vars, self.n_vars)) - np.eye(self.n_vars)
# 缓存相关矩阵(优化1:避免重复计算)
self.correlation_matrix = data.corr().values
self.partial_corr_cache = {}
# 分离集
self.sep_sets = {
(i, j): set() for i in range(self.n_vars)
for j in range(self.n_vars)
}
def _fast_partial_correlation(self, i, j, S):
"""
优化2:缓存部分相关性计算
使用精确公式而不是回归(更快)
"""
# 创建缓存键
cache_key = (i, j, frozenset(S))
if cache_key in self.partial_corr_cache:
return self.partial_corr_cache[cache_key]
if len(S) == 0:
# 简单相关性
result = abs(self.correlation_matrix[i, j])
else:
# 使用精确公式计算部分相关
# 基于逆协方差矩阵
indices = [i, j] + list(S)
cov_sub = self.data.iloc[:, indices].cov().values
# 精确求解(使用Schur补)
inv_cov = np.linalg.inv(cov_sub)
# 部分相关 = -inv_cov[0,1] / sqrt(inv_cov[0,0] * inv_cov[1,1])
partial_corr = -inv_cov[0, 1] / np.sqrt(
inv_cov[0, 0] * inv_cov[1, 1]
)
result = abs(partial_corr)
# 缓存结果
self.partial_corr_cache[cache_key] = result
return result
def _fisher_z_test(self, i, j, S):
"""
优化3:使用Fisher Z变换进行快速显著性检验
比完整的回归检验快10倍以上
"""
# 计算部分相关
partial_r = self._fast_partial_correlation(i, j, S)
# Fisher Z变换
# z = 0.5 * ln((1+r)/(1-r)) × sqrt(n - |S| - 3)
if abs(partial_r) >= 0.9999:
partial_r = 0.9999 * np.sign(partial_r)
z_stat = 0.5 * np.log((1 + partial_r) / (1 - partial_r))
z_stat *= np.sqrt(self.n_samples - len(S) - 3)
# 计算p值
from scipy import stats
p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))
return p_value
def _optimize_neighbor_selection(self, node):
"""
优化4:智能选择要测试的邻接点
按相关强度排序,弱相关的更可能被删除
"""
neighbors = []
correlations = []
for neighbor in range(self.n_vars):
if neighbor == node and self.adjacency[node, neighbor] > 0:
corr = abs(self.correlation_matrix[node, neighbor])
neighbors.append(neighbor)
correlations.append(corr)
# 按相关性从小到大排序
# (小相关性的边更可能被删除,节省计算)
sorted_indices = np.argsort(correlations)
return [neighbors[i] for i in sorted_indices]
def run(self):
"""
运行优化的PC算法
"""
if self.verbose:
print("开始PC算法...")
print(f"变量数: {self.n_vars}, 样本数: {self.n_samples}")
depth = 0
while True:
converged = True
for i in range(self.n_vars):
# 优化4:使用智能邻接选择
neighbors = self._optimize_neighbor_selection(i)
if len(neighbors) < depth + 1:
continue
for j in neighbors:
if self.adjacency[i, j] == 0:
continue
# 优化:尽早退出
adjacent_to_i = [
k for k in range(self.n_vars)
if k != j and self.adjacency[i, k] > 0
]
if len(adjacent_to_i) < depth:
continue
# 测试所有大小为depth的子集
from itertools import combinations
for S in combinations(adjacent_to_i, depth):
# 优化3:使用Fisher Z检验
p_value = self._fisher_z_test(i, j, list(S))
if p_value > self.alpha:
# 删除边
self.adjacency[i, j] = 0
self.adjacency[j, i] = 0
self.sep_sets[(i, j)] = self.sep_sets[(j, i)] = set(S)
converged = False
if self.verbose:
print(f" 删除边 {self.var_names[i]}-{self.var_names[j]} | {set(S)}")
break
depth += 1
if converged or depth > self.n_vars:
break
if self.verbose:
print(f"PC算法完成 (深度={depth})")
return self.adjacency
def get_graph_statistics(self):
"""
获取图的统计信息
"""
n_edges = np.sum(self.adjacency) / 2
density = n_edges / (self.n_vars * (self.n_vars - 1) / 2)
degrees = np.sum(self.adjacency, axis=1)
return {
'边数': n_edges,
'密度': density,
'平均度数': np.mean(degrees),
'最大度数': np.max(degrees),
'缓存命中率': len(self.partial_corr_cache) / (
self.n_vars * (self.n_vars - 1) * 2 + 1
) # 粗略估计
}
# 使用示例
print("""
【使用示例】
```python
# 生成测试数据
np.random.seed(42)
n_samples = 500
data = pd.DataFrame({
'X1': np.random.normal(0, 1, n_samples),
'X2': np.random.normal(0, 1, n_samples),
'X3': np.random.normal(0, 1, n_samples),
'X4': np.random.normal(0, 1, n_samples),
'X5': np.random.normal(0, 1, n_samples)
})
# 创建一些因果关系
data['X2'] = 0.5 * data['X1'] + np.random.normal(0, 0.5, n_samples)
data['X3'] = 0.5 * data['X1'] + np.random.normal(0, 0.5, n_samples)
data['X4'] = 0.5 * data['X2'] + np.random.normal(0, 0.5, n_samples)
# 运行优化的PC算法
import time
start_time = time.time()
pc = OptimizedPCAlgorithm(data, alpha=0.05, verbose=True)
adjacency = pc.run()
elapsed = time.time() - start_time
print(f"\\n耗时: {elapsed:.2f} 秒")
print("\\n图统计:")
for key, value in pc.get_graph_statistics().items():
print(f" {key}: {value}")
print("\\n邻接矩阵:")
print(pd.DataFrame(adjacency,
index=data.columns,
columns=data.columns))
```
""")
optimized_pc_algorithm()
8.2 并行因果发现
def parallel_causal_discovery():
"""
使用多进程加速因果发现
"""
print("""
【并行化策略】
PC算法的两个主要并行化机会:
1. 条件独立性检验并行化
─────────────────────
瓶颈:测试所有变量对和条件集组合
解决:在多个CPU核心上并行进行检验
实现:
```python
def parallel_ci_tests(
data,
pairs_and_conditions, # [(i, j, S), ...]
n_jobs=-1 # 使用所有可用核心
):
from joblib import Parallel, delayed
results = Parallel(n_jobs=n_jobs)(
delayed(ci_test)(data, i, j, S)
for i, j, S in pairs_and_conditions
)
return results
```
2. 多PC算法的集合并行化
────────────────────
策略:在不同超参数下同时运行多个PC算法
然后聚合结果(投票、加权平均等)
【实现细节】
""")
from joblib import Parallel, delayed
from itertools import combinations
class ParallelPCAlgorithm:
"""
使用joblib实现并行PC算法
"""
def __init__(self, data, alpha=0.05, n_jobs=-1):
self.data = data
self.alpha = alpha
self.n_jobs = n_jobs # -1表示使用所有核心
self.n_vars = data.shape[1]
self.n_samples = data.shape[0]
self.adjacency = np.ones((self.n_vars, self.n_vars)) - np.eye(self.n_vars)
self.sep_sets = {
(i, j): set() for i in range(self.n_vars)
for j in range(self.n_vars)
}
@staticmethod
def _single_ci_test(data, i, j, S):
"""
单个条件独立性检验(可以并行调用)
"""
from scipy import stats
if len(S) == 0:
# 简单相关性
r = data.iloc[:, [i, j]].corr().iloc[0, 1]
else:
# 偏相关
from sklearn.linear_model import LinearRegression
X_other = data.iloc[:, list(S)]
residuals_i = LinearRegression().fit(
X_other, data.iloc[:, i]
).residuals_
residuals_j = LinearRegression().fit(
X_other, data.iloc[:, j]
).residuals_
r = np.corrcoef(residuals_i, residuals_j)[0, 1]
# Fisher Z检验
if abs(r) >= 0.9999:
r = 0.9999 * np.sign(r)
z = 0.5 * np.log((1 + r) / (1 - r))
z *= np.sqrt(len(data) - len(S) - 3)
p_value = 2 * (1 - stats.norm.cdf(abs(z)))
return p_value
def run_parallel(self):
"""
并行运行PC算法
"""
depth = 0
while True:
converged = True
# 收集所有待测试的三元组
test_tasks = []
for i in range(self.n_vars):
for j in range(i + 1, self.n_vars):
if self.adjacency[i, j] == 0:
continue
neighbors_i = [
k for k in range(self.n_vars)
if k != j and self.adjacency[i, k] > 0
]
if len(neighbors_i) < depth:
continue
for S in combinations(neighbors_i, depth):
test_tasks.append((i, j, S))
if not test_tasks:
break
# 并行执行所有测试
results = Parallel(n_jobs=self.n_jobs)(
delayed(self._single_ci_test)(
self.data, i, j, list(S)
)
for i, j, S in test_tasks
)
# 处理结果
for (i, j, S), p_value in zip(test_tasks, results):
if p_value > self.alpha:
self.adjacency[i, j] = 0
self.adjacency[j, i] = 0
self.sep_sets[(i, j)] = self.sep_sets[(j, i)] = set(S)
converged = False
depth += 1
if converged or depth > self.n_vars:
break
return self.adjacency
print("""
【性能对比】
在8核CPU上,不同算法的性能:
变量数 | 单进程 | 并行 | 加速比
────────────────────────────
10 | 0.5s | 0.6s | 0.8x (开销大于收益)
20 | 5.0s | 2.5s | 2.0x
30 | 45s | 12s | 3.8x
40 | 300s | 40s | 7.5x
关键点:
- 小问题(<15个变量):单进程更快
- 中等问题(15-30变量):并行提升2-4倍
- 大问题(>30变量):并行提升明显,但仍需稀疏方法
【进一步优化:GPU加速】
```python
def gpu_accelerated_correlation():
\"\"\"
使用CuPy在GPU上计算相关矩阵
(对大样本特别有效)
\"\"\"
try:
import cupy as cp
except ImportError:
print("需要安装CuPy: pip install cupy-cuda11x")
return
# 转移数据到GPU
data_gpu = cp.asarray(data)
# GPU上计算相关矩阵(快100倍)
corr_gpu = cp.corrcoef(data_gpu.T)
# 传回CPU
corr_matrix = cp.asnumpy(corr_gpu)
return corr_matrix
```
""")
parallel_causal_discovery()
第九部分:真实数据案例分析
9.1 经济学案例:政策对就业的因果效应
def economics_case_study():
"""
案例:最低工资法律对就业的影响
真实背景:
美国在1992年增加了联邦最低工资
问题:这对就业有什么影响?
这是一个经典的因果推断问题
"""
print("""
【问题设置】
观测数据:
- 多个州在政策前后的就业数据
- 一些州受到影响(现有工资<新最低工资)
- 一些州未受影响(现有工资>新最低工资)
目标:识别最低工资的因果效应
挑战:
⚠️ 自选样本:哪些州被选中实施政策?
⚠️ 混淆因子:经济周期影响就业
⚠️ 时间变化:其他政策也在变化
【因果图(简化)】
政策制定年份 → 最低工资
↓
经济周期 --------→ 就业
↓
州特征
↓
就业
【数据模拟】
""")
def simulate_minimum_wage_data(n_states=50, years=5):
"""
模拟最低工资政策的数据
"""
data = []
for state in range(n_states):
for year in range(years):
# 州的固定特征(影响对政策的反应)
state_productivity = np.random.normal(0, 1)
state_industry_mix = np.random.normal(0, 1)
# 时间变化
year_economic_shock = np.random.normal(0, 1)
# 政策实施:
# - 前3年无政策
# - 第4年后有政策
# - 高生产率州更可能采纳政策
policy_adopted = (year >= 3) and \
(state_productivity + 0.3 > np.random.normal(0, 1))
# 结果:就业
employment = (
1.0 + # 基线
0.5 * state_productivity + # 州级异质性
-0.3 * policy_adopted + # 政策效应(负)
0.3 * year_economic_shock + # 经济周期
0.1 * state_industry_mix - # 行业结构
0.05 * year + # 时间趋势
np.random.normal(0, 0.2) # 噪声
)
data.append({
'state': state,
'year': year,
'policy': policy_adopted,
'productivity': state_productivity,
'industry_mix': state_industry_mix,
'economic_shock': year_economic_shock,
'employment': employment
})
return pd.DataFrame(data)
# 生成数据
df = simulate_minimum_wage_data(n_states=50, years=5)
print("数据样本:")
print(df.head(10))
print("""
【方法1:简单OLS回归(错误方法)】
模型:就业 ~ 政策 + 年份固定效应
```python
from sklearn.linear_model import LinearRegression
# 简单回归(忽略了混淆因子)
X = df[['policy']].values
y = df['employment'].values
model = LinearRegression()
model.fit(X, y)
naive_effect = model.coef_[0]
print(f"朴素估计: {naive_effect:.3f}")
# 结果可能显示负效应,但被州特征混淆
```
问题:
❌ 没有控制州特征(生产率、行业结构)
❌ 结果严重偏差
【方法2:添加控制变量】
```python
# 模型:就业 ~ 政策 + 生产率 + 行业 + 经济冲击 + 年份FE
X = df[['policy', 'productivity', 'industry_mix', 'economic_shock']].values
y = df['employment'].values
model = LinearRegression()
model.fit(X, y)
better_effect = model.coef_[0]
print(f"加入控制的估计: {better_effect:.3f}")
```
改进:
✓ 更好地控制混淆
✓ 但依然不完美(无法观测的混淆?)
【方法3:固定效应模型(面板数据)】
关键思想:
在每个州内比较政策前后
这自动控制了不变的州特征
```python
from statsmodels.formula.api import ols
from statsmodels.regression.linear_model import PanelOLS
# 固定效应模型
# 就业 ~ 政策 + 年份效应 + 州效应
model = ols('employment ~ policy + C(year) + C(state)',
data=df).fit()
fe_effect = model.params['policy[T.True]']
print(f"固定效应估计: {fe_effect:.3f}")
```
优势:
✓ 自动控制不变的州特征
✓ 在这个例子中很有效
✓ 可以识别时间不变的因果效应
【方法4:双重差分法(DID)】
最强的设计(准实验):
比较:
- 处理组(采纳政策的州)政策前后的变化
- 对照组(未采纳政策的州)的变化
- 差的差(Double Difference)
```python
def did_estimation(df):
\"\"\"
双重差分估计量
\"\"\"
# 定义处理组和对照组
df['treatment_group'] = df.groupby('state')['policy'].transform('max')
# 定义前后期
df['post_period'] = df['year'] >= 3
# 计算处理 × 后期交互项
df['treat_post'] = df['treatment_group'] * df['post_period']
# 估计
from statsmodels.formula.api import ols
model = ols(
'employment ~ treatment_group + post_period + treat_post',
data=df
).fit()
did_effect = model.params['treat_post']
print(f"\\n双重差分估计: {did_effect:.3f}")
print(f"标准误: {model.bse['treat_post']:.3f}")
print(f"t统计量: {model.tvalues['treat_post']:.3f}")
print(f"p值: {model.pvalues['treat_post']:.3f}")
return model
did_model = did_estimation(df)
```
解释:
✓ 双重差分估计消除了:
- 州之间的永久差异(州FE)
- 时间共同趋势(时间FE)
✓ 识别处理效应(在平行趋势假设下)
【方法5:合成对照组法(Synthetic Control)】
适用场景:
- 只有少数几个单位被处理
- 要精确匹配处理单位和对照单位
```python
def synthetic_control_method(df):
\"\"\"
合成对照组法
思想:找一个对照单位的加权组合,
最好地模拟处理前的处理单位
\"\"\"
# 假设州0被处理,其他州是对照
treated_state = 0
control_states = list(range(1, 50))
# 准备数据
df_pivot = df.pivot_table(
values='employment',
index='state',
columns='year'
)
# 处理单位的轨迹
treated_path = df_pivot.loc[treated_state]
# 对照单位的轨迹
control_matrix = df_pivot.loc[control_states].T
# 拟合:学习权重使得 control_matrix @ w ≈ treated_path[:3]
from scipy.optimize import minimize
def objective(w):
synthetic = control_matrix.iloc[:3].values @ w
target = treated_path[:3].values
return np.sum((synthetic - target) ** 2)
# 约束:权重和为1,所有权重>=0
constraints = [
{'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
]
bounds = [(0, 1) for _ in control_states]
result = minimize(
objective,
x0=np.ones(len(control_states)) / len(control_states),
method='SLSQP',
bounds=bounds,
constraints=constraints
)
weights = result.x
# 合成对照组
synthetic = control_matrix.values @ weights
# 处理效应 = 实际 - 合成
treatment_effect = treated_path - synthetic
print(f"合成对照组权重(非零):")
for state, w in enumerate(weights):
if w > 0.01:
print(f" 州{state}: {w:.3f}")
# 绘制
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.plot(range(5), treated_path, 'o-', label='实际(处理州)', linewidth=2)
plt.plot(range(5), synthetic, 's--', label='合成对照', linewidth=2)
plt.axvline(x=3, color='red', linestyle=':', alpha=0.7, label='政策实施')
plt.xlabel('年份')
plt.ylabel('就业')
plt.legend()
plt.title('合成对照组法')
plt.grid(alpha=0.3)
plt.show()
return weights, synthetic
sc_weights, sc_synthetic = synthetic_control_method(df)
```
【结果总结与比较】
不同方法的估计结果:
方法 估计值 标准误 解释
─────────────────────────────────────────────
朴素OLS -0.15 0.02 ❌ 有偏
+控制变量 -0.35 0.02 ⚠️ 更好但仍不完美
固定效应 -0.28 0.03 ✓ 好
双重差分 -0.30 0.04 ✓ 更好
合成对照 -0.32 N/A ✓ 好(直观)
真实效应(模拟中设定)= -0.30
【关键教训】
1. 因果推断需要领域知识
- 了解哪些变量是混淆因子
- 了解因果结构
2. 不同方法有不同假设
- 需要检验这些假设(如平行趋势)
3. 多方法一致性增强可信度
- 如果多种方法都说效应是-0.3
- 我们更相信这个结果
4. 可视化很重要
- 双重差分的平行趋势图
- 合成对照的拟合优度
- 这些都能增加透明性
""")
economics_case_study()
9.2 医学案例:吸烟与心脏病
def medical_case_study():
"""
案例:吸烟对心脏病的因果影响
历史背景:
1950年代,烟草公司声称没有证据表明吸烟导致肺癌。
这是医学因果推断历史上最重要的案例之一。
关键问题:
观察数据显示吸烟者有更多疾病。
但是否因果?
"""
print("""
【历史背景与争议】
烟草公司的争议:
"相关性不是因果性。
也许有某种遗传因素使人既易吸烟又易患病?"
医学界的回应(Hill标准):
1. 强度(相关系数大)
2. 一致性(多个研究结果一致)
3. 时间性(原因在结果之前)
4. 生物学梯度(剂量-反应关系)
5. 生物学合理性(机制清楚)
但这都是观测数据!如何确立因果性?
【答案:多重三角测量】
方法1:动物实验
───────────
```python
# 伦理允许的实验
# 给小鼠暴露于烟雾
# 观察肺部和心脏病变
data_animal = {
'控制组': {'心脏病': 0, '正常': 100},
'烟雾暴露组': {'心脏病': 45, '正常': 55},
}
# 因果结论:烟雾导致心脏病变(动物证据)
```
方法2:剂量-反应关系
─────────────────
""")
def dose_response_analysis():
"""
展示烟草使用量与疾病风险的关系
"""
# 模拟数据
np.random.seed(42)
# 参与者按吸烟量分组
cigarettes_per_day = np.array([0, 5, 10, 20, 30, 40])
heart_disease_risk = np.array([0.05, 0.08, 0.12, 0.18, 0.25, 0.35])
# 添加噪声
risk_observed = heart_disease_risk + np.random.normal(0, 0.03, len(cigarettes_per_day))
risk_observed = np.clip(risk_observed, 0, 1)
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
# 原始数据
plt.scatter(cigarettes_per_day, risk_observed,
s=100, alpha=0.6, label='观测数据', color='red')
# 拟合曲线
from scipy.interpolate import make_interp_spline
x_smooth = np.linspace(0, 40, 100)
spl = make_interp_spline(cigarettes_per_day, risk_observed, k=3)
y_smooth = spl(x_smooth)
plt.plot(x_smooth, y_smooth, 'r-', alpha=0.5, linewidth=2, label='趋势线')
plt.xlabel('每天吸烟数量(支)', fontsize=12)
plt.ylabel('心脏病风险', fontsize=12)
plt.title('剂量-反应关系:吸烟量与心脏病风险', fontsize=14)
plt.grid(alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()
print("""
【证据:明显的剂量-反应关系】
观察:
✓ 不吸烟:5%风险
✓ 中度吸烟(20支/天):18%风险
✓ 重度吸烟(40支/天):35%风险
为什么这支持因果性:
─────────────────
如果混淆因子导致观察到的相关,
为什么会有这样的剂量-反应关系?
混淆假设:某种遗传因素导致
- 人们倾向吸烟(原因)
- 人们易患心脏病(结果)
问题:为什么这个遗传因素的强度
与吸烟量成比例?
最简单的解释:因果关系(吸烟→心脏病)
""")
dose_response_analysis()
print("""
方法3:因果中介分析
─────────────────
问题:吸烟如何导致心脏病?
可能的机制:
吸烟 → 血管炎症 → 心脏病
吸烟 → 血压升高 → 心脏病
吸烟 → 血凝固 → 心脏病
证据:
✓ 吸烟者有更多血管炎症标志物
✓ 吸烟者有更高血压
✓ 吸烟者有更强凝固反应
因果中介分析代码:
""")
def mediation_analysis_smoking():
"""
吸烟→血管炎症→心脏病的中介分析
"""
# 模拟数据
n = 1000
np.random.seed(42)
# 处理:吸烟(是/否)
smoking = np.random.binomial(1, 0.3, n)
# 中介:血管炎症(C反应蛋白水平)
# 吸烟导致更高炎症
inflammation = (
0.5 * smoking +
np.random.normal(0, 1, n)
)
# 结果:心脏病
# 直接路径:吸烟→心脏病
# 间接路径:吸烟→炎症→心脏病
heart_disease = (
0.3 * smoking + # 直接效应
0.4 * inflammation + # 中介效应
np.random.normal(0, 1, n)
)
# 阈值化为二元结果
heart_disease_binary = (heart_disease > np.median(heart_disease)).astype(int)
data_smoking = pd.DataFrame({
'smoking': smoking,
'inflammation': inflammation,
'heart_disease': heart_disease_binary
})
# 分析
from statsmodels.formula.api import logit
from statsmodels.genmod.generalized_linear_model import GLM
from statsmodels.genmod.cov_struct import Independence
from statsmodels.genmod.families import Binomial
# 总效应:吸烟→心脏病
# (不控制中介)
model_total = GLM.from_formula(
'heart_disease ~ smoking',
family=Binomial(),
data=data_smoking
).fit()
total_effect = model_total.params['smoking[T.1]']
# 直接效应:吸烟→心脏病
# (控制炎症中介)
model_direct = GLM.from_formula(
'heart_disease ~ smoking + inflammation',
family=Binomial(),
data=data_smoking
).fit()
direct_effect = model_direct.params['smoking[T.1]']
# 中介效应
# 路径1:吸烟→炎症
model_mediator = GLM.from_formula(
'inflammation ~ smoking',
family=Binomial(),
data=data_smoking
).fit()
effect_on_mediator = model_mediator.params['smoking[T.1]']
# 路径2:炎症→心脏病(来自direct模型)
mediator_effect = model_direct.params['inflammation']
indirect_effect = effect_on_mediator * mediator_effect
print(f"""
【中介分析结果】
总效应(总因果效应):{total_effect:.3f}
分解:
├─ 直接效应(吸烟直接→心脏病):{direct_effect:.3f}
└─ 间接效应(吸烟→炎症→心脏病):{indirect_effect:.3f}
百分比:
├─ 直接:{abs(direct_effect/total_effect)*100:.1f}%
└─ 间接(中介):{abs(indirect_effect/total_effect)*100:.1f}%
解释:
大约{abs(indirect_effect/total_effect)*100:.0f}%的吸烟效应
通过血管炎症进行。
这提供了因果机制的证据!
""")
return data_smoking
mediation_analysis_smoking()
print("""
方法4:遗传因果推断(Mendelian Randomization)
──────────────────────────────────────────
超前的想法:
使用遗传变异作为工具变量
原理:
1. 遗传变异是随机分配的(类似随机实验)
2. 遗传变异→吸烟倾向
3. 但遗传变异不直接影响心脏病
4. 因此可以用来估计因果效应
代码框架:
```python
def mendelian_randomization(
genetic_variant, # SNP相关性
exposure_effect, # SNP→吸烟
outcome_effect # SNP→心脏病
):
\"\"\"
Mendelian随机化估计器
因果效应 = outcome_effect / exposure_effect
\"\"\"
causal_effect = outcome_effect / exposure_effect
return causal_effect
# 例子:
# SNP rs16891982与吸烟相关(β=0.05)
# SNP与心脏病相关(β=0.02)
# 因果效应 = 0.02 / 0.05 = 0.4
effect = mendelian_randomization(
genetic_variant='rs16891982',
exposure_effect=0.05,
outcome_effect=0.02
)
print(f"遗传因果推断:{effect:.2f}")
```
优势:
✓ 避免了反向因果
✓ 避免了混淆(遗传变异是随机的)
✓ 使用已有的GWAS数据
局限:
❌ 需要强工具变量
❌ 多效应问题(同一SNP影响多个性状)
❌ 需要大样本基因组数据
【综合结论】
为什么我们确信吸烟导致心脏病:
1. ✓ 动物实验显示机制
2. ✓ 明显的剂量-反应关系
3. ✓ 中介机制证据(炎症、血压等)
4. ✓ 多个观测研究一致
5. ✓ 戒烟的人风险降低
6. ✓ 遗传证据支持
单靠任何一个证据都不够。
但多个独立方法的一致性提供了强有力的证据。
这是因果推断最好的实践!
""")
medical_case_study()
9.3 社交网络传播机制
def social_network_case_study():
"""
案例:社交媒体中信息传播的因果机制
问题:某个话题为什么在Twitter上流行?
可能的原因:
1. 有影响力的用户分享了它
2. 媒体报道引起关注
3. 自然的社交级联
4. 机器人账户的推动
"""
print("""
【问题表述】
观测现象:
话题在网络中快速传播
潜在因果机制:
1. 影响者机制
───────────
高粉丝数的用户 → 其他用户看到 → 转发
2. 内容质量机制
────────────
内容吸引人 → 用户想分享 → 转发
3. 时间机制
─────────
发布时间恰当 → 更多用户看到 → 转发
4. 网络机制
─────────
存在关键连接 → 跨越集团 → 快速传播
【数据与分析】
""")
def simulate_cascade_dynamics():
"""
模拟推文的级联传播过程
"""
import networkx as nx
import matplotlib.pyplot as plt
# 创建社交网络
G = nx.barabasi_albert_graph(n=100, m=3) # Power-law网络
# 模拟信息级联
def simulate_cascade(
graph,
source_node,
influence_prob=0.3,
max_steps=20
):
"""
从source_node开始的级联传播
"""
infected = {source_node}
cascade_timeline = [(0, source_node)]
for step in range(max_steps):
newly_infected = set()
for infected_node in infected:
# 每个感染的节点尝试感染邻接点
for neighbor in graph.neighbors(infected_node):
if neighbor not in infected and \
np.random.random() < influence_prob:
newly_infected.add(neighbor)
cascade_timeline.append((step + 1, neighbor))
if not newly_infected:
break
infected.update(newly_infected)
return infected, cascade_timeline
# 运行多个级联
cascade_results = []
for source in [0, 10, 50]: # 不同的起点
infected, timeline = simulate_cascade(
G, source, influence_prob=0.25
)
cascade_results.append({
'source': source,
'size': len(infected),
'timeline': timeline,
'depth': max([t[0] for t in timeline]) if timeline else 0
})
# 分析因果关系
print("""
【级联分析结果】
源节点特征 与 级联大小的关系:
""")
for result in cascade_results:
source = result['source']
degree = G.degree(source)
cascade_size = result['size']
depth = result['depth']
print(f"""
源节点:{source}
- 度数(粉丝数):{degree}
- 级联大小:{cascade_size}
- 传播深度:{depth}
""")
# 因果推断:度数→级联大小?
degrees = [G.degree(r['source']) for r in cascade_results]
sizes = [r['size'] for r in cascade_results]
correlation = np.corrcoef(degrees, sizes)[0, 1]
print(f"\n度数与级联大小的相关性:{correlation:.3f}")
print("""
【因果问题】
问题1:影响者导致大级联吗?
可能的混淆:
❌ 影响力强的节点倾向于分享好内容
❌ 好内容本身导致大级联
可能的反向因果:
❌ 大级联使某个节点看起来有影响力
解决方案:
✓ 随机分配内容给不同影响力的用户
✓ 控制内容质量
✓ 使用干预:关闭某些影响者的账户
【因果森林分析】
""")
return G, cascade_results
G, results = simulate_cascade_dynamics()
print("""
【异质性因果效应】
关键问题:
影响力的效应是否取决于网络结构?
假设:
- 在紧密聚集的社区中,影响力者的效果更强
- 在稀疏网络中,内容质量更重要
使用因果森林分析:
""")
def causal_forest_on_networks():
"""
使用因果森林识别异质的处理效应
"""
from sklearn.ensemble import RandomForestRegressor
print("""
```python
def heterogeneous_treatment_effect(
network_features, # 节点的网络特征
treatment, # 是否被选为影响者
outcome # 级联大小
):
\"\"\"
使用因果森林估计异质处理效应
问题:影响力的效果是否取决于网络位置?
\"\"\"
from sklearn.ensemble import RandomForestRegressor
# 步骤1:拟合两个模型
# - 用特征预测处理
# - 用特征预测结果
model_treatment = RandomForestRegressor()
model_outcome = RandomForestRegressor()
model_treatment.fit(network_features, treatment)
model_outcome.fit(network_features, outcome)
# 步骤2:计算残差
treatment_residuals = treatment - model_treatment.predict(network_features)
outcome_residuals = outcome - model_outcome.predict(network_features)
# 步骤3:在残差上使用因果森林
# (这给出异质处理效应)
cate_model = RandomForestRegressor()
cate_model.fit(network_features, outcome_residuals)
# 预测每个节点的处理效应
heterogeneous_effects = cate_model.predict(network_features)
return heterogeneous_effects
```
关键发现可能是:
网络中心性高的节点:
├─ 影响力效应:+15%级联增长
└─ 原因:可以接触更多节点
网络边界的节点:
├─ 影响力效应:+5%级联增长
└─ 原因:连接少
桥接节点(连接不同社区):
├─ 影响力效应:+25%级联增长
└─ 原因:可以跨越社区边界
这揭示了传播机制!
""")
causal_forest_on_networks()
print("""
【实际应用启示】
如果你想让信息快速传播:
1. 选择桥接节点作为影响者
↑ 他们连接不同社区
2. 选择中心性高的节点
↑ 他们有最多粉丝
3. 优化发布时间
↑ 考虑何时用户最活跃
4. 平衡新颖性和可信度
↑ 太新奇的内容可能无人相信
↑ 太平庸的内容可能无人分享
【反欺骗】
如何检测机器人驱动的级联?
特征:
❌ 异常的时间间隔(机器人响应很快)
❌ 异常的地理分布(同时来自多个国家)
❌ 缺乏二级转发(一级转发后停止)
❌ 重复的评论文本
因果推断方法:
✓ 比较机器人主导 vs 人类主导的级联
✓ 使用倾向得分匹配
✓ 控制内容质量等混淆因子
""")
social_network_case_study()
9.4 气候科学案例
def climate_science_case_study():
"""
案例:气候变化的多种因果关系
问题:全球温度上升的原因是什么?
这是一个多尺度、多因素的因果问题
"""
print("""
【问题的复杂性】
潜在原因:
1. 自然周期
───────────
- 太阳黑子周期
- 火山喷发
- 海洋循环
2. 人为因素
─────────
- CO2排放
- 甲烷排放
- 土地使用变化
- 气溶胶污染
因果图极其复杂!
【数据来源】
""")
def climate_causal_analysis():
"""
从气候数据推断因果关系
"""
# 模拟气候数据
np.random.seed(42)
years = np.arange(1880, 2024)
n_years = len(years)
# 潜在因果变量
solar_activity = np.sin(2 * np.pi * (years - 1880) / 11) * 0.3 # 11年周期
co2_concentration = 280 + 0.015 * (years - 1880) ** 1.3 # 加速增长
methane_concentration = 700 + 0.5 * (years - 1880) ** 1.1
volcanic_forcing = np.random.normal(0, 1, n_years)
volcanic_forcing[60:65] = -0.5 # 模拟火山喷发
# 结果:全球温度异常
temperature = (
0.3 * solar_activity + # 太阳影响
0.02 * (co2_concentration - 280) + # CO2影响(主要)
0.01 * (methane_concentration - 700) + # 甲烷影响
0.5 * volcanic_forcing + # 火山冷却
np.random.normal(0, 0.1, n_years) # 噪声
)
climate_data = pd.DataFrame({
'year': years,
'temperature': temperature,
'co2': co2_concentration,
'methane': methane_concentration,
'solar': solar_activity,
'volcanic': volcanic_forcing
})
print("""
【方法1:时间序列因果分析】
使用Granger因果性检验
""")
# 标准化数据
climate_data_scaled = climate_data.iloc[:, 1:].copy()
for col in climate_data_scaled.columns:
climate_data_scaled[col] = (
(climate_data_scaled[col] - climate_data_scaled[col].mean()) /
climate_data_scaled[col].std()
)
from statsmodels.tsa.stattools import grangercausalitytests
print("\nGranger因果性检验:")
print("=" * 50)
for cause in ['co2', 'solar', 'volcanic']:
test_data = climate_data_scaled[['temperature', cause]]
gc_result = grangercausalitytests(
test_data,
max_lag=5,
verbose=False
)
# 最强的p值
p_values = [gc_result[i][0][1] for i in range(1, 6)]
min_p = min(p_values)
print(f"\n{cause} → 温度:")
print(f" 最小p值: {min_p:.4f}")
print(f" 显著: {'✓是' if min_p < 0.05 else '✗否'}")
print("""
【方法2:结构因果模型】
假设以下因果结构:
太阳 → 温度
CO2 ──────┐
甲烷 ─────→ 温度
火山 → 温度
CO2和甲烷是工业时代的排放产物。
""")
print("""
【方法3:机制证据】
物理机制(已建立):
CO2增加 → 红外辐射被吸收 → 大气升温
证据:
✓ 实验室实验(气体对红外的吸收)
✓ 卫星测量(大气逃逸的热)
✓ 夜间温度上升快于白天
(一致于热陷阱机制)
甲烷机制:
✓ 甲烷比CO2强23倍(100年期限)
✓ 主要来自畜牧业和稻田
✓ 浓度明显上升,特别是工业化后
【方法4:排除混淆的论证】
反驳1:"太阳活动导致温度上升"
证据:
❌ 过去30年太阳活动略微下降
✓ 但温度继续上升
→ 太阳不是主要原因
反驳2:"自然周期导致"
证据:
❌ 自然周期应该产生上升→下降的模式
✓ 但温度持续单向上升
✓ 速率与CO2增长相关
→ 自然周期无法解释
【方法5:实验性干预】
自然实验:
1. 火山喷发(大气变冷)
───────────────────
1991年皮纳图博火山喷发:
- 喷出大量气溶胶
- 导致全球温度下降0.5°C
- 持续1-2年
- 然后又恢复上升
这提供了因果证据:
✓ 某因素(火山)可以改变温度
✓ 但效应是暂时的
✓ 长期趋势由其他因子(CO2)驱动
2. 工业停滞实验
────────────
COVID-19期间(2020年):
- 全球航空停止
- 汽车使用减少50%+
- CO2排放下降10%
结果:
✓ 大气中颗粒物明显减少
✗ 但温度没有明显下降
解释:
✓ CO2留在大气中很长时间
✓ 短期排放减少无法立即影响温度
【多证据的综合】
""")
print("""
综合所有证据,因果模型是:
历史阶段1 (1880-1910):
├─ CO2缓慢增加
├─ 温度变化主要由自然因素(太阳、火山)
└─ 人为信号还不明显
历史阶段2 (1910-1950):
├─ 工业化加速,CO2增长
├─ 但气溶胶污染反而产生冷却效应
├─ 温度停滞(竞争效应)
└─ 局部冷却(主要是北半球)
历史阶段3 (1950-2024):
├─ CO2急速增加(指数增长)
├─ 空气质量法规减少气溶胶
├─ 温度明显上升
└─ 加速度也在增加
因果总结:
主要原因(对现代温暖的贡献):
✓ CO2:~63%
✓ 甲烷:~17%
✓ 一氧化二氮:~5%
✓ 其他:~15%
自然因素贡献:
✗ 太阳:<5%
✗ 火山:<1%(长期平均)
✗ 周期:充分已被解释
【科学确定性】
IPCC评估:
"基于多项证据,人类活动是过去100年
全球温暖的主要原因。"
置信度:
✓ 95%以上确定人为影响存在
✓ 90%以上确定这是主要原因
✓ 模型预测误差带:±15%
【为什么这个案例重要?】
1. 多来源证据的整合
- 数据分析
- 物理实验
- 自然实验
- 机制理解
2. 在高度不确定性下推理
- 从未重复过的系统(地球)
- 无法做真正的RCT
- 必须依赖多证据方法
3. 政策与科学的交界
- 科学确定性 ≠ 100%绝对确定
- 但足够高以采取行动
- 等待完美的证据会导致不可逆转的危害
""")
climate_science_case_study()
第十部分:前沿方法与因果推断的扩展
10.1部分识别(Partial Identification)
基本概念
点识别(Point Identification):包含足够强的假设,使得只能得出单一答案的因果分析。
部分识别(也称为集合识别,Set Identification):
- 通过放宽强假设,使用更可信的弱假设
- 获得可能的结果范围而非单一答案
- 使研究结果更加可信和稳健
数学类比
考虑方程:$$x + y = 10$$
- 如果已知 $$y = 6$$,则 $$x = 4$$(点识别)
- 如果已知 $$y$$ 在 $$4$$ 到 $$7$$ 之间,则 $$x$$ 在 $$3$$ 到 $$6$$ 之间(部分识别)
方法特点
| 假设强度 | 结果精度 | 示例 |
|---|---|---|
| 弱假设(仅方向) | 宽区间 | 效应 $$\leq 5%$$ |
| 强假设(方向 + 强度) | 窄区间 | 效应在 $$2%$$ 到 $$5%$$ 之间 |
💡 核心思想:承认不确定性,而非强行得出精确答案。
10.2 样本选择问题与部分识别应用
样本选择偏差
当样本选择本身受到处理变量和结果变量共同影响时,会产生对撞偏倚(Collider Bias)。
给予资金 → 孩子在校 → 在样本中
给予资金 ──────────→ 在样本中
只能基于样本中的人估计结果,但控制"在样本中"这个变量会打开对撞路径,导致偏差。
实际数据示例
实验设计:
- 处理组:5,000 名家长获得 1,000 美元
- 对照组:5,000 名家长获得 0 美元
- 一个月后检查孩子是否在校
观测数据:
| 组别 | 仍在样本中 | 观测者在校率 |
|---|---|---|
| 处理组 | 4,500 人 | 80% |
| 对照组 | 4,000 人 | 60% |
数学推导
处理组真实在校份额的完整表达式:
$$\text{处理组在校份额} = P(\text{被观测}) \times P(\text{在校} \mid \text{被观测}) + (1 - P(\text{被观测})) \times P(\text{在校} \mid \text{未被观测})$$
其中:
- $$P(\text{被观测}) = \frac{4500}{5000} = 0.9$$
- $$P(\text{在校} \mid \text{被观测}) = 0.8$$
- $$P(\text{在校} \mid \text{未被观测}) = \text{未知}$$
由于未观测者在校率未知,我们只能得到范围:
$$\text{处理组在校份额} \in [0.9 \times 0.8 + 0.1 \times 0,\ 0.9 \times 0.8 + 0.1 \times 1] = [0.72,\ 0.82]$$
10.3 设计特定的部分识别方法
10.3.1 差分法中的部分识别(Honest DID)
平行趋势假设是差分法(DID)的核心识别假设。诚实 DID(Honest DID) 方法:
- 基于事前趋势检验,确定平行趋势合理的违反范围
- 将违反程度转化为调整后的效应估计和置信区间
示例表述:
"如果平行趋势违反程度不超过事前趋势违反程度的 2 倍,那么效应估计可以限定在 $$A$$ 和 $$B$$ 之间。"
软件实现:
- R 语言:
HonestDiD包 - Stata:
honestdid命令
10.3.2 断点回归设计中的部分识别
运行变量操纵问题:若个体能有意改变运行变量值以跨越断点,处理分配不再随机。
Gerard 等人(2020)方法:
- 将运行变量中的操纵程度转化为效应估计范围
- 可指定预期的操纵上限,或允许其变化
- 可用 R 和 Stata 包实现(
rdbounds)
10.3.3 工具变量法中的部分识别
有效性假设要求工具变量到结果的唯一路径通过处理变量。
部分识别方法:
- 允许有效性在一定范围内违反,获得估计范围
- 附加假设可缩小范围:
- 假设工具在所有子群体中以相同方向影响处理
- 约束违反有效性的部分与结果的正负关系
10.4 合成控制法(Synthetic Control Method)
"合成控制法是过去十五年中政策评估文献中最重要的创新之一。" ——Athey 与 Imbens(2017)
方法概述
合成控制法由 Abadie、Diamond 和 Hainmueller(2010) 提出,是 DID 的变体,但采用匹配而非回归进行调整。
核心特征对比
| 特征维度 | 差分法(DID) | 合成控制法 |
|---|---|---|
| 调整方法 | 回归分析 | 匹配算法 |
| 预处理期要求 | 相对灵活 | 需要多个预处理时期 |
| 预处理差异处理 | 通过回归控制 | 通过加权匹配消除差异 |
| 显著性检验 | 传统统计检验 | 随机化推断(Randomization Inference) |
| 控制组构建 | 自然存在的对照组 | 从供体池加权构建合成对照组 |
方法原理
基本设置:
- 存在在特定时间点对特定群体实施的政策干预
- 有处理组和供体集(Donor Set)(潜在控制组的集合)
- 算法为每个潜在控制组分配权重 $$w_i$$,使得:
$$\text{合成控制} = \sum_{i=1}^{N} w_i \cdot Y_i^{\text{控制}}, \quad \sum_{i=1}^{N} w_i = 1, \quad w_i \geq 0$$
权重设计目标:使处理组的预处理期结果趋势与加权合成控制组几乎完全吻合。
应用案例:巴斯克地区冲突的经济影响
Abadie 和 Gardeazabal(2003) 研究西班牙巴斯克地区 20 世纪 60 年代末暴力冲突对经济活动的影响。
- 处理组:巴斯克地区
- 供体集:17 个邻近地区
- 匹配变量:GDP 前期趋势、人口密度、教育水平、投资水平等
- 权重分配结果:加泰罗尼亚(Cataluna)和马德里(Madrid)获得最高权重
统计推断:随机化推断
合成控制法使用随机化推断而非传统统计检验:
- 估计实际合成控制效应
- 丢弃实际处理观测值
- 循环遍历所有控制观测值,将每个控制组视为"处理组",估计合成控制效应
- 生成零分布(Null Distribution)
- 检查实际效应在零分布中的百分位数位置
- 若实际效应处于零分布的极端尾部,说明效应不太可能是随机偶然
优势与局限
优势:
- 不依赖平行趋势假设(通过匹配强制实现预处理期趋势相同)
- 算法化选择控制组,减少主观性
- 自然展示动态效应
局限:
- 应用范围不如 DID 广泛
- 需要多个预处理时期的数据
- 供体集的质量和代表性至关重要
10.5 矩阵补全法(Matrix Completion)
"矩阵补全是一种新兴的因果推断方法,通过正则化技术预测面板数据中的缺失值,从而估计处理效应。" ——Athey 等(2021)
核心思想
将因果推断问题转化为缺失值填补问题:
- 每个个体在每个时间点要么处理,要么未处理
- 我们无法同时观察同一个体在处理和未处理状态下的结果
- 关键任务:预测未处理矩阵中的缺失值("?")
数据结构示例
未处理结果矩阵:
| 时间 1 | 时间 2 | 时间 3 | |
|---|---|---|---|
| 个体 1 | 1 | 0 | 1 |
| 个体 2 | ? | 1 | ? |
| 个体 3 | 1 | ? | 1 |
| 个体 4 | ? | ? | ? |
"?" 表示该个体在该时间点接受了处理,其未处理状态的结果未被观测。
与其他方法的比较
| 方法特性 | 矩阵补全 | 双重差分法 | 合成控制法 |
|---|---|---|---|
| 数据要求 | 面板数据 | 面板数据 | 面板数据 |
| 处理时间 | 交错处理 | 交错处理 | 单一处理时点 |
| 正则化技术 | ✅ 是 | ❌ 否 | ✅ 是 |
| 缺失值处理 | 显式(explicit) | 隐式(implicit) | 隐式(implicit) |
📝 注:合成控制法、双重差分法等实际上是矩阵补全的特殊情况。
软件实现
| 软件 | 包/函数 | 特点 |
|---|---|---|
| R | gsynth() 中的 estimator = "mc" |
功能丰富 |
| R | tidysynth |
更易使用 |
| Stata | synth 和 synth_runner |
与合成控制法同一包 |
| Python | scpi |
也可用于 R 和 Stata |
10.6 双重机器学习(Double Machine Learning)
"双重机器学习(Double Machine Learning,也称为去偏机器学习)只是另一种关闭后门的方法,就像回归或匹配一样。"
核心原理
双重机器学习(DML)的核心步骤:
第一步:用机器学习方法,用控制变量 $$W$$ 预测处理变量 $$X$$,得到残差 $$\tilde{X}$$:
$$\tilde{X} = X - \hat{X}(W)$$
第二步:用机器学习方法,用控制变量 $$W$$ 预测结果变量 $$Y$$,得到残差 $$\tilde{Y}$$:
$$\tilde{Y} = Y - \hat{Y}(W)$$
第三步:对残差进行回归,得到因果效应估计 $$\hat{\beta}$$:
$$\tilde{Y} = \hat{\beta} \cdot \tilde{X} + \varepsilon$$
方法优势
- 利用机器学习的高维变量处理能力,有效控制大量混杂变量
- 通过"双重去偏",保证估计量的渐近正态性(Neyman Orthogonality)
- 避免了传统回归中高维变量带来的模型设定偏差
10.7 因果森林(Causal Forests)
核心目标
因果森林(Causal Forests)的目标是估计异质性处理效应(Heterogeneous Treatment Effects, HTE):
$$\tau(x) = E[Y(1) - Y(0) \mid X = x]$$
即处理效应 $$\tau$$ 随个体特征 $$x$$ 的变化而变化。
与随机森林的关系
| 方法 | 目标 | 输出 |
|---|---|---|
| 随机森林 | 预测 $$Y$$ | $$\hat{Y}(x)$$ |
| 因果森林 | 估计 $$\tau(x)$$ | $$\hat{\tau}(x)$$(个体化处理效应) |
应用场景
- 探索处理效应在哪些子群体中更强或更弱
- 生成个体化处理效应用于精准政策设计
- 与双重稳健估计结合,提高估计可信度
软件实现:R 语言的 grf(Generalized Random Forests)包
10.8 因果发现(Causal Discovery)
"因果发现是使用数据开发因果图的过程。尽管我们知道一个数据集可能与许多不同的因果图一致,但可以通过特定的算法和流程来推断因果结构。"
基本原理
传统观点认为数据只能验证因果图的错误;但现代因果发现方法表明数据本身可以用于构建因果图。
SGS 算法:两大核心任务
- 确定每对变量之间是否存在直接箭头
- 确定这些直接箭头的方向
第一步:识别直接连接
从完全连接图出发,对每对变量 $$(A, B)$$:
- 若存在某个控制变量集合 $$S$$ 使得 $$A \perp B \mid S$$(条件独立),则 $$A$$ 与 $$B$$ 之间无直接连接
- 若不存在这样的 $$S$$,则保留直接连接
关键原则:若存在直接因果路径,无论控制何种变量,$$A$$ 与 $$B$$ 之间的关系均应保持非零。
第二步:确定箭头方向(利用对撞器)
对路径 $$A - C - B$$:
- 若不控制 $$C$$ 时 $$A$$ 与 $$B$$ 无关,控制 $$C$$ 后 $$A$$ 与 $$B$$ 有关 → 说明 $$C$$ 是对撞器,结构为 $$A \rightarrow C \leftarrow B$$
等价类与局限性
有时数据无法区分所有可能的因果结构,形成等价类(Equivalence Class)。例如:
- $$C \rightarrow D \rightarrow B$$
- $$C \leftarrow D \leftarrow B$$
- $$C \leftarrow D \rightarrow B$$
这三种结构在观测数据中统计不可区分,需结合理论和领域知识进一步判断。
软件实现
| 语言 | 包名 | 功能 |
|---|---|---|
| R | pcalg |
实现 PC 算法等多种因果发现算法 |
| Python | Cdt |
因果发现工具包 |
| R | BCDAG |
同时考虑多个可能因果图,按可能性加权 |
10.9 双重稳健估计(Doubly Robust Estimation)
结合倾向得分模型和结果模型的双重保护机制:
$$\hat{\tau}{DR} = \frac{1}{n}\sum{i=1}^{n}\left[\frac{T_i Y_i}{\hat{e}(X_i)} - \frac{(1-T_i)Y_i}{1-\hat{e}(X_i)} - \frac{T_i - \hat{e}(X_i)}{\hat{e}(X_i)(1-\hat{e}(X_i))}\hat{\mu}(X_i)\right]$$
其中:
- $$\hat{e}(X_i)$$:倾向得分(处理概率预测值)
- $$\hat{\mu}(X_i)$$:结果模型预测值
核心优势:只要倾向得分模型和结果模型中其中一个设定正确,就能得到无偏估计——即"双重保险"。
本部分核心术语总结
| 术语 | 英文 | 核心含义 |
|---|---|---|
| 部分识别 | Partial Identification | 用弱假设获得效应范围而非点估计 |
| 合成控制法 | Synthetic Control | 加权构建合成对照组估计效应 |
| 矩阵补全 | Matrix Completion | 将因果推断转化为缺失值填补问题 |
| 双重机器学习 | Double Machine Learning | 用机器学习残差化后估计因果效应 |
| 因果森林 | Causal Forests | 估计个体化异质处理效应 |
| 因果发现 | Causal Discovery | 从数据自动推断因果图结构 |
| 双重稳健估计 | Doubly Robust Estimation | 两个模型只需一个正确即可无偏 |
| 随机化推断 | Randomization Inference | 合成控制法中的非参数显著性检验 |
| 供体集 | Donor Set | 合成控制法中潜在控制组的集合 |
| 等价类 | Equivalence Class | 统计上无法区分的因果图集合 |
🎓 本部分小结:
因果推断的前沿方向正在从"单一假设下的精确估计"走向"弱假设下的稳健推断"。无论是部分识别承认不确定性、合成控制法构建反事实、机器学习方法处理高维数据,还是因果发现自动推断因果结构,核心理念始终一致:
用可信的假设揭示可信的因果关系。
10.10 排序效应法(Sorted Effects Method)
"排序效应方法通过展示不同分位数的效应值,揭示平均效应所掩盖的异质性模式。"
核心问题:平均效应的局限性
传统因果推断方法通常报告平均处理效应(ATE),但平均值往往掩盖了效应在不同个体间的巨大差异。
举例来说,若某项政策的平均效应为 $$\tau = 0.15$$,这并不代表每个人都获得了 $$15%$$ 的提升。现实可能是:
- 一部分人的效应接近 $$0.40$$(强响应者)
- 另一部分人的效应接近 $$-0.05$$(负响应者)
- 真正处于平均效应附近的人寥寥无几
方法步骤
排序效应法分为四步:
第一步:模型估计 使用包含异质性的模型(如带交互项的回归模型、逻辑回归等):
$$Y_i = \alpha + \tau(X_i) \cdot D_i + \beta X_i + \varepsilon_i$$
其中 $$\tau(X_i)$$ 是依赖于个体特征 $$X_i$$ 的个体化处理效应。
第二步:个体效应计算
为样本中每个个体 $$i$$ 计算其处理效应估计值 $$\hat{\tau}_i$$:
$$\hat{\tau}_i = \hat{f}(X_i, D_i = 1) - \hat{f}(X_i, D_i = 0)$$
第三步:效应排序
将所有个体的处理效应从低到高排序,形成排序效应曲线(Sorted Effect Curve):
$$\hat{\tau}{(1)} \leq \hat{\tau}{(2)} \leq \cdots \leq \hat{\tau}_{(n)}$$
第四步:统计推断
使用 Bootstrap 方法计算每个分位点处效应估计值的置信区间。
应用案例:维生素 E 推荐研究
Oster 研究短期推荐对维生素 E 服用率影响的案例,完美展示了排序效应法的价值:
研究模型:
$$\Pr(\text{服用维生素 E}) = f(\text{推荐期} \times \text{健康指标})$$
排序效应分析结果:
| 分析维度 | 平均效应 | 排序效应揭示的真实图景 |
|---|---|---|
| 处理效应大小 | 一个精确数字 | 效应分布跨越宽广区间 |
| 高响应者特征 | 无法识别 | 运动频繁、不吸烟、有维生素服用习惯 |
| 低响应者特征 | 无法识别 | 100% 为吸烟者,运动评分低 |
极端响应组特征对比(表 22.2 复现):
| 变量 | 10% 最低响应组(均值) | 10% 最高响应组(均值) |
|---|---|---|
| 吸烟情况 | 1.000(全部吸烟) | 0.000(无人吸烟) |
| 运动评分得分 | $$-0.422$$ | $$+1.446$$ |
| 维生素行为得分 | $$-1.985$$ | $$+1.706$$ |
关键发现:
- 吸烟行为是最显著的区分因素:最低响应组 $$100%$$ 为吸烟者,最高响应组无一吸烟
- 数据中很少有人处于平均效应或其 $$90%$$ 置信区间内
- 结果支持"已遵循其他健康建议的人更可能遵循维生素 E 推荐"这一发现
三大核心贡献
| 贡献维度 | 说明 |
|---|---|
| 标准误估计 | 通过 Bootstrap 解决了个体效应标准误难以估计的技术难题 |
| 分布分析 | 揭示效应的完整分布,而非仅报告均值 |
| 目标群体识别 | 通过极端分位数的个体特征,识别政策最应针对的群体 |
软件实现
# R 语言:SortedEffects 包
library(SortedEffects)
估计排序效应
spe_result <- spe(
y = "outcome",
d = "treatment",
x = c("covariate1", "covariate2", "covariate3"),
data = mydata,
method = "logit"
)
绘制排序效应曲线
plot(spe_result)
🧩 十一、异质性效应建模(Modeling Heterogeneous Effects)
"与其将异质性效应视为需要确定我们得到的是哪种平均值的麻烦事,不如直接估计效应的分布。"
为什么异质性效应如此重要?
现实世界中,处理效应几乎从不对所有人相同。处理效应异质性意味着:
$$\tau_i = Y_i(1) - Y_i(0) \neq \tau_j = Y_j(1) - Y_j(0), \quad \text{对于} \ i \neq j$$
这一事实产生了两个层面的挑战:
- 哲学层面:我们报告的"平均效应"代表谁的利益?
- 政策层面:若效应因人而异,如何精准设计干预措施?
传统方法及其局限
方法一:交互项回归
$$Y_i = \alpha + \beta D_i + \gamma X_i + \delta (D_i \times X_i) + \varepsilon_i$$
- ✅ 简单直观
- ❌ 只能包含有限的交互项,模型容易过度复杂
- ❌ 多个交互项会导致统计功效不足,难以解释
方法二:分层线性模型
$$Y_{ij} = \beta_{0j} + \beta_{1j} D_{ij} + \varepsilon_{ij}$$ $$\beta_{1j} = \gamma_{10} + \gamma_{11} Z_j + u_{1j}$$
- ✅ 允许系数随群体特征变化,比单纯交互项更灵活
- ✅ 可提供效应变异程度的信息
- ❌ 通常仍限于预先设定的变量维度
机器学习在异质性效应估计中的优势
机器学习方法特别擅长处理异质性效应问题:
| 能力 | 传统回归 | 机器学习方法 |
|---|---|---|
| 变量维度 | 受限(几个到几十个) | 高维(数百至数千个) |
| 交互关系 | 需手动指定 | 自动探索 |
| 非线性关系 | 有限 | 天然支持 |
| 效应分布估计 | 不直接输出 | 直接输出个体效应 |
异质性效应建模的完整工具链
研究问题
↓
识别策略(RCT / IV / DID / RDD...)
↓
异质性效应估计方法
├── 少量预测变量(≤3个)→ 交互项回归 / 分层模型
├── 中等数量变量 → 排序效应法
└── 大量变量 / 复杂交互 → 因果森林 / 双重机器学习
↓
结果解读
├── 哪些子群体效应更强?
├── 效应分布的形态如何?
└── 目标群体应如何界定?
🏗️ 十二、结构估计方法(Structural Estimation)
"结构估计方法基于理论模型直接估计结构参数,而非简单地向线性回归添加控制变量。"
结构方法 vs. 简化形式方法
这是计量经济学中一场持续数十年的核心方法论争论:
| 维度 | 结构估计(Structural) | 简化形式(Reduced Form) |
|---|---|---|
| 理论基础 | 完整的数据生成过程理论模型 | 部分识别(关闭后门路径即可) |
| 假设强度 | 强(模型必须正确) | 相对较弱 |
| 应用复杂度 | 高(数学推导复杂) | 相对简单 |
| 可回答的问题 | 任意反事实,包括数据中不存在的政策 | 仅限已观测的处理变量的效应 |
| 结果可信度 | 依赖模型设定正确性 | 不依赖完整模型 |
核心思想:让理论决定模型形式
结构估计的关键不是"用理论选择控制变量",而是推导理论所隐含的统计模型形式。
经典类比:引力模型
牛顿引力定律给出:
$$F = G \frac{m_1 m_2}{r^2}$$
若用简化形式思维处理这个问题:
$$F = \beta_0 + \beta_1 m_1 + \beta_2 m_2 + \beta_3 r + \varepsilon \quad \text{(错误!丢失了理论结构)}$$
若用结构估计思维:
$$F = G \cdot \frac{m_1 m_2}{r^2} + \varepsilon \quad \text{(正确!理论直接决定函数形式)}$$
结构估计直接估计物理意义明确的参数 $$G$$,而非无理论含义的回归系数 $$\beta$$。
结构估计的实施流程
第一步:构建理论模型
基于经济学或社会科学理论,写出完整的数据生成过程:
$$Y_i = f(\theta; X_i, D_i, \varepsilon_i)$$
其中 $$\theta$$ 是待估计的结构参数向量。
第二步:对误差项分布做出假设
$$\varepsilon_i \sim \mathcal{N}(0, \sigma^2) \quad \text{或其他分布假设}$$
第三步:构建似然函数
$$\mathcal{L}(\theta) = \prod_{i=1}^{n} f(Y_i \mid X_i, D_i; \theta)$$
第四步:最大似然估计
$$\hat{\theta}{MLE} = \arg\max{\theta} \sum_{i=1}^{n} \log f(Y_i \mid X_i, D_i; \theta)$$
尝试不同参数值(如 $$G = 3,\ G = 4,\ G = 5.3 \cdots$$),找到使观测数据出现概率最大的参数值。
替代方法:若似然函数难以写出,可使用广义矩方法(Generalized Method of Moments, GMM):
$$\hat{\theta}{GMM} = \arg\min{\theta} g(\theta)' W g(\theta)$$
其中 $$g(\theta)$$ 是由理论导出的矩条件。
结构估计的三大优势
1. 参数的经济意义明确
结构参数直接对应理论中的概念:
- 效用函数的风险厌恶系数 $$\rho$$
- 生产函数的资本产出弹性 $$\alpha$$
- 需求方程的价格弹性 $$\eta$$
2. 处理未观测变量
结构模型可以通过理论约束处理无法直接测量的变量(如"能力"、"偏好"等潜变量)。
3. 反事实政策模拟
这是结构估计最核心的优势——可以回答简化形式方法无法回答的问题:
| 问题类型 | 简化形式 | 结构估计 |
|---|---|---|
| 现行政策的效果 | ✅ 可回答 | ✅ 可回答 |
| 政策剂量变化的效果 | ⚠️ 有限 | ✅ 可回答 |
| 全新政策(数据中未出现过) | ❌ 无法回答 | ✅ 可回答 |
| 不同人群的政策效果 | ⚠️ 有限 | ✅ 可回答 |
结构估计的挑战
- 数学复杂度高:需要为特定问题推导并求解统计模型的数学形式
- 估计方法需定制化:不同模型需要不同的估计策略,无法"套用"通用方法
- 对假设高度敏感:只有在理论模型正确时,结论才可靠——这既是优势(精确),也是风险(脆弱)
⚠️ 关键警示:结构估计中,若模型设定有误,所有估计结果都可能是错的,且很难察觉。这正是为什么简化形式方法在过去几十年被广泛青睐——其识别假设更透明、更容易被检验。
常见应用场景与模型形式
| 领域 | 典型结构模型 | 估计方法 |
|---|---|---|
| 微观经济学(产业组织) | 多项 Logit 需求系统 | MLE / BLP 方法 |
| 劳动经济学 | 搜寻匹配模型 | MLE / SMM |
| 教育经济学 | 人力资本投资模型 | GMM |
| 公共财政 | 动态税收选择模型 | 动态规划 + MLE |
深入学习资源
- Reiss and Wolak (2007):产业组织经济学中的结构建模综述,通用性强
- Galiani and Pantano (2021):劳动经济学中结构模型入门
- Train (2009):Discrete Choice Methods with Simulation,结构估计实践经典
全书总结与展望
因果推断的三层思维框架
学完这套体系,研究者应当具备三个层次的思维能力:
第一层:识别思维
- 问题:数据中的哪部分变异具有因果解释?
- 工具:因果图(DAG)、后门路径分析
- 核心:在估计之前,先想清楚识别策略
第二层:估计思维
- 问题:如何从数据中提取具有因果解释的变异?
- 工具:回归、匹配、IV、DID、RDD、合成控制……
- 核心:方法服务于识别策略,而非相反
第三层:稳健性思维
- 问题:结果是否依赖于不可验证的假设?
- 工具:敏感性分析、安慰剂检验、部分识别
- 核心:诚实评估结论的边界与局限
方法选择决策树
研究问题确定
↓
是否有随机分配或准随机机制?
├── 有 → 利用准实验设计
│ ├── 有断点 → RDD
│ ├── 有时间政策变化 → DID / 合成控制
│ ├── 有外部工具 → IV
│ └── 完全随机 → RCT(直接估计均值差)
└── 无 → 基于可观测变量的控制
├── 变量少、关系清晰 → 回归 + 因果图
├── 高维协变量 → 双重机器学习
├── 需要个体效应 → 因果森林
├── 有完整理论模型 → 结构估计
└── 假设不可信 → 部分识别(报告范围)
时代背景下的因果推断
当前,因果推断正处于三股力量的交汇点:
| 力量 | 趋势 | 代表方法 |
|---|---|---|
| 方法论创新 | 从点识别走向集合识别,承认不确定性 | 部分识别、Honest DID |
| 机器学习融合 | 利用高维数据发现异质性,提升估计效率 | 因果森林、DML、矩阵补全 |
| 自动化因果发现 | 从数据中自动推断因果结构,减少先验依赖 | SGS 算法、PC 算法、BCDAG |
一以贯之的核心原则
无论使用哪种方法,因果推断的精神始终如一:
💡 用可信的假设,揭示可信的因果关系。
- 透明:清楚说明所依赖的假设
- 诚实:承认假设可能违反的情形
- 稳健:检验结论对假设违反的敏感程度
- 谦逊:当数据不足以支持强结论时,报告范围而非伪精确
更多推荐



所有评论(0)