每一个对基于大语言模型的功能进行因果推断的产品实验团队最终都会遇到同样的问题:当用户点击“尝试我们的AI助手”时,这些志愿者其实并不是随机样本。
上个季度,你们的产品推出了新的代理模式。用户需要手动点击“启用代理模式”按钮才能使用该功能。数据显示,使用代理模式的用户完成任务的数量比不使用的用户多出了21个百分点。首席产品官称这是今年最成功的功能更新。
但你们其实察觉到了不对劲的地方——那些频繁使用该功能的用户会主动选择新功能,而那些很少使用该功能的用户则完全忽略这些选项。那21个百分点的差距实际上反映了代理模式带来的效果,以及高级用户与普通用户之间原本就存在的差异。
这就是“自愿参与陷阱”。在任何一款通过用户操作开关来提供新功能的人工智能产品中,都会出现这种情况:“尝试我们的AI助手”、“启用智能回复”、“开启代码建议”。那些选择使用这些功能的用户,与那些直接跳过这些选项的用户之间存在系统性差异。如果对这两组用户进行简单的比较,就会将这个功能的实际效果误认为是导致某些用户选择使用它的原因。
将某个人工智能功能设置为需要用户主动选择的模式,本质上就是在进行一项产品实验。其假设是:这一功能确实能够改善使用它的用户的体验。
与A/B测试不同,在A/B测试中,随机分配的样本在其他方面是完全相同的;而在这种模式下,两个群体在做出选择之前就已经存在差异了。这种先天的差异才是真正需要解决的问题,而仅通过查看仪表盘上的数据来进行t检验是无法解决这个问题的。
倾向得分方法是数据科学家用来区分用户选择使用某项功能的意愿与这项功能实际效果的工具。它们会对比较结果进行重新加权处理,使得选择使用该功能的群体和未选择的群体在可观察的特征上变得相似,从而接近于随机实验所能得到的结果。
本教程会详细讲解整个分析流程:包括倾向得分的计算、逆概率加权、最近邻匹配、平衡性检验以及自助法置信区间的计算。实验数据来自一个包含50,000名用户的合成SaaS数据集,其中实际的因果效应是已知的。通过这个实验,你将能够估算出倾向得分、量化其中的不确定性,并了解这种方法在哪些情况下会失效。
配套代码:所有代码块都可以在github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm/tree/main/02_propensity_opt_in处的配套笔记本中端到端运行。该笔记本中的psm_demo.ipynb文件已经预先运行过了所有代码,因此你可以在GitHub上先查看结果,再在本地进行测试。
目录
为什么选择性使用某些功能会破坏简单的比较结果
A/B测试的数学模型之所以如此简洁,是因为它基于一个假设:实验组的分配与其他所有因素无关。比如抛硬币来决定:让一半的用户使用该功能,这种随机分配方式从本质上就能消除所有可能的干扰因素。然而,在选择性使用的环境中,并不存在这样的随机机制。
有三种原因会导致基于选择性使用某些功能的比较结果出现偏差。
1. 基于用户参与度的选择性分配
那些活跃度较高的用户往往会尝试所有新功能。如果活跃用户中有65%选择了该功能,而不活跃用户中只有12%选择了该功能,那么选择该功能的群体中自然就会包含那些本来就会完成更多任务的用户。
这种分布上的不平衡本身就已经解释了观察到的效果提升现象,而这与该功能实际产生的作用无关。
2. 基于用户使用意图的选择性分配
选择使用新功能的用户往往有特定的使用需求。例如,那些点击“尝试代码建议”功能的开发者,本身就已经有需要编写的代码了;因此,即使使用控制组界面,这些用户也会表现出更高的任务完成率。
3. 基于用户风险承受能力的选择性分配
早期采用新功能的用户通常能忍受其中的一些缺陷。那些点击“尝试测试版”功能且能接受延迟较长的用户会继续使用该功能,而那些规避风险的用户则会立即放弃。
因此,选择性使用某些功能的群体中自然会包含更多愿意忍受不良体验的用户,而这会影响你测量的所有后续指标。
以上三种情况都会导致同样的结果:将选择使用该功能的用户与其他所有用户进行简单比较,往往会高估该功能的实际效果,其偏差幅度可能达到2倍甚至更多,这取决于选择该功能的用户主要集中在哪些活跃用户中。
在本教程使用的模拟数据集中,这种简单的比较方法会将实际为+8个百分点的效果夸大到+21个百分点,即误差幅度达到了2.6倍。为此,人们开发出了倾向得分分析法来纠正这一问题。
倾向得分分析法到底能做什么

图1:两个假设群体的倾向得分分布示意图。选择使用该功能的群体(红色部分)的倾向得分普遍较高,而未选择的群体(蓝色部分)的倾向得分则较低。
在上面的图中,x轴下方的括号区域将分数范围分为了三个部分:低倾向得分区域的控制组中很少有使用了该功能的用户;中间部分的共同支持区域内,两组用户的比例大致相同;高倾向得分区域的实验组中也有很少控制组用户使用该功能。倾向得分分析法通过在共同支持区域内重新分配样本权重或重新匹配样本,使得两组用户在可观察的指标上看起来是均衡的。对于极端情况,则需要谨慎处理或直接剔除这些数据。
倾向得分是指根据用户可观察到的特征,他们选择参与某项活动的概率。如果能够准确估算这一概率,就可以利用它来重新调整样本的权重,使得选择参与与未选择的用户在各种可观察特征上表现出相似性,就如同参与选项是随机分配的一样。
有两种实用的方法可以利用倾向得分:
-
逆概率加权法会为每位用户分配一个权重,该权重等于他们实际接受某项处理措施的概率的倒数。选择参与的用户会被赋予1/P(选择参与)的权重,而未选择参与的用户则会被赋予1/P(未选择参与)的权重。经过加权处理后,这两组用户在各种可观察特征上就会变得均衡,而它们在结果上的差异也就近似于该处理措施的平均效果。
-
匹配法会将每位选择参与的用户与一个或多个倾向得分相似的未选择参与的用户进行配对。这些配对组之间的平均结果差异就可以用来估算接受该处理措施后的平均效果,也就是选择参与的用户实际上因此获得了多少收益。
这两种方法都依赖于三个相互配合的识别假设。
-
首先,无混杂因素:所有会影响用户是否选择参与以及最终结果的可观察变量都必须被纳入倾向得分模型中。
-
其次,存在重叠概率:每位用户都有一定的概率选择参与,同时也有一定的概率不选择参与。
-
第三,不存在干扰效应
:某位用户的参与决策不会影响另一位用户的最终结果(即满足“稳定单元处理值假设”)。
如果其中任何一个假设不成立,即使另外两个假设成立,估计结果也会出现偏差。本教程的最后会详细分析这些可能导致的错误情况。
先决条件
你需要使用Python 3.11或更高版本,并且需要对pandas和scikit-learn有基本的了解,同时还需要对逻辑回归有一定的熟悉程度。
请安装本教程所需的软件包:
pip install numpy pandas scikit-learn matplotlib
具体工作流程如下:这四个软件包共同完成了整个数据分析流程。pandas用于加载数据,NumPy负责处理权重计算及数组运算,scikit-learn用于构建倾向得分模型并执行匹配操作,而matplotlib则用于生成用于诊断分析的数据图表。
请克隆配套的代码仓库以获取合成数据集:
git clone https://github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm.git
cd product-experimentation-causal-inference-genai-llm
python data/generate_data.py --seed 42 --n-users 50000 --out data/synthetic_llm_logs.csv
具体操作步骤如下:克隆代码仓库后,运行generate_data.py命令即可生成本教程中会用到的合成数据集。使用参数“seed 42”可以确保数据集的可重复性,而50,000名用户的样本量可以为本教程中的各种分析方法提供可靠的数据支持。最终生成的CSV文件会被保存在data/synthetic_llm_logs.csv路径下。
设置测试示例
这个合成数据集模拟了一个SaaS产品,在该产品中,用户可以选择使用成本更高的模型。在拥有五万名用户的情况下,不同用户群体的选择比例存在显著差异:重度使用者的选择比例为65%,中度使用者为35%,而轻度使用者的选择比例仅为12%。
数据生成器中设定的真实因果效应表现为:选择使用高级模型的用户的任务完成率会提高8个百分点。但如果进行简单的比较,这一数值会被夸大到约21个百分点,因为选择偏差使得被纳入分析的群体中包含了那些参与度最高的用户。
只有了解真实的因果关系,才能验证你所使用的倾向得分方法是否能够准确反映这一关系。
现在来加载数据,并观察其中存在的选择偏差问题:
import pandas as pd
df = pd.read_csv("data/synthetic_llm_logs.csv")
print(df.groupby("engagement_tier").opt_in_agent_mode.mean().round(3))
naive_effect = (
df[df.opt_in_agent_mode == 1].task_completed.mean()
- df[df/opt_in_agent_mode == 0].task_completed.mean()
)
print(f"\n朴素选择效应:{naive_effect:+.4f}")
预期输出结果:
engagement_tier
heavy 0.647
light 0.120
medium 0.353
Name: opt_in_agent_mode, dtype: float64
Naive opt-in effect: +0.2106
实际分析结果如下:你加载了50,000条数据,按照用户参与程度对它们进行分组,然后计算了每组中选择使用高级模型的用户的比例。结果显示,重度使用者的选择比例远高于轻度使用者,这正是数据中存在的选择偏差现象。朴素方法计算出的效应值为+0.2106,几乎是真实值+0.08的三倍。正是这种差距,需要通过倾向得分方法来消除。
步骤1:估算倾向得分
倾向得分是一种模型输出的结果,该模型能够根据可观察的特征预测用户是否会选择使用某种功能。逻辑回归是一个合适的起点,因为它既易于解释,计算速度也很快;但在步骤4中要关注平衡诊断结果——如果任何加权SMD值超过0.1,那就说明逻辑回归模型忽略了某些交互作用因素,此时应该改用梯度提升算法。
对于这个数据集来说,相关的可观察特征包括用户参与程度和查询的置信度。在现实产品中,你还可以纳入其他可能影响用户选择行为的变量,比如设备类型、使用时长、套餐等级以及历史使用习惯等。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
X = pd.get_dummies(
df[["engagement_tier", "query_confidence"],
drop_first=True
).astype(float)
y_treat = df.opt_in_agent_mode
ps_model = LogisticRegression(max_iter=1000).fit(X, y_treat)
df["propensity"] = ps_model.predict_proba(X)[:, 1]
# 基本验证步骤
print(df.groupby("engagement_tier").propensity.mean().round(3))
print(
f"\n处理组倾向得分范围: "
f"{df[df.opt_in_agent_mode == 1].propensity.min():.3f} - "
f"{df[df/opt_in_agent_mode == 1].propensity.max():.3f}"
)
print(
f"对照组倾向得分范围: "
f"{df[df(opt_in_agent_mode == 0).propensity.min():.3f} - "
f"{df[df.opt_in_agent_mode == 0].propensity.max():.3f}"
)
print(f"倾向得分模型的AUC值:{roc_auc_score(y_treat, df.propensity):.3f}")
预期输出:
engagement_tier
heavy 0.646
light 0.120
medium 0.353
名称:倾向得分,数据类型:float64
倾向得分范围(实验组):0.114 – 0.675
倾向得分范围(对照组):0.114 – 0.673
倾向得分模型的AUC值:0.744
具体处理过程如下:你将用户参与程度划分为不同的类别,并将这些类别转换为虚拟变量;同时保持查询结果的置信度为连续值,然后使用逻辑回归模型进行拟合。该模型预测出的概率值即为每个用户的倾向得分。
Scikit-learn中的`LogisticRegression`默认会应用L2正则化(`C=1.0`),这种正则化方式会使倾向得分的值略微向0.5靠近。如果在实际生产环境中使用,你可以将`penalty=None`设置为无正则化处理。
在每个参与程度类别中,计算出的平均倾向得分几乎能够准确反映该类别中用户的真实选择比例,因此该模型已经经过了校准。0.744的AUC值说明该模型在区分用户是否选择参与时,其表现远优于随机猜测的结果(0.5)。
此外,实验组与对照组的倾向得分范围存在重叠(两者的范围都大致在0.11到0.67之间),这正是我们所要求的视觉上的重叠条件。

图2:在同一包含50,000个用户的合成数据集上进行的两次正向性检验结果。
在上图中,顶部面板展示了各组用户倾向得分的平滑核密度曲线。三个峰值分别与三个参与程度类别相对应(轻度参与的用户的倾向得分约为0.12,中度参与的约为0.35,重度参与的约为0.65),这一结果符合预期,因为用户的参与程度确实是由其所处类别决定的。底部面板将同样的分布转换为各类别中的用户数量:每个类别中都包含数千名选择参与和未选择参与的用户,这正是进行正向性检验所需要的数据结构。
虽然图1只是以示意图的形式说明了这一概念,但该图证实这一概念在真实数据中也同样成立,因此后续进行的加权处理和匹配操作将能够基于实际的数据结果来进行。
步骤2:逆概率加权
逆概率加权方法会根据用户的倾向得分为其分配一个权重,这个权重的大小与用户的倾向得分成反比。例如,一个倾向得分为0.12的用户属于罕见情况(即尽管参与程度较低但仍选择参与的用户),因此他的权重相当于1 / 0.12 ≈ 8,这意味着他的数据能够代表群体中大约8个类似用户的情况;而一个倾向得分为0.12的对照组用户,则属于典型情况,因此他的权重为1 / (1 – 0.12) ≈ 1.14。
import numpy as np
# ATE加权:实验组的权重为1/P(treat),对照组的权重为1/P(no treat)
df["ipw"] = np.where(
df.opt_in_agent_mode == 1,
1 / df.propensity,
1 / (1 - df.propensity)
)
t = df[df(opt_in_agent_mode == 1]
c = df[df/opt_in_agent_mode == 0]
ate_ipw = (
(t.task_completed * t.ipw).sum() / t.ipw.sum()
- (c.task_completed * c.ipw).sum() / c.ipw.sum()
)
print(f"IPW平均处理效应(ATE):{ate_ipw:+.4f}")
# ATT:选择参与的用户实际获得的收益
df["ipw_att"] = np.where(
df.opt_in_agent_mode == 1,
1,
df.propensity / (1 - df.propensity)
)
t = df[df(opt_in_agent_mode == 1] # 在计算ATT时重新划分数据集
c = df[df/opt_in_agent_mode == 0]
treated_mean = t.task_completed.mean()
control_w_mean = (c.task_completed * c.ipw_att).sum() / c.ipw_att.sum()
att_ipw = treated_mean - control_w_mean
print(f"IPW对实验组的平均处理效应(ATT):{att_ipw:+.4f}")
预期输出:
IPW平均处理效应:+0.0851
接受处理的用户的IPW平均处理效应:+0.0770
具体计算过程如下:首先,为每位用户计算ATE权重,然后比较选择参与实验的用户与未选择参与实验的用户在任务完成度上的差异。接着计算ATT权重,这些权重仅用于重新调整对照组的分布,使其与接受处理用户的协变量分布相匹配,从而计算出接受处理用户的平均处理效应。
ATE用于回答“对于那些可能选择参与实验、也可能没有选择的随机用户来说,这一措施会产生什么影响?”这个问题;而ATT则用于回答“选择参与实验的用户实际上获得了哪些收益?”这个问题。在这个数据集上,ATE的值为+0.0851,ATT的值为+0.0770,这两个数值都接近真实值+0.08,并且远高于简单的+0.2106这一估计结果。
在实际应用中,这两种方法的区别非常重要。决定是否向未选择参与实验的用户推出某项功能需要使用ATE来进行评估;而要了解选择参与实验的用户所获得的实际收益,则需要使用ATT。
步骤3:最近邻匹配
最近邻匹配采用另一种计算方法:将每位选择参与实验的用户与倾向得分最接近的未选择参与实验的用户进行配对,然后计算这些配对对之间的结果差异平均值,从而得到ATT的估计值。
from sklearn.neighbors import NearestNeighbors
treated_ps = df[df.opt_in_agent_mode == 1][["propensity"]].values
control_ps = df[df(opt_in_agent_mode == 0][["propensity"]].values)
nn = NearestNeighbors(n_neighbors=1).fit(control_PS)
_, idx = nn.kneighbors(treated_ps)
treated_outcomes = df[df.opt_in_agent_mode == 1].task_completed.values
matched_control_outcomes = (
df[df/opt_in_agent_mode == 0][["task_completed"]].values[idx.flatten()]
)
att_match = (treated_outcomes - matched_control_outcomes).mean()
print(f"1-NN匹配得到的ATT值:{att_match:+.4f}")
预期输出:
1-NN匹配得到的ATT值:+0.0752
具体计算过程如下:首先提取每组用户的倾向得分,然后对对照组使用最近邻匹配算法进行建模,从而为每位接受处理的用户找到倾向得分最接近的对照用户。由于NearestNeighbors算法允许同一个对照用户被多个接受处理的用户所选中,因此这是一种带有替换机制的匹配方法。
接下来,获取每位接受处理用户的实际结果以及与其配对的对照用户的结果,计算每对数据之间的差异值,并对这些差异值进行平均处理,从而得到ATT的估计值。这个数值反映了选择参与实验的用户相比那些未选择参与实验但情况相似的用户所获得的实际收益。
最终得到的+0.0752这一结果接近真实值+0.08,但略低于使用IPW方法计算得出的ATT值。这是1-NN匹配方法的典型特征,因为使用最近邻匹配得到的估计值通常具有较高的方差。
值得注意的是,这种匹配方法还存在两种变体。其中一种变体允许同一个对照用户被多个接受处理的用户所选中,这样可以在缺乏合适的对照对象时减少偏差,但也会导致方差的增加。
不进行替换的匹配方式会确保每个对照组用户至多只与一个实验组用户配对,这样虽然能降低方差,但当实验组的人数远超过可用的对照组人数时,就会导致配对质量下降。
对于大多数实际分析来说,使用k=3-5的k最近邻匹配方法,并允许进行替换,是一个合理的默认选择。
步骤4:检查协变量平衡性
只有当倾向得分方法真正实现了各组之间协变量的平衡时,它们才能发挥应有的作用。你必须验证这一点,因为如果平衡性没有得到保障,你的估计结果就会出错。
判断协变量平衡性的标准方法是计算每个协变量的标准化均值差(SMD)。SMD是通过将实验组的平均值与对照组的平均值相减,然后除以合并后的标准差来得出的。
在加权之前,SMD可以反映原始分组之间的不平衡程度;而在加权之后,这些数值应该会变小(通常认为|SMD|小于0.1表示平衡性良好)。
def smd(treated_vals, control_vals, treated_w=None, control_w=None):
"""标准化均值差,可选项包括使用权重。"""
if treated_w is None:
treated_w = np.ones(len(treated_vals))
if control_w is None:
control_w = np.ones(len(control_vals))
t_mean = np.average(treated_vals, weights=treated_w)
c_mean = np_average(control_vals, weights=control_w)
pooled_std = np.sqrt((treated_vals.var() + control_vals.var()) / 2)
return (t_mean - c_mean) / pooled_std
engagement_heavy = (df.engagement_tier == "heavy").astype(float).values
qc = df.query_confidence.values
tr = (df.opt_in_agent_mode == 1).values
covariates = {
"engagement_tier_heavy": engagement_heavy,
"query_confidence": qc,
}
print(f"{'Covariate':<30} {'Raw SMD':>10} {'Weighted SMD':>15}")
for name, vals in covariates.items():
smd_raw = smd(vals[tr], vals[~tr])
smd_weighted = smd(
vals[tr], vals[~tr],
treated_w=df[tr].ipw.values,
control_w=df[~tr].ipw.values,
)
print(f"{name:<30} {smd_raw:>+10.3f} {smd_weighted:>+15.3f}")
预期输出结果:
Covariate Raw SMD Weighted SMD
engagement_tier_heavy +0.742 +0.002
query_confidence -0.032 -0.003
具体计算过程如下:该辅助函数会为任意协变量计算标准化均值差,用户也可以选择使用权重进行计算。
随后,你会看到每个协变量的原始SMD值和加权后的SMD值。对于engagement_tier_heavy这一协变量来说,原始SMD值为+0.742,这意味着重度使用用户的参与比例远高于其他用户;而加权后的SMD值降到了+0.002,说明这种不平衡在加权处理后得到了消除。对于query_confidence这一协变量而言,其在原始数据上就已经接近平衡状态,加权处理后依然保持了这种平衡性。如果任何加权后的SMD值的绝对值超过了0.1,那就说明你的倾向得分模型存在问题;通常解决方法是在逻辑回归中添加更多的特征或交互项。
从视觉上看,上图已经证实了那些通过数值分析得出的结论:重叠条件确实成立,因此实现平衡是完全可能的。
步骤5:自助法置信区间
仅凭点估计值是无法完全说明问题的。任何你需要向产品团队报告的估算结果,都应当附带一个置信区间,以便他们能够判断+0.08这个数值与+0.03或+0.12这两个数值是否有显著差异。由于需要使用估算得到的倾向得分,因此对于IPW和匹配分析来说,计算标准误差会显得相当复杂;所以,最简单且最可靠的方法就是使用非参数自助法。
def estimate_all(sample):
"""在自助抽样样本的基础上返回(ATE_IPW, ATT_IPW, ATT_match)这三个数值。"""
s = sample.copy()
X_s = pd.get_dummies(
s[["engagement_tier", "query_confidence"]], drop_first=True
).astype(float)
ps = LogisticRegression(max_iter=1000).fit(X_s, s.opt_in_agent_mode)
s["p"] = ps.predict_proba(X_s)[:, 1]
s["w_ate"] = np.where(
s.opt_in_agent_mode == 1, 1 / s.p, 1 / (1 - s.p)
)
s["w_att"] = npwhere(
s.opt_in_agent_mode == 1, 1, s.p / (1 - s.p)
)
t, c = s[s(opt_in_agent_mode == 1], s[s.opt_in_agent_mode == 0])
ate = (
(t.task_completed * t.w_ate).sum() / t.w_ate.sum()
- (c.task_completed * c.w_ate).sum() / c.w_ate.sum()
)
att = t.task_completed.mean() - (
(c.task_completed * c.w_att).sum() / c.w_att.sum()
)
nn_b = NearestNeighbors(n_neighbors=1).fit(c[["p"]].values)
_, idx_b = nn_b.kneighbors(t[["p"]].values)
match = (
t.task_completed.values
- c.task_completed.values[idx_b.flatten()]
).mean()
return ate, att, match
rng = np.random.default_rng(7)
n_reps = 500
results = np.zeros((n_reps, 3))
for i in range(n_reps):
boot = df.iloc[rng.integers(0, len(df), size=len(df))]
results[i] = estimate_all(boot)
for name, col in zip(["IPW ATE", "IPW ATT", "1-NN ATT"], range(3)):
lo, hi = np.percentile(results[:, col], [2.5, 97.5])
print(f"{name:<10} 95%置信区间: [{lo:+.4f}, {hi:+.4f}]")
预期输出结果:
IPW ATE 95%置信区间: [+0.0745, +0.0954]
IPW ATT 95%置信区间: [+0.0687, +0.0865]
1-NN ATT 95%置信区间: [+0.0659, +0.0940]
具体计算过程如下:首先对数据集进行500次有放回的重采样,然后重新拟合倾向得分模型,并针对每次重采样结果重新计算各项估算值;最后将自助法分布中第2.5百分位和第97.5百分位的数值作为95%置信区间。这三个置信区间都包含了真实值+0.08这个范围,而明显排除了+0.21这个不合理的数值。其中,IPW ATT的置信区间最为狭窄,因为这种估算方法仅对对照组进行了加权处理;而1-NN匹配方法的置信区间最为宽泛,因为这种匹配方式会排除那些不在匹配组中的对照用户。在笔记本电脑上运行这段代码大约需要90秒的时间。在撰写面向利益相关者的报告时,应该以点估计值作为主要内容,并同时列出置信区间,这样团队就能清楚地看到这个数值所包含的不确定性。
当倾向得分方法失效时
在假设成立的情况下,倾向得分能使基于用户自愿参与的比较变得严谨。然而,一旦这些假设不成立,它们就会产生有偏的估计结果。
四种常见的失效情况与前面提到的三个识别假设有关。
1. 未测量的混杂因素(违反无混杂性假设)
如果某些因素同时影响了用户是否自愿参与以及最终的结果,但这些因素并未被纳入倾向得分模型中,那么使用IPW或匹配方法所得到的估计结果就会产生偏差。这在实际应用中是最常见的失效原因。
举个例子:那些选择使用“代理模式”的用户,往往也是会关注你的技术博客并阅读发布说明的用户。如果阅读博客这一行为本身就能独立影响任务完成率,而这一因素被忽略的话,那么由此产生的效果就会被错误地归因于“代理模式”,从而导致估计结果失真。
应对这种问题的有效方法包括:深入了解哪些因素会促使用户选择自愿参与、在倾向得分模型中加入更多相关的特征变量,以及使用正式的敏感性分析工具(如Rosenbaum界限、E值)来评估未测量混杂因素的影响程度。
2. 正性重叠问题(违反正向重叠假设)
如果某些用户的自愿参与概率接近于零或接近于1,那么我们就无法为这些用户找到合适的对照组来进行比较。
在这种情况下,倾向得分方法会生成极端的权重值(例如1 / 0.001 = 1,000),从而导致某个极端值主导最终的估计结果。因此,匹配过程也会产生质量较差的配对结果。
如果存在极端值,建议先检查倾向得分的分布情况,并剔除那些位于[0.05, 0.95]区间之外的数据,然后再进行加权处理。
3. 指定错误的倾向得分模型(在实际应用中降低无混杂性)
线性逻辑回归模型无法捕捉非线性的关系。例如,如果用户是否自愿参与取决于其参与程度与查询复杂度之间的交互作用,那么仅使用主效应模型就无法准确反映这一关系,从而导致估计结果失真。
应使用更灵活的模型(比如基于倾向得分的梯度提升算法,或在加权之后再进行回归调整),并且务必在加权后检查最终的平衡性。如果加权后的平衡性不佳,那就说明模型设定有误。
4>用户之间的影响传递(违反独立处理单元假设)
倾向得分方法假定所有用户是相互独立的。但如果某个用户选择使用“代理模式”会影响到其他用户的任务完成情况(比如团队成员在共享工作空间中同时采用这一功能),那么所估计的效果就会包含这种影响传递效应。
这种情况违反了“独立处理单元假设”。要妥善处理这类问题,需要采用不同的方法:对于在工作空间层面采用的共同功能,可以使用集群随机化方法;而对于用户层面的影响传递现象,则需要设计基于网络结构的实验设计。
这些错误机制在回归系数中是无法被察觉的。它们表现为那些在理论上看起来很合理的估计值,但当这些功能真正推广给更广泛的用户群体时,这些问题就会暴露出来。
进行平衡性诊断分析,查看数据重叠图,并记录下你可能忽略的任何问题——这些才是你真正的防护措施。
下一步该怎么做
当你的功能是通过用户主动选择来使用的,并且你有丰富的协变量可以用于模型选择时,倾向得分方法就是最适合的工具。
如果用户的参与是按照明确的规则来决定的(比如根据查询复杂度设置门槛,或者需要支付费用才能使用),那么使用回归分析来处理这种差异会更为合适。如果你怀疑存在未被观察到的干扰因素,并且拥有外部随机化手段(例如随机部署实验或基于速率限制的路由机制),那么工具变量方法会更加有效。
为了防止由于倾向得分模型设定错误而导致的估计偏差,双重稳健估计方法结合了倾向得分加权与回归调整,只要这两个组成部分中的任何一个模型设定正确,最终的估计结果就会保持一致性。
本教程配套的笔记本文件可以在这里找到。你可以克隆这个代码仓库,生成合成数据集,然后运行psm_demo.ipynb(或psm_demo.py),从而重现本教程中的所有代码示例、计算结果和图表。
当某个人工智能功能是通过用户主动选择来使用的机制进行推广时,单纯使用“选择使用该功能的用户”与“未选择的用户”进行对比通常会得到错误的结果。倾向得分方法能为你提供“与那些选择了该功能的用户相似的用户群体”作为对照组,而自助法则能帮你得出一个区间范围,这样当利益相关者询问你的结论有多可靠时,你就可以用这个区间来回应他们。