主题
第7章 价格敏感度与 GMV 预估
7.1 为什么还需要两个模型
经过前两章,我们已经有了:
- 意图模型:预测用户会不会买 →
- Uplift 模型:预测发券能不能让用户买 →
但这还不够。要做预算约束优化(第 8 章),我们必须能计算每个用户发 不同面额 券的期望收益和成本。这需要两个额外的信号:
| 模型 | 输出 | 含义 | 下游用途 |
|---|---|---|---|
| 价格敏感度模型 | 用户对价格/面额的敏感程度 | 面额响应曲线的参数 | |
| GMV 预估模型 | 用户首单的期望 GMV(元) | 计算增量毛利 |
两者的关系可以用一个公式串起来:
其中
建模哲学
工业系统中的"一个模型"往往拆成多个互相正交的子模型。这样做的好处是:
- 每个子模型的目标清晰,更容易优化
- 子模型可以独立迭代,不互相影响
- 组合方式灵活,策略层可以调整权重
7.2 价格敏感度模型
问题定义
| 要素 | 定义 |
|---|---|
| 任务类型 | 回归 |
| 标签 | 价格敏感度分数 |
| 特征 | 浏览特征 + 用户画像 |
| 评估指标 | MAE、RMSE、R² |
敏感度标签的构造方式(在 generate_sample_data.py 中):
:高敏感用户,对面额变化反应强烈(例如:价格敏感型消费者) :低敏感用户,面额大小对转化影响不大(例如:品质驱动型消费者)
实现详解
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 回归模型的输出不受 np.clip 保证输出语义正确。
为什么不用 sigmoid?
有人会想用 sigmoid 把输出映射到
③ 评估指标
回归模型使用三个互补指标:
| 指标 | 含义 | 关注点 |
|---|---|---|
| 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)函数的直觉解释:
其中
- 当
时,调节因子 ,效果等于 base uplift - 当
时,调节因子 ,大面额效果更好 越大,面额增大带来的效果提升越明显(高敏感用户对大面额响应更强) - 对数曲线保证了边际效应递减:从 5 元加到 10 元的提升远大于从 45 元加到 50 元
为什么用对数曲线?
经济学中的边际效用递减规律:消费者从额外一元优惠中获得的效用随面额增加而递减。对数函数
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) ─────→ 值不值得发它们的输出汇入预算优化器,为每个用户、每个面额计算一个综合分数:
化简后就是边际效率:
有趣的发现
化简后发现,边际效率只取决于 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: # 状态检查这种统一接口的好处:
- 降低认知负担:看懂一个模型就能看懂所有模型
- 流水线友好:
train_models.py可以用统一的循环训练所有模型 - 测试简化:测试代码可以复用相同的 fixture 和断言模式
- 替换灵活:如果要把 LightGBM 换成 XGBoost,只需改模型类内部
7.6 本章小结
本章实现了预算优化器的两个输入模型:
- 价格敏感度模型:LightGBM 回归,输出
,驱动面额响应曲线 - GMV 预估模型:LightGBM 回归,输出
,决定增量收益上限 - 面额响应函数:对数曲线调节,高敏感度用户对大面额响应更强
- 四模型协同:意图 + Uplift + 敏感度 + GMV,每个模型回答一个正交问题
至此,模型层已经完整。下一章我们将进入策略层——在日预算 100 万的约束下,用这些模型的输出做最优预算分配。
思考题
- 如果 GMV 标签中有少量极端值(如一个用户买了 10 万元的商品),你会如何处理?(提示:考虑对标签取对数、Winsorize、或使用 Huber loss)
- 面额响应函数为什么用
而不是 或其他凹函数?不同函数的选择会如何影响最终的预算分配? - 四个模型使用相同的特征矩阵,这是否合理?是否有些模型应该有自己独特的特征?