Product Experimentation with Uplift Modeling: Targeting Your LLM Feature Rollout to Users Who Actually Benefit (Python Implementation)

TL;DR · AI 摘要
Product Experimentation with Uplift Modeling: Targeting Your LLM Feature Rollout to Users Who Actually Benefit Python Im...
核心要点
- 主题聚焦:Product Experimentation with Uplift Modeling: Ta
- 来源:freeCodeCamp.org,建议结合原文判断细节。
- AI 分析暂不可用,本条为保底评分与摘要。
使用提升模型进行产品实验:将LLM功能 rollout 定向到真正受益的用户(Python实现)
2026年7月9日
/
#product experimentation
Rudrendu Paul
您的LLM产品实验刚刚传来好消息,任务完成率提升了8个百分点。您上线了该功能,管理层也为此庆祝。三个月后,核心指标却几乎没有变化。
该实验在统计学上是严谨的,但它回答了错误的问题。
平均处理效应将整个用户群体的处理响应压缩成一个数字。这种压缩在决定是否开发某个功能时很有用。
但一旦您决定开发该功能,平均处理效应就不再是最重要的指标。您的AI摘要工具重度用户已经优化了工作流程,通常认为新摘要内容冗余。轻度用户经常丢失上下文,真正需要快速回顾。
仅仅因为平均效果为正就向所有人统一推送功能,忽略了重要信息:该功能对部分用户帮助显著,对另一些用户影响甚微,甚至可能对第三类用户造成干扰。
这就是异质性问题。标准产品实验回答的是关于平均效果的二元问题。提升模型将这种二元性转化为细致的光谱。产生正向平均效果的实验数据中,隐藏着哪些用户真正推动了成功的信息,您可以据此采取行动。
提升模型基于每个用户的特定特征,估计条件平均处理效应(CATE)。您会立即获得可采取行动的评分。
预测CATE值高的用户获得该功能。CATE值接近零的用户则跳过。结果是分段推送,将处理集中在真正产生价值的用户群体,使推理成本和用户干扰与实际收益成比例。
对于负责协调个性化AI推送的机器学习工程师和产品数据科学家,本指南将从零开始讲解提升模型的实现,使用scikit-learn库。我们将在不依赖causalml或econml等重型依赖库的情况下构建该模型,以便您理解底层机制。
您将实现两种元学习器方法,构建Qini曲线评估模型对用户的排序效果,并编写分段推送决策规则。数据集模拟了一个包含50,000名用户的SaaS产品,其中不同参与层级的异质性已内建。
最终,您将了解何时信任您的估计值,以及如何将模型转化为实际部署策略。
目录
- 为什么平均处理效应会误导AI个性化
- 提升模型实际上做了什么
- 先决条件
- 搭建工作示例
- 第1步:T-学习器(最简单的元学习器)
- 第2步:X-学习器(处理不平衡处理组)
- 第3步:Qini曲线与K处的Iplift
- 第4步:分段推送规则
- 第5步:引导置信区间
- 提升模型失效时的情形
- 接下来该做什么
为什么平均处理效应会误导AI个性化
想想平均处理效应实际上在平均什么。在典型的SaaS产品中,重度用户在参与实验时会过度代表自己,因为他们更频繁地使用新功能。轻度用户则会低估自己,因为他们经常忽略切换选项。
平均效应反映了实验中实际参与的用户混合情况,而这种混合很可能与全面推广时面对的总体人群完全不同。
更关键的是,平均处理效应会掩盖不同子群体中处理效应的方向。
设想一种场景:AI摘要功能对轻度用户带来9.6个百分点的提升,对中度用户带来7.4个百分点的提升,对重度用户仅带来6.7个百分点的提升。这些数据平均后会呈现出统一积极的效果。
但此时的战略决策应聚焦于向轻度用户集中推广,同时监控重度用户以确保其优化的工作流程未被干扰。统一推广则完全忽视了这种差异。
这种模式在所有AI功能类别中都存在。以企业团队的AI会议摘要功能为例:新成员在难以跟上长对话时会显著受益,而阅读速度超过AI写作速度的资深成员可能会发现摘要反而减慢了他们的效率。积极的平均值可以证明构建该功能的合理性,但它无法告诉你是否应向所有用户完全一致地部署该功能。
提升建模通过估计CATE(条件平均处理效应)来解决这个问题:即根据用户观察到的特征,预测特定用户预期的处理效应。对于CATE显著为正的用户给予处理,而对于低CATE用户则暂缓处理。你将在第3步构建的Qini曲线会告诉你,仅对高CATE群体进行处理而忽略其他群体能带来多少价值。
提升建模的实际作用
提升建模基于因果推断。其核心概念是个体处理效应,表示特定用户的潜在结果差异:
ITE(i) = Y_i(1) - Y_i(0)Y_i(1)表示用户i使用该功能时的行为,Y_i(0)表示用户i不使用该功能时的行为。问题在于,对于任何用户你只能观察到这两个量中的一个:接受处理的用户只能观察到Y_i(1),对照组用户只能观察到Y_i(0),每个用户只出现在一个实验组中。
CATE是群体层面的对应概念:给定用户特征时预期的个体处理效应:
CATE(x) = E[Y(1) - Y(0) | X = x]元学习者方法通过分别在处理组和对照组上拟合结果模型,然后计算预测值的差异来估计CATE。T-学习者和X-学习者(Künzel等)都基于三个识别假设:
- 无混淆性(条件可忽略性):在给定观察到的协变量下,处理分配与潜在结果独立,T ⊥ (Y(0), Y(1)) | X。在随机实验中,这一条件自动成立。在观察性自愿参与研究中,需要特征集足够丰富以控制混淆因素。
- 重叠性(正性):每个用户接受处理或对照的概率都不为零,0 < P(T=1|X=x) < 1。当某些用户的自愿参与概率接近零(如本数据集中轻度用户参与概率为12%),这些区域的CATE估计值方差会更高。
- SUTVA:每个用户的结局仅取决于其自身的处理,与其他用户的行为无关。如果用户共享工作空间或社交网络,这一假设可能被违反(详见"下一步该怎么做"部分)。
安装本教程所需的软件包:
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 个合成 SaaS 产品用户的可复现数据集。每个用户都有参与层级(轻度、中度、重度)、查询置信度评分以及 AI 摘要功能的启用标志。启用该功能的真实因果效应约为 +8 个百分点任务完成率,且在不同参与层级中存在差异。本教程中所有数据均来自这个精确的数据集。
本文所有代码均可在配套笔记本 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}: treated={t_rate:.3f}, control={c_rate:.3f}, "
f"diff={t_rate - c_rate:+.3f}")按层级处理组与对照组完成率对比:
light : treated=0.551, control=0.455, diff=+0.096
medium : treated=0.745, control=0.670, diff=+0.075
heavy : treated=0.891, control=0.824, diff=+0.067这里发生了什么:即使原始混淆差距也显示了light > medium > heavy的排序(+0.096 > +0.075 > +0.067)。轻度用户在层级内显示出最大的差距,重度用户则最小。
如果假设高阶用户总是获益最多,这个结果会显得反直觉,但对AI摘要功能来说是合理的。轻度用户在长对话中经常丢失上下文,确实需要顶部摘要。重度用户已经掌握了产品使用方式,反而觉得摘要更具有破坏性。下一步的T-learner会通过控制每个层级内的查询置信度来细化这些估计。
图1:异质处理效应的概念图示。每个参与层级的对照组和处理组分布(虚线和实线)均被展示。层级内CATE(两条曲线之间的差距)从轻度用户到重度用户逐渐减小。底部面板展示了ATE如何将这种分布压缩成单一平均值,从而错误地表示了该功能对每个用户群体的实际效果。
第一步:T-learner(最简单的元学习器)
T-learner为处理组和对照组分别拟合两个完全独立的模型。对任何用户的预测CATE是处理组模型预测值与对照组模型预测值的差值。
from sklearn.linear_model import LinearRegression
import pandas as pd
import numpy as np
# 构建特征矩阵:query_confidence + engagement_tier虚拟变量
X_full = pd.get_dummies(
df[["query_confidence", "engagement_tier"]],
drop_first=False
).astype(float)
feature_cols = X_full.columns.tolist()
print("特征列:", feature_cols)
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"\n平均CATE (T-learner): {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']
平均CATE (T-learner): +0.0847
按参与层级划分的平均预测CATE:
engagement_tier
heavy 0.0665
light 0.0954
medium 0.0744
Name: cate_tlearner, dtype: float64
以下是具体实现方式:你将参与层级编码为独热编码列,同时将查询置信度保留为连续特征。分别训练两个线性回归模型:m1学习已参与用户的任务完成条件期望,m0学习未参与用户的相同指标。对于任意具有特征x的用户,预测的CATE值为m1(x) - m0(x)。
输出结果验证了朴素差距的结论方向,但显著提升了估计精度。所有50,000名用户的平均CATE值为+0.0847,接近真实值+0.08。各层级排序为轻度(+0.0954) > 中度(+0.0744) > 重度(+0.0665)。此前+0.2106的朴素ATE隐藏了轻度用户与重度用户之间1.4倍的差异,这个差异就是你的分群信号。
T-learner有一个重要注意事项需要说明:当某一臂样本量远小于另一臂(此处为13,451个处理组 vs 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已在上文步骤1中拟合
步骤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]
Kunzel等(2019):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"平均CATE (X-learner): {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:
engagement_tier
heavy 0.0665
light 0.0954
medium 0.0744
Name: cate_xlearner, dtype: float64
T-learner 与 X-learner 按参与层级比较:
cate_tlearner cate_xlearner
engagement_tier
heavy 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 模型的用户排名与实际处理响应排序一致时,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"Naive ATE = {naive_ate:.4f}")
ax.set_xlabel("前 k% 用户(按预测 CATE 排序)")
ax.set_ylabel("前 k 组的观察到的提升率")
ax.set_title("Qini 曲线:T-learner 排名与观察到的提升率")
ax.legend()
plt.tight_layout()
plt.savefig("qini_curve.png", dpi=140)
print("Saved 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}")Saved qini_curve.png
选定截断点的 Qini 值:
前 10%: 观察到的提升率 = 0.0895
前 20%: 观察到的提升率 = 0.1018
前 30%: 观察到的提升率 = 0.0959
前 50%: 观察到的提升率 = 0.0966
前 70%: 观察到的提升率 = 0.1454
前 100%: 观察到的提升率 = 0.2106以下是技术细节的说明:你按T-learner预测的CATE值对所有50,000名用户进行排序,从最高值开始。对于每个百分位数截断点,你计算该子群体内任务完成率的处理组与对照组的原始差异。
前10%用户组观察到+0.0895的提升效果,前20%用户组观察到+0.1018的提升效果,这两个数值都远低于原始ATE值+0.2106。原始ATE值受到选择偏差的影响,更多反映的是用户参与度而非特征影响。
此处的Qini值也混合了CATE信号与残余选择偏差:根据预测CATE值排名前54%的用户都是轻度用户(该层级的注册率最低,仅12%),因此该群体内的处理组与对照组比较仍然受到层级内选择偏差的干扰。
前70%用户组的提升效果突增至+0.1454,这使得混淆效应变得明显:当中度和重度用户进入排名组时,处理组突然包含大量高完成率的重度用户(注册率64.7%),而对照组仍主要由低完成率的轻度用户主导。这个峰值是选择偏差造成的,背后并没有真实的CATE信号。
在观察性提升分析场景中,Qini曲线的有效区域大致在前20%到50%之间,该区域的排名能更清晰地反映模型的CATE估计值,而更高百分位数区域则主要受倾向得分与结果水平的相关性主导。
第4步:分层部署规则
CATE模型为每个用户分配一个预测的处理效应。通过设置阈值将这一预测转化为部署策略:将特征推送给预测CATE值超过某个值的用户,对其他用户则进行抑制。
# 首先检查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-learner):
count 50000.0000
mean 0.0847
std 0.0126
min 0.0515
25% 0.0731
50% 0.0897
75% 0.0963
max 0.1021
Name: cate_tlearner, dtype: float64
已保存cate_distribution.png
部署阈值: CATE >= 0.085
被选中部署的用户数量: 27,203 (54%)
被抑制的用户数量: 22,797 (46%)
选中组的层级构成:
engagement_tier
light 1.0
dtype: float64预测的平均CATE(已选): 0.0955 预测的平均CATE(被抑制): 0.0719
这里发生了什么:在设置阈值之前,你需要检查完整的CATE分布。所有50,000名用户的平均CATE为+0.0847,标准差为+0.0126。将阈值设为+0.085(略高于平均值+0.0847)时,会选中27,203名用户(54%)。
所选群体的层级构成是100%轻度用户:使用线性模型和这些特征时,每个层级的CATE范围在阈值处没有重叠。轻度用户的预测CATE都在+0.0807到+0.1021之间。中度用户的预测CATE在+0.0592到+0.0812之间。0.085的阈值清晰地将两者分隔开。
所选群体的预测平均CATE(+0.0955)比被抑制群体(+0.0719)高出33%。这种集中度就是分层上线的价值:你将AI摘要部署给最可能受益的54%用户,对预测收益较小的中度和重度用户保持克制,并收集两组数据以每季度优化阈值。
图2:来自50,000用户合成数据集的各层级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"Heavy user, conf=0.72: show feature={show}, CATE={cate}")
show, cate = should_show_feature(0.72, "light") print(f"Light user, conf=0.72: show feature={show}, CATE={cate}")
show, cate = should_show_feature(0.45, "medium") print(f"Medium user, conf=0.45: show feature={show}, CATE={cate}")
重度用户,conf=0.72: 显示功能=False,CATE=0.0667 轻度用户,conf=0.72: 显示功能=True,CATE=0.0955 中度用户,conf=0.45: 显示功能=False,CATE=0.0681
这里发生了什么:你将CATE计算封装成一个函数,该函数的行为与真实的功能标志服务在请求时的运行方式一致。一个查询置信度中等的重度用户会得到show feature=False和CATE=+0.0667,低于0.085阈值。相同查询置信度的轻度用户会得到show feature=True和CATE=+0.0955。一个置信度较低的中度用户则会低于+0.0681阈值。
这些输出与领域故事一致:AI摘要帮助那些难以在会话间保持上下文的用户,而参与层级是衡量这种困难的强代理指标。
## 第5步:自助法置信区间
以上 CATE 估计值均为点估计,未包含不确定性量化。在基于这些估计值构建部署规则前,你需要了解这些估计值在用户群体不同样本中的稳定性。
def bootstrap_cate_ci(df, X_all, feature_cols, n_reps=500, seed=7): """对总体及各参与层级的平均 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(np.percentile(arr, 2.5)), float(np.percentile(arr, 97.5))) arr = np.array(mean_reps) cis["mean"] = (float(np.percentile(arr, 2.5)), float(np.percentile(arr, 97.5))) return cis
print("Running bootstrap (500 replicates, seed=7)...") cis = bootstrap_cate_ci(df, X_all, feature_cols, n_reps=500, seed=7) print(f"Mean CATE 95% CI: [{cis['mean'][0]:+.4f}, {cis['mean'][1]:+.4f}]") print(f"Light tier 95% CI: [{cis['light'][0]:+.4f}, {cis['light'][1]:+.4f}]") print(f"Medium tier 95% CI: [{cis['medium'][0]:+.4f}, {cis['medium'][1]:+.4f}]") print(f"Heavy tier 95% CI: [{cis['heavy'][0]:+.4f}, {cis['heavy'][1]:+.4f}]")
Running bootstrap (500 replicates, seed=7)... Mean CATE 95% CI: [+0.0744, +0.0951] Light tier 95% CI: [+0.0781, +0.1125] Medium tier 95% CI: [+0.0596, +0.0892] Heavy tier 95% CI: [+0.0483, +0.0842]
以下是实现过程:对完整的 50,000 用户数据集进行 500 次有放回重采样,每次重采样后重新拟合 T-learner 模型,并计算自助法迭代中平均 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. 尾部外推(重叠假设违反)
线性回归会在训练范围外进行平滑外推。如果模型为某个特征值落在某组训练数据完全缺失区域的用户分配了 CATE 预测值,该估计值将缺乏实证支持。
重叠假设的失效是静默发生的:模型会返回一个数值,但该区域的 P(T=1|X=x) 接近 0 或 1,导致 CATE 无法识别。
请同时检查倾向得分与 CATE 预测值,并在倾向得分超出 [0.05, 0.95] 范围时对估计值进行截断或标记。
### 3. 小 k 值下的 Qini 噪声
在极小 k 值(前 5% 或更少)时,Qini 曲线会出现显著噪声。当评估组仅包含几百名用户时,该组内的治疗用户数量可能过少,导致观察到的提升效果被抽样噪声主导。
应基于 20% 至 50% 的 Qini 范围进行发布决策,该范围内的信号更稳定。在观察性场景中,大 k 值下的高 Qini 值(如本教程中前 70% 的 +0.1454)可能反映选择偏差,掩盖了真实的 CATE 信号。在解读提升值前,请检查每个 top-k 组的层级构成。
### 4. CATE 模型过拟合
此处对治疗组使用线性回归模型时,有 13,451 个观测值和 4 个特征,这是一个较为宽松的条件。如果您用梯度提升替代线性回归并增加 30 个特征,可能会将治疗效应的估计值过度拟合到训练噪声中。CATE 预测值在训练集上会表现出显著异质性,但在验证集上会向全局均值回归。只有当 CATE 模型在验证集上的提升效果优于层级平均值时,其复杂度才值得存在。在使用该模型构建发布规则前,请在验证数据集上进行评估。
## 下一步行动
上述实现未使用外部提升库,以便您能清晰看到每一步的具体计算过程。在生产环境中,causalml 和 econml 提供了更强大的估计器实现:基于树的 T-学习器、双重稳健 X-学习器,以及通过分离训练样本和估计样本以降低过拟合风险的诚实因果森林。这两个库都遵循您在此处构建的相同概念结构。
causalml 包含生产级 Qini 曲线计算功能和 AUUC(提升曲线下的面积)指标,该指标可将 Qini 曲线压缩为单一比较数值。在 A/B 测试框架中进行提升模型比较时,AUUC 是标准的排行榜指标。
需要特别指出的一个结构性限制:本教程假设了 SUTVA(每个用户的结局仅取决于其自身的治疗状态)。在基于工作空间的 AI 产品中,这一假设通常不成立。同一工作空间内的用户共享共同环境,对一个用户的治疗可能通过共享输出、改变响应模式或改变工作空间动态影响其队友。
当您怀疑存在此类干扰时,能够将组内相关性传播到CATE估计中的DR-learner变体可以提供更现实的不确定性边界。标准的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%用户上进行分段发布,比将同一功能推广给所有人能产生更显著的效果。统一发布是一种策略选择。请基于充分信息做出决策。
我是应用AI/ML和营销测量科学领域的领导者,拥有15年以上在财富50强公司构建和扩展应用AI及机器学习产品的经验。我专长于因果推理、实验、自主代理AI系统以及企业AI战略,这些技术已成功应用于零售媒体网络(RMN)、广告、广告技术、营销技术、消费品(CPG)和电子商务领域。我是Springer Nature、Elsevier、ICML(顶级AI/ML会议)和IEEE的出版作者,也是领先AI/ML和分析出版物的常驻贡献者(个人观点)。
如果本文对您有帮助,请分享它。
免费学习编程。freeCodeCamp的开源课程已帮助超过40,000人成为开发者。立即开始
ADVERTISEMENT