Skip to content

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 的线性预测值)。

第二阶段

过度识别的好处

  1. 更强的第一阶段(更多 IV 解释 的变异更大 → F 统计量更高)
  2. 可以进行过度识别检验(Sargan/Hansen J 检验)来检验 IV 的有效性

过度识别的风险

  • 更多的 IV 意味着更多的排他性约束需要满足
  • 如果某些 IV 无效,估计量会有偏

🎯 LATE:IV 估计的到底是"谁"的效应?

一个关键问题

我们之前推导了 IV 估计量,但一个根本性的问题是:

IV 估计的因果效应是对整个人群的平均效应(ATE),还是对某些特定人群的效应?

答案令人惊讶:IV 估计的不是 ATE,而是局部平均处理效应(Local Average Treatment Effect, LATE)——只针对特定的一群人:顺从者(Compliers)

四种人群(Imbens & Angrist 1994)

假设工具变量 ,处理变量

每个人有两个潜在处理状态

  • :如果 ,此人的处理状态
  • :如果 ,此人的处理状态

根据 的组合,可以把人群分为四类:

类型含义例子(征兵抽签)
Always-takers11无论 如何,都接受处理不管抽签号码,主动参军
Never-takers00无论 如何,都不接受处理不管抽签号码,都不参军
Compliers01 才接受处理被抽到才参军,没抽到就不去
Defiers10 反而不接受处理被抽到反而逃避,没抽到反而参军(不太合理)

大白话

  • Always-takers(总是接受者):无论怎样都会上大学的人
  • Never-takers(从不接受者):无论怎样都不上大学的人
  • Compliers(顺从者):工具变量"推"了一下就去的人 ⭐
  • Defiers(违抗者):工具变量让你去,你偏不去

单调性假设(Monotonicity)

假设

含义:不存在 Defiers! 从 0 变成 1,不会让任何人从"接受处理"变成"不接受处理"。

在征兵抽签的例子中

  • (抽签号码低,被征兵)只会增加参军概率,不会减少
  • 没有人因为被抽到反而不去参军
  • 这个假设很合理

在其他例子中

  • = 住得离学院近 → 只会增加上大学概率(合理)
  • = 收到招生宣传册 → 只会增加申请概率(合理)

LATE 定理(Imbens & Angrist 1994)

在以下条件下:

  1. 相关性
  2. 排他性 只通过 影响
  3. 独立性
  4. 单调性 对所有

Wald/IV 估计量等于 Compliers 的平均处理效应

这就是 LATE(局部平均处理效应)

为什么只能识别 Compliers 的效应?

直觉推导

从 0 变成 1 时:

  • Always-takers:处理状态不变(),对 的差异贡献 = 0
  • Never-takers:处理状态不变(),对 的差异贡献 = 0
  • Compliers:处理状态改变(),对 的差异贡献 =

所以, 完全来自 Compliers 的效应:

而分母:

两者相除:

LATE 的含义与局限

含义

  • IV 估计的是被工具变量影响处理状态的那群人的因果效应
  • 不同的 IV 可能对应不同的 Compliers → 不同的 LATE!
  • 例:用"出生季度"作 IV 得到的 LATE,和用"距离学院"作 IV 得到的 LATE,可能不同

局限

  1. LATE ≠ ATE:如果 Compliers 不具有代表性,LATE 不能推广到整个人群
  2. 不同 IV 的 LATE 不可比:Compliers 群体不同
  3. 政策含义需要谨慎:LATE 告诉我们的是"被这个 IV 影响的人"的效应,而不是"所有人"的效应

LATE vs ATE vs ATT

参数定义估计方法适用人群
ATERCT, OLS(无混淆)全体
ATT匹配/PSM处理组
LATEIV/2SLS顺从者

💻 Python 实现:linearmodels.iv.IV2SLS

模拟数据(含内生性和四种人群)

python
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(有偏)

python
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

python
# --- 第一阶段 ---
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(推荐)

python
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 估计量(验证)

python
# 简化形式
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}")

结果汇总

python
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),它会自动修正标准误。

python
# 错误的做法(标准误不对):
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 的两个阶段

python
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 的可视化

python
# 展示四种人群的处理效应异质性
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 语法详解

基本语法

python
from linearmodels.iv import IV2SLS

# 公式语法:
# Y ~ exogenous_vars [endogenous_vars ~ instruments]
#
# 其中:
# - Y: 因变量
# - exogenous_vars: 外生变量(包括常数项 1)
# - endogenous_vars: 内生变量
# - instruments: 工具变量

常用场景

python
# 场景 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()

稳健标准误

python
# 异方差稳健标准误(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
)

提取关键信息

python
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)

📝 本节要点

  1. 2SLS 的核心逻辑:第一阶段用 预测 得到 ,第二阶段用 解释
  2. 为什么有效 只包含被 解释的变异,这部分变异是"干净的"(外生的)
  3. 恰好识别时:2SLS = IV = Wald 估计量
  4. LATE:IV 估计的是**顺从者(Compliers)**的因果效应,不是整个人群的 ATE
  5. 单调性假设:排除 Defiers 的存在
  6. 标准误:手动 2SLS 的标准误不正确,必须使用专门的 IV 软件包
  7. linearmodels:Python 中做 IV/2SLS 的推荐工具

下一节8.4 弱工具变量与诊断检验 — 当工具变量太"弱"时会发生什么?如何诊断和应对?

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