Skip to content

第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):

τ(x)=E[Y(1)−Y(0)∣X=x]

其中:

  • Y(1):发券后的转化结果(potential outcome under treatment)
  • Y(0):不发券的转化结果(potential outcome under control)
  • τ(x):发券对特征为 x 的用户的增量效果

根本困难在于:对于同一个用户,我们只能观测到 Y(1) 和 Y(0) 中的一个——这就是因果推断中著名的 "反事实问题"(Fundamental Problem of Causal Inference)。

解决方案是利用随机实验数据:将用户随机分为发券组(treatment)和对照组(control),用统计方法估计群体层面的增量效果。

6.2 Meta-Learner 全家族 ​

Meta-Learner 是一类利用任意机器学习模型来估计 CATE 的方法。它们不修改底层模型结构,而是通过巧妙的数据拆分和预测组合来逼近因果效应。

S-Learner(Single Model) ​

最简单的思路:把 treatment 标识 T 作为一个普通特征塞进模型。

τ^(x)=μ^(x,T=1)−μ^(x,T=0)
python
# S-Learner 伪代码
model.fit(X_with_T, y)  # T 作为一列特征
tau = model.predict(X, T=1) - model.predict(X, T=0)

优点:实现最简单,只训练一个模型。

缺点:如果 treatment 效果较小,树模型可能根本不选 T 做分裂——等于学了个寂寞。在样本量大、treatment 效果弱的场景(发券增量只有 2-3 个百分点),S-Learner 几乎无法工作。

T-Learner(Two Models)⭐ 本项目采用 ​

训练两个独立模型,分别拟合实验组和对照组:

τ^(x)=μ^1(x)−μ^0(x)

其中 μ^1 在发券用户上训练,μ^0 在未发券用户上训练。

为什么选 T-Learner

T-Learner 是工业界最常用的 Uplift 方法,因为:

  1. 实现简单:两个标准分类模型,任何 ML 框架都能跑
  2. 效果稳健:不依赖 treatment 效果大小
  3. 可解释:可以分别查看两个模型的特征重要度
  4. 易迭代:可以独立优化两个子模型

本项目的实现:

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 的含义:

  • τ>0:发券能增加转化概率 → Persuadable
  • τ≈0:发券没影响 → Sure Thing 或 Lost Cause
  • τ<0:发券降低转化概率 → Sleeping Dog

X-Learner(交叉学习器) ​

X-Learner 是 T-Learner 的改进,特别适合实验组和对照组样本量差异大的场景。

核心思想:用一组的模型去预测另一组的反事实,然后对残差建模。

Step 1:同 T-Learner,训练两个模型 μ^0, μ^1

Step 2:计算个体处理效应的代理(imputed treatment effect)

D~1(i)=Y1(i)−μ^0(X1(i))(实验组用户的效应)D~0(j)=μ^1(X0(j))−Y0(j)(对照组用户的效应)

Step 3:分别对 D~1 和 D~0 训练回归模型 τ^1, τ^0

Step 4:加权合并

τ^(x)=g(x)⋅τ^0(x)+(1−g(x))⋅τ^1(x)

其中 g(x) 通常取倾向性分数 P(T=1∣X=x)。

X-Learner 的优势

当对照组远大于实验组时(如只有 10% 的用户被发券),X-Learner 能更好地利用对照组的大量数据来修正 treatment effect 的估计。

R-Learner(残差学习器) ​

R-Learner 的思路更激进——不直接估计两组的转化概率,而是对 "去中心化"后的残差 建模。

Step 1:估计边际转化 m^(x)=E[Y∣X=x](无论是否发券)

Step 2:估计倾向性分数 e^(x)=P(T=1∣X=x)

Step 3:对残差建模,最小化 Robinson's loss:

τ^=arg⁡minτ∑i[(Yi−m^(Xi))−τ(Xi)⋅(Ti−e^(Xi))]2

R-Learner 的优势是正交化:通过先去除 X 对 Y 和 T 的直接影响,CATE 的估计受 nuisance parameter 的影响更小,理论收敛速度更快。

DR-Learner(双重鲁棒学习器) ​

DR-Learner 结合了逆概率加权(IPW)和结果回归,只要其中一个估计正确,最终的 CATE 估计就是一致的。

τ^DR(x)=μ^1(x)−μ^0(x)+T(Y−μ^1(X))e^(X)−(1−T)(Y−μ^0(X))1−e^(X)

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 ≈ 整体 uplift × 0.5。

Qini 系数 ​

AUUC 的绝对值受数据集影响大,不同实验之间难以比较。Qini 系数做了归一化:

Qini=模型 AUUC−随机 AUUC理想 AUUC−随机 AUUC
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 =1:完美模型
  • Qini =0:和随机一样
  • Qini <0:比随机还差(模型排反了)

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 个百分点。

评估注意事项

  1. 样本量要求:Uplift 指标需要实验组和对照组都有足够样本,建议每组至少 5000+
  2. 置信区间:单次评估结果波动大,建议做 bootstrap 计算 95% CI
  3. 时间窗口:确保训练数据和评估数据的转化窗口一致(本项目为 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 本章小结 ​

本章是整个系统的核心,覆盖了以下关键内容:

  1. 四象限用户模型:Sure Things / Persuadables / Lost Causes / Sleeping Dogs,只对 Persuadables 发券
  2. CATE 定义:τ(x)=E[Y(1)−Y(0)∣X=x],反事实问题是核心挑战
  3. T-Learner 实现:两个 LightGBM 分类器,分别拟合 treatment 和 control 的转化概率
  4. Meta-Learner 全家族:S / T / X / R / DR 五种方法各有适用场景
  5. 评估指标:Uplift Curve → AUUC → Qini 系数 → Uplift@K,缺一不可
  6. 实战陷阱:样本偏差、特征漂移、评估不稳定的应对策略

思考题 ​

  1. 如果 Uplift Curve 在 Top 30% 处出现急剧下降,说明什么?这对预算分配策略有什么启示?
  2. 在 T-Learner 中,如果 treatment 组有 100 万样本,control 组只有 10 万样本,模型效果会受到什么影响?如何缓解?
  3. S-Learner 在什么条件下会退化为"完全忽略 treatment"?请从决策树分裂的角度分析。
  4. 假设你需要在 T-Learner 和 X-Learner 之间做选择,你会设计什么样的离线实验来对比?
  5. DR-Learner 的"双重鲁棒"具体指什么?如果两个 nuisance model 都估计错了会怎样?

基于 Uplift Modeling 的电商优惠券智能发放系统