主题
第6章 Uplift 增量模型
Uplift 模型是整个优惠券发放系统的核心引擎。它回答的不是"用户会不会买",而是一个更精准的因果问题:发券能不能让本来不买的用户买?
6.1 从预测到因果:为什么需要 Uplift
四象限用户模型
想象你是一个电商运营,面对 100 万新用户要决定发不发券。如果只看购买意图模型,你会给"最可能买的人"发券——但这恰恰是最大的浪费。
根据用户对优惠券的真实响应,所有用户可以分为四类:
不发券也买 发券才买
┌─────────────┬─────────────┐
│ │ │
发券也买 → │ Sure Thing │ Persuadable │ ← 发券才买
│ (必买型) │ (可说服型) │
│ 浪费预算 ❌ │ 发券目标 ✅ │
│ │ │
├─────────────┼─────────────┤
│ │ │
都不买 → │ Lost Cause │ Sleeping │ ← 发券反而不买
│ (无望型) │ Dog │
│ 不浪费 ✓ │ (反效果型) │
│ │ │
└─────────────┴─────────────┘
对照组买 对照组不买- Sure Things(必买型):不发券也会买,发券是白送钱
- Persuadables(可说服型):只有发券才会买,这才是我们的目标
- Lost Causes(无望型):怎么都不买,券发了也没用
- Sleeping Dogs(反效果型):不发券反而会买,发券后因为"被推销"而反感
核心洞察
意图模型会把 Sure Things 排在最前面(因为他们最可能买),但这恰恰是最不该发券的人群。Uplift 模型的目标是找到 Persuadables——只对他们发券。
从关联到因果
数学上,Uplift 定义为条件平均处理效应(CATE):
其中:
:发券后的转化结果(potential outcome under treatment) :不发券的转化结果(potential outcome under control) :发券对特征为 的用户的增量效果
根本困难在于:对于同一个用户,我们只能观测到
解决方案是利用随机实验数据:将用户随机分为发券组(treatment)和对照组(control),用统计方法估计群体层面的增量效果。
6.2 Meta-Learner 全家族
Meta-Learner 是一类利用任意机器学习模型来估计 CATE 的方法。它们不修改底层模型结构,而是通过巧妙的数据拆分和预测组合来逼近因果效应。
S-Learner(Single Model)
最简单的思路:把 treatment 标识
python
# S-Learner 伪代码
model.fit(X_with_T, y) # T 作为一列特征
tau = model.predict(X, T=1) - model.predict(X, T=0)优点:实现最简单,只训练一个模型。
缺点:如果 treatment 效果较小,树模型可能根本不选
T-Learner(Two Models)⭐ 本项目采用
训练两个独立模型,分别拟合实验组和对照组:
其中
为什么选 T-Learner
T-Learner 是工业界最常用的 Uplift 方法,因为:
- 实现简单:两个标准分类模型,任何 ML 框架都能跑
- 效果稳健:不依赖 treatment 效果大小
- 可解释:可以分别查看两个模型的特征重要度
- 易迭代:可以独立优化两个子模型
本项目的实现:
python
# src/models/uplift_model.py
class UpliftModel:
"""Two-Model (T-Learner) Uplift 模型."""
def train(
self,
X: pd.DataFrame,
y: pd.Series,
treatment: pd.Series,
) -> dict:
self._feature_names = list(X.columns)
treat_mask = treatment == 1
X_treat, y_treat = X[treat_mask], y[treat_mask]
X_ctrl, y_ctrl = X[~treat_mask], y[~treat_mask]
p = self._params.to_dict()
p["objective"] = "binary"
self._treatment_model = lgb.LGBMClassifier(**p)
treat_metrics = self._fit_one(self._treatment_model, X_treat, y_treat)
self._control_model = lgb.LGBMClassifier(**p)
ctrl_metrics = self._fit_one(self._control_model, X_ctrl, y_ctrl)
return {
"treatment_metrics": treat_metrics,
"control_metrics": ctrl_metrics,
}注意几个关键点:
① 数据拆分而非特征拆分
T-Learner 对数据按 treatment 标识做行拆分(treatment 组和 control 组各训练一个模型),而非像 S-Learner 那样做列拆分(把 T 当特征加进去)。
② 各自独立评估
返回两个子模型的独立验证指标,而不是一个合并指标。这让我们可以分别判断:如果 control 模型的 AUC 很低,说明对照组数据可能不足或特征区分度差。
③ 子模型训练方法
python
# src/models/uplift_model.py
@staticmethod
def _fit_one(
model: lgb.LGBMClassifier,
X: pd.DataFrame,
y: pd.Series,
) -> dict:
"""训练单个子模型并返回验证指标."""
X_tr, X_val, y_tr, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y,
)
model.fit(
X_tr,
y_tr,
eval_set=[(X_val, y_val)],
callbacks=[lgb.early_stopping(50, verbose=False), lgb.log_evaluation(-1)],
)
y_pred = model.predict_proba(X_val)[:, 1]
return {
"auc": float(roc_auc_score(y_val, y_pred)),
"logloss": float(log_loss(y_val, y_pred)),
}每个子模型内部再做 80/20 split,各自 early stopping。这确保两个模型都不过拟合。
Uplift 预测
python
# src/models/uplift_model.py
def predict_uplift(self, X: pd.DataFrame) -> np.ndarray:
"""返回 uplift score."""
return self.predict_treatment(X) - self.predict_control(X)Uplift score 的含义:
:发券能增加转化概率 → Persuadable :发券没影响 → Sure Thing 或 Lost Cause :发券降低转化概率 → Sleeping Dog
X-Learner(交叉学习器)
X-Learner 是 T-Learner 的改进,特别适合实验组和对照组样本量差异大的场景。
核心思想:用一组的模型去预测另一组的反事实,然后对残差建模。
Step 1:同 T-Learner,训练两个模型
Step 2:计算个体处理效应的代理(imputed treatment effect)
Step 3:分别对
Step 4:加权合并
其中
X-Learner 的优势
当对照组远大于实验组时(如只有 10% 的用户被发券),X-Learner 能更好地利用对照组的大量数据来修正 treatment effect 的估计。
R-Learner(残差学习器)
R-Learner 的思路更激进——不直接估计两组的转化概率,而是对 "去中心化"后的残差 建模。
Step 1:估计边际转化
Step 2:估计倾向性分数
Step 3:对残差建模,最小化 Robinson's loss:
R-Learner 的优势是正交化:通过先去除
DR-Learner(双重鲁棒学习器)
DR-Learner 结合了逆概率加权(IPW)和结果回归,只要其中一个估计正确,最终的 CATE 估计就是一致的。
Meta-Learner 选型指南
| 方法 | 复杂度 | 数据要求 | 推荐场景 |
|---|---|---|---|
| S-Learner | ⭐ | 低 | treatment 效果强,快速 baseline |
| T-Learner | ⭐⭐ | 中 | 通用首选,工业级默认方案 |
| X-Learner | ⭐⭐⭐ | 中 | 实验组/对照组样本不平衡 |
| R-Learner | ⭐⭐⭐⭐ | 高 | 追求理论最优,有倾向性分数 |
| DR-Learner | ⭐⭐⭐⭐ | 高 | 需要双重鲁棒保证 |
实践建议:从 T-Learner 开始,用 AUUC 评估效果,只有在 T-Learner 瓶颈时才升级。
6.3 Uplift 评估指标
传统分类指标(AUC、F1)无法用于 Uplift 模型,因为我们没有真实的 uplift 标签——每个用户只能观测到发券或不发券中的一个结果。
我们需要专门的评估方法。
Uplift Curve
核心思想:按 uplift score 降序排列用户,逐步扩大发券范围,观察累积增量转化率如何变化。
python
# src/experiment/metrics.py
def uplift_curve(
y_true: np.ndarray,
uplift_scores: np.ndarray,
treatment: np.ndarray,
) -> tuple[np.ndarray, np.ndarray]:
"""计算 Uplift Curve."""
order = np.argsort(-uplift_scores)
y_sorted = y_true[order]
t_sorted = treatment[order]
n = len(y_true)
percentiles = np.arange(1, n + 1) / n
cum_treat_conv = np.cumsum(y_sorted * t_sorted)
cum_treat_n = np.cumsum(t_sorted)
cum_ctrl_conv = np.cumsum(y_sorted * (1 - t_sorted))
cum_ctrl_n = np.cumsum(1 - t_sorted)
treat_cvr = np.divide(
cum_treat_conv, cum_treat_n,
out=np.zeros_like(cum_treat_conv),
where=cum_treat_n > 0,
)
ctrl_cvr = np.divide(
cum_ctrl_conv, cum_ctrl_n,
out=np.zeros_like(cum_ctrl_conv),
where=cum_ctrl_n > 0,
)
uplift_values = treat_cvr - ctrl_cvr
return percentiles, uplift_values怎么读 Uplift Curve:
增量转化率
↑
│ /‾‾‾‾‾‾‾‾‾‾\
│ / \
│ / 好模型 \
│/ \
│- - - - - - - - - -\- - - 随机基线
│ \
│ \
└──────────────────────→ 用户比例 (0% → 100%)- 理想曲线:前 k% 用户(Persuadables)的增量效果最大,之后逐渐下降
- 随机基线:一条水平线,增量恒等于整体 uplift
- 好模型:曲线大部分在基线之上
AUUC(Area Under Uplift Curve)
python
# src/experiment/metrics.py
def auuc(
y_true: np.ndarray,
uplift_scores: np.ndarray,
treatment: np.ndarray,
) -> float:
"""Area Under the Uplift Curve (AUUC)."""
pct, values = uplift_curve(y_true, uplift_scores, treatment)
return float(np.trapezoid(values, pct))AUUC 是 Uplift Curve 下的面积,数值越大,模型排序能力越强。随机模型的 AUUC
Qini 系数
AUUC 的绝对值受数据集影响大,不同实验之间难以比较。Qini 系数做了归一化:
python
# src/experiment/metrics.py
def qini_coefficient(
y_true: np.ndarray,
uplift_scores: np.ndarray,
treatment: np.ndarray,
) -> float:
"""Qini 系数 — 归一化的 Uplift AUC."""
model_auuc = auuc(y_true, uplift_scores, treatment)
treat_mask = treatment == 1
ctrl_mask = treatment == 0
overall_uplift = y_true[treat_mask].mean() - y_true[ctrl_mask].mean()
random_auuc = overall_uplift * 0.5
ideal_auuc = auuc(
y_true,
y_true * treatment - y_true * (1 - treatment),
treatment,
)
denom = ideal_auuc - random_auuc
if abs(denom) < 1e-12:
return 0.0
return float((model_auuc - random_auuc) / denom)- Qini
:完美模型 - Qini
:和随机一样 - Qini
:比随机还差(模型排反了)
Uplift@K
在预算有限的场景下,我们更关心 Top K% 用户的增量效果:
python
# src/experiment/metrics.py
def uplift_at_k(
y_true: np.ndarray,
uplift_scores: np.ndarray,
treatment: np.ndarray,
k: float = 0.1,
) -> float:
"""Top k% 用户的增量转化率."""
pct, values = uplift_curve(y_true, uplift_scores, treatment)
idx = max(0, np.searchsorted(pct, k) - 1)
return float(values[idx])例如 uplift_at_k(k=0.1) = 0.08 表示:对 uplift score 最高的 10% 用户发券,增量转化率为 8 个百分点。
评估注意事项
- 样本量要求:Uplift 指标需要实验组和对照组都有足够样本,建议每组至少 5000+
- 置信区间:单次评估结果波动大,建议做 bootstrap 计算 95% CI
- 时间窗口:确保训练数据和评估数据的转化窗口一致(本项目为 7 天)
6.4 实战建议
T-Learner 的常见陷阱
陷阱 1:样本选择偏差
如果实验分组不是完全随机的(比如高价值用户更容易被分到发券组),T-Learner 估计的 uplift 会有偏。解决方案:确保上游 AB 实验的 hash 分桶是随机的(详见第 9 章)。
陷阱 2:两个模型的特征漂移方向不同
发券组和对照组的模型可能对同一个特征学到不同的重要性,导致 uplift score 的波动很大。解决方案:监控两个子模型的特征重要度差异,差异过大时检查数据质量。
陷阱 3:Uplift 评估指标不稳定
由于同一个用户只有一个观测值,uplift 指标天然比分类指标的方差大。解决方案:
- 增大评估样本量
- 使用 5-fold 交叉评估
- 同时看多个指标(AUUC + Qini + Uplift@10%)
从 T-Learner 升级路径
T-Learner (baseline)
│
├── 效果还行 → 保持,专注特征工程
│
├── 样本不平衡严重 → X-Learner
│
├── 需要理论保证 → DR-Learner
│
└── 需要异质性分析 → Causal Forest(第13章)6.5 本章小结
本章是整个系统的核心,覆盖了以下关键内容:
- 四象限用户模型:Sure Things / Persuadables / Lost Causes / Sleeping Dogs,只对 Persuadables 发券
- CATE 定义:
,反事实问题是核心挑战 - T-Learner 实现:两个 LightGBM 分类器,分别拟合 treatment 和 control 的转化概率
- Meta-Learner 全家族:S / T / X / R / DR 五种方法各有适用场景
- 评估指标:Uplift Curve → AUUC → Qini 系数 → Uplift@K,缺一不可
- 实战陷阱:样本偏差、特征漂移、评估不稳定的应对策略
思考题
- 如果 Uplift Curve 在 Top 30% 处出现急剧下降,说明什么?这对预算分配策略有什么启示?
- 在 T-Learner 中,如果 treatment 组有 100 万样本,control 组只有 10 万样本,模型效果会受到什么影响?如何缓解?
- S-Learner 在什么条件下会退化为"完全忽略 treatment"?请从决策树分裂的角度分析。
- 假设你需要在 T-Learner 和 X-Learner 之间做选择,你会设计什么样的离线实验来对比?
- DR-Learner 的"双重鲁棒"具体指什么?如果两个 nuisance model 都估计错了会怎样?