Contextual Bandits, Bayesian Methods & Small Data Strategies for Service Industry Yield Management
2026-03-22 · 为 Celoria Yield Management 功能调研所有 bandit 算法的起点。想象你面前有 K 台老虎机,每台的期望收益不同但你不知道具体值。你要在"尝试新机器"(exploration)和"用已知最好的"(exploitation)之间平衡。
每次决策: 以概率 ε (如 10%) → 随机选一个臂(exploration) 以概率 1-ε (如 90%) → 选当前最优臂(exploitation) 优点: 实现极简,3 行代码 缺点: exploration 完全随机,不智能;ε 难调 适用: 快速原型,数据量大时效果还行
每次选择得分最高的臂:
Score(a) = Q(a) + c × sqrt(ln(N) / n(a))
───── ─────────────────────────
已知收益 不确定性奖励
Q(a) = 臂 a 的平均收益
N = 总决策次数
n(a) = 臂 a 被选中次数
c = exploration 系数(通常 sqrt(2))
核心思想: 被尝试次数少的臂,不确定性大 → 给额外加分 → 自然被选中尝试
优点: 理论最优 regret bound(O(sqrt(T ln T))),不需要调参
缺点: 假设收益分布固定(non-stationary 环境下需要变体)
为每个臂维护一个概率分布(后验分布):
初始化: 每个臂 a → Beta(1, 1) (均匀先验)
每次决策:
1. 为每个臂 a 从 Beta(α_a, β_a) 采样一个值 θ_a
2. 选择 θ 最大的臂
3. 观察结果(成功/失败)
4. 更新后验:
成功 → α_a += 1
失败 → β_a += 1
核心思想: 不确定的臂 → 分布宽 → 采样可能很高 → 自然被探索
确定性高的臂 → 分布窄 → 采样稳定 → 如果均值高就被利用
普通 bandit 的问题:每台老虎机的收益是固定的。但在定价场景,同一个价格在不同上下文下效果不同(周二 vs 周六、VIP vs 新客)。Contextual Bandit 在每次决策时加入上下文特征。
每次决策:
1. 观察上下文 x = { 星期几, 时段, 客户类型, 门店, 当前空位率, ... }
2. 对每个可能的动作 a (如: 不发 / $5 coupon / $10 coupon / $15 coupon)
预测 E[reward | x, a]
3. 选择预期 reward 最高的动作(+ 一些 exploration)
4. 执行,观察实际 reward(客户来了没、花了多少)
5. 更新模型
关键区别:
普通 Bandit: 学 "哪个价格最好"(全局最优)
Contextual: 学 "在什么情况下哪个价格最好"(条件最优)
| 方法 | 原理 | 数据需求 | 适用场景 |
|---|---|---|---|
| LinUCB | 假设 reward 是 context 的线性函数 + UCB exploration | 中等(几千条) | 特征维度低,关系近似线性 |
| Neural Contextual Bandit | 用神经网络建模 context → reward 的关系 | 大(几万条+) | 特征维度高,非线性关系(Uber 用的) |
| Thompson Sampling + 线性模型 | 贝叶斯线性回归 + Thompson 采样 | 小(几百条即可启动) | 小数据、需要快速收敛 |
| Decision Tree Bandit | 用决策树划分 context 空间,每个叶子节点是一个 bandit | 中等 | 需要可解释性 |
Uber 的 Contextual Bandit 流程:
Context(上下文特征):
x = [时间, 地点, 天气, 需求量, 供给量, 历史完成率, 事件标记, ...]
Actions(可选价格):
a ∈ { base×1.0, base×1.2, base×1.5, base×2.0, base×3.0 }
Reward(奖励):
r = {
rider_accepted: 1/0, // 乘客是否接受
revenue: trip_price × take_rate, // 平台收入
wait_time: minutes, // 等待时间(影响长期留存)
}
实际 reward 是加权组合:
R = w1 × revenue + w2 × completion_rate - w3 × wait_time
模型训练:
- 用历史数据训练 neural network: f(x, a) → E[R]
- 每小时增量更新(在线学习)
- 10% exploration 保证持续学习
关键: Uber 每天几百万次决策 → 10% = 几十万次 exploration → 统计显著性小时级达到
问题: 180 个座位,3 个舱位(经济 $200, 弹性 $400, 商务 $800)
经济能现在卖,商务可能明天才有人买。保留多少给商务?
EMSR-b 算法:
对高价舱的需求建模为正态分布 D ~ N(μ, σ^2)
保护 n 个座位给高价舱,当且仅当:
P(D > n) × fare_high > fare_low
即: "多保护一个高价座的期望收入" > "现在卖一个低价座的确定收入"
数据需求: 需要预估高价舱需求的均值和方差(通常用历史同期数据)
优点: 简单、可解释、几十年验证有效
局限: 假设需求是正态分布;不处理 network 效应(跨航线蚕食)
每个房间每晚都有一个 "bid price"(底价): 只有当客人愿意付的价格 > bid price 时才接受预订 Bid price 的计算: = 未来卖出这个房间的期望收入 = f(剩余房间数, 距入住日天数, 历史需求曲线) 关键区别: 航空的 EMSR 是"保护多少座位",酒店的 Bid Price 是"设多高的底价" 数据需求: 历史预订曲线(按提前天数的预订累积)
思路: 把定价问题当成一个黑箱函数优化问题 f(price) = revenue at this price 目标: 找到使 f(price) 最大的 price 方法: 1. 用高斯过程 (GP) 建模 f(price) 的后验分布 2. 用 acquisition function (如 EI: Expected Improvement) 选择下一个要尝试的价格 3. 观察实际收入 4. 更新 GP 后验 5. 重复 优点: - 极少的实验次数就能找到近似最优(10-20 次实验) - 天然处理不确定性 - 每次实验都选"最有信息量"的价格点 局限: - 不天然处理上下文(需要扩展为 contextual BO) - 假设函数光滑(价格-收入关系通常是光滑的,OK)
问题: 你给 Lisa 发了 $10 coupon,她来了。但她如果不发 coupon 也会来吗? 核心: 区分 "因为 coupon 来的" vs "反正也会来的" (增量归因) 方法: 1. Propensity Model: P(发 coupon | 客户特征) — 预测谁会被发 coupon 2. Outcome Model: E[收入 | 客户特征, 有/没 coupon] — 预测收入 3. Doubly Robust: 两个模型组合,只要一个对就能得到无偏估计 增量收入 = 实际收入 - 反事实收入(如果没发 coupon 的估计收入) 为什么重要: - 避免"给本来就会来的人发 coupon"(cannibalization 检测) - 能计算 coupon 的真实 ROI 数据需求: 需要有"发了"和"没发"的对照组 → 前期随机实验是必须的
| 算法 | 最少数据量 | 可解释性 | 冷启动 | 适合 Celoria? |
|---|---|---|---|---|
| Neural Contextual Bandit (Uber) | 10 万+ | ❌ 黑箱 | ❌ 差 | 不适合 |
| Gradient Boosting (XGBoost) | 5000+ | ⚠ 中等 (SHAP) | ❌ 差 | 6 个月后可考虑 |
| LinUCB | 1000+ | ✅ 线性系数可解释 | ⚠ 需要冷启动 | Phase 2 候选 |
| Thompson Sampling + Beta | 50+ | ✅ 完全可解释 | ✅ 可注入先验 | Phase 1 推荐 |
| Bayesian Optimization | 10-20 次实验 | ✅ 高斯过程可视化 | ✅ 先验 + 几次实验即可 | Phase 1 推荐(最优面额搜索) |
| Causal Inference (DR) | 200+(需对照组) | ✅ 增量归因 | ❌ 需要 RCT | Phase 2 候选(归因分析) |
| LLM 策略分析 | 几百条摘要 | ✅ 自然语言解释 | ✅ 有世界知识 | Phase 2 辅助(提炼规律) |
架构:
每个 (门店, 时段, 客户类型) 组合 = 一个 bandit 问题
臂 (Actions):
a0 = 不发 coupon
a1 = $5 coupon
a2 = $10 coupon
a3 = $15 coupon
每个臂维护 Beta(α, β):
初始: Beta(1, 1) — 无先验
或: Beta(3, 7) — 先验:"30% 转化率" (来自行业经验)
每次决策:
1. 检测波谷时段
2. 筛选候选客户(到访周期快到 + 偏好该时段)
3. 对每个候选客户,Thompson Sampling 选最优 coupon 面额
4. 发送 → 等待结果 → 更新 Beta
Bayesian Optimization 用于:
搜索最优面额连续值($5-$20 之间的最优点)
每周 1-2 次实验即可收敛
可解释输出:
"Store A 周二下午: $10 coupon 转化率 35% [Beta(18, 33)], 置信度 85%"
"Store B 周二下午: $5 coupon 已足够 [Beta(22, 41)], $10 无显著提升"
数据量达到 ~1000 条实验记录后:
LinUCB 加入上下文特征:
x = [days_since_visit, membership_tier, preferred_day_match,
store_utilization, weather, nearby_store_promo]
模型学习: "在什么上下文下,哪个面额最优"
输出线性系数 → 可解释:
"days_since_visit 的权重最大 → 越久没来的客户越值得发大额 coupon"
Causal Inference 评估增量:
对比: 发了 coupon 的客户 vs 没发的同类客户
计算: 真正因为 coupon 多来的客户占比
目标: 确保不是在给"反正也会来的人"发折扣
跨店约束优化: 目标函数: max Σ revenue(store_i) 约束: store_i 和 store_j 距离 < 3 mile → 不同时发 coupon 方法: 线性规划 (LP) 或整数规划 (IP),不需要 NN LLM 规律提炼(每月一次): 输入: 3-6 个月的实验数据 + Thompson Sampling 的 Beta 参数 输出: 5-10 条可跨店验证的经营规律(带置信度和样本量) 用途: 新店冷启动、知识沉淀、Owner 报告
因为这是我们 Phase 1 最可能用的算法,详细展开实现细节。
// 数据结构: 每个 (store, timeslot, customer_tier) 的每个 coupon 面额
class VoucherBandit {
constructor(actions = [0, 5, 10, 15]) {
this.arms = {}
for (const amount of actions) {
this.arms[amount] = { alpha: 1, beta: 1 } // 均匀先验
}
}
// 用先验知识初始化
setPrior(amount, expectedRate, confidence) {
// expectedRate = 0.3 表示预期 30% 转化率
// confidence = 10 表示"相当于看过 10 个样本"
this.arms[amount] = {
alpha: expectedRate * confidence,
beta: (1 - expectedRate) * confidence
}
}
// Thompson Sampling 选择
selectAction() {
let bestAction = null, bestSample = -1
for (const [amount, params] of Object.entries(this.arms)) {
// 从 Beta 分布采样
const sample = betaSample(params.alpha, params.beta)
if (sample > bestSample) {
bestSample = sample
bestAction = amount
}
}
return { action: bestAction, confidence: this.getConfidence(bestAction) }
}
// 更新后验
update(amount, converted) {
if (converted) {
this.arms[amount].alpha += 1 // 成功 +1
} else {
this.arms[amount].beta += 1 // 失败 +1
}
}
// 当前估计的转化率和置信度
getStats(amount) {
const { alpha, beta } = this.arms[amount]
return {
mean: alpha / (alpha + beta), // 估计转化率
samples: alpha + beta - 2, // 有效样本数
ci95: betaCI(alpha, beta, 0.95), // 95% 置信区间
variance: (alpha * beta) / ((alpha+beta)**2 * (alpha+beta+1))
}
}
}
// 使用
const bandit = new VoucherBandit([0, 5, 10, 15])
bandit.setPrior(10, 0.30, 5) // 先验: $10 coupon 约 30% 转化率
// 每次决策
const { action, confidence } = bandit.selectAction()
// → action = 10, confidence = 0.6 (还不确定)
// 结果回来后
bandit.update(10, true) // $10 coupon, 客户来了
// 100 次实验后
bandit.getStats(10)
// → { mean: 0.35, samples: 100, ci95: [0.26, 0.44] }
100 次实验后的 Beta 参数: Store A, 周二 2-5PM, 金卡会员: $0 (不发): Beta(2, 48) → 转化率 4% ← 不发 coupon 几乎没人来 $5 coupon: Beta(12, 38) → 转化率 24% ← 有效果但不够 $10 coupon: Beta(18, 33) → 转化率 35% ← 最优 $15 coupon: Beta(19, 31) → 转化率 38% ← 比 $10 只好 3%,但成本多 50% → 可解释规律: "金卡会员对 $10 coupon 响应最佳(35%),$15 的边际提升不值得成本" → 置信度: 样本量 50,95% CI [0.23, 0.48] → 可验证: 拿 Store B 的数据交叉验证 这些 Beta 参数本身就是知识的表示形式 — 不是黑箱权重,是可读的"成功次数/失败次数"
你的数据量有多大?
│
├── < 100 个实验数据点
│ → Thompson Sampling (Beta-Bernoulli)
│ → 可注入先验知识,立即开始
│ → 输出: 每个 action 的转化率估计 + 置信区间
│
├── 100 — 1,000 个数据点
│ → Thompson Sampling + 简单分段(按客户类型/时段)
│ → 或 Bayesian Optimization(搜索连续最优价格)
│ → 输出: 分客群/时段的最优策略 + 置信度
│
├── 1,000 — 10,000 个数据点
│ → LinUCB(加入上下文特征)
│ → + Causal Inference(增量归因)
│ → 输出: 特征重要性 + 增量 ROI
│
├── 10,000 — 100,000 个数据点
│ → Gradient Boosting Bandit 或 Neural Bandit
│ → 输出: 精确的个性化定价
│
└── > 100,000 个数据点 (Uber 级别)
→ Full Neural Contextual Bandit
→ 持续在线学习
→ 输出: 实时最优价格(黑箱)