Causal Identification / Matching
PSM 倾向得分匹配
PSM 用一个一维得分压缩高维协变量,再在共同支持上比较处理组和相似对照组,目标是让可比性从“看起来像”变成可诊断的平衡条件。
Mechanism Lab
动画:PSM 如何在共同支持上构造匹配对
动画把处理组和对照组投影到同一条倾向得分轴上,逐步显示共同支持、最近邻、卡尺筛选和匹配后的平衡诊断。
Step 1 / 5
Raw scores
先估计每个样本接受处理的概率,把高维协变量压到一条得分轴。
e(X)=P(D=1|X)Animation Control
Reduced-motion users receive the same step states without continuous motion.
01 / 直觉
核心直觉
倾向得分 e(X) 是个体在协变量 X 下接受处理的概率。它不是处理效应,而是进入处理组的选择机制摘要。
匹配的目标不是让结果 Y 相近,而是让处理组和对照组在处理前协变量上平衡。
PSM 的识别仍依赖“可观测变量条件独立”:所有同时影响处理选择和结果的混杂因素必须已经在 X 中。
02 / 数学
从可忽略性到一维匹配估计量
01 / 潜在结果与可忽略性
令 D 表示是否接受处理,Y(1), Y(0) 表示潜在结果。PSM 不能解决未观测混杂;它从给定 X 后处理近似随机这个假设出发。
(Y(1), Y(0)) independent of D | X
0 < P(D=1|X) < 102 / 倾向得分
倾向得分是条件处理概率。它把多维 X 压成一维,但保留了处理分配所需的概率信息。
e(X) = P(D=1 | X)03 / 平衡性质证明
在离散 X 的情形下,对任意满足 e(x)=p 的 x,用贝叶斯公式可得处理组在给定 e(X)=p 后的 X 分布等于总体在该得分层里的 X 分布。
P(X=x | D=1, e(X)=p)
= P(D=1 | X=x, e(X)=p) P(X=x | e(X)=p) / P(D=1 | e(X)=p)
= p P(X=x | e(X)=p) / p
= P(X=x | e(X)=p)04 / 可忽略性转移
若给定 X 后处理分配与潜在结果独立,并且 e(X) 是平衡得分,则给定 e(X) 后处理也与潜在结果独立。这让一维匹配可以替代高维精确匹配。
(Y(1), Y(0)) independent of D | e(X)05 / ATT 匹配估计量
对每个处理个体 i,找得分接近的对照个体集合 J(i),用权重 w_ij 构造其未处理反事实,再对处理组平均。
tau_ATT_hat = (1/N_T) sum_{i:D_i=1} [Y_i - sum_{j:D_j=0} w_ij Y_j]
w_ij >= 0, sum_j w_ij = 106 / 平衡诊断
匹配后应检查协变量标准化均差,而不只报告匹配算法。经验上 |SMD| 小于 0.1 常作为平衡改善的参考阈值。
SMD_k = (mean(X_k|D=1) - mean(X_k|D=0, matched)) / sqrt((s_Tk^2 + s_Ck^2)/2)03 / 代码
Python 代码:估计倾向得分、最近邻匹配与平衡表
下面是可复现研究中常见的 PSM skeleton:先估计处理概率,再限制共同支持,做最近邻匹配,最后报告 ATT 和匹配前后平衡。
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors
# df columns:
# outcome, treated, age, baseline_score, income, school_size
covariates = ["age", "baseline_score", "income", "school_size"]
X = df[covariates]
D = df["treated"].astype(int)
ps_model = LogisticRegression(max_iter=2000)
ps_model.fit(X, D)
df = df.copy()
df["pscore"] = ps_model.predict_proba(X)[:, 1]
# Common support: keep treated and control observations whose scores overlap.
treat = df[df["treated"] == 1].copy()
control = df[df["treated"] == 0].copy()
lower = max(treat["pscore"].min(), control["pscore"].min())
upper = min(treat["pscore"].max(), control["pscore"].max())
support = df[df["pscore"].between(lower, upper)].copy()
treat = support[support["treated"] == 1].copy()
control = support[support["treated"] == 0].copy()
matcher = NearestNeighbors(n_neighbors=1, metric="euclidean")
matcher.fit(control[["pscore"]])
distance, index = matcher.kneighbors(treat[["pscore"]])
matched_control = control.iloc[index[:, 0]].copy()
matched_control.index = treat.index
att = (treat["outcome"] - matched_control["outcome"]).mean()
print({"ATT": att, "matched_pairs": len(treat), "max_distance": float(distance.max())})
def standardized_mean_difference(left, right, columns):
rows = []
for col in columns:
pooled_sd = np.sqrt((left[col].var() + right[col].var()) / 2)
rows.append({
"covariate": col,
"smd": (left[col].mean() - right[col].mean()) / pooled_sd,
})
return pd.DataFrame(rows)
before = standardized_mean_difference(
df[df["treated"] == 1],
df[df["treated"] == 0],
covariates,
)
after = standardized_mean_difference(treat, matched_control, covariates)
print(before.assign(stage="before"))
print(after.assign(stage="after"))04 / 案例
案例:助学项目参与学生的可比对照组
- 研究问题:参加某助学项目是否提高后续成绩?处理选择明显依赖年龄、基线成绩、家庭收入和学校规模。
- 直接比较参与者和未参与者会混入选择偏差,因为高动机或资源更好的学生更可能参与。
- PSM 的工作流是先估计参与概率,再剔除没有共同支持的样本,最后把每个参与者和倾向得分相近的未参与者配对。
- 报告时不应只给一个 ATT;还要给匹配前后协变量平衡图、共同支持图、卡尺敏感性和未观测混杂的讨论。
05 / 风险
常见误区
参考资料
- Rosenbaum and Rubin (1983), The Central Role of the Propensity Scorehttps://doi.org/10.1093/biomet/70.1.41
- Austin (2011), An Introduction to Propensity Score Methodshttps://doi.org/10.1080/00273171.2011.568786
- Imbens and Rubin (2015), Causal Inferencehttps://www.cambridge.org/core/books/causal-inference/71126BE90C58F1A431FE9B2DD07938AB