Dynamic Pricing Algorithms

Contextual Bandits, Bayesian Methods & Small Data Strategies for Service Industry Yield Management

2026-03-22 · 为 Celoria Yield Management 功能调研

1. Multi-Armed Bandit (MAB) — 基础概念

所有 bandit 算法的起点。想象你面前有 K 台老虎机,每台的期望收益不同但你不知道具体值。你要在"尝试新机器"(exploration)和"用已知最好的"(exploitation)之间平衡。

目标:最小化 Regret = T × μ* − Σ r(t)
T = 总决策次数, μ* = 最优臂的期望收益, r(t) = 第 t 次获得的收益

1.1 三种经典策略

Epsilon-Greedy 最简单
每次决策:
  以概率 ε (如 10%) → 随机选一个臂(exploration)
  以概率 1-ε (如 90%) → 选当前最优臂(exploitation)

优点: 实现极简,3 行代码
缺点: exploration 完全随机,不智能;ε 难调
适用: 快速原型,数据量大时效果还行
UCB (Upper Confidence Bound) 乐观面对不确定性
每次选择得分最高的臂:
  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 环境下需要变体)
Thompson Sampling 贝叶斯方法 推荐用于小数据
为每个臂维护一个概率分布(后验分布):

初始化: 每个臂 a → Beta(1, 1)  (均匀先验)

每次决策:
  1. 为每个臂 a 从 Beta(α_a, β_a) 采样一个值 θ_a
  2. 选择 θ 最大的臂
  3. 观察结果(成功/失败)
  4. 更新后验:
     成功 → α_a += 1
     失败 → β_a += 1

核心思想: 不确定的臂 → 分布宽 → 采样可能很高 → 自然被探索
         确定性高的臂 → 分布窄 → 采样稳定 → 如果均值高就被利用
为什么 Thompson Sampling 最适合小数据?

2. Contextual Bandit — Uber 用的核心算法

普通 bandit 的问题:每台老虎机的收益是固定的。但在定价场景,同一个价格在不同上下文下效果不同(周二 vs 周六、VIP vs 新客)。Contextual Bandit 在每次决策时加入上下文特征

普通 Bandit: 选择 action a → 获得 reward r
Contextual Bandit: 观察 context x → 选择 action a → 获得 reward r

2.1 工作原理

每次决策:
  1. 观察上下文 x = { 星期几, 时段, 客户类型, 门店, 当前空位率, ... }
  2. 对每个可能的动作 a (如: 不发 / $5 coupon / $10 coupon / $15 coupon)
     预测 E[reward | x, a]
  3. 选择预期 reward 最高的动作(+ 一些 exploration)
  4. 执行,观察实际 reward(客户来了没、花了多少)
  5. 更新模型

关键区别:
  普通 Bandit:  学 "哪个价格最好"(全局最优)
  Contextual:  学 "在什么情况下哪个价格最好"(条件最优)

2.2 常见实现方式

方法原理数据需求适用场景
LinUCB 假设 reward 是 context 的线性函数 + UCB exploration 中等(几千条) 特征维度低,关系近似线性
Neural Contextual Bandit 用神经网络建模 context → reward 的关系 大(几万条+) 特征维度高,非线性关系(Uber 用的)
Thompson Sampling + 线性模型 贝叶斯线性回归 + Thompson 采样 小(几百条即可启动) 小数据、需要快速收敛
Decision Tree Bandit 用决策树划分 context 空间,每个叶子节点是一个 bandit 中等 需要可解释性

2.3 Uber 的具体实现

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 → 统计显著性小时级达到

3. 其他动态定价算法

3.1 航空业: EMSR-b (Expected Marginal Seat Revenue)

EMSR-b 航空经典
问题: 180 个座位,3 个舱位(经济 $200, 弹性 $400, 商务 $800)
     经济能现在卖,商务可能明天才有人买。保留多少给商务?

EMSR-b 算法:
  对高价舱的需求建模为正态分布 D ~ N(μ, σ^2)

  保护 n 个座位给高价舱,当且仅当:
    P(D > n) × fare_high > fare_low

  即: "多保护一个高价座的期望收入" > "现在卖一个低价座的确定收入"

数据需求: 需要预估高价舱需求的均值和方差(通常用历史同期数据)
优点: 简单、可解释、几十年验证有效
局限: 假设需求是正态分布;不处理 network 效应(跨航线蚕食)

3.2 酒店业: Bid Price Control

Bid Price 酒店标准
每个房间每晚都有一个 "bid price"(底价):
  只有当客人愿意付的价格 > bid price 时才接受预订

Bid price 的计算:
  = 未来卖出这个房间的期望收入
  = f(剩余房间数, 距入住日天数, 历史需求曲线)

关键区别: 航空的 EMSR 是"保护多少座位",酒店的 Bid Price 是"设多高的底价"
数据需求: 历史预订曲线(按提前天数的预订累积)

3.3 电商: Bayesian Optimization for Pricing

Bayesian Optimization 适合小数据
思路: 把定价问题当成一个黑箱函数优化问题
  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)

3.4 Causal Inference (因果推断)

Doubly Robust Estimator 反事实推理
问题: 你给 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

数据需求: 需要有"发了"和"没发"的对照组 → 前期随机实验是必须的

4. 小数据下的最佳算法选择

Celoria 的数据约束:

4.1 算法适用性对比

算法 最少数据量 可解释性 冷启动 适合 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 辅助(提炼规律)

4.2 推荐的 Celoria 技术路线

Phase 1 (Day 1 — 月 3): Thompson Sampling + Bayesian Optimization
架构:
  每个 (门店, 时段, 客户类型) 组合 = 一个 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 无显著提升"
Phase 2 (月 3 — 月 6): LinUCB + Causal Inference
数据量达到 ~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 多来的客户占比
  目标: 确保不是在给"反正也会来的人"发折扣
Phase 3 (月 6+): 跨店网络优化 + LLM 规律提炼
跨店约束优化:
  目标函数: max Σ revenue(store_i)
  约束: store_i 和 store_j 距离 < 3 mile → 不同时发 coupon
  方法: 线性规划 (LP) 或整数规划 (IP),不需要 NN

LLM 规律提炼(每月一次):
  输入: 3-6 个月的实验数据 + Thompson Sampling 的 Beta 参数
  输出: 5-10 条可跨店验证的经营规律(带置信度和样本量)
  用途: 新店冷启动、知识沉淀、Owner 报告

5. Thompson Sampling 详细实现

因为这是我们 Phase 1 最可能用的算法,详细展开实现细节。

5.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] }

5.2 为什么 Thompson Sampling 自然产生可解释的规律

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 参数本身就是知识的表示形式 — 不是黑箱权重,是可读的"成功次数/失败次数"

6. 算法选择决策树

你的数据量有多大?
│
├── < 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
    → 持续在线学习
    → 输出: 实时最优价格(黑箱)

7. 对 Celoria 的建议总结

Phase 1 技术栈:
Phase 2 技术栈:
不要做的事: