8.3 两阶段最小二乘法(2SLS)
"Two-stage least squares is the workhorse estimator for instrumental variables.""两阶段最小二乘法是工具变量估计的主力方法。"— Jeffrey Wooldridge, Introductory Econometrics
从 Wald 估计量到完整的 2SLS 框架
📌 本节概要
在本节中,我们将深入探讨:
- 2SLS 的两个阶段及其直觉
- 为什么 是"干净"的:从 DAG 和代数两个角度
- 从单一工具变量到多工具变量
- 过度识别(overidentification)的概念
- LATE(局部平均处理效应):IV 估计的到底是"谁"的效应?
- 四种人群:Compliers, Always-takers, Never-takers, Defiers
- 单调性假设
- 完整的 Python 实现(
linearmodels.iv.IV2SLS)
🔧 2SLS 的两个阶段
问题回顾
我们想估计:
但 (内生性),OLS 有偏。
我们有一个工具变量 ,满足三大条件。
第一阶段(First Stage):用 Z 预测 D
- 用 OLS 回归 对
- 得到预测值
- 是 中被 解释的那部分
关键: 是 的线性函数,而 与 不相关(独立性条件),所以 也与 不相关!
大白话:第一阶段的作用是"净化" ——把 分解为两部分:
- :被 解释的部分("干净"的外生变异)
- :残差("脏"的部分,包含与 相关的变异)
第二阶段(Second Stage):用 解释 Y
- 用 OLS 回归 对 (而不是 !)
- 因为 与 不相关,所以这个回归是一致的
图示:2SLS 的逻辑
第一阶段:
Z ——→ D = π₀ + π₁Z + v
↓
D̂ = π̂₀ + π̂₁Z (干净的部分)
v̂ = D - D̂ (脏的部分)
第二阶段:
Y = β₀ + β₁D̂ + error (用干净的部分估计因果效应)从 DAG 理解 2SLS
原始 DAG(有内生性):
U (不可观测)
↗ ↘
Z ——→ D ——→ Y第一阶段做了什么?
- 只包含 对 的影响
- 不包含 对 的影响(因为 )
第二阶段的 DAG:
Z ——→ D̂ ——→ Y现在 与 无关了!混淆路径被切断了。
📐 2SLS 的数学推导
恰好识别的情形(Just-identified)
当只有一个工具变量 对应一个内生变量 时:
第一阶段:
第二阶段:
化简(因为 ):
结论:在恰好识别的情形下,2SLS 估计量等于 IV 估计量(协方差比)等于 Wald 估计量!
包含外生控制变量
更一般的模型:
其中 是外生控制变量(如年龄、性别、地区等), 是内生变量。
第一阶段:
第二阶段:
注意:
- 控制变量 同时出现在两个阶段
- 只有 被替换为 , 保持不变
- 控制变量的加入可以提高效率(减小标准误)
📊 多工具变量与过度识别
过度识别(Overidentification)
定义:
- 恰好识别(Just-identified):工具变量数 = 内生变量数
- 过度识别(Over-identified):工具变量数 > 内生变量数
- 不可识别(Under-identified):工具变量数 < 内生变量数
例子:
- 1 个内生变量(教育),1 个 IV(出生季度)→ 恰好识别
- 1 个内生变量(教育),3 个 IV(Q1, Q2, Q3 的出生季度虚拟变量)→ 过度识别
多 IV 的 2SLS
假设有 个工具变量 :
第一阶段:
得到 (使用所有 个 IV 的线性预测值)。
第二阶段:
过度识别的好处:
- 更强的第一阶段(更多 IV 解释 的变异更大 → F 统计量更高)
- 可以进行过度识别检验(Sargan/Hansen J 检验)来检验 IV 的有效性
过度识别的风险:
- 更多的 IV 意味着更多的排他性约束需要满足
- 如果某些 IV 无效,估计量会有偏
🎯 LATE:IV 估计的到底是"谁"的效应?
一个关键问题
我们之前推导了 IV 估计量,但一个根本性的问题是:
IV 估计的因果效应是对整个人群的平均效应(ATE),还是对某些特定人群的效应?
答案令人惊讶:IV 估计的不是 ATE,而是局部平均处理效应(Local Average Treatment Effect, LATE)——只针对特定的一群人:顺从者(Compliers)。
四种人群(Imbens & Angrist 1994)
假设工具变量 ,处理变量 。
每个人有两个潜在处理状态:
- :如果 ,此人的处理状态
- :如果 ,此人的处理状态
根据 的组合,可以把人群分为四类:
| 类型 | 含义 | 例子(征兵抽签) | ||
|---|---|---|---|---|
| Always-takers | 1 | 1 | 无论 如何,都接受处理 | 不管抽签号码,主动参军 |
| Never-takers | 0 | 0 | 无论 如何,都不接受处理 | 不管抽签号码,都不参军 |
| Compliers ⭐ | 0 | 1 | 才接受处理 | 被抽到才参军,没抽到就不去 |
| Defiers | 1 | 0 | 反而不接受处理 | 被抽到反而逃避,没抽到反而参军(不太合理) |
大白话:
- Always-takers(总是接受者):无论怎样都会上大学的人
- Never-takers(从不接受者):无论怎样都不上大学的人
- Compliers(顺从者):工具变量"推"了一下就去的人 ⭐
- Defiers(违抗者):工具变量让你去,你偏不去
单调性假设(Monotonicity)
假设:
含义:不存在 Defiers! 从 0 变成 1,不会让任何人从"接受处理"变成"不接受处理"。
在征兵抽签的例子中:
- (抽签号码低,被征兵)只会增加参军概率,不会减少
- 没有人因为被抽到反而不去参军
- 这个假设很合理
在其他例子中:
- = 住得离学院近 → 只会增加上大学概率(合理)
- = 收到招生宣传册 → 只会增加申请概率(合理)
LATE 定理(Imbens & Angrist 1994)
在以下条件下:
- 相关性:
- 排他性: 只通过 影响
- 独立性:
- 单调性: 对所有
Wald/IV 估计量等于 Compliers 的平均处理效应:
这就是 LATE(局部平均处理效应)。
为什么只能识别 Compliers 的效应?
直觉推导:
从 0 变成 1 时:
- Always-takers:处理状态不变(),对 的差异贡献 = 0
- Never-takers:处理状态不变(),对 的差异贡献 = 0
- Compliers:处理状态改变(),对 的差异贡献 =
所以, 完全来自 Compliers 的效应:
而分母:
两者相除:
LATE 的含义与局限
含义:
- IV 估计的是被工具变量影响处理状态的那群人的因果效应
- 不同的 IV 可能对应不同的 Compliers → 不同的 LATE!
- 例:用"出生季度"作 IV 得到的 LATE,和用"距离学院"作 IV 得到的 LATE,可能不同
局限:
- LATE ≠ ATE:如果 Compliers 不具有代表性,LATE 不能推广到整个人群
- 不同 IV 的 LATE 不可比:Compliers 群体不同
- 政策含义需要谨慎:LATE 告诉我们的是"被这个 IV 影响的人"的效应,而不是"所有人"的效应
LATE vs ATE vs ATT
| 参数 | 定义 | 估计方法 | 适用人群 |
|---|---|---|---|
| ATE | RCT, OLS(无混淆) | 全体 | |
| ATT | 匹配/PSM | 处理组 | |
| LATE | IV/2SLS | 顺从者 |
💻 Python 实现:linearmodels.iv.IV2SLS
模拟数据(含内生性和四种人群)
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
np.random.seed(2024)
n = 10000
# --- 数据生成过程 ---
# 不可观测的混淆变量
U = np.random.normal(0, 1, n)
# 工具变量:二值(如征兵抽签)
Z = np.random.binomial(1, 0.5, n)
# 外生控制变量
age = np.random.normal(35, 5, n)
female = np.random.binomial(1, 0.4, n)
# 处理变量(内生):受 Z, U, 和随机因素影响
# D* = -0.5 + 1.2*Z + 0.8*U + noise
D_star = -0.5 + 1.2 * Z + 0.8 * U + np.random.normal(0, 0.5, n)
D = (D_star > 0).astype(int) # 二值处理
# 结果变量:真实效应 = 5
beta_true = 5
Y = 30 + beta_true * D + 3 * U + 0.5 * age - 2 * female + np.random.normal(0, 3, n)
df = pd.DataFrame({
'Y': Y, 'D': D, 'Z': Z,
'age': age, 'female': female, 'U': U
})
# 识别 Complier 类型(在实际中无法观测!)
D_if_Z1 = ((-0.5 + 1.2 * 1 + 0.8 * U + np.random.normal(0, 0.5, n)) > 0).astype(int)
D_if_Z0 = ((-0.5 + 1.2 * 0 + 0.8 * U + np.random.normal(0, 0.5, n)) > 0).astype(int)
complier_type = np.where(
(D_if_Z0 == 0) & (D_if_Z1 == 1), 'Complier',
np.where(
(D_if_Z0 == 1) & (D_if_Z1 == 1), 'Always-taker',
np.where(
(D_if_Z0 == 0) & (D_if_Z1 == 0), 'Never-taker',
'Defier'
)
)
)
df['type'] = complier_type
print("=" * 70)
print("数据概览")
print("=" * 70)
print(f"样本量: {n}")
print(f"真实处理效应: β = {beta_true}")
print(f"\n四种人群的比例:")
for t in ['Complier', 'Always-taker', 'Never-taker', 'Defier']:
pct = (complier_type == t).mean() * 100
print(f" {t:15s}: {pct:.1f}%")方法 1:OLS(有偏)
ols_result = smf.ols('Y ~ D + age + female', data=df).fit()
print("\n" + "=" * 70)
print("方法 1:OLS(有偏)")
print("=" * 70)
print(f"OLS 估计的 D 的效应: {ols_result.params['D']:.4f}")
print(f"标准误: {ols_result.bse['D']:.4f}")
print(f"95% CI: [{ols_result.conf_int().loc['D', 0]:.4f}, {ols_result.conf_int().loc['D', 1]:.4f}]")
print(f"真实效应: {beta_true}")
print(f"偏差: {ols_result.params['D'] - beta_true:.4f}(因为能力混淆,OLS 高估)")方法 2:手动 2SLS
# --- 第一阶段 ---
first_stage = smf.ols('D ~ Z + age + female', data=df).fit()
df['D_hat'] = first_stage.fittedvalues
print("\n" + "=" * 70)
print("方法 2:手动 2SLS")
print("=" * 70)
print("\n--- 第一阶段:D ~ Z + controls ---")
print(f"Z 的系数 (π₁): {first_stage.params['Z']:.4f}")
print(f"Z 的 t 统计量: {first_stage.tvalues['Z']:.2f}")
print(f"第一阶段 F 统计量: {first_stage.fvalue:.2f}")
print(f"第一阶段 R²: {first_stage.rsquared:.4f}")
# --- 第二阶段 ---
second_stage = smf.ols('Y ~ D_hat + age + female', data=df).fit()
print(f"\n--- 第二阶段:Y ~ D̂ + controls ---")
print(f"D̂ 的系数 (β₁): {second_stage.params['D_hat']:.4f}")
print(f"标准误: {second_stage.bse['D_hat']:.4f}")
print(f"(注意:手动 2SLS 的标准误不正确!需要使用专门的 IV 软件包)")方法 3:linearmodels.iv.IV2SLS(推荐)
from linearmodels.iv import IV2SLS
# IV2SLS 语法:dependent ~ exogenous [endogenous ~ instruments]
# - dependent: 结果变量
# - exogenous: 外生控制变量(包括常数项)
# - endogenous: 内生变量
# - instruments: 工具变量
iv_model = IV2SLS.from_formula('Y ~ 1 + age + female [D ~ Z]', data=df)
iv_result = iv_model.fit(cov_type='robust')
print("\n" + "=" * 70)
print("方法 3:linearmodels IV2SLS(推荐)")
print("=" * 70)
print(iv_result.summary)方法 4:Wald 估计量(验证)
# 简化形式
rf = smf.ols('Y ~ Z + age + female', data=df).fit()
# 第一阶段
fs = smf.ols('D ~ Z + age + female', data=df).fit()
wald = rf.params['Z'] / fs.params['Z']
print("\n" + "=" * 70)
print("方法 4:Wald 估计量(验证)")
print("=" * 70)
print(f"简化形式 Z 的系数: {rf.params['Z']:.4f}")
print(f"第一阶段 Z 的系数: {fs.params['Z']:.4f}")
print(f"Wald 估计量: {wald:.4f}")结果汇总
print("\n" + "=" * 70)
print("所有方法汇总比较")
print("=" * 70)
print(f"{'方法':<25} {'估计值':<12} {'标准误':<12} {'偏差':<12}")
print("-" * 61)
print(f"{'真实效应':<25} {beta_true:<12.4f} {'—':<12} {'—':<12}")
print(f"{'OLS(有偏)':<23} {ols_result.params['D']:<12.4f} {ols_result.bse['D']:<12.4f} {ols_result.params['D']-beta_true:<12.4f}")
print(f"{'手动 2SLS':<23} {second_stage.params['D_hat']:<12.4f} {'(不准确)':<12} {second_stage.params['D_hat']-beta_true:<12.4f}")
print(f"{'linearmodels 2SLS':<25} {iv_result.params['D']:<12.4f} {iv_result.std_errors['D']:<12.4f} {iv_result.params['D']-beta_true:<12.4f}")
print(f"{'Wald 估计量':<23} {wald:<12.4f} {'—':<12} {wald-beta_true:<12.4f}")⚠️ 手动 2SLS 的标准误问题
为什么手动 2SLS 的标准误不正确?
如果你按照上面的步骤手动做两步回归,第二阶段的标准误是不正确的!
原因:第二阶段用的是 (预测值),但回归软件以为它是一个"真实的"变量。实际上, 本身有估计误差(来自第一阶段),但普通 OLS 不会考虑这一点。
数学上:
- 手动第二阶段使用的残差是
- 但正确的残差应该是 (用原始的 ,不是 )
- 两者的方差不同 → 标准误不同
解决方案:使用专门的 IV 软件包(如 linearmodels),它会自动修正标准误。
# 错误的做法(标准误不对):
first = smf.ols('D ~ Z', data=df).fit()
df['D_hat'] = first.fittedvalues
second = smf.ols('Y ~ D_hat', data=df).fit() # SE 不正确!
# 正确的做法:
from linearmodels.iv import IV2SLS
result = IV2SLS.from_formula('Y ~ 1 [D ~ Z]', data=df).fit() # SE 正确!📊 可视化:2SLS 的两个阶段
import matplotlib.pyplot as plt
import seaborn as sns
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
sns.set_style("whitegrid")
fig, axes = plt.subplots(1, 3, figsize=(20, 6))
# --- 图 1:第一阶段 ---
group_means_D = df.groupby('Z')['D'].mean()
colors = ['steelblue', 'coral']
axes[0].bar([0, 1], group_means_D.values, color=colors,
edgecolor='black', width=0.5, alpha=0.8)
axes[0].set_xticks([0, 1])
axes[0].set_xticklabels(['Z = 0\n(未被抽中)', 'Z = 1\n(被抽中)'], fontsize=11)
axes[0].set_ylabel('P(D = 1)', fontsize=12)
axes[0].set_title('第一阶段\nZ 对 D 的效应', fontsize=14, fontweight='bold')
fs_effect = group_means_D[1] - group_means_D[0]
axes[0].annotate(f'差值 = {fs_effect:.3f}',
xy=(0.5, max(group_means_D) * 0.5),
fontsize=13, fontweight='bold', color='purple',
ha='center',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.3))
# --- 图 2:简化形式 ---
group_means_Y = df.groupby('Z')['Y'].mean()
axes[1].bar([0, 1], group_means_Y.values, color=colors,
edgecolor='black', width=0.5, alpha=0.8)
axes[1].set_xticks([0, 1])
axes[1].set_xticklabels(['Z = 0\n(未被抽中)', 'Z = 1\n(被抽中)'], fontsize=11)
axes[1].set_ylabel('E[Y]', fontsize=12)
axes[1].set_title('简化形式\nZ 对 Y 的效应', fontsize=14, fontweight='bold')
rf_effect = group_means_Y[1] - group_means_Y[0]
axes[1].annotate(f'差值 = {rf_effect:.3f}',
xy=(0.5, group_means_Y.mean()),
fontsize=13, fontweight='bold', color='purple',
ha='center',
bbox=dict(boxstyle='round', facecolor='yellow', alpha=0.3))
# --- 图 3:Wald 估计 ---
wald_est = rf_effect / fs_effect
bar_labels = ['OLS\n(有偏)', '2SLS/IV\n(一致)', '真实值']
bar_values = [ols_result.params['D'], wald_est, beta_true]
bar_colors = ['salmon', 'mediumseagreen', 'gold']
axes[2].bar(range(3), bar_values, color=bar_colors,
edgecolor='black', width=0.5, alpha=0.85)
axes[2].set_xticks(range(3))
axes[2].set_xticklabels(bar_labels, fontsize=11)
axes[2].set_ylabel('D 对 Y 的效应估计', fontsize=12)
axes[2].set_title('OLS vs 2SLS vs 真实值', fontsize=14, fontweight='bold')
for i, v in enumerate(bar_values):
axes[2].text(i, v + 0.1, f'{v:.2f}', ha='center', fontsize=12, fontweight='bold')
plt.suptitle('两阶段最小二乘法(2SLS)图解', fontsize=16, fontweight='bold', y=1.02)
plt.tight_layout()
plt.savefig('2sls_illustration.png', dpi=300, bbox_inches='tight')
plt.show()🎓 LATE 的可视化
# 展示四种人群的处理效应异质性
fig, ax = plt.subplots(figsize=(10, 6))
type_effects = {}
for t in ['Complier', 'Always-taker', 'Never-taker', 'Defier']:
mask = df['type'] == t
if mask.sum() > 0:
# 计算该类型中 D=1 和 D=0 的平均 Y 差异(近似处理效应)
type_effects[t] = {
'count': mask.sum(),
'pct': mask.mean() * 100
}
# 人群比例饼图
labels = []
sizes = []
colors_pie = ['#2ecc71', '#3498db', '#e74c3c', '#95a5a6']
for i, (t, info) in enumerate(type_effects.items()):
labels.append(f'{t}\n({info["pct"]:.1f}%)')
sizes.append(info['count'])
ax.pie(sizes, labels=labels, colors=colors_pie[:len(sizes)],
autopct='%1.1f%%', startangle=90, textprops={'fontsize': 12})
ax.set_title('四种人群的比例分布\n(IV 只识别 Compliers 的效应)',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.savefig('late_compliers.png', dpi=300, bbox_inches='tight')
plt.show()
print("\n" + "=" * 70)
print("LATE 解释")
print("=" * 70)
print(f"IV/2SLS 估计值 ≈ {wald_est:.4f}")
print(f"这个估计值是 Compliers(约 {type_effects.get('Complier', {}).get('pct', 0):.1f}% 的人群)的处理效应")
print(f"不是全体人群的 ATE!")🔑 linearmodels 语法详解
基本语法
from linearmodels.iv import IV2SLS
# 公式语法:
# Y ~ exogenous_vars [endogenous_vars ~ instruments]
#
# 其中:
# - Y: 因变量
# - exogenous_vars: 外生变量(包括常数项 1)
# - endogenous_vars: 内生变量
# - instruments: 工具变量常用场景
# 场景 1:一个内生变量,一个 IV,无控制变量
result = IV2SLS.from_formula('Y ~ 1 [D ~ Z]', data=df).fit()
# 场景 2:一个内生变量,一个 IV,有控制变量
result = IV2SLS.from_formula('Y ~ 1 + age + female [D ~ Z]', data=df).fit()
# 场景 3:一个内生变量,多个 IV
result = IV2SLS.from_formula('Y ~ 1 + age [D ~ Z1 + Z2 + Z3]', data=df).fit()
# 场景 4:多个内生变量
result = IV2SLS.from_formula('Y ~ 1 + age [D1 + D2 ~ Z1 + Z2 + Z3]', data=df).fit()稳健标准误
# 异方差稳健标准误(HC1)
result = IV2SLS.from_formula('Y ~ 1 [D ~ Z]', data=df).fit(cov_type='robust')
# 聚类标准误
result = IV2SLS.from_formula('Y ~ 1 [D ~ Z]', data=df).fit(
cov_type='clustered', clusters=df['cluster_id']
)
# 核(kernel)标准误
result = IV2SLS.from_formula('Y ~ 1 [D ~ Z]', data=df).fit(
cov_type='kernel', kernel='bartlett', bandwidth=5
)提取关键信息
result = IV2SLS.from_formula('Y ~ 1 + age + female [D ~ Z]', data=df).fit(cov_type='robust')
# 系数
print("系数:", result.params)
# 标准误
print("标准误:", result.std_errors)
# t 统计量
print("t 统计量:", result.tstats)
# p 值
print("p 值:", result.pvalues)
# 置信区间
print("置信区间:", result.conf_int())
# 第一阶段诊断
print("第一阶段 F 统计量:", result.first_stage.diagnostics)
# 完整摘要
print(result.summary)📝 本节要点
- 2SLS 的核心逻辑:第一阶段用 预测 得到 ,第二阶段用 解释
- 为什么有效: 只包含被 解释的变异,这部分变异是"干净的"(外生的)
- 恰好识别时:2SLS = IV = Wald 估计量
- LATE:IV 估计的是**顺从者(Compliers)**的因果效应,不是整个人群的 ATE
- 单调性假设:排除 Defiers 的存在
- 标准误:手动 2SLS 的标准误不正确,必须使用专门的 IV 软件包
linearmodels:Python 中做 IV/2SLS 的推荐工具
下一节:8.4 弱工具变量与诊断检验 — 当工具变量太"弱"时会发生什么?如何诊断和应对?