Skip to content

第7章 价格敏感度与 GMV 预估 ​

7.1 为什么还需要两个模型 ​

经过前两章,我们已经有了:

  • 意图模型:预测用户会不会买 → P(convert∣X)
  • Uplift 模型:预测发券能不能让用户买 → τ(x)

但这还不够。要做预算约束优化(第 8 章),我们必须能计算每个用户发 不同面额 券的期望收益和成本。这需要两个额外的信号:

模型输出含义下游用途
价格敏感度模型s(x)∈[0,1]用户对价格/面额的敏感程度面额响应曲线的参数
GMV 预估模型g^(x)≥0用户首单的期望 GMV(元)计算增量毛利

两者的关系可以用一个公式串起来:

期望增量毛利=τ(x)⏟Uplift×f(d,s(x))⏟面额响应×g^(x)⏟GMV×r⏟毛利率

其中 f(d,s) 是面额 d 在敏感度 s 下的响应调节函数,r=0.20 是毛利率。

建模哲学

工业系统中的"一个模型"往往拆成多个互相正交的子模型。这样做的好处是:

  1. 每个子模型的目标清晰,更容易优化
  2. 子模型可以独立迭代,不互相影响
  3. 组合方式灵活,策略层可以调整权重

7.2 价格敏感度模型 ​

问题定义 ​

要素定义
任务类型回归
标签价格敏感度分数 ∈[0,1]
特征浏览特征 + 用户画像
评估指标MAE、RMSE、R²

敏感度标签的构造方式(在 generate_sample_data.py 中):

  • s≈1:高敏感用户,对面额变化反应强烈(例如:价格敏感型消费者)
  • s≈0:低敏感用户,面额大小对转化影响不大(例如:品质驱动型消费者)

实现详解 ​

python
# src/models/sensitivity_model.py

class SensitivityModel:
    """基于 LightGBM 回归的价格敏感度预估模型."""

    def train(self, X: pd.DataFrame, y: pd.Series) -> dict:
        X_train, X_val, y_train, y_val = train_test_split(
            X, y, test_size=0.2, random_state=self._params.random_state,
        )
        self._feature_names = list(X.columns)

        p = self._params.to_dict()
        p["objective"] = "regression"
        self._model = lgb.LGBMRegressor(**p)
        self._model.fit(
            X_train,
            y_train,
            eval_set=[(X_val, y_val)],
            callbacks=[lgb.early_stopping(50, verbose=False), lgb.log_evaluation(-1)],
        )

        y_pred = np.clip(self._model.predict(X_val), 0.0, 1.0)
        return {
            "mae": float(mean_absolute_error(y_val, y_pred)),
            "rmse": float(np.sqrt(mean_squared_error(y_val, y_pred))),
            "r2": float(r2_score(y_val, y_pred)),
        }

与意图模型的关键区别:

① 回归而非分类

价格敏感度是连续值,使用 LGBMRegressor + objective="regression"(最小化 MSE)。

② 输出裁剪

python
def predict(self, X: pd.DataFrame) -> np.ndarray:
    """返回价格敏感度分数, clamp 到 [0, 1]."""
    raw = self._model.predict(X)
    return np.clip(raw, 0.0, 1.0)

LightGBM 回归模型的输出不受 [0,1] 约束,可能产生负值或大于 1 的值。np.clip 保证输出语义正确。

为什么不用 sigmoid?

有人会想用 sigmoid 把输出映射到 [0,1]:σ(y^)。但这会改变值的分布,使得原本均匀分布在 [0,1] 的敏感度分数被压缩到两端。简单的 clip 保持了线性关系,更符合下游面额响应函数的预期。

③ 评估指标

回归模型使用三个互补指标:

指标含义关注点
MAE平均绝对误差平均偏差多大
RMSE均方根误差对大误差更敏感
R²决定系数模型比均值预测好多少

面额响应函数 ​

敏感度模型的预测直接用于面额响应函数,这是预算优化器中的核心组件:

python
# src/optimizer/budget_optimizer.py

def _amount_response_factor(
    self, base_uplift: float, amount: int, sensitivity: float
) -> float:
    """面额响应函数 — 对数曲线调整."""
    sensitivity = max(0.0, min(1.0, sensitivity))
    ref_amount = float(np.median(self.config.coupon_amounts))
    scale = 0.5 + sensitivity  # 范围 [0.5, 1.5]

    numerator = math.log1p(amount * scale)
    denominator = math.log1p(ref_amount * scale)
    if denominator == 0:
        return base_uplift

    return base_uplift * (numerator / denominator)

函数的直觉解释:

adjusted_uplift=τbase×ln⁡(1+d×(0.5+s))ln⁡(1+dref×(0.5+s))

其中 d 是面额,s 是敏感度,dref 是面额集合的中位数。

  • 当 d=dref 时,调节因子 =1,效果等于 base uplift
  • 当 d>dref 时,调节因子 >1,大面额效果更好
  • s 越大,面额增大带来的效果提升越明显(高敏感用户对大面额响应更强)
  • 对数曲线保证了边际效应递减:从 5 元加到 10 元的提升远大于从 45 元加到 50 元

为什么用对数曲线?

经济学中的边际效用递减规律:消费者从额外一元优惠中获得的效用随面额增加而递减。对数函数 ln⁡(1+x) 完美地捕捉了这一特性。

7.3 GMV 预估模型 ​

问题定义 ​

要素定义
任务类型回归
标签用户首单实际 GMV(元)
特征浏览特征 + 用户画像
评估指标MAE、RMSE、R²

实现详解 ​

python
# src/models/gmv_model.py

class GMVModel:
    """基于 LightGBM 回归的首单 GMV 预估模型."""

    def train(self, X: pd.DataFrame, y: pd.Series) -> dict:
        X_train, X_val, y_train, y_val = train_test_split(
            X, y, test_size=0.2, random_state=self._params.random_state,
        )
        self._feature_names = list(X.columns)

        p = self._params.to_dict()
        p["objective"] = "regression"
        self._model = lgb.LGBMRegressor(**p)
        self._model.fit(
            X_train,
            y_train,
            eval_set=[(X_val, y_val)],
            callbacks=[lgb.early_stopping(50, verbose=False), lgb.log_evaluation(-1)],
        )

        y_pred = np.maximum(self._model.predict(X_val), 0.0)
        return {
            "mae": float(mean_absolute_error(y_val, y_pred)),
            "rmse": float(np.sqrt(mean_squared_error(y_val, y_pred))),
            "r2": float(r2_score(y_val, y_pred)),
        }

与敏感度模型几乎相同的结构,但有一个关键区别:

输出约束不同

python
# 敏感度模型:clip 到 [0, 1]
y_pred = np.clip(raw, 0.0, 1.0)

# GMV 模型:只保证非负
y_pred = np.maximum(raw, 0.0)

GMV 没有上界——一个用户可能买 50 元的商品,也可能买 5000 元的。但 GMV 不可能是负数,所以用 np.maximum(raw, 0.0) 做下界约束。

GMV 在系统中的作用 ​

GMV 预估值直接参与预算优化器中的 期望收益计算:

python
# src/optimizer/budget_optimizer.py(简化版)

for amount in self.config.coupon_amounts:
    inc_prob = self._amount_response_factor(
        row.uplift_score, amount, row.sensitivity_score
    )
    exp_cost = amount * inc_prob          # 期望成本 = 面额 × 增量转化概率
    exp_gain = inc_prob * row.predicted_gmv * self.config.margin_rate  # 期望收益
    efficiency = exp_gain / exp_cost      # 边际效率

最终的发券决策基于边际效率 = 期望收益 / 期望成本:

  • 效率 > 1:发券"赚钱"(增量毛利 > 券成本)
  • 效率 < 1:发券"亏钱"

GMV 高的用户,即使 uplift 相同,期望收益也更高——因为拉动一个大单比拉动一个小单产生的毛利更多。

7.4 四个模型的协同 ​

现在我们有了完整的模型矩阵:

用户特征 X
    │
    ├── Intent Model ──→ P(convert) ──→ 兜底策略排序
    │
    ├── Uplift Model ──→ τ(x) ─────→ 发不发券
    │
    ├── Sensitivity ───→ s(x) ─────→ 发多大面额
    │
    └── GMV Model ─────→ ĝ(x) ─────→ 值不值得发

它们的输出汇入预算优化器,为每个用户、每个面额计算一个综合分数:

score(x,d)=τ(x)⋅f(d,s(x))⋅g^(x)⋅r⏞期望增量毛利d⋅τ(x)⋅f(d,s(x))⏟期望券成本

化简后就是边际效率:

efficiency(x,d)=g^(x)⋅rd

有趣的发现

化简后发现,边际效率只取决于 GMV / 面额(乘以毛利率)。uplift 和 sensitivity 影响的是"要不要发"和"发多少",但不影响效率排序。这意味着:

  • 高 GMV 用户可以发更大面额的券还保持高效率
  • 低 GMV 用户应该发小面额券
  • uplift 为负的用户不管 GMV 多高都不该发券(被提前过滤)

7.5 模型统一设计模式 ​

回顾四个模型的代码,它们遵循相同的设计模式:

python
class XxxModel:
    def __init__(self, params: LGBMParams | None = None) -> None: ...
    def train(self, X, y, ...) -> dict:    # 训练 + 返回验证指标
    def predict(self, X) -> np.ndarray:    # 推理 + 输出约束
    def save(self, path: str) -> None:     # joblib 序列化
    @classmethod
    def load(cls, path) -> XxxModel:       # 反序列化
    def _check_fitted(self) -> None:       # 状态检查

这种统一接口的好处:

  1. 降低认知负担:看懂一个模型就能看懂所有模型
  2. 流水线友好:train_models.py 可以用统一的循环训练所有模型
  3. 测试简化:测试代码可以复用相同的 fixture 和断言模式
  4. 替换灵活:如果要把 LightGBM 换成 XGBoost,只需改模型类内部

7.6 本章小结 ​

本章实现了预算优化器的两个输入模型:

  1. 价格敏感度模型:LightGBM 回归,输出 [0,1],驱动面额响应曲线
  2. GMV 预估模型:LightGBM 回归,输出 ≥0,决定增量收益上限
  3. 面额响应函数:对数曲线调节,高敏感度用户对大面额响应更强
  4. 四模型协同:意图 + Uplift + 敏感度 + GMV,每个模型回答一个正交问题

至此,模型层已经完整。下一章我们将进入策略层——在日预算 100 万的约束下,用这些模型的输出做最优预算分配。

思考题 ​

  1. 如果 GMV 标签中有少量极端值(如一个用户买了 10 万元的商品),你会如何处理?(提示:考虑对标签取对数、Winsorize、或使用 Huber loss)
  2. 面额响应函数为什么用 ln⁡(1+x) 而不是 x 或其他凹函数?不同函数的选择会如何影响最终的预算分配?
  3. 四个模型使用相同的特征矩阵,这是否合理?是否有些模型应该有自己独特的特征?

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