2.5 识别策略与有效性
"No causation without manipulation.""没有操纵就没有因果关系。"— Paul Holland, Statistician (统计学家)
因果推断的核心假设与威胁因素
本节目标
- 理解因果识别的核心假设(SUTVA、独立性)
- 掌握内部有效性与外部有效性的区别
- 识别 RCT 的威胁因素
- 学习敏感性分析方法
因果识别的核心假设
什么是"识别"(Identification)?
因果识别:能否从观测数据中唯一确定因果效应
通俗理解:
- 不可识别:数据与多个不同的因果效应都兼容(无法区分)
- 可识别:数据只与一个因果效应兼容(可以估计)
1️⃣ SUTVA 假设
定义
SUTVA(Stable Unit Treatment Value Assumption):稳定单元处理值假设
包含两个子假设:
(1) 无干预溢出(No Interference)
含义:个体 的潜在结果只依赖于自己的处理状态,不受他人处理状态影响
案例:
- 满足:药物试验(每人独立服药)
- 违反:疫苗接种(群体免疫效应)、教育政策(同伴效应)
(2) 处理一致性(No Hidden Variations of Treatment)
含义:处理只有一种形式,没有隐藏的变异
案例:
- 违反:"在线课程"可能包括:
- 直播课(高互动)
- 录播课(低互动)
- 自学材料(无互动)
不同形式的"在线课程"效应可能不同
SUTVA 违反的后果
import numpy as np
import pandas as pd
# 模拟溢出效应
np.random.seed(42)
n = 100
# 生成社交网络(邻居关系)
friends = np.random.randint(0, n, size=(n, 3)) # 每人 3 个朋友
# 随机分配
data = pd.DataFrame({
'id': range(n),
'treatment': np.random.binomial(1, 0.5, n)
})
# 计算朋友中接受处理的比例
data['friends_treated'] = 0
for i in range(n):
friend_ids = friends[i]
data.loc[i, 'friends_treated'] = data.loc[friend_ids, 'treatment'].mean()
# 结果变量(包含溢出效应)
# 自己的处理 + 朋友的处理都有影响
data['Y'] = (100 +
30 * data['treatment'] + # 直接效应
15 * data['friends_treated'] + # 溢出效应
np.random.normal(0, 10, n))
# 简单对比会混合直接效应和溢出效应
simple_diff = (data[data['treatment'] == 1]['Y'].mean() -
data[data['treatment'] == 0]['Y'].mean())
print(f"简单对比: {simple_diff:.2f}")
print("真实直接效应: 30")
print("溢出效应: 15 × (朋友接受处理的比例)")
print("\n️ SUTVA 违反 → 简单对比估计有偏!")应对 SUTVA 违反
| 方法 | 适用场景 | 示例 |
|---|---|---|
| 聚类随机化 | 溢出效应局限在群体内 | 以学校为单位随机化 |
| 两阶段实验 | 随机化群体和个体 | 先随机选村庄,再随机选村民 |
| 网络实验设计 | 已知社交网络结构 | 随机化不相邻节点 |
| 结构化模型 | 估计直接效应和溢出效应 | 空间计量模型 |
2️⃣ 独立性假设(Unconfoundedness)
随机化条件下的独立性
强独立性:
含义:潜在结果与处理分配统计独立
RCT 的核心优势:随机化自动满足这个假设
条件独立性
在观察性研究中(无随机化),需要更强的假设:
含义:给定协变量 ,处理分配与潜在结果独立
俗称:"选择基于可观测变量"(Selection on Observables)
检验独立性:平衡性检验
from scipy import stats
import pandas as pd
import numpy as np
def independence_test(data, covariates, treatment_col='treatment'):
"""
检验处理分配与协变量的独立性
"""
results = []
for cov in covariates:
if data[cov].dtype in ['float64', 'int64']:
# 连续变量:t 检验
treated = data[data[treatment_col] == 1][cov]
control = data[data[treatment_col] == 0][cov]
stat, p_value = stats.ttest_ind(treated, control)
test_type = 't-test'
else:
# 分类变量:卡方检验
contingency = pd.crosstab(data[cov], data[treatment_col])
stat, p_value, _, _ = stats.chi2_contingency(contingency)
test_type = 'chi2'
results.append({
'协变量': cov,
'检验类型': test_type,
'统计量': stat,
'p-value': p_value,
'平衡': '' if p_value > 0.05 else ''
})
return pd.DataFrame(results)
# 示例
np.random.seed(42)
n = 200
data = pd.DataFrame({
'treatment': np.random.binomial(1, 0.5, n),
'age': np.random.normal(30, 10, n),
'income': np.random.lognormal(10, 1, n),
'gender': np.random.choice(['M', 'F'], n),
'education': np.random.choice(['高中', '本科', '研究生'], n)
})
balance_results = independence_test(
data,
covariates=['age', 'income', 'gender', 'education']
)
print(balance_results)3️⃣ 内部有效性(Internal Validity)
定义
内部有效性:因果推断在研究样本内是否正确
含义:估计量能否无偏且一致地估计样本内的因果效应
威胁因素
1. 选择偏误(Selection Bias)
来源:处理组和对照组基线不可比
RCT 中可能出现的情况:
- 随机化失败(技术问题)
- 样本量太小(随机波动)
- 分层不当
检验:平衡性检验(见上文)
应对:
- 重新随机化
- 回归控制不平衡变量
- 匹配或 IPW(逆概率加权)
2. 样本流失(Attrition)
定义:部分参与者退出研究,导致结果数据缺失
# 模拟样本流失
np.random.seed(42)
n = 500
data = pd.DataFrame({
'treatment': np.random.binomial(1, 0.5, n),
'baseline_health': np.random.normal(50, 10, n)
})
# 潜在结果
data['Y0'] = 50 + 0.5 * data['baseline_health'] + np.random.normal(0, 5, n)
data['Y1'] = data['Y0'] + 10 + np.random.normal(0, 5, n)
# 样本流失(健康状况差的人更可能退出)
attrition_prob = 1 / (1 + np.exp((data['baseline_health'] - 40) / 5))
data['attrited'] = np.random.binomial(1, attrition_prob)
# 观测结果(只有未退出的人有数据)
data['Y_obs'] = np.where(data['treatment'] == 1, data['Y1'], data['Y0'])
data.loc[data['attrited'] == 1, 'Y_obs'] = np.nan
# 完整样本 ATE
complete_ATE = data['Y1'].mean() - data['Y0'].mean()
# 有流失的 ATE
observed_data = data.dropna(subset=['Y_obs'])
attrited_ATE = (observed_data[observed_data['treatment'] == 1]['Y_obs'].mean() -
observed_data[observed_data['treatment'] == 0]['Y_obs'].mean())
print(f"完整样本 ATE: {complete_ATE:.2f}")
print(f"有流失的 ATE: {attrited_ATE:.2f}")
print(f"偏误: {attrited_ATE - complete_ATE:.2f}")
# 流失率对比
attrition_by_treatment = data.groupby('treatment')['attrited'].mean()
print("\n流失率:")
print(attrition_by_treatment)检测方法:
- 比较两组流失率(应该相似)
- 检查流失者与留存者的基线特征
应对方法:
- Lee Bounds:估计效应的上下界
- IPW:逆概率加权(对留存概率加权)
- 敏感性分析:假设不同的流失机制
3. Hawthorne 效应(观察者效应)
定义:参与者知道自己被观察,改变行为
案例:
- 健康研究:知道被监测,更注意饮食和锻炼
- 教育实验:教师和学生因为被研究而更努力
应对:
- 双盲设计(Double Blind):参与者和研究者都不知道分配
- Placebo 对照组:对照组接受安慰剂处理
- 隐蔽测量:使用行政数据而非问卷
4. 溢出效应(Spillover)
见 SUTVA 部分
5. 霍桑效应的反面:John Henry 效应
定义:对照组因为不想"输"而额外努力
案例:对照学校听说另一所学校在试点新教学法,竞争性地提高教学质量
4️⃣ 外部有效性(External Validity)
定义
外部有效性:因果效应能否推广到其他总体
威胁因素
1. 样本选择偏误
问题:实验样本不代表目标总体
案例:
- 大学生样本 → 推广到全社会?
- 志愿者样本 → 推广到强制参与?
- 单一地区 → 推广到全国?
# 模拟样本选择偏误
np.random.seed(42)
# 总体(N = 10,000)
population = pd.DataFrame({
'id': range(10000),
'ability': np.random.normal(100, 20, 10000)
})
# 真实 ATE(在总体中)
population['tau'] = 10 + 0.1 * population['ability']
true_population_ATE = population['tau'].mean()
# 实验样本(只有高能力者参加)
sample_prob = 1 / (1 + np.exp(-(population['ability'] - 100) / 10))
sample = population[np.random.binomial(1, sample_prob, 10000) == 1].copy()
# 在样本中进行 RCT
sample['treatment'] = np.random.binomial(1, 0.5, len(sample))
sample['Y0'] = 50 + 0.5 * sample['ability']
sample['Y1'] = sample['Y0'] + sample['tau']
sample['Y_obs'] = np.where(sample['treatment'] == 1, sample['Y1'], sample['Y0'])
# 估计 ATE(在样本中)
sample_ATE = (sample[sample['treatment'] == 1]['Y_obs'].mean() -
sample[sample['treatment'] == 0]['Y_obs'].mean())
print(f"总体 ATE: {true_population_ATE:.2f}")
print(f"样本 ATE: {sample_ATE:.2f}")
print(f"外部有效性偏误: {sample_ATE - true_population_ATE:.2f}")
print(f"\n总体平均能力: {population['ability'].mean():.2f}")
print(f"样本平均能力: {sample['ability'].mean():.2f}")2. 实验环境与真实环境不同
问题:实验条件过于理想化
案例:
- 实验室实验 → 真实决策环境
- 小规模试点 → 大规模推广
- 短期效应 → 长期效应
3. 处理效应的情境依赖性
问题:效应依赖于特定的背景条件
案例:
- 在线教育在疫情期间效果好 → 疫情后呢?
- 某政策在经济繁荣期有效 → 衰退期呢?
提升外部有效性的方法
| 方法 | 描述 |
|---|---|
| 多地点重复实验 | 在不同地区、不同时间重复 |
| 异质性分析 | 研究不同子群的效应,识别边界条件 |
| Meta 分析 | 综合多个研究的结果 |
| 重新加权 | 按总体分布对样本加权 |
# 重新加权提升外部有效性
from sklearn.linear_model import LogisticRegression
# 1. 估计样本选择概率(倾向性得分)
X = population[['ability']]
y = np.isin(population['id'], sample['id']).astype(int)
ps_model = LogisticRegression()
ps_model.fit(X, y)
population['ps'] = ps_model.predict_proba(X)[:, 1]
# 2. 计算权重
sample_with_weights = population[population['id'].isin(sample['id'])].copy()
sample_with_weights['weight'] = 1 / sample_with_weights['ps']
# 3. 加权估计 ATE
# (这里简化处理,实际需要结合处理分配)
weighted_mean_tau = (sample_with_weights['tau'] * sample_with_weights['weight']).sum() / sample_with_weights['weight'].sum()
print(f"未加权 ATE: {sample['tau'].mean():.2f}")
print(f"加权 ATE: {weighted_mean_tau:.2f}")
print(f"总体 ATE: {true_population_ATE:.2f}")5️⃣ 敏感性分析(Sensitivity Analysis)
目的
评估结果对假设违反的稳健性
方法 1:遗漏变量偏误分析
问题:如果存在未观测的混淆变量,结果会改变多少?
def sensitivity_analysis_omitted_variable(data, treatment, outcome, r2_confounder_treatment, r2_confounder_outcome):
"""
遗漏变量敏感性分析
参数:
- r2_confounder_treatment: 遗漏变量能解释处理变异的比例
- r2_confounder_outcome: 遗漏变量能解释结果变异的比例
"""
import statsmodels.api as sm
# 观测到的 ATE
X = sm.add_constant(data[treatment])
model = sm.OLS(data[outcome], X).fit()
observed_ATE = model.params[treatment]
# 计算偏误
# 简化公式(Cinelli & Hazlett, 2020)
bias = np.sqrt(r2_confounder_treatment * r2_confounder_outcome) * data[outcome].std()
# 调整后的估计
adjusted_ATE_upper = observed_ATE + bias
adjusted_ATE_lower = observed_ATE - bias
return {
'观测 ATE': observed_ATE,
'偏误上限': bias,
'调整后 ATE 上界': adjusted_ATE_upper,
'调整后 ATE 下界': adjusted_ATE_lower
}
# 示例
result = sensitivity_analysis_omitted_variable(
sample,
treatment='treatment',
outcome='Y_obs',
r2_confounder_treatment=0.1, # 遗漏变量解释 10% 的处理变异
r2_confounder_outcome=0.2 # 遗漏变量解释 20% 的结果变异
)
print("敏感性分析结果:")
for k, v in result.items():
print(f" {k}: {v:.2f}")方法 2:样本流失的 Lee Bounds
def lee_bounds(data, treatment, outcome, attrited):
"""
Lee (2009) 边界估计
处理样本流失导致的选择偏误
"""
# 流失率
attrition_treated = data[data[treatment] == 1][attrited].mean()
attrition_control = data[data[treatment] == 0][attrited].mean()
# 计算需要修剪的比例
trim_prop = abs(attrition_treated - attrition_control)
# 获取非流失样本
observed = data[data[attrited] == 0].copy()
# 上界:修剪处理组的高值
if attrition_treated > attrition_control:
treated_obs = observed[observed[treatment] == 1][outcome].sort_values(ascending=False)
n_trim = int(len(treated_obs) * trim_prop / (1 - attrition_treated))
treated_trimmed_upper = treated_obs.iloc[n_trim:]
upper_bound = treated_trimmed_upper.mean() - observed[observed[treatment] == 0][outcome].mean()
else:
upper_bound = np.nan
# 下界:修剪处理组的低值
if attrition_treated > attrition_control:
treated_trimmed_lower = treated_obs.iloc[:-n_trim] if n_trim > 0 else treated_obs
lower_bound = treated_trimmed_lower.mean() - observed[observed[treatment] == 0][outcome].mean()
else:
lower_bound = np.nan
# 简单估计(不修剪)
simple_ATE = (observed[observed[treatment] == 1][outcome].mean() -
observed[observed[treatment] == 0][outcome].mean())
return {
'简单估计': simple_ATE,
'Lee 下界': lower_bound,
'Lee 上界': upper_bound
}
# 使用之前的流失数据
bounds = lee_bounds(data, 'treatment', 'Y_obs', 'attrited')
print("\nLee Bounds:")
for k, v in bounds.items():
if not np.isnan(v):
print(f" {k}: {v:.2f}")方法 3:Placebo 检验
def placebo_test(data, treatment, outcome, placebo_treatment):
"""
Placebo 检验:使用一个不应该有效应的"虚假处理"
"""
import statsmodels.api as sm
# 真实处理的效应
X_real = sm.add_constant(data[treatment])
model_real = sm.OLS(data[outcome], X_real).fit()
real_effect = model_real.params[treatment]
real_pvalue = model_real.pvalues[treatment]
# Placebo 处理的"效应"
X_placebo = sm.add_constant(data[placebo_treatment])
model_placebo = sm.OLS(data[outcome], X_placebo).fit()
placebo_effect = model_placebo.params[placebo_treatment]
placebo_pvalue = model_placebo.pvalues[placebo_treatment]
print("Placebo 检验:")
print(f" 真实处理效应: {real_effect:.2f} (p={real_pvalue:.4f})")
print(f" Placebo 效应: {placebo_effect:.2f} (p={placebo_pvalue:.4f})")
if placebo_pvalue > 0.05:
print(" Placebo 不显著,通过检验")
else:
print(" Placebo 显著,可能存在问题(如选择偏误)")
# 示例:使用滞后的处理变量作为 placebo
sample['placebo_treatment'] = np.roll(sample['treatment'], 1)
placebo_test(sample, 'treatment', 'Y_obs', 'placebo_treatment')识别策略对比
| 策略 | 识别来源 | 核心假设 | 内部有效性 | 外部有效性 | 常见威胁 |
|---|---|---|---|---|---|
| RCT | 随机分配 | SUTVA | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 样本流失、Hawthorne |
| DID | 平行趋势 | 无差异趋势 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 趋势违反、反向因果 |
| RDD | 连续性 | 连续性假设 | ⭐⭐⭐⭐ | ⭐⭐ | 操纵、函数形式 |
| IV | 外生冲击 | 排除性约束 | ⭐⭐⭐ | ⭐⭐⭐ | 弱工具变量 |
| PSM | 选择可观测 | 条件独立性 | ⭐⭐ | ⭐⭐⭐ | 隐藏偏误 |
小结
核心要点
SUTVA 假设
- 无溢出效应
- 处理一致性
- 违反 → 聚类随机化
独立性假设
- RCT 自动满足
- 平衡性检验验证
内部有效性
- 样本内因果推断是否正确
- 威胁:选择偏误、流失、Hawthorne
外部有效性
- 能否推广到其他总体
- 提升:多地点、异质性分析、重新加权
敏感性分析
- 评估假设违反的影响
- 方法:遗漏变量、Lee Bounds、Placebo
思考题
理解题:解释"内部有效性高但外部有效性低"的含义,并举例说明。
案例题:某药物 RCT 发现:
- 处理组流失率 30%,对照组流失率 15%
- 完整数据显示 ATE = 10
问题:
- (a) 这个 ATE 可信吗?为什么?
- (b) 如何应对?
设计题:你在设计一个教育 RCT,担心溢出效应(同班同学互相影响)。
- (a) SUTVA 会如何被违反?
- (b) 如何修改实验设计?
- (c) 如果必须在班级内随机化,如何分析数据?
点击查看答案提示
问题 1:
- 内部有效性高:样本内因果推断正确(如严格的 RCT)
- 外部有效性低:样本不代表总体(如只在名校做实验)
- 例子:斯坦福大学的在线课程实验 → 推广到社区大学?
问题 2:
- (a) 不可信!流失率差异很大,可能存在差异流失偏误
- (b) 使用 Lee Bounds 估计效应的上下界,或 IPW 方法
问题 3:
- (a) 处理学生会影响对照学生(同伴效应)
- (b) 以班级为单位随机化(聚类随机化)
- (c) 使用 Cluster-Robust 标准误
下一步
下一节我们将进行 Python 完整实战,整合本章所有知识,分析一个真实的 RCT 数据集。
准备好了吗?
参考文献:
- Rubin, D. B. (1980). "Randomization analysis of experimental data: The Fisher randomization test comment". JASA.
- Lee, D. S. (2009). "Training, wages, and sample selection: Estimating sharp bounds on treatment effects". Review of Economic Studies.
- Cinelli, C., & Hazlett, C. (2020). "Making sense of sensitivity: Extending omitted variable bias". Journal of the Royal Statistical Society.
- Manski, C. F. (1990). "Nonparametric bounds on treatment effects". American Economic Review.