你的大型语言模型产品实验取得了积极的结果,任务完成率提高了8个百分点,这一数据非常令人满意。你将这项功能上线后,领导层也对此表示庆祝。然而三个月后,核心指标几乎没有任何变化。
这个实验在统计学上是可靠的,但它只是回答了一个错误的问题。
“平均处理效应”这个指标能够将整个用户群体中的响应情况压缩成一个数字。在决定是否开发某个功能时,这个指标确实很有用。
但一旦你决定开发这个功能,平均处理效应就不再是最具有实际指导意义的指标了。那些频繁使用你的AI摘要工具的用户已经优化了自己的工作流程,他们往往认为新的摘要功能是多余的;而轻度用户则常常会因为缺乏上下文信息而无法理解这些摘要,因此他们确实可以从快速回顾中获益。
仅仅因为平均效果是积极的,就向所有用户统一推出这项功能,其实忽略了一个重要的问题:这项功能对某些用户帮助很大,但对另一些用户来说几乎没有任何影响,而对于第三类用户来说,它甚至会带来负面影响。
这就是所谓的“异质性问题”。标准的产品实验只能回答关于平均效果的问题,而提升效应建模则能将这种二分判断转化为一个更为细致的评估范围。那些产生积极平均效果的实验数据中,隐藏着有关究竟是哪些用户推动了这一成功的详细信息,而这些信息是可以被用来指导实际操作的。
提升效应建模能够根据每个用户的特定特征,估算出他们所获得的条件平均处理效应。这样你就能得到一个可以直接用于决策的数值。
那些预测得到的条件平均处理效应较高的用户会被赋予这项功能;而那些这一数值接近零的用户则会被忽略。这样一来,功能的推出就可以有针对性地进行,将资源集中在真正能产生价值的地方,同时使推理成本和给用户带来的干扰与实际收益保持成正比。
对于那些负责设计个性化AI产品推出的机器学习工程师和产品数据科学家来说,本指南会从零开始教你如何使用scikit-learn进行提升效应建模。我们不会依赖causalml或econml这样的复杂工具,这样你就能真正理解其背后的原理。
你将学习两种元学习方法,构建Qini曲线来评估你的模型在用户排序方面的表现,并编写出用于制定分段推出策略的规则。这个数据集模拟了一个拥有50,000名用户的SaaS产品,其中不同用户群体的参与程度也各不相同。
通过学习本指南,你将明白何时应该相信自己的估算结果,以及如何将模型转化为实际的部署方案。
目录
为什么平均处理效果会误导人工智能的个性化应用
仔细想想,平均处理效果实际上反映的是什么。在典型的SaaS产品中,高频使用用户会在自愿参与实验的情况下占比过高,因为他们更频繁地使用新功能;而低频使用用户则会被低估,因为他们往往忽略这些功能的开关选项。
平均处理效果所反映的只是那些实际参与了实验的用户群体构成,而这种构成很可能与产品全面推广后面对的实际用户群体完全不同。
更为关键的是,平均处理效果会掩盖不同子群体之间处理效果的差异方向。
举个例子:如果某个人工智能摘要功能能为低频使用用户带来9.6个百分点的提升,为中等频率使用用户带来7.4个百分点的提升,而只为高频使用用户带来6.7个百分点的提升,那么总体来看这个功能的效果似乎就是积极的。
但实际的战略应该是优先针对低频使用用户推出这项功能,同时密切关注高频使用用户的体验,确保他们的日常工作流程不会因此受到干扰。而如果统一地对所有用户应用这一功能,就会完全忽略这种差异。
这种现象在所有类型的人工智能功能中都会出现。以企业团队的人工智能会议摘要功能为例:新加入团队、难以跟上长篇讨论的用户会从中受益匪浅;而那些阅读速度比人工智能生成的内容快得多的经验丰富的团队成员,则可能会觉得这个摘要功能反而拖慢了他们的工作效率。虽然平均效果看起来是积极的,但这并不能说明将这一功能均匀地应用于所有用户才是正确的做法。
“提升效应建模”正是为了解决这个问题而出现的。它通过估算特定用户的预期处理效果来弥补这一缺陷:对于那些预期处理效果为正的用户,就会实际应用这项功能;而对于预期效果为负的用户,则暂时不予以应用。在第三步中你会构建的“Qini曲线”,可以帮助你计算出只针对预期效果为正的用户群体应用这一功能所能带来的价值提升。
提升效应建模究竟能做什么
提升效应建模建立在因果推断的基础上。其核心概念是个体处理效果,即特定用户在使用某项功能后,其潜在结果与未使用该功能时的潜在结果之间的差异:
ITE(i) = Y_i(1) - Y_i(0)
Y_i(1)表示用户i在使用该功能后的行为;Y_i(0)则表示用户i在未使用该功能时的行为。问题在于,对于任何给定的用户来说,我们只能观察到这两种情况中的一种:对于接受实验处理的用户来说就是Y_i(1),而对于对照组用户来说就是Y_i(0),因为每个用户只会在其中一个组别中出现。
而“预期个体处理效果”则是在整体用户群体层面得出的类似概念:即根据某用户的特征,预计该用户在使用该功能后其结果会发生的改变:
CATE(x) = E[Y(1) - Y(0) | X = x]
“元学习方法”则是通过分别为实验组和对照组建立不同的结果预测模型,然后计算这两组模型预测结果的差异来估算预期个体处理效果。无论是“T-learner”方法还是“X-learner”方法(Künzel等人提出的方法),都依赖于三个关键假设来进行运算。
-
无混杂因素(条件性可忽略性):在观察到相关协变量后,治疗分配与潜在结果之间是独立的,即 T ⊥ (Y(0), Y(1)) | X。在随机实验中,这一条件自动得到满足;而在观察性研究中,则需要有一组足够丰富的特征来控制各种混杂因素。
-
重叠概率
: 每位用户都有非零的概率接受治疗或对照组处理,且 0 < P(T=1|X=x) < 1。当某些用户的参与概率接近于零时(例如在这个数据集中,轻度使用者的参与概率仅为12%),在该区域进行的CATE估计结果的方差会增大。
-
独立性假设: 每位用户的最终结果仅取决于他们自己接受的治疗方案,而与其他用户的行为无关。如果用户们共享工作空间或处于相同的社交网络中,这一假设就可能被违反(具体处理方法详见“后续步骤”部分)。
先决条件
您需要准备以下工具:
-
Python 3.11或更高版本
-
熟练掌握pandas和scikit-learn库
-
对线性回归和逻辑回归有基本了解
请安装本教程所需的软件包:
pip install numpy pandas scikit-learn matplotlib scipy
这些软件的作用是:它们用于安装本教程所需的所有数值计算工具。scipy库在图表生成过程中用于对Qini曲线进行KDE平滑处理;其余软件均为常用的机器学习工具。
请克隆配套代码仓库以获取合成数据集:
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
执行此命令后会发生什么:数据生成工具会创建一个包含50,000名合成用户的数据库。这些用户被分为轻度、中等和重度三个使用级别,每个用户都拥有查询置信度分数以及是否启用AI摘要功能的选项。启用该功能所带来的实际效果约为+8个百分点的task_completed完成率,这一差异会在不同使用级别的用户中体现出来。本教程中使用的所有数据均来自这个合成数据集。
本文中的所有代码都可以在08_uplift_modeling/uplift_demo.ipynb链接处的配套笔记本中完整运行。请克隆该仓库并执行uplift_demo.py文件,即可重现所有实验结果。
设置示例环境
这个数据集模拟了一个提供AI摘要功能的SaaS产品,用户可以通过开关选项来决定是否启用这一功能。数据集中包含50,000名用户,其中opt_in_agent_mode字段代表治疗组变量,task_completed字段表示二元结果变量;用户的参与程度(轻度、中等、重度)则反映了他们与该产品的互动活跃度。
加载数据并确定基准值:
import pandas as pd
import numpy as np
df = pd.read_csv("data/synthetic_llm_logs.csv")
print(df.shape)
print(df[["engagement_tier", "opt_in_agent_mode", "task_completed"]].head(10))
# 不同等级的用户参与率
print("\n不同参与等级的用户参与率:")
print(df.groupby("engagement_tier").opt_in_agent_mode.mean().round(3))
# 简单处理后的平均处理效应:实验组与对照组的差异
naive_ate = (
df[df.opt_in_agent_mode == 1].task_completed.mean()
- df[df/opt_in_agent_mode == 0].task_completed.mean()
)
print(f"\n简单处理后的平均处理效应(实验组 - 对照组): {naive_ate:+.4f}")
print(f"实验组用户数量: {(df.opt_in_agent_mode == 1).sum():,}")
print(f>对照组用户数量: {(df/opt_in_agent_mode == 0).sum():,}")
预期输出结果:
(50000, 16)
engagement_tier opt_in_agent_mode task_completed
0 medium 0 0
...
不同参与等级的用户参与率:
engagement_tier
heavy 0.647
light 0.120
medium 0.353
Name: opt_in_agent_mode, dtype: float64
简单处理后的平均处理效应(实验组 - 对照组): +0.2106
实验组用户数量: 13,451
对照组用户数量: 36,549
实际情况如下:你加载了50,000条数据,立刻发现了一个明显的选择偏差现象:重度使用者的参与率为64.7%,中度使用者为35.3%,而轻度使用者的参与率仅为12%。简单处理后的平均处理效应为+0.2106,这一数值是真实效果的两倍多。
这种差异反映了选择偏差——实验组主要由那些会完成更多任务的重度使用者构成。因此,+0.21这个数字实际上反映的是使用者的参与程度,而非特定功能的影响。
现在来看一下按不同等级划分的简单处理效应差异,这些数据能帮助我们更准确地了解各种情况:
# 按等级划分的简单处理效应差异
print("不同等级的实验组与对照组完成率差异:")
for tier in ["light", "medium", "heavy"]:
sub = df[df.engagement_tier == tier]
t_rate = sub[sub.opt_in_agent_mode == 1].task_completed.mean()
c_rate = sub[sub/opt_in_agent_mode == 0].task_completed.mean()
print(f" {tier:8s}: 实验组完成率={t_rate:.3f}, 对照组完成率={c_rate:.3f}, "
f"差异={t_rate - c_rate:+.3f}")
预期输出结果:
不同等级的实验组与对照组完成率差异:
light : 实验组完成率=0.551, 对照组完成率=0.455, 差异=+0.096
medium : 实验组完成率=0.745, 对照组完成率=0.670, 差异=+0.075
heavy : 实验组完成率=0.891, 对照组完成率=0.824, 差异=+0.067
实际情况如下:即使不考虑各种混淆因素,这些数据也显示出了明显的顺序:轻度使用者 > 中度使用者 > 重度使用者(差异分别为+0.096、+0.075、+0.067)。轻度使用者在同一个等级内表现出的差异最大,而重度使用者的差异最小。
如果假设高级用户总是受益最多,这种结果可能会让人感到困惑,但对于人工智能生成的摘要功能来说,这一现象其实是合理的。轻度使用者在阅读长篇内容时往往难以跟上节奏,因此标题摘要对他们来说非常有用;而重度使用者已经习惯了如何使用该产品,对他们而言,摘要功能反而可能干扰他们的使用体验。在后续步骤中,我们会通过控制每个等级内用户的查询信心程度来进一步优化这些估算结果。

图1:异质处理效应的概念性示意图。图中展示了每个参与等级下的对照组与实验组数据分布情况(虚线与实线分别代表两组)。对于不同等级的用户而言,CATE值(两条曲线之间的差距)会逐渐减小。下方的图表进一步说明了如何通过ATE将这种差异转化为一个平均数值,但这种处理方式实际上会误导人们了解该功能在各个用户群体中的实际效果。
步骤1:T-learner(最简单的元学习器)
T-learner会分别构建两个完全独立的模型:一个用于实验组,另一个用于对照组。对于任何用户而言,其预测得到的CATE值其实就是实验组模型对该用户特征所做出的预测结果与对照组模型对应预测结果之间的差值。
from sklearn.linear_model import LinearRegression
import pandas as pd
import numpy as np
# 构建特征矩阵:包含查询置信度以及参与等级信息
X_full = pd.get_dummies(
df[["query_confidence", "engagement_tier"],
drop_first=False
).astype(float)
feature_cols = X_full.columns.tolist()
print("特征列:", featurecols)
X_all = X_full.values
treated_mask = df.opt_in_agent_mode == 1
control_mask = ~treated_mask
X1 = X_all[treated_mask] # 实验组用户的特征数据
Y1 = df[treated_mask].task_completed.values
X0 = X_all[control_mask] # 对照组用户的特征数据
Y0 = df[control_mask].task_completed.values
# 分别为实验组和对照组构建模型
m1 = LinearRegression().fit(X1, Y1) # 实验组的结果预测模型
m0 = LinearRegression().fit(X0, Y0) # 对照组的结果预测模型
# CATE = mu_1(x) - mu_0(x)
cate_t = m1.predict(X_all) - m0.predict(X_all)
df["cate_tlearner"] = cate_t
print(f"\nT-learner模型的平均CATE值:{cate_t.mean():+.4f}")
print("\n按参与等级划分的平均预测CATE值:")
print(df.groupby("engagement_tier").cate_tlearner.mean().round(4))
预期输出结果:
特征列: ['query_confidence', 'engagement_tier_heavy', 'engagement_tier_light', 'engagement_tier_medium']
T-learner模型的平均CATE值:+0.0847
按参与等级划分的平均预测CATE值:
参与等级 均值
heavy 0.0665
light 0.0954
medium 0.0744
名称: cate_tlearner, 数据类型: float64
具体工作原理如下:首先,将参与等级信息编码为独热特征,而查询置信度则保持为连续型特征。随后分别构建两个LinearRegression模型:m1用于分析选择参与实验的用户中任务完成情况的条件概率,m0则用于分析未选择参与实验的用户中的这一概率。对于任何具有特征值x的用户而言,其预测得到的CATE值即为m1(x) - m0(x)。
这些测试结果证实了初始分析中得出的分类方向,同时也使各项估算数值更加精确。在全部50,000名用户中,平均CATE值为+0.0847,这一数值与真实值+0.08非常接近。各用户群体的排序为:轻度用户组(+0.0954) > 中度用户组(+0.0744) > 重度用户组(+0.0665)。最初得到的+0.2106这一数值其实掩盖了轻度用户与重度用户之间1.4倍的差异,而这种差异本身就构成了进行用户分类的依据。
T-learner存在一个需要特别注意的问题:当其中一条数据臂的数量远少于另一条数据臂时(例如,实验组有13,451个样本,而对照组有36,549个样本),使用较少样本的数据臂训练得到的模型会表现出较大的方差。在总用户数为50,000的情况下,线性回归模型能够较好地处理这一问题;而下一步中使用的X-learner则直接解决了这种数据不平衡的问题。
步骤2:X-learner(用于解决治疗组数据不平衡问题)
X-learner通过利用数量较多的数据臂来辅助估计数量较少的数据臂中的CATE值,从而改进了T-learner的不足。具体而言,它为每位用户计算模拟治疗效应:首先使用跨数据臂模型预测这些用户的反事实结果,然后再将这些预测结果与他们实际观察到的结果进行比较。
这一过程包含四个步骤:
-
分别对每条数据臂拟合结果模型
m0和m1(这与T-learner的处理方式相同)。 -
对于实验组用户:计算
D1 = Y1 - m0(X1),即每位实验组用户的实际结果与对照组模型预测的他们未接受治疗时可能取得的结果之间的差异。 -
对于对照组用户:计算
D0 = m1(X0) - Y0,即对照组模型预测的每位对照组用户在接受治疗后可能取得的结果与他们实际取得的结果之间的差异。 -
分别为两条数据臂拟合两个tau回归器,然后利用倾向得分作为权重将这两个回归器的结果结合起来。具体计算公式如下(参考Künzel等人的研究):
tau(x) = g(x) * tau_1(x) + (1 - g(x)) * tau_0(x),其中g(x)表示倾向得分。当g(x)较低时(即该特征区域内的实验组用户数量较少),从数量较多的对照组数据中估计得到的tau_0会占据更大的权重;而当g(x)较高时,tau_1则会占据更大的权重。
from sklearn.linear_model import LinearRegression, LogisticRegression
# 步骤1:m0和m1已经在前面的步骤中拟合完成
# 步骤2:计算实验组用户的模拟治疗效应
D1 = Y1 - m0.predict(X1) # Y(1) - mu_0(X1)
# 步骤3:计算对照组用户的模拟治疗效应
D0 = m1.predict(X0) - Y0 # mu_1(X0) - Y(0)
# 分别为两条数据臂拟合tau回归器
tau1_model = LinearRegression().fit(X1, D1) # 实验组数据的tau回归器
tau0_model = LinearRegression().fit(X0, D0) # 对照组数据的tau回归器
# 步骤4:计算倾向得分e(x) = P(T=1 | X)
ps_model = LogisticRegression(max_iter=1000).fit(X_all, df.opt_in_agent_mode.values)
e_x = ps_model.predict_proba(X_all)[:, 1]
# 参考Künzel等人的研究公式:tau(x) = g(x)*tau_1(x) + (1 - g(x))*tau_0(x)
tau1_all = tau1_model.predict(X_all)
tau0_all = tau0_model.predict(X_all)
cate_x = e_x * tau1_all + (1 - e_x) * tau0_all
df["cate_xlearner"] = cate_x
print(f"X-learner计算出的平均CATE值:{cate_x.mean():+.4f}")
print("\n按参与程度划分的平均预测CATE值:")
print(df.groupby("engagement_tier").cate_xlearner.mean().round(4))
# 比较T-learner和X-learner的结果
print("\n各参与程度组中T-learner与X-learner的计算结果对比:")
comp = df.groupby("engagement_tier")[["cate_tlearner", "cate_xlearner"]].mean().round(4)
print(comp)
预期输出:
平均CATE值(X-learner模型):+0.0847
按参与程度划分的平均预测CATE值:
参与程度 CATE值
high 0.0665
light 0.0954
medium 0.0744
模型名称:cate_xlearner,数据类型:float64
各层次上T-learner模型与X-learner模型的对比结果:
参与程度 CATE值(T-learner) CATE值(X-learner)
high 0.0665 0.0665
light 0.0954 0.0954
medium 0.0744 0.0744
具体原因如下:当使用线性结果模型且特征数量为四个时,T-learner模型与X-learner模型会得出相同的各层次CATE值。这种一致性是在结果模型设计合理的情况下才会出现的:因为X-learner模型中的交叉插补操作并不会提供线性模型本来就已经能够获取的信息。
在实际应用中,当使用梯度提升或因果森林作为结果模型时,X-learner模型的优势就会显现出来。因为基于树的模型会放大“处理组与对照组之间的差异”,而X-learner模型通过加权组合的方式能够纠正这种差异。
每当你对基础模型进行升级时,都应该同时运行这两种估计方法,并选择在独立测试集上表现更好的那个模型作为最终结果。
步骤3:Qini曲线与K值处的提升效应
只有当CATE模型对用户的排序与其实际的处理效果顺序一致时,该模型才具有实际意义。Qini曲线(Radcliffe, 2007)通过以下方式来验证这一点:如果按照预测的CATE值对用户进行降序排序,然后只对排名前k%的用户进行处理,那么实际观察到的提升效果与预期值之间会有多大的差异呢?
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
# 按预测的CATE值降序排列用户数据
df_sorted = df.sort_values("cate_tlearner", ascending=False).copy()
n = len(df_sorted)
# 计算每个百分位点处的提升效应
top_ks = np.arange(0.01, 1.01, 0.01)
qini_vals = []
for k in top_ks:
top_n = max(1, int(k * n))
sub = df_sorted.iloc[:top_n]
treated_sub = sub[sub.opt_in_agent_mode == 1]
control_sub = sub[sub(opt_in_agent_mode == 0]
if len(treated_sub) > 0 and len(control_sub) > 0:
uplift = (treated_sub.task_completed.mean()
- control_sub.task_completed.mean())
else:
uplift = np.nan
qini_vals.append(uplift)
# 绘制图表
fig, ax = plt.subplots(figsize=(8, 4.5))
ax.plot(top_ks * 100, qini_vals, linewidth=2, label="T-learner模型Qini曲线")
ax.axhline(naive_ate, color="gray", linestyle="--",
label=f"朴素ATE值 = {naive_ate:.4f}")
ax.set_xlabel("按预测CATE值排序的前k%用户")
ax.set_ylabel("前k%组用户的实际提升效应")
ax.set_title("Qini曲线:T-learner模型排名与实际提升效应的对比")
ax.legend()
plt.tight_layout()
plt.savefig("qini_curve.png", dpi=140)
print("图表已保存为qini_curve.png")
# 打印选定百分位点处的数值
print("\n选定百分位点处的Qini曲线值:")
for target_k in [10, 20, 30, 50, 70, 100]:
idx = target_k - 1
print(f"排名前{target_k:3d}%的用户:实际提升效应 = {qini_vals[idx]:.4f}")
预期输出:
已保存文件 qini_curve.png
在所选分位数处观察到的效果:
前10%:观察到的效果提升值为0.0895
前20%:观察到的效果提升值为0.1018
前30%:观察到的效果提升值为0.0959
前50%:观察到的效果提升值为0.0966
前70%:观察到的效果提升值为0.1454
前100%:观察到的效果提升值为0.2106
具体操作过程如下:首先,按照T-learner预测的CATE值对全部50,000名用户进行排序,排序顺序为CATE值从高到低。对于每个指定的百分位数区间,计算该区间内实验组与对照组在任务完成率方面的差异。
前10%的用户群体显示出的效果提升值为+0.0895,前20%的用户群体显示出的效果提升值为+0.1018,这两个数值都远低于简单的ATE值+0.2106。后者之所以会高于前者,是因为选择偏差的影响;实际上,这个数值更多地反映了用户的参与程度,而非该特征本身的实际影响。
在这里,Qini值同时包含了CATE信号以及剩余的选择偏差:在预测的CATE值排名前54%的所有用户中,都属于轻度使用者(他们的注册率仅为12%),因此在这个群体内进行的实验组与对照组对比仍然会受到同一层级内部选择偏差的影响。
在前70%的用户群体中观察到的效果提升值为+0.1454,这一数据清楚地体现了选择偏差的作用:当中等程度和重度使用者被纳入到这个排名区间时,实验组中突然出现了完成率较高的重度使用者(他们的注册率为64.7%),而对照组则仍然以完成率较低的轻度使用者为主。这种差异完全是由选择偏差造成的,并不代表真正的CATE效应。
在观察性效果提升的分析中,Qini值所反映的有效区间大致位于前20%到前50%这个范围内;在这个区间内,排名结果能更清晰地反映模型预测的CATE效应,因为在这一区间内,倾向得分与实际结果之间的相关性较低。
步骤4:分段部署规则
# 首先查看CATE值的分布情况
print("CATE值分布(T-learner):")
print(pd.Series(df.cate_tlearner).describe().round(4))
print()
# 绘制CATE值分布图
fig, ax = plt.subplots(figsize=(8, 4))
ax.hist(df.cate_tlearner, bins=50, edgecolor="white", linewidth=0.5)
ax.axvline(0.085, color="red", linestyle="--", label="阈值 = 0.085")
ax(axvline(df.cate_tlearner.mean(), color="gray", linestyle=":",
label=f"平均CATE值 = {df.cate_tlearner.mean():.4f}")
ax.set_xlabel("预测的CATE值(T-learner)")
ax.set_ylabel("用户数量")
ax.set_title("预测的CATE值分布图")
ax.legend()
plt.tight_layout()
plt.savefig("cate_distribution.png", dpi=140)
print("已保存文件 cate_distribution.png")
# 应用分段部署规则
threshold = 0.085
selected = df[df.cate_tlearner >= threshold].copy()
suppressed = df[df.cate_tlearner < threshold].copy()
print(f"\n部署阈值:CATE值 >= {threshold}")
print(f"被选中进行部署的用户数量:{len(selected):,} ({100*len(selected)/len(df):.0f}%)")
print(f"被屏蔽的用户数量:{len(suppressed):,} ({100*len(suppressed)/len(df):.0f}%)")
print()
print("被选中的用户群体的层级构成:")
print((selected.groupby("engagement_tier").size() / len(selected)).round(3))
print()
print(f"被选中用户的平均预测CATE值:{selected.cate_tlearner.mean():.4f}")
print(f"被屏蔽用户的平均预测CATE值:{suppressed.cate_tlearner.mean():.4f}")
预期输出:
CATE分布(T学习器):
计数 50000.0000
平均值 0.0847
标准差 0.0126
最小值 0.0515
25% 0.0731
50% 0.0897
75% 0.0963
最大值 0.1021
名称:cate_tlearner,数据类型:float64
已保存文件cate_distribution.png
推广阈值:CATE >= 0.085
被选中进行推广的用户数:27,203人(占54%)
被抑制使用的用户数:22,797人(占46%)
被选中用户的层级构成:
轻度用户层
占比 1.0
数据类型:float64
被选中用户的平均预测CATE值:0.0955
被抑制使用的用户的平均预测CATE值:0.0719
具体操作过程如下:在设置阈值之前,会先查看完整的CATE分布情况。所有50,000名用户的平均CATE值为0.0847,标准差为0.0126。将阈值设定为0.085(略高于平均值0.0847),这样就选出了27,203名用户(占54%)。
被选中用户的层级构成全部为轻度用户:由于使用了线性模型以及这些特征,不同层级的CATE值范围在阈值处并没有重叠。所有轻度用户的预测CATE值都在0.0807到0.1021之间;中度用户的预测CATE值则在0.0592到0.0812之间。0.085这个阈值能够清晰地将这两类用户区分开来。
被选中用户的平均预测CATE值为0.0955,这一数值比被抑制使用的用户的平均预测CATE值0.0719高出33%。这种针对性的推广方式正是其价值所在:将人工智能生成的摘要信息推送给那些能从中获得最大收益的54%的用户,而不对那些预期收益较低的中度及重度用户进行推送。同时,通过收集这两组用户的反馈数据,可以每季度调整阈值。

图2:来自这个包含50,000名用户的合成数据集的各层级CATE分布情况。上图显示了不同用户层级的平滑KDE曲线:轻度用户(蓝色曲线)对应的预测CATE值最高,而重度用户(绿色曲线)对应的预测CATE值最低。下图展示了各层级的平均CATE值以及95%置信区间,同时还标出了作为参考的简单ATE值(0.2106)。所有层级的置信区间都明显低于简单ATE值,这进一步证明了平均值受到选择偏差的影响。
这种推广规则可以直接转化为一个特征标志系统来实现:
# 模拟针对新用户的推广决策
def should_show_feature(query_confidence, engagement_tier, threshold=0.085):
"""如果预测的CATE值超过推广阈值,则返回True。"""
x = pd.get_dummies(
pd.DataFrame([{"query_confidence": query_confidence,
"engagement_tier": engagement_tier}]),
drop_first=False
).reindex(columns=feature_cols, fill_value=0).astype(float).values
cate = m1.predict(x)[0] - m0.predict(x)[0]
return cate >= threshold, round(cate, 4)
show, cate = should_show_feature(0.72, "heavy")
print(f"重度用户,置信度为0.72:是否显示该功能?{show}, CATE值为{cate}")
show, cate = should_show_feature(0.72, "light")
print(f"轻度用户,置信度为0.72:是否显示该功能?{show}, CATE值为{cate})
show, cate = should_show_feature(0.45, "medium")
print(f>中度用户,置信度为0.45:是否显示该功能?{show}, CATE值为{cate}")
预期输出:
重度用户,置信度为0.72:不显示该功能,CATE值为0.0667
轻度用户,置信度为0.72:显示该功能,CATE值为0.0955
中度用户,置信度为0.45:不显示该功能,CATE值为0.0681
具体原理如下:我们将CATE的计算过程封装在一个函数中,这个函数的运行方式与真实的特征开关服务在接收到请求时的处理流程完全相同。对于一个查询置信度为中等的重度用户,系统会决定不显示该功能,其CATE值为0.0667,低于0.085的阈值;而对于同一个查询置信度的轻度用户,系统则会选择显示该功能,其CATE值为0.0955。中度用户的置信度较低,因此也会导致不显示该功能的结论。
这些输出结果与我们的预期相符:人工智能生成的摘要确实能够帮助那些在不同会话之间难以保持上下文关联的用户,而用户的参与程度正是反映这种困难程度的一个重要指标。
步骤5:构建置信区间
上述的CATE估计值仅仅是点估计值,并没有包含任何不确定性量化信息。在根据这些估计值制定推广规则之前,我们需要了解这些估计值在用户群体的不同样本中是否具有稳定性。
def bootstrap_cate_ci(df, X_all, feature_cols, n_reps=500, seed=7):
"""通过自助法计算整体平均CATE值以及各参与程度等级的平均CATE值的95%置信区间。"""
rng = np.random.default_rng(seed)
n = len(df)
tier_reps = {"light": [], "medium": [], "heavy": []}
mean_reps = []
for _ in range(n_reps):
idx = rng.integers(0, n, size=n)
df_b = df.iloc[idx].reset_index(drop=True)
X_b = X_all[idx]
treated_b = df_b.opt_in_agent_mode == 1
m1_b = LinearRegression().fit(X_b[treated_b], df_b[treated_b].task_completed.values)
m0_b = LinearRegression().fit(X_b[~treated_b], df_b[~treated_b].task_completed.values)
cate_b = m1_b.predict(X_b) - m0_b.predict(X_b)
df_b["cate"] = cate_b
for tier in tier_reps:
tier_reps[tier].append(df_b[df_b.engagement_tier == tier].cate.mean())
mean_reps.append(cate_b.mean())
cis = {}
for tier, vals in tier_reps.items():
arr = np.array(vals)
cis[tier] = (float(nppercentile(arr, 2.5)),
float(nppercentile(arr, 97.5)))
arr = np.array(mean_reps)
cis["mean"] = (float(nppercentile(arr, 2.5)),
float(nppercentile(arr, 97.5)))
return cis
print("正在执行自助法计算(500次重复实验,种子值=7)...")
cis = bootstrap_cate_ci(df, X_all, feature_cols, n_reps=500, seed=7)
print(f"平均CATE值及95%置信区间:[{cis['mean'][0]:+.4f}, {cis['mean')[1]:+.4f}]")
print(f"轻度用户组的95%置信区间:[{cis['light][0]:+.4f}, {cis['light][1]:+.4f}]")
print(f"中度用户组的95%置信区间:[{cis['medium][0]:+.4f}, {cis['medium][1]:+.4f}]")
print(f>重度用户组的95%置信区间:[{cis['heavy][0]:+.4f}, {cis['heavy][1]:+.4f}]")
预期输出:
正在运行bootstrap分析(500次重复实验,种子值=7)……
平均CATE值 95%置信区间:[+0.0744, +0.0951]
“轻量级”选项 95%置信区间:[+0.0781, +0.1125]
“中等级”选项 95%置信区间:[+0.0596, +0.0892]
“重型级”选项 95%置信区间:[+0.0483, +0.0842]
具体操作过程如下:你需要对包含50,000名用户的完整数据集进行500次有放回的重采样,每次重采样后都重新应用T学习器,并计算出这些重采样过程中平均CATE值的分布情况。该分布的2.5%和97.5%分位数即可构成每个估计值的95%置信区间。
在分析这些置信区间时需要注意三点:首先,整体平均置信区间(+0.0744, +0.0951)位于真实值+0.08的范围内,这说明该估算方法是有效的;其次,低等级用户的置信区间(+0.0781, +0.1125)比高等级用户的置信区间(+0.0483, +0.0842)更宽,这是因为低等级用户的参与率较低(仅为12%),因此可用于估算的数据量也较少;第三,这些置信区间的尾部并没有完全分开:低等级用户的下限值(+0.0781)仅略高于高等级用户的上限值(+0.0842),这意味着“低等级用户 > 高等级用户”这一排序关系虽然存在,但差距并不明显。
对于需要基于差异化策略来做出商业决策的情况而言,这种稳定性已经足够了;但在监管或临床领域,显然需要更大的样本量才能获得更可靠的结果。
当提升模型失效时
CATE模型看起来很有吸引力,因为它们能够生成连续的、针对个体用户的评估分数。但在实际应用基于CATE模型的策略之前,有四种情况需要特别关注。
1. 细分用户群体之间的重叠问题
对于低等级用户而言,他们的CATE值是根据13,451名受治疗用户中的12%(即大约1,614人)的数据来估算的。这样的样本量虽然足以计算出整体平均数值,但无法准确评估各个细分用户群体在具体特征值下的个体差异。
当某个特征维度上的受治疗用户占比较低时,CATE值的估算结果会存在较大的波动性。模型虽然能给出较为平滑的预测结果,但其背后的实证依据可能并不充分。
在根据这些评估结果进行决策之前,务必检查那些CATE值最高的用户的特征分布情况,并确认每个特征维度上都有受治疗组和对照组的数据存在。
2. 尾部区域的外推问题
线性回归模型能够在训练数据范围之外进行平滑的外推预测。但如果某个用户的特征值属于某个没有训练数据的区域,那么该模型的预测结果就缺乏实证依据。
在这种情况下,模型虽然会给出一个数值,但实际上在该区域内P(T=1|X=x)的值几乎为0或1,因此这个CATE值实际上是无法被准确确定的。
在查看CATE预测结果的同时,还需要检查用户的倾向得分;对于那些倾向得分不在[0.05, 0.95]区间内的情况,应该直接剔除这些数据或对其进行标记。
3>当k值较小时出现的Qini噪声问题
当k值非常小的时候(前5%或更少),Qini曲线会出现较大的波动。如果评估组中只有几百名用户,那么其中受治疗用户的数量可能会很少,从而导致观察到的效果主要受到抽样误差的影响。
在决定是否推行某项措施时,应参考Qini值介于20%到50%之间的区间,因为在这个区间内信号更为稳定。在观察性研究中,当k值较大时较高的Qini值(例如本教程中前70%的数据中Qini值为+0.1454)可能会掩盖真实的CATE信号,从而导致选择偏差。在解读提升效应的值之前,先检查每个前k组中的数据构成情况。
4. CATE模型的过拟合问题
在这里,使用训练有素的LinearRegression模型对处理组的数据进行分析时,共有13,451条观测记录和4个特征变量,这样的配置属于比较理想的。但如果将线性回归模型替换为梯度提升算法,并增加30个特征变量,就很容易导致模型过拟合训练数据中的噪声。在这种情况下,CATE模型的预测结果在训练集上会表现出明显的异质性,而在独立测试集上的预测值则会趋近于全局平均值。只有当CATE模型的预测效果优于基于层次划分的平均值时,这种模型的复杂性才具有实际意义。在使用该模型来制定推行规则之前,务必先在独立测试数据集上进行评估。
下一步该做什么
上述实现方案并未使用任何外部工具库,因此你可以清楚地看到每一步的具体计算过程。对于实际生产环境而言,causalml和econml提供了更为功能强大的估计工具:基于树的T-learner、双重鲁棒的X-learner,以及能够将训练数据和估计数据分开处理以减少过拟合现象的诚实因果森林模型。这两个库所遵循的概念结构与你在本教程中构建的方法是一致的。
causalml支持生成符合生产环境要求的Qini曲线,并提供了AUUC(提升曲线下面积)这一评估指标,该指标能够将Qini曲线转化为一个便于比较的数值。在A/B测试框架中,AUUC被广泛用作衡量模型性能的标准指标。
需要指出的是,本教程假设每个用户的结果仅受其自身是否接受了处理措施的影响,即满足SUTVA条件。但在基于工作空间的AI产品中,这种假设往往是不成立的。同一工作空间内的用户共享相同的环境,对其中一名用户进行干预,很可能会通过共享结果、改变响应模式或影响工作空间内的互动方式,进而影响到其他用户。
当你怀疑存在这种干扰现象时,使用那些能够将组内相关性纳入CATE模型估计结果的变体,就能得到更为真实的不确定性范围。而标准的T-learner和X-learner模型则会将所有观测记录视为独立的,因此在工作空间层面的因素产生影响时,这些模型会低估实际的不确定性。
本教程对应的代码仓库地址为:github.com/RudrenduPaul/product-experimentation-causal-inference-genai-llm/tree/main/08_uplift_modeling。你可以克隆这个仓库,使用--n-users 50000 --seed 42命令生成数据集,然后运行uplift_demo.py程序来重现本教程中的所有实验结果。
“ATE”这个数值是你在决定是否开发某个功能时需要参考的指标;而“CATE”这个数值则有助于你决定谁应该优先使用该功能。如果针对那些被预测为会对该功能产生最强烈反应的54%的用户来逐步推出这一功能,那么所取得的效果将会比将同一功能同时推广给所有用户要好得多。是否采用统一推广的方式,其实是一种策略选择——因此,请务必在做出这种选择之前充分了解各种相关因素。