Lecture 15:Dynamic pricing — 企業の動学的価格設定

計量経済学II

作者

Kei Ikegami

重要この講義で押さえたいこと
  • 消費者が将来の値下げを待つなら、企業はその待機を見込んで価格を決める必要がある。2期間の例で、待機を無視した価格案と比較する。
  • 顧客の好みに応じた価格と、時間・在庫に応じた価格を区別する。両者は組み合わせられるが、必要なデータやモデルは異なる。
  • 残り販売期間と在庫を状態として、企業の価格設定をBellman方程式で表す。今売る利益と、在庫を残す価値を比較する。
  • 需要を推定してから価格を選び、その価格の収益を評価する。需要の推定誤差が意思決定にどう伝わるかを確認する。
  • 市場全体の需要、個人別の予測と施策、時間を通じた行動を、これまでの講義との関係から整理する。

前回とのつながり・今回のゴール

第14回では、同じブランドを繰り返し選ぶ理由を、固定的な好みと状態依存に分けて考えた。さらに、セール時に買いだめする消費者をモデル化し、今の購入が増えても、その分だけ将来の購入が減る場合があることを確認した。

今回は、その消費者に商品を売る企業の側を考える。消費者が来月の値下げを待つなら、企業が今と来月の価格をそれぞれ独立に決めるわけにはいかない。また、企業に在庫と販売期限があるなら、今安く売るか、後で高く買う人のために残すかという判断も必要になる。

まず、価格を2期間分あらかじめ示す簡単な例で、消費者の待機を踏まえた値付けを計算する。その後、残り販売期間と在庫を状態とする企業の動的計画問題を解く。後者では仕組みを分けて理解するため、各期に新しい顧客が来て、その場で購入を決めると仮定する。消費者の待機と企業の在庫制約を一度に入れたモデルではないことに注意しよう。

第10回のターゲティングでは、個人ごとの需要から、その人に行う施策を選んだ。今回はそこに時間と状態を加える。推定した需要を使って価格を決め、その価格で得られる収益を確かめるところまでがゴールである。

1. 「dynamic」の交通整理

「dynamic pricing」という言葉は、いくつか異なる価格設定を指して使われる。ここでは、次の3つを区別しておこう。

  1. 時間帯・需給で価格が変わる:電気料金のピーク時間帯課金、ライドシェアのサージ、深夜のタクシー割増。
  2. 販売期限と在庫で価格が変わる:航空券やホテルの価格が、出発日・チェックイン日が近づくにつれて、また空席数に応じて変わる。revenue management (RM) と呼ばれる領域。
  3. 顧客ごとに価格が変わる:会員ランクによる割引、購買履歴に基づくクーポン配布、過去の閲覧履歴を使ったパーソナライズ価格。

この3つは共通の需要推定を使い得るが、主に解く状態・識別・制約が異なる。(1)(2)は「同じ商品を、同じ客に対して、状況に応じて違う価格で売る」問題であり、(3)は「同じ状況で、違う客に対して違う価格で売る」問題だ。ニュースで「AIが個人の購買履歴から価格を決めている」という話と「航空券が満席に近づくと値上がりする」という話は、企業がやっていることとしては別物なのに、同じ「ダイナミックプライシング」という言葉で語られてしまう。

この講義では用語を曖昧にしないため、(1)(2)を dynamic pricing、(3)を personalized pricing と呼び分ける。両者を同時に実装することはできるが、同じ概念ではない。

企業の価格「ルール」が、時間・在庫・市場需要などの運用状態の関数になっている。 これをこの講義では dynamic pricing と呼ぶ。顧客の属性・履歴に応じる personalized pricing は隣接領域だが、区別する。

今日は主に(2)を扱う。(3)は第10回の個人別需要とクーポン配布に対応する。(1)のピーク時料金とサージ価格は、本文の後の補足で取り上げる。

ノートビジネスの現場で:「うちもダイナミックプライシングを」と言われたら

「在庫が余っているので値下げしたい」と「価格に敏感な顧客に割引を出したい」では、知りたい需要が異なる。前者では、残り期間と在庫に応じて、今売る価値と待つ価値を比べる。後者では、第9・10回のように、個人の履歴で条件づけた需要が必要になる。まず何に応じて価格を変えるのかを決めると、用意すべきデータも明確になる。

2. 静学ベンチマーク:逆弾力性ルールの復習

dynamic pricingを理解する前に、まず「動かない価格」つまり静学的な独占価格づけを復習しておく。すべてはここが出発点になる。

第2回で学んだように、企業が価格 \(p\) の財を1つだけ売っている独占企業だとする。需要量を \(q(p)\)、限界費用を \(mc\) とすると、利潤は

\[ \pi(p) = (p - mc)\, q(p) \]

これを \(p\) について最大化する一階条件を整理すると、おなじみの逆弾力性ルール(Lerner指数)が出てくる。

\[ \frac{p - mc}{p} = \frac{1}{|\eta(p)|}, \qquad \eta(p) = \frac{dq}{dp}\frac{p}{q} \]

内点の最適価格では、マージン率は需要の価格弾力性の絶対値の逆数に等しくなる。したがって、需要が価格にどう反応するかを知らなければ、この価格は計算できない。第2〜4回で学んだ需要推定が、ここで価格設定に結びつく。単純logitの自己価格弾力性は \(\eta_{jj} = -\alpha p_j(1-s_j)\) だった。

逆弾力性ルールは需要側の静学ベンチマークである。Revenue managementでは、これに状態ごとの在庫の機会費用(bid price)が加わる。surge pricingを二面市場として考えるなら、さらにドライバーの供給反応、空間的なマッチング摩擦、企業利潤か社会厚生かという目的関数も必要になる。したがってどちらも需要弾力性を無視できないが、静学的な逆弾力性ルールを適用し直すだけではない。

Rシミュ1:logit需要のもとでの最適価格

第2回のロジット需要を思い出そう。財1つ(買う/買わないの2択、outside optionが \(j=0\))に対する購買確率を

\[ \lambda(p) = \frac{\exp(\theta_0 - \alpha p)}{1 + \exp(\theta_0 - \alpha p)} \]

とする。市場規模を \(M\)、限界費用を \(mc = 0\) とすると(航空券1席のように限界費用がほぼゼロに近いケースを念頭に置く)、利潤は \(\pi(p) = M \cdot \lambda(p) \cdot p\) になる。これを価格についてプロットし、最適価格を数値的に探す。

theta0 <- 6.0
alpha  <- 0.045

lambda_p <- function(p, theta0, alpha) {
  u <- theta0 - alpha * p
  1 / (1 + exp(-u))
}

p_grid <- seq(1, 350, length.out = 400)
profit_grid <- lambda_p(p_grid, theta0, alpha) * p_grid

static_df <- data.frame(price = p_grid, profit = profit_grid)
p_star_static <- p_grid[which.max(profit_grid)]

ggplot(static_df, aes(x = price, y = profit)) +
  geom_line(color = "steelblue", linewidth = 1) +
  geom_vline(xintercept = p_star_static, linetype = "dashed", color = "firebrick") +
  annotate("text", x = p_star_static + 30, y = max(profit_grid) * 0.9,
           label = paste0("p* = ", round(p_star_static, 1)), color = "firebrick") +
  labs(x = "価格 p", y = "期待収益(1人あたり)",
       title = "logit需要のもとでの利潤カーブと最適価格")

この単一商品・二値logit・一定限界費用の設定では、\(p\ge mc\) 上の利潤は1つの山を持つ。ただし、これはlogit対数尤度の凹性とは別の問題である。複数商品、非線形費用、制約を加えれば利潤が大域的に凹とは限らないので、グリッド上の利潤カーブと一階条件の両方を確認する。

\(\pi(p) = M\lambda(p)(p-mc)\) を \(p\) について微分してゼロと置くと

\[ \lambda'(p)(p-mc) + \lambda(p) = 0 \]

ロジットでは \(\lambda'(p) = -\alpha\lambda(p)(1-\lambda(p))\) なので、これを代入すると

\[ p^* = mc + \frac{1}{\alpha(1 - \lambda(p^*))} \]

という固定点方程式になる。初等関数だけでは閉形式にならないが、Lambert \(W\) 関数を使えば

\[ p^*=mc+\frac{1+W\!\left(\exp(\theta_0-\alpha mc-1)\right)}{\alpha} \]

とも書ける。本講義では特殊関数に依存せず、利潤を直接最大化する。なお本文の数値例は \(mc=0\) なので、最初の式へ代入すればよい。

この静学の1点、「弾力性を知らないと価格は決められない」が、今日の話全体の土台になる。この後出てくる時間・在庫・需給の状態がどれだけ複雑になっても、やっていることは「その状態のもとでの実効的な弾力性・機会費用を計算し、逆弾力性ルールを適用する」ことの繰り返しでしかない。

3. 消費者が値下げを待つとき、価格はどう変わるか

新しいゲームを発売し、2期間だけ販売するとしよう。消費者は一人1本まで購入でき、企業の在庫は十分ある。企業は価格\(p_1,p_2\)を最初に公表し、その価格を守ると約束する。消費者は両方の価格を知っている。この約束ができる場合のモデルなので、2期目に企業が価格を変更するゲームとは区別しておこう。

消費者\(i\)の評価額を\(v_i\)とし、\(v_i\)は0から20の一様分布に従うとする。価格と評価額の単位は千円である。1期目に買う効用は\(v_i-p_1\)、待って2期目に買う効用は\(\beta_c(v_i-p_2)\)、買わない効用は0とする。\(\beta_c=0.8\)は消費者の割引因子である。第13回の買い替え問題を2期間に縮めた例だが、今回は行動固有の効用ショックを省き、評価額の異質性だけを置く。

消費者は、

\[ \max\{v_i-p_1,\;\beta_c(v_i-p_2),\;0\} \]

を比べる。評価額が高い人でも、値下げ幅が大きく、待つことの不利益が小さければ、発売時には買わない。したがって、1期目の需要は\(p_1\)だけでなく\(p_2\)にも依存する。

企業の限界費用は0とし、この短い販売期間では企業の割引は省く。企業は

\[ \Pi(p_1,p_2)=p_1\Pr(\text{1期目に購入})+p_2\Pr(\text{2期目に購入}) \]

を最大にする価格の組を選ぶ。ここでは評価額の分布を既知として、価格の計算に集中する。実証分析なら、この分布や待つことの費用を需要データから推定する必要がある。将来価格の予想が価格判断に関わるという論点は、Su (2007)でも扱われている。以下はその論文の再現ではなく、この講義用の単純な数値例である。

values_wait <- (seq_len(5000) - 0.5) / 5000 * 20
beta_consumer <- 0.8
waiting_outcome <- function(p1, p2, forward = TRUE) {
  now <- values_wait - p1
  later <- beta_consumer * (values_wait - p2)
  buy_now <- if (forward) now >= pmax(0, later) else now >= 0
  buy_later <- !buy_now & values_wait >= p2
  c(share_now = mean(buy_now), share_later = mean(buy_later),
    profit = p1 * mean(buy_now) + p2 * mean(buy_later))
}
price_pairs <- expand.grid(p1 = seq(4, 16, by = 0.25),
                           p2 = seq(4, 16, by = 0.25))
price_pairs$profit_forward <- mapply(function(a, b) waiting_outcome(a, b)[3],
                                    price_pairs$p1, price_pairs$p2)
price_pairs$profit_myopic <- mapply(function(a, b) waiting_outcome(a, b, FALSE)[3],
                                   price_pairs$p1, price_pairs$p2)
wait_best <- price_pairs[which.max(price_pairs$profit_forward), ]
myopic_best <- price_pairs[which.max(price_pairs$profit_myopic), ]
price_rules <- data.frame(rule = c("同じ価格を維持", "待機を無視して選ぶ", "待機を考慮して選ぶ"),
                          p1 = c(10, myopic_best$p1, wait_best$p1),
                          p2 = c(10, myopic_best$p2, wait_best$p2))
wait_results <- t(mapply(waiting_outcome, price_rules$p1, price_rules$p2))
wait_table <- cbind(price_rules, wait_results)
kable(wait_table, digits = 3, row.names = FALSE,
      col.names = c("価格の選び方", "1期目価格", "2期目価格", "1期目購入率", "2期目購入率", "顧客1人あたり利益"),
      caption = "すべての価格案を、将来の価格を知っている同じ消費者に対して評価する(価格・利益:千円)")
すべての価格案を、将来の価格を知っている同じ消費者に対して評価する(価格・利益:千円)
価格の選び方 1期目価格 2期目価格 1期目購入率 2期目購入率 顧客1人あたり利益
同じ価格を維持 10.00 10.00 0.500 0.000 5.000
待機を無視して選ぶ 13.25 6.75 0.000 0.663 4.473
待機を考慮して選ぶ 10.50 9.50 0.275 0.250 5.263

待機を無視したモデルは、まず高い価格で評価額の高い人に売り、その後大きく値下げすればよいと考える。しかし、その予定を知った消費者が待つと、想定していた1期目の売上を得られない。表では、どの価格案も実際には将来を考える消費者に対して評価している。価格案を作るときの予測と、その案を実施したときの結果を分けた比較である。

ggplot(price_pairs, aes(p1, p2, fill = profit_forward)) +
  geom_tile() +
  geom_point(data = price_rules[-1, ], aes(p1, p2, shape = rule),
             inherit.aes = FALSE, size = 3, color = "black", fill = "white") +
  scale_shape_manual(values = c(21, 24)) +
  scale_fill_viridis_c(breaks = c(3.5, 4, 4.5, 5)) +
  labs(x = "1期目価格(千円)", y = "2期目価格(千円)", fill = "顧客1人あたり利益(千円)", shape = NULL) +
  guides(fill = guide_colorbar(barwidth = grid::unit(5, "cm"),
                             barheight = grid::unit(0.35, "cm"), order = 1),
         shape = guide_legend(nrow = 1, order = 2)) +
  theme(legend.position = "bottom", legend.box = "vertical")
図 1: 将来の値下げを待つ消費者に対する利益。候補価格は0.25千円刻み。

ここでの最適価格は、評価額の分布、割引因子、価格を守る約束ができることに依存する。常に値下げ幅を小さくすればよい、という一般則ではない。第14回の買いだめと同じく、価格の変更が購入の時点をどう変えるかを需要モデルに入れる必要がある、というのがこの例で確認したい点である。

4. Revenue management:残り期間と在庫で変わる価格

いよいよ今日の核心に入る。航空券やホテルの価格が、なぜ「予約が進むにつれて」「出発日が近づくにつれて」動くのかを、動的計画法で正面から解く。

設定

ある航空券1路線を考える。販売開始からの残り日数を \(t\)(\(t=T\) が今日、\(t=0\) が出発日=販売終了)、残り座席数を \(c\)(\(c=0\) なら完売)とする。毎期、確率 \(\bar\lambda\)(到着確率)で1人の客が訪れる。到着した客は、提示された価格 \(p\) に対して、第2〜4回と同じ形のロジットで購買を決める。

\[ \Pr(\text{購入} \mid p, \text{到着}) = \frac{\exp(\theta_0 - \alpha p)}{1 + \exp(\theta_0 - \alpha p)} \]

これに到着確率をかけたものを、この講義では1期あたりの購買確率 \(\lambda(p)\) と呼ぶ。

\[ \lambda(p) = \bar\lambda \cdot \frac{\exp(\theta_0 - \alpha p)}{1 + \exp(\theta_0 - \alpha p)} \]

「客が来るかどうか」と「来た客が買うかどうか」を分けて書いているのは、実務データとの対応が良いからだ(POSやWebログでは「訪問数」と「訪問あたり購買率」を別々に観測できることが多い)。

第13回で断った通り、この3回では効用パラメータに \(\theta\) を使い、割引因子には \(\beta\) を予約している(需要推定の回では \(\beta\) は製品属性への選好パラメータだったが、動学の慣習に合わせて割引因子に転用している)。今日の設定では企業は近視眼的ではなく将来の座席の価値まで織り込んで今日の価格を決めるので、実質的に割引因子 \(\beta=1\)(将来の座席が売れる価値を割り引かない)の動学最適化を解いていると考えてよい。長期在庫や与信のように将来価値を割り引く必要がある文脈では \(\beta<1\) を入れるが、今日の航空券・ホテルの例(販売期限が数十日程度)ではこの単純化で十分である。

状態は「残り期間 \(t\)」と「残り在庫 \(c\)」の組 \((t,c)\)、行動は「価格 \(p\)」。第13回の記法に合わせれば、状態 \(x = (t,c)\)、行動 \(a = p\) にあたる。

Bellman方程式

企業の問題は、残り期間 \(t\)・残り在庫 \(c\) のもとで、今期の価格 \(p\) をいくらにすれば、今期の期待収益と将来の期待収益の合計が最大になるか、である。第13回のBellman方程式の形を思い出そう。今日の設定に翻訳すると

\[ V(t, c) = \max_{p} \Big\{ \lambda(p)\,\big[p + V(t-1, c-1)\big] + \big(1-\lambda(p)\big)\,V(t-1, c) \Big\} \]

解釈は単純だ。今期、確率 \(\lambda(p)\) で客が来て \(p\) 円で買ってくれる。そのときは席が1つ減るので、続きは \(V(t-1, c-1)\)。確率 \(1-\lambda(p)\) で客が来ないか、来ても買わない。そのときは席がそのまま残るので、続きは \(V(t-1,c)\)。境界条件は「在庫が尽きたら以後の収益はゼロ」(\(V(t,0)=0\))、「期限が来たら以後の収益はゼロ」(\(V(0,c)=0\))。

第13回と同じく、今の収益と行動後の将来価値を足し合わせるBellman方程式である。ただし、第13回は無限期間の問題を価値関数反復で解いた。今回は有限期間なので、販売期限での価値をゼロと置き、そこから後ろ向きに解ける。状態は残り期間と在庫の2次元、行動は価格となる。

初めてこの式を見ると4つの添字が一度に出てくるので、1期の順序を先に固定しておこう。

順序 その期に起きること Bellman式で対応する部分
1 期首に状態 \((t,c)\) を観察する \(V(t,c)\)
2 価格 \(p\) を提示する \(\max_p\)
3a 確率 \(\lambda(p)\) で販売し、収益 \(p\) を得る \(p+V(t-1,c-1)\)
3b 確率 \(1-\lambda(p)\) で販売しない \(V(t-1,c)\)

この「状態を見る → 価格を決める → 売れたかで次状態が分岐する」という順序が、後でbid priceの時間添字を判断する基準になる。

警告計算上の落とし穴:Rの1始まり索引でのズレに要注意

ここでは、Bellman方程式の右辺をそのまま計算して最大化する。Rの配列は1から始まる一方、残り期間と在庫の状態には0が含まれるため、数式とコードの添字が1つずれる。例えば、状態 \((t,c)\) の価値は V[t + 1, c + 1] に保存する。販売後の \(V(t-1,c-1)\) と非販売時の \(V(t-1,c)\) を参照するときも、この対応を確認しよう。後のコードでは、動的価格の期待収益が最適な固定価格の期待収益以上になることも確認する。

Rシミュ2(i):backward inductionで解く

残り期間がゼロの \(t=0\) から出発し、\(t\) を1つずつ増やしながら \(V(t,\cdot)\) を埋めていく。各 \((t,c)\) で、価格のグリッドを全部試して一番良い価格を選ぶ。

# パラメータ設定:航空券1路線のイメージ
theta0      <- 6.0     # 価格ゼロでの基準効用
alpha       <- 0.045   # 価格係数(第2回以来の記法、alpha > 0)
lambda_bar  <- 0.95    # 1期あたりの客の到着確率
T_period    <- 60      # 販売期間(残り日数の最大値)
C_capacity  <- 30      # 座席数

p_grid <- seq(1, 350, length.out = 300)

purchase_prob <- function(p, theta0, alpha, lambda_bar) {
  u <- theta0 - alpha * p
  lambda_bar * (1 / (1 + exp(-u)))
}
solve_dp <- function(T_period, C_capacity, theta0, alpha, lambda_bar, p_grid) {
  # 行:t=0..T, 列:c=0..C。R索引はt+1, c+1にずれることに注意。
  V      <- matrix(0, nrow = T_period + 1, ncol = C_capacity + 1)
  pstar  <- matrix(NA_real_, nrow = T_period + 1, ncol = C_capacity + 1)
  lamstar <- matrix(0, nrow = T_period + 1, ncol = C_capacity + 1)

  lam_grid <- purchase_prob(p_grid, theta0, alpha, lambda_bar)  # 価格に依存、(t,c)に依らないので外で計算

  for (t in 1:T_period) {
    for (c in 1:C_capacity) {
      # Bellman方程式の右辺をそのまま評価(bid priceへの簡略化はしない)
      # V[t, c]     = V(t-1, c-1) (購入時:1期進み、在庫が1つ減る)
      # V[t, c + 1] = V(t-1, c)   (非購入時:1期進み、在庫は変わらない)
      obj <- lam_grid * (p_grid + V[t, c]) + (1 - lam_grid) * V[t, c + 1]
      best_idx <- which.max(obj)
      V[t + 1, c + 1]      <- obj[best_idx]
      pstar[t + 1, c + 1]  <- p_grid[best_idx]
      lamstar[t + 1, c + 1] <- lam_grid[best_idx]
    }
    V[t + 1, 1] <- 0  # 在庫ゼロ(c=0)なら収益なし
  }
  list(V = V, pstar = pstar, lamstar = lamstar)
}
警告Rの1始まり索引と、DPの0始まり状態の対応関係

DPの状態は \(t=0,\dots,T\)、\(c=0,\dots,C\) と0始まりで書くのが自然だが、Rの行列は1始まりの索引しか使えない。このノートでは「行 t+1 が状態 \(t\)」「列 c+1 が状態 \(c\)」という対応で統一する。この対応のもとでは、ループ変数t, c(1始まりで回る)に対して、状態 \((t-1,c-1)\)(購入時の1期前)は V[t, c]、状態 \((t-1,c)\)(非購入時の1期前)は V[t, c + 1] と書かなければならない。計算結果の代入先は状態 \((t,c)\) を指す V[t + 1, c + 1] になる。上のコードで V[t, c]・V[t, c + 1]・V[t + 1, c + 1] という3つの似た添字が並んでいるのはこのズレのせいであり、バグではない。base Rでこの手のDPを書くときは、常に「今書いている添字が、数式のどの \(t,c\) に対応するか」を紙に書いてから実装することを強く勧める。これを怠ると、今日のこのノートの検証段階で実際に踏んだような「非購入時の項のつもりで書いた添字が、実は計算中の状態自身を指してしまっていた」というバグに悩まされる。

dp_result <- solve_dp(T_period, C_capacity, theta0, alpha, lambda_bar, p_grid)
V_mat      <- dp_result$V
pstar_mat  <- dp_result$pstar
lamstar_mat <- dp_result$lamstar

cat("期待総収益 V(T=60, C=30):", round(V_mat[T_period + 1, C_capacity + 1], 1), "\n")
期待総収益 V(T=60, C=30): 3833.2 
cat("残り60日・満席時の最適価格:", round(pstar_mat[T_period + 1, C_capacity + 1], 1), "\n")
残り60日・満席時の最適価格: 130.6 
cat("残り60日・残り1席時の最適価格:", round(pstar_mat[T_period + 1, 2], 1), "\n")
残り60日・残り1席時の最適価格: 221.6 
cat("出発前日・満席時の最適価格:", round(pstar_mat[2, C_capacity + 1], 1), "\n")
出発前日・満席時の最適価格: 103.7 

この実行で期待総収益は 3833.2、残り60日・満席時の価格は 130.6、残り60日・残り1席では 221.6、出発前日・満席では 103.7 である。絶対額は \(\theta_0,\alpha\) と価格グリッドに依存するので、状態に沿った相対的な動きを読む。

Rシミュ2(ii):最適価格のヒートマップ

この \((t,c)\) に対する最適価格 \(p^*(t,c)\) を、残り期間×残り座席の全ての組み合わせについて図示する。

heat_df <- expand.grid(t = 1:T_period, c = 1:C_capacity) %>%
  rowwise() %>%
  mutate(price = pstar_mat[t + 1, c + 1]) %>%
  ungroup()

ggplot(heat_df, aes(x = t, y = c, fill = price)) +
  geom_tile() +
  scale_fill_viridis_c(name = "最適価格", option = "magma") +
  scale_x_continuous(breaks = seq(0, 60, 10)) +
  labs(x = "残り期間(日)", y = "残り座席数",
       title = "航空券の最適価格ヒートマップ",
       subtitle = "横軸が0(出発直前)に近く、縦軸が下(在庫僅少)に近いほど、価格の挙動が対照的になる")

最適価格 p*(t,c) のヒートマップ:残り期間×残り座席

図の見方はこうだ。横軸を右へ行くほど残り期間が長く、縦軸を上へ行くほど残り座席が多い。したがって「期間が長く在庫が少ない」は右下、「期限直前で在庫が多い」は左上である。前者では将来の高WTP客へ座席を残す価値が高く、後者では売れ残りを避ける圧力が強い。

Rシミュ2(iii):bid price(席1つの機会費用)

RM実務でよく使われる概念にbid priceがある。状態 \((t,c)\) で今1席売ると、次期は \((t-1,c-1)\)、売らなければ \((t-1,c)\) へ進む。したがって今期の販売判断に入る機会費用は

\[ b(t,c)=V(t-1,c)-V(t-1,c-1) \]

である。これは「売らずに \(c\) 席を次期へ持ち越す価値」と「今売って \(c-1\) 席を次期へ持ち越す価値」の差だ。一方、\(V(t,c)-V(t,c-1)\) は同じ期首時点で追加1席を持つ価値であり、有用な限界在庫価値だが、今期販売のBellman式へ直接入るbid priceとは時間添字が違う。

Bellman方程式をこのbid priceで書き直すと

\[ V(t,c)=V(t-1,c)+\max_p\lambda(p)\{p-b(t,c)\}. \]

条件付き購買確率を \(s(p)=\operatorname{logit}^{-1}(\theta_0-\alpha p)\)、\(\lambda(p)=\bar\lambda s(p)\) とすれば、内部解の一階条件は

\[ p^*(t,c)=b(t,c)+\frac{1}{\alpha\{1-s(p^*(t,c))\}}. \]

つまり価格はbid priceに等しいのでも、静学価格へ単純にbid priceを足したものでもない。bid priceを「限界費用」の位置へ入れたlogit markupの固定点である。

bid_price_df <- expand.grid(t = 1:T_period, c = 1:C_capacity) %>%
  rowwise() %>%
  mutate(bid_price = V_mat[t, c + 1] - V_mat[t, c]) %>%
  ungroup()

ggplot(bid_price_df, aes(x = t, y = c, fill = bid_price)) +
  geom_tile() +
  scale_fill_viridis_c(name = "bid price", option = "viridis") +
  scale_x_continuous(breaks = seq(0, 60, 10)) +
  labs(x = "残り期間(日)", y = "残り座席数",
       title = "座席1つの機会費用(bid price)のヒートマップ")

bid priceが高い状態では、「今安く売ると、将来高く売る機会を失う」コストが大きい。最適価格は、この機会費用を限界費用として扱い、その上に需要弾力性で決まるmarkupを載せる。上式は連続価格の内部解であり、実装では価格グリッド・上下限制約により境界解になる場合がある。

Rシミュ2(iv):予約パスのシミュレーション

実際にこの価格政策のもとで、日々どのように価格が動いていくかを、いくつかのシミュレーションパスで見てみる。

simulate_booking_path <- function(seed, T_period, C_capacity, pstar_mat, lamstar_mat) {
  set.seed(seed)
  c_remain <- C_capacity
  t_remain <- T_period
  path <- data.frame(day = integer(0), t_remain = integer(0), c_remain = integer(0), price = numeric(0))
  day <- 0
  while (t_remain >= 1 && c_remain >= 1) {
    day <- day + 1
    p   <- pstar_mat[t_remain + 1, c_remain + 1]
    lam <- lamstar_mat[t_remain + 1, c_remain + 1]
    path <- rbind(path, data.frame(day = day, t_remain = t_remain, c_remain = c_remain, price = p))
    bought <- runif(1) < lam
    if (bought) c_remain <- c_remain - 1
    t_remain <- t_remain - 1
  }
  path
}

paths_list <- lapply(1:4, function(i) {
  p <- simulate_booking_path(30000 + i, T_period, C_capacity, pstar_mat, lamstar_mat)
  p$path_id <- factor(i)
  p
})
paths_df <- bind_rows(paths_list)

ggplot(paths_df, aes(x = day, y = price, color = path_id)) +
  geom_line(linewidth = 0.9) +
  geom_point(size = 1.2) +
  labs(x = "販売開始からの日数", y = "提示価格",
       title = "予約パスに沿った価格の推移(シミュレーション4本)",
       color = "パスID")

各パスで、売れ行きが良ければ在庫が早く減って価格が早めに上がり、売れ行きが悪ければ価格が抑えられたまま推移する。同じ価格ルール \(p^*(t,c)\) から出発しても、実現する需要のばらつき次第で価格の軌道は変わる。これがブッキングカーブ(累積販売数の推移)を毎日眺めながら価格を微調整する、RM実務の日常の姿そのものだ。

Rシミュ2(v):最適固定価格 vs 動的価格

では、この動的な価格づけは、期間中ずっと同じ価格を使い続ける「最適固定価格」と比べてどれだけ得なのか。固定価格 \(p\) を1つ選んで使い続けたときの期待収益も、同じ形の再帰式で評価できる。

\[ W_p(t,c) = \lambda(p)\big[p + W_p(t-1,c-1)\big] + (1-\lambda(p))\,W_p(t-1,c) \]

これは価格を最適化せず、外から与えられた固定の \(p\) について評価するだけなので、\(\max_p\) が無い分だけ単純だ。

evaluate_fixed_price <- function(p, T_period, C_capacity, theta0, alpha, lambda_bar) {
  lam <- purchase_prob(p, theta0, alpha, lambda_bar)  # スカラー
  W <- matrix(0, nrow = T_period + 1, ncol = C_capacity + 1)
  for (t in 1:T_period) {
    for (c in 1:C_capacity) {
      W[t + 1, c + 1] <- lam * (p + W[t, c]) + (1 - lam) * W[t, c + 1]
    }
    W[t + 1, 1] <- 0
  }
  W[T_period + 1, C_capacity + 1]
}

fixed_p_grid <- seq(1, 350, length.out = 150)
fixed_revenues <- sapply(fixed_p_grid, evaluate_fixed_price,
                          T_period = T_period, C_capacity = C_capacity,
                          theta0 = theta0, alpha = alpha, lambda_bar = lambda_bar)

best_fixed_idx <- which.max(fixed_revenues)
best_fixed_p   <- fixed_p_grid[best_fixed_idx]
best_fixed_rev <- fixed_revenues[best_fixed_idx]

dynamic_rev <- V_mat[T_period + 1, C_capacity + 1]
lift_pct <- 100 * (dynamic_rev - best_fixed_rev) / best_fixed_rev

comparison_df <- data.frame(
  方式 = c("最適固定価格", "動的価格(DP最適)"),
  価格の目安 = c(round(best_fixed_p, 1), "状態に応じて変動"),
  期待収益 = c(round(best_fixed_rev, 1), round(dynamic_rev, 1))
)
kable(comparison_df, caption = "固定価格 vs 動的価格の期待収益比較")
固定価格 vs 動的価格の期待収益比較
方式 価格の目安 期待収益
最適固定価格 129.8 3739.7
動的価格(DP最適) 状態に応じて変動 3833.2
cat("\n動的価格による収益リフト:", round(lift_pct, 2), "%\n")

動的価格による収益リフト: 2.5 %

動的価格は最適固定価格に対して、この設定で 2.5% の収益リフトをもたらす。動的政策は固定価格を選べる政策集合を含むので、同じ真のモデル・同じ価格集合で正しく計算すればリフトは負にならない。

ノートなぜリフトは「意外と小さい」のか

動的価格の価値は、到着・WTPの不確実性、有限在庫と期限の厳しさ、需要タイプの時間変化、利用できる価格集合に依存する。ただし「在庫を減らせば必ずリフトが単調に増える」とは限らない。導入判断では、複数の容量・需要シナリオについて同じコードでリフトを再計算し、推定誤差と運用費用も差し引く。

ヒント実証研究コーナー:Gallego and van Ryzin (1994)

研究の問い:残り期間・残り在庫という2つの状態のもとで、企業はどのような価格政策をとれば期待収益を最大化できるか。また、それは実務でよく使われる「良い固定価格」とどれだけ違うのか。

モデル:価格に敏感な確率的需要のもとで、期限までに所与の在庫を売る問題をintensity controlとして定式化し、有限期間・有限在庫の最適政策を分析する。本講義の離散時間Bellman方程式は、この問題を1期1人までの到着へ単純化した類似物である。

主要な発見:最適な需要強度(したがって価格)が在庫と残り期間にどう反応するかについて単調性を導く。さらに、決定論版から得る上界を用い、期待販売量を大きくスケールさせる漸近領域では、単純な固定価格政策が漸近的に最適になることを示す。本講義の有限標本シミュレーションでリフトが2〜3%だからこの定理が実証された、という関係ではない。ここでは、固定価格も有力なベンチマークになりうることを数値で確認した、と読む。

なぜこの講義のトピックと繋がるか:この論文はrevenue managementという実務分野の理論的支柱であり、今日のBellman方程式・backward inductionによる価格政策の構成は、この論文の枠組みを最小構成に単純化したものである。「動的最適化がいつ本当に重要になるか」という実務的な問い(在庫逼迫度・需要不確実性の大きさで判断する)も、この論文の理論的結果から直接導かれる。

ヒント実証研究コーナー:Williams (2022)

研究の問い:航空会社が実際に行っている動的価格設定は、消費者の厚生(welfare)にどのような影響を与えているか。動的価格は「価格差別の道具」なのか、それとも「効率的な配分の道具」なのか。

データ・識別戦略:米国の航空市場における、出発日までの日数・残り座席数と実際の運賃の関係を、詳細な予約データを用いて分析している。需要側の異質性(早めに予約する価格に敏感なレジャー客と、直前に予約する価格に鈍感なビジネス客)を明示的にモデル化し、動的価格が無かった場合(一物一価で運営した場合)の反実仮想と比較する、という枠組みを取っている。

主要な発見:動的価格は、価格に敏感な客ほど早期に安く購入し、価格に鈍感な客ほど直前に高く購入する、という形で異なるタイプの客に異なる価格を配分している。これは一種の価格差別だが、同時に、限られた座席を「支払意思額が高い客」に優先的に配分するという効率性の側面も持つ。動的価格を一律価格に置き換えた反実仮想では、総余剰・消費者余剰・企業収益のバランスが変化し、動的価格には価格差別的な側面と効率改善的な側面の両方が併存することが示されている。

なぜこの講義のトピックと繋がるか:本講義で構築したRMのDPモデルは「企業が収益を最大化する」という一方向の視点だったが、Williams (2022) はそこに「消費者にとってどうなのか」という厚生の視点を持ち込んでいる。第9・10回で学んだ個人の需要と施策の視点と、今日の動学の視点(いつ・どの状態で)を組み合わせると、価格政策の「儲かるかどうか」だけでなく「誰が得をして誰が損をするか」まで問えるようになる、というこの講義の終盤にふさわしい統合的な研究である。

5. 需要を推定して価格を選ぶ

コース最後の数値例として、今日学んだRMの枠組みと、コース前半の需要推定を1つのパイプラインでつなげる。ここでのメッセージは1つ。需要推定の誤差は、消えてなくなるわけではなく、価格政策の収益ロスという「金額」に変換される。

パイプラインは次の3段階だ。

  1. 真の需要パラメータ \((\theta_0,\alpha)\) のもとで生成された予約データから、\((\hat\theta_0,\hat\alpha)\) を最尤推定する(第2回の再演)。
  2. \((\hat\theta_0,\hat\alpha)\) を真の値だと思い込んで、backward inductionで価格政策 \(\hat p^*(t,c)\) を作る。
  3. その政策 \(\hat p^*(t,c)\) を、真のモデルの上で走らせたときの期待収益を評価する。

\((\hat\theta_0,\hat\alpha)\) が真値からずれていれば、政策 \(\hat p^*(t,c)\) も真に最適な政策 \(p^*(t,c)\) からずれうる。その同時推定誤差が、どれだけの収益の目減りになるかを数値で確認する。

# ステップ0:価格実験データの生成(複数の価格水準をランダムに割り当てて需要を観測する)
price_menu <- c(90, 110, 130, 150, 170)

generate_booking_data <- function(seed, T_period, C_capacity, theta0, alpha, lambda_bar, price_menu) {
  set.seed(seed)
  c_remain <- C_capacity
  prices_obs   <- numeric(0)
  purchase_obs <- numeric(0)
  for (t in T_period:1) {
    if (c_remain == 0) break
    p <- sample(price_menu, 1)
    arrived <- runif(1) < lambda_bar
    if (arrived) {
      u <- theta0 - alpha * p
      prob_buy <- 1 / (1 + exp(-u))
      purchased <- as.numeric(runif(1) < prob_buy)
      prices_obs   <- c(prices_obs, p)
      purchase_obs <- c(purchase_obs, purchased)
      if (purchased == 1) c_remain <- c_remain - 1
    }
  }
  list(prices = prices_obs, purchase = purchase_obs)
}

booking_data <- generate_booking_data(20265, T_period, C_capacity, theta0, alpha, lambda_bar, price_menu)
cat("観測された到着客数:", length(booking_data$prices), "\n")
観測された到着客数: 51 
# ステップ1:ニュートン法による自作MLE(logitの対数尤度は大域的に凹なので、
# 解析的な勾配・ヘシアンを使うと安定して速く収束する)
sigmoid <- function(x) 1 / (1 + exp(-x))

fit_logit_newton <- function(prices, purchase, theta_init = c(0, 0.01), max_iter = 100, tol = 1e-10) {
  n <- length(prices)
  X <- cbind(1, -prices)  # u = X %*% c(const, alpha)
  theta <- theta_init
  for (it in 1:max_iter) {
    u     <- as.vector(X %*% theta)
    p_hat <- sigmoid(u)
    resid <- purchase - p_hat
    grad  <- as.vector(t(X) %*% resid)
    W     <- p_hat * (1 - p_hat)
    H     <- -t(X * W) %*% X
    step  <- solve(H, grad)
    theta_new <- theta - step
    if (max(abs(theta_new - theta)) < tol) {
      theta <- theta_new
      break
    }
    theta <- theta_new
  }
  list(theta = theta, n_iter = it)
}

mle_result <- fit_logit_newton(booking_data$prices, booking_data$purchase)
const_hat <- mle_result$theta[1]
alpha_hat <- mle_result$theta[2]

cat("真の (theta0, alpha):", theta0, alpha, "\n")
真の (theta0, alpha): 6 0.045 
cat("推定 (theta0_hat, alpha_hat):", round(const_hat, 3), round(alpha_hat, 4), "\n")
推定 (theta0_hat, alpha_hat): 9.213 0.0677 
cat("収束までの反復回数:", mle_result$n_iter, "\n")
収束までの反復回数: 6 
# ステップ2:推定した(theta0_hat, alpha_hat)でDPを解いて価格政策を作る
dp_hat <- solve_dp(T_period, C_capacity, const_hat, alpha_hat, lambda_bar, p_grid)
pstar_hat <- dp_hat$pstar

# ステップ3:その政策を「真のモデル」の上で評価する
evaluate_policy_on_true_model <- function(pstar_policy, T_period, C_capacity, theta0, alpha, lambda_bar) {
  W <- matrix(0, nrow = T_period + 1, ncol = C_capacity + 1)
  for (t in 1:T_period) {
    for (c in 1:C_capacity) {
      p <- pstar_policy[t + 1, c + 1]
      lam_true <- purchase_prob(p, theta0, alpha, lambda_bar)
      W[t + 1, c + 1] <- lam_true * (p + W[t, c]) + (1 - lam_true) * W[t, c + 1]
    }
    W[t + 1, 1] <- 0
  }
  W[T_period + 1, C_capacity + 1]
}

revenue_true_policy <- V_mat[T_period + 1, C_capacity + 1]
revenue_hat_policy   <- evaluate_policy_on_true_model(pstar_hat, T_period, C_capacity, theta0, alpha, lambda_bar)
revenue_loss <- revenue_true_policy - revenue_hat_policy

cat("真の政策の期待収益:", round(revenue_true_policy, 1), "\n")
真の政策の期待収益: 3833.2 
cat("推定需要パラメータで作った政策を真のモデルで評価した期待収益:", round(revenue_hat_policy, 1), "\n")
推定需要パラメータで作った政策を真のモデルで評価した期待収益: 3822.3 
cat("収益ロス:", round(revenue_loss, 1), "(真の収益の", round(100*revenue_loss/revenue_true_policy, 2), "%)\n")
収益ロス: 10.9 (真の収益の 0.29 %)

1回のデータ生成・推定だと、この収益ロスがたまたま大きいか小さいかは分からない。そこで、この一連のパイプライン(データ生成→MLE→DP→評価)を200回繰り返すモンテカルロで、推定誤差がどんな収益ロスの分布を生むかを可視化する。

n_mc <- 200
theta0_hat_vec   <- rep(NA_real_, n_mc)
alpha_hat_vec    <- rep(NA_real_, n_mc)
revenue_loss_vec <- rep(NA_real_, n_mc)

for (m in 1:n_mc) {
  bd <- generate_booking_data(10000 + m, T_period, C_capacity, theta0, alpha, lambda_bar, price_menu)
  if (length(bd$prices) < 5 || sd(bd$prices) < 1e-8) {
    next
  }
  fit_m <- fit_logit_newton(bd$prices, bd$purchase)
  a_hat_m <- fit_m$theta[2]
  c_hat_m <- fit_m$theta[1]
  theta0_hat_vec[m] <- c_hat_m
  alpha_hat_vec[m] <- a_hat_m
  if (!is.finite(c_hat_m) || !is.finite(a_hat_m) || a_hat_m <= 0 || a_hat_m > 1) {
    next
  }
  dp_m <- solve_dp(T_period, C_capacity, c_hat_m, a_hat_m, lambda_bar, p_grid)
  rev_m <- evaluate_policy_on_true_model(dp_m$pstar, T_period, C_capacity, theta0, alpha, lambda_bar)
  revenue_loss_vec[m] <- revenue_true_policy - rev_m
}

mc_df <- data.frame(theta0_hat = theta0_hat_vec,
                    alpha_hat = alpha_hat_vec,
                    revenue_loss = revenue_loss_vec) %>%
  filter(!is.na(revenue_loss))

cat("有効な反復数:", nrow(mc_df), "/", n_mc, "\n")
有効な反復数: 200 / 200 
cat("theta0_hatの平均:", round(mean(mc_df$theta0_hat), 3), "(真値:", theta0, ")\n")
theta0_hatの平均: 6.325 (真値: 6 )
cat("theta0_hatの標準偏差:", round(sd(mc_df$theta0_hat), 3), "\n")
theta0_hatの標準偏差: 1.933 
cat("alpha_hatの平均:", round(mean(mc_df$alpha_hat), 4), "(真値:", alpha, ")\n")
alpha_hatの平均: 0.0475 (真値: 0.045 )
cat("alpha_hatの標準偏差:", round(sd(mc_df$alpha_hat), 4), "\n")
alpha_hatの標準偏差: 0.0146 
cat("収益ロスの平均:", round(mean(mc_df$revenue_loss), 1),
    "(真の収益の", round(100*mean(mc_df$revenue_loss)/revenue_true_policy, 2), "%)\n")
収益ロスの平均: 49 (真の収益の 1.28 %)
cat("収益ロスの中央値:", round(median(mc_df$revenue_loss), 1), "\n")
収益ロスの中央値: 20.5 
p1 <- ggplot(mc_df, aes(x = alpha_hat)) +
  geom_histogram(bins = 25, fill = "steelblue", alpha = 0.8) +
  geom_vline(xintercept = alpha, linetype = "dashed", color = "firebrick") +
  labs(x = expression(hat(alpha)), y = "頻度", title = "alpha_hatの標本分布")

p2 <- ggplot(mc_df, aes(x = revenue_loss)) +
  geom_histogram(bins = 25, fill = "darkorange", alpha = 0.8) +
  labs(x = "収益ロス(真の政策の収益 − 推定政策の収益)", y = "頻度",
       title = "収益ロスの分布")

p1 + p2

推定誤差から生まれる収益ロスの分布(モンテカルロ200回)

この図が今日、そしてこのコース全体の最後のメッセージだ。政策は \((\hat\theta_0,\hat\alpha)\) の両方から作られるため、需要パラメータの同時推定誤差がDPの政策選択を通じて非線形に収益ロスへ変換される。 左図の \(\hat\alpha\) はその同時分布の一つの周辺分布を見せているだけで、左図と右図のばらつきが一対一に対応するわけではない。収益ロスの分布は右に長く裾を引いており、「たいていはそこそこうまくいくが、稀に推定が大きく外れると収益が大きく毀損する」という構造になっている。需要推定・因果推論・異質性のモデリングにどれだけ手を抜くかが、巡り巡って「いくら儲け損なうか」という金額に直結する。推定・推論の質が、意思決定を経てお金に変わる。

フィードバックループ問題:本当に内生な価格DGPを作る

価格が単に時間・在庫の決定論的関数で、当期の需要ショックがiidなら、価格変動が狭くても、それだけで価格内生性は生じない。識別が弱くなることと、誤差項との相関は別である。第3回の価格内生性を再現するには、企業には見えるが分析者には見えない当期需要ショックが、需要と価格の両方に入る必要がある。

複数の販売シーズン \(s\) を作り、需要ショックを

\[ \xi_{s,t}=\rho\xi_{s,t+1}+\nu_{s,t} \]

と持続的にする。企業は \(\xi_{s,t}\) を予約検索や競合情報から把握し、高需要時に価格を上げるが、分析者は \(\xi_{s,t}\) を観測できない:

ここで \(t\) は残り期間なので、カレンダー時間が1期進むと添字は \(t+1\) から \(t\) へ減る。このため通常のAR(1)と見た目の添字方向が逆になっている。

\[ p_{s,t}=p^*(t,c)+\gamma\xi_{s,t},\qquad \Pr(y_{s,t}=1)=\Lambda(\theta_0+\xi_{s,t}-\alpha p_{s,t}). \]

generate_price_panel <- function(seed, n_seasons, pricing,
                                 T_period, C_capacity, theta0, alpha,
                                 lambda_bar, pstar_policy, price_menu,
                                 rho_xi = 0.75, sigma_nu = 0.55,
                                 gamma_xi = 10) {
  set.seed(seed)
  out <- vector("list", n_seasons)
  for (s in 1:n_seasons) {
    c_remain <- C_capacity
    xi <- rnorm(1, 0, sigma_nu / sqrt(1 - rho_xi^2))
    rows <- vector("list", T_period)
    k <- 0
    for (t in T_period:1) {
      if (c_remain == 0) break
      xi <- rho_xi * xi + rnorm(1, 0, sigma_nu)
      base_price <- pstar_policy[t + 1, c_remain + 1]
      p <- if (pricing == "endogenous") {
        pmin(max(p_grid), pmax(min(p_grid), base_price + gamma_xi * xi))
      } else {
        sample(price_menu, 1)  # xiと独立にランダム割当
      }
      if (runif(1) < lambda_bar) {
        purchased <- as.numeric(runif(1) < sigmoid(theta0 + xi - alpha * p))
        k <- k + 1
        rows[[k]] <- data.frame(season = s, t = t, c = c_remain,
                                price = p, purchase = purchased, xi = xi)
        if (purchased == 1) c_remain <- c_remain - 1
      }
    }
    out[[s]] <- bind_rows(rows[seq_len(k)])
  }
  bind_rows(out)
}

obs_panel <- generate_price_panel(
  15015, 300, "endogenous", T_period, C_capacity, theta0, alpha,
  lambda_bar, pstar_mat, price_menu)
exp_panel <- generate_price_panel(
  15016, 300, "randomized", T_period, C_capacity, theta0, alpha,
  lambda_bar, pstar_mat, price_menu)

fit_obs <- fit_logit_newton(obs_panel$price, obs_panel$purchase)
fit_exp <- fit_logit_newton(exp_panel$price, exp_panel$purchase)
# xiを観測できたと仮定する診断用のoracle。価格係数の符号を反転してalphaに直す。
fit_exp_oracle <- glm(purchase ~ price + xi, family = binomial(), data = exp_panel)
alpha_exp_oracle <- -unname(coef(fit_exp_oracle)["price"])

endog_tbl <- data.frame(
  data = c("企業がxiを見て価格設定(分析者はxiを欠落)",
           "価格をxiと独立にランダム化(xiを欠落)",
           "ランダム化+xiを含むoracle", "構造パラメータの真値"),
  alpha_hat = c(fit_obs$theta[2], fit_exp$theta[2], alpha_exp_oracle, alpha),
  cor_price_xi = c(cor(obs_panel$price, obs_panel$xi),
                   cor(exp_panel$price, exp_panel$xi),
                   cor(exp_panel$price, exp_panel$xi), 0)
)
kable(endog_tbl, digits = 4,
      caption = "価格支持の狭さではなく、価格と欠落需要ショックの相関が内生性を作る")
価格支持の狭さではなく、価格と欠落需要ショックの相関が内生性を作る
data alpha_hat cor_price_xi
企業がxiを見て価格設定(分析者はxiを欠落) 0.0008 0.7046
価格をxiと独立にランダム化(xiを欠落) 0.0395 0.0015
ランダム化+xiを含むoracle 0.0450 0.0015
構造パラメータの真値 0.0450 0.0000

観察価格では高需要ショック時に高価格を付けるため、価格と欠落した \(\xi\) が正に相関し、価格の負の効果を過小評価する方向へ働く。ランダム価格ではこの相関が切れ、各価格での周辺的な購買確率は因果的に比較できる。ただし、\(\xi\) を積分した周辺需要曲線は一般に単純なlogitではない。logit係数は非可約(non-collapsible)でもあるので、\(\xi\) を欠落した2行目の係数は標本を増やしても一般に真の構造係数 \(\alpha\) へは一致しない。これはランダム化の失敗ではなく、構造logitと周辺的なworking logitのestimandが違うためである。構造係数を回収するには、oracle行のように \(\xi\) を条件づけるか、その分布を明示して積分する必要がある。

推論を行うなら同一シーズン内の系列相関を考慮し、シーズン単位でクラスタ化する。実務では完全なランダム化だけでなく、許容範囲内の探索価格、IV、需要ショックのproxy、方策の既知な割当確率を使う方法もある。

6. 市場の需要から個人への施策、動学的な判断へ

講義全体を、何を知り、何を計算したかったかという順番で振り返ろう。

回 データから知りたいこと その推定を使った判断
2〜4 価格反応、代替性、好みの分布、費用 合併や投入価格の変化が市場に与える影響
6〜7 購買履歴や選択課題から分かる好み 新商品の設計、個人について集めたい情報
8〜9 母集団の情報と個人の履歴を合わせた選好分布 不確実性を含めた個人別の需要予測
10 個人への施策が生む増分利益 クーポンの配布と実験による評価
11 購買頻度・離脱の異質性 将来購買と長期の顧客価値の予測
13〜14 将来を考えた選択、固定的な好みと状態依存 購入時点や継続購買を変える施策
15 消費者の待機、企業の在庫と販売期限 時間を通じた価格設定

第4回では、個人の好みの違いが市場全体の代替性を変えた。第9回では、同じ異質性を、個人の履歴からその人について学ぶために使った。第10回で施策の増分利益を考え、第11回で時間軸を延ばし、第13回以降では消費者自身の先読みや状態の変化を加えた。対象は広がったが、モデルを推定して、条件を変えたときの行動を計算するという作業は共通している。

因果推論も、この流れを通じて必要だった。価格がどう決まったのか、選択課題の属性をどう割り当てたのか、施策をどの顧客に配ったのかによって、データから学べる反応は変わる。モデルを詳しくすることと、介入への反応を識別できることは、別々に確認しよう。

まとめ

重要この講義のまとめ
  • 消費者が将来価格を見て待つ場合、現在の需要は将来の価格にも依存する。待機を無視して作った価格案は、実施したときの利益を過大に予測しうる。
  • 在庫と販売期限を持つ企業は、今売る利益と在庫を残す価値を比べる。Bellman方程式から状態に応じた価格を計算できる。
  • 需要を推定して価格を選ぶと、その推定誤差が価格と収益に伝わる。価格案の作成と評価を分けて確認する。
  • 個人別の価格を選ぶときには個人の条件付き需要、時間や在庫に応じた価格を選ぶときには状態と需要の関係が必要になる。
  • 市場全体の需要から個人への施策、時間を通じた選択へと対象を広げても、何を変え、何を固定する反実仮想なのかを明確にすることは共通である。

このコースでは、logit需要、IV、階層モデル、動的計画法について、モデルからデータを作り、そのデータを使って推定と反実仮想を確かめてきた。真の仕組みが分かるシミュレーションなら、どの仮定が結果を左右しているかを調べられる。新しい手法を学ぶときにも、小さな例を自分で動かして確かめることから始めてほしい。

宿題

  • ブラウザ実験室(playground15.html)で、今回のシミュレーションの数値を自分でいじって遊んでみよう。インストール不要、ブラウザだけでRが動く(初回ロードのみ30秒〜1分かかる)。
  1. 今日のRシミュ2のヒートマップで、lambda_bar(到着確率)を0.95から0.5まで下げるとヒートマップの色の分布がどう変わるか、手元で動かして確認してみよう。需要が弱くなると、在庫と期限による価格差はどうなるだろうか。
  2. Uber、JAL、ANA、あるいは身近なECサイトの価格が「いつ」「どんな条件で」動いているように見えるか、思い出してChatGPTに「この価格変動は、状態依存の価格ルールとして、どんな状態変数に反応していそうか」と聞いてみよう。今日の交通整理((a)(b)(c)のどれに近いか)を当てはめて考えてみるとよい。
  3. coding課題は assignment15.qmd を参照。航空券1路線の予約データを自分で生成し、backward inductionで価格政策を作り、推定誤差が収益ロスにどう変換されるかをモンテカルロで確認してもらう。

このコースの最後の課題でもある。期末レポート(research proposal)を書く上でも、「市場の需要→個人別の予測と施策→動学的な意思決定」のどこか1つの軸を深掘りするだけでなく、「その先にどんな意思決定(価格、ターゲティング、在庫)が待っているか」まで考えてみると、問いの立て方に厚みが出るはずだ。

補足1:ピーク時料金と時間帯ごとの機会費用

Steinerの最小モデル

電力会社を考える。需要は昼と夜で違う。昼の需要 \(q_{\text{day}}(p)\) は夜の需要 \(q_{\text{night}}(p)\) より大きいとしよう。発電容量 \(K\) を1単位用意するコストを \(c_K\)(固定費、季節や時間帯によらない)、実際に発電する限界費用を \(c_v\)(可変費)とする。

Steiner (1957) の洞察はシンプルだ。容量は「一番厳しい期」のために用意しなければならない。 昼の需要が容量制約に当たっているなら、容量費用 \(c_K\) を負担すべきなのは昼の客だけであり、夜の客は可変費用 \(c_v\) だけを払えばよい。逆に言えば、需要が少ない時間帯の客に容量費用を負担させるのは非効率だ。

最小の数値例で確認する。

# 昼と夜、2期間の需要(線形、簡易的に)
q_day   <- function(p) pmax(100 - 2 * p, 0)
q_night <- function(p) pmax(40 - 2 * p, 0)

c_v <- 5   # 可変費用(発電1単位あたり)
c_K <- 20  # 容量費用(容量1単位を用意するコスト、固定費)
K_capacity <- 50

# ケースA: 容量費用を両期間で分担すると仮定した場合の価格
# (素朴な「平均費用」的発想、非効率の例として)
p_naive <- c_v + c_K / 2

# ケースB: Steinerのルール。容量を「使う」期(昼)だけが容量費用を負担する。
# 夜は可変費用のみ。昼は可変費用+容量費用。
p_night_efficient <- c_v
p_day_efficient    <- c_v + c_K

peak_df <- data.frame(
  ルール = c("素朴な平均費用配分", "Steinerの効率的配分(昼)", "Steinerの効率的配分(夜)"),
  価格   = c(p_naive, p_day_efficient, p_night_efficient),
  需要量 = c(q_day(p_naive), q_day(p_day_efficient), q_night(p_night_efficient)),
  注記 = c("比較用:昼需要", paste0("K=", K_capacity, "に一致(容量制約がbinding)"),
           paste0("30<K=", K_capacity, "(slack)"))
)
kable(peak_df, digits = 1, caption = "peak-load pricing:ピークだけが容量の影価格を負担")
peak-load pricing:ピークだけが容量の影価格を負担
ルール 価格 需要量 注記
素朴な平均費用配分 15 70 比較用:昼需要
Steinerの効率的配分(昼) 25 50 K=50に一致(容量制約がbinding)
Steinerの効率的配分(夜) 5 30 30<K=50(slack)

効率的ルールでは昼の需要は \(q_D(25)=50\) なので設備容量 \(K=50\) をちょうど使い切り、夜は \(q_N(5)=30<K\) なので容量制約が緩い。昼の価格に入る \(c_K\) は、容量投資の限界費用と一致する影価格である。素朴に容量費用を両期間へ機械的に折半すると、どの期が実際に限界容量を必要としているかを反映できない。

Real-time pricing と日本の実例

Borenstein (2005) は、電力のreal-time pricing(RTP:卸売市場の変動をそのまま小売価格に反映させる仕組み)について、長期的な効率性の観点から論じている。多くの家庭は現在も「時間帯によらない一律料金」で電気を買っており、これはピーク時間帯の需要を過大にし、オフピーク時間帯の需要を過小にする非効率を生む。RTPを導入して価格が需給の逼迫を正しく反映するようにすれば、需要側が自然にピークを平準化するインセンティブを持つ、というのが基本的な主張である。

日本でも時間帯別電力料金(深夜電力割引など)は昔からあるし、近年は再エネの出力変動に対応するためのダイナミックプライシング実証実験も各地で行われている。考え方はSteinerのモデルと同じで、「容量が逼迫する時間帯には高い価格を、余裕がある時間帯には低い価格を」という原理の実装だ。

ノートビジネスの現場で:コンビニの発注と「時間帯別の機会費用」

電力の話は大掛かりに聞こえるが、同じ発想は身近な小売にもある。コンビニの弁当売り場を考えると、昼のピーク時間帯に売り切れることは大きな機会損失(来た客が買えずに他店に流れる)だが、深夜に売れ残ることも廃棄コストという別の損失を生む。値引きのタイミングを「時間帯別の需給逼迫度」で決める、というのは発想としてはpeak-load pricingと同じ機会費用の考え方に立っている。この後のRMのbid price(席1つの機会費用)の考え方も、根っこは同じだ。

補足2:サージ価格と需給の調整

ride-hailing(配車サービス)のsurge pricing(需要急増時の価格上乗せ)は、航空券のRMとは少し違う顔をしている。RMは「限られた在庫を、期限までにどう配分するか」という問題だったが、surge pricingは「今この瞬間の需要と供給を、どうマッチさせるか」という問題だ。

コンサートやスポーツイベントの終演直後を想像してほしい。何千人もの人が同時に配車を呼ぼうとするのに、その場にいるドライバーの数は限られている。価格を動かさなければ何が起きるか。まず、需要が供給をはるかに上回るので、多くの客が配車されずに待たされる。次に、通常価格のままだと、遠方のドライバーがわざわざそこまで向かうインセンティブがない。結果として、需給のミスマッチが解消されないまま待ち時間だけが伸びていく。

サージ価格の役割は3つに整理できる。

  1. 需要抑制:価格が上がることで、「今すぐでなくてもいい」客は配車を諦めたり、時間をずらしたりする。
  2. 供給喚起:価格が上がることで、周辺エリアのドライバーがそのエリアに向かうインセンティブが生まれる。
  3. マッチング摩擦の緩和:需給が均衡に近づくことで、客とドライバーが出会うまでの待ち時間(マッチング摩擦)が短くなる。

Uber自身は、需要が供給を上回ると価格が上がり、一部の利用者が待つ一方で混雑地域へ向かうドライバーの誘因が強まり、市場が再均衡へ向かう、と公式ページで説明している。ただし、これは企業による制度説明であって因果効果の推定ではない。「サージを止めた技術障害で待ち時間が何倍になった」といった出典不明の逸話を実証結果として扱わず、次の査読論文が示す理論機序と実証的証拠を区別して読む。

サージ価格をめぐっては、単純に「価格が上がれば供給が増える」というモデルだけでは説明しきれない現象も報告されている。

ヒント研究コーナー:Castillo, Knoepfle, and Weyl (2024)

研究の問い:サージ価格が発動していない、あるいは価格が低すぎる状況では、ride-hailing市場の需給調整はどのように失敗しうるか。

モデル・メカニズム:需要が高いのに価格が低いままだと、空車の減少、迎車時間の増加、ドライバー収益の低下が互いを悪化させるフィードバックが起きうる。このマッチング失敗を”wild goose chase”(無駄足)と呼ぶ。迎車時間の増加は個々のドライバーの手間を増やすだけでなく、供給側の実効的な処理能力(単位時間あたりに対応できる客数)も引き下げる。

主要な発見:価格が低すぎることは、単に「需要超過を放置する」だけでなく、この”wild goose chase”を通じて「供給側の効率そのものを壊す」方向に作用しうる。逆に言えば、適切なサージ価格は、需要を抑えるだけでなく、マッチングの空間的な効率性を保つことで、供給側の処理能力を守る役割も果たしている。

なぜこの講義のトピックと繋がるか:RMの文脈では「価格は将来の座席という資源の機会費用を反映すべきだ」という話をしたが、surge pricingの文脈では「価格はマッチングの効率性(誰と誰がどれだけの手間で出会えるか)にも影響する」という、RMには無かった側面が登場する。今日学んだ「状態に応じて価格を変える」という枠組みが、在庫だけでなく空間的なマッチング効率にまで応用されている例として押さえておいてほしい。

Galichon and Hsieh (2017) は、価格が規制されて市場を清算できず、待ち時間が非価格配分の役割を持つ分権的マッチング市場を分析する。その上で、ランダムな需要・供給についての信念が与えられたとき、市場設計者が期待非効率を小さくする価格をどう設定すべきか、というsurge pricing問題を考える。したがって、この論文は「あらゆる配車市場でサージが均衡を実現する」と保証するものではなく、待ち時間による配分と価格設計のトレードオフを明示する理論である。

重要サージの再フレーミング

「サージ価格は企業が儲けるための値上げだ」という理解だけでは不十分である。サージは、需要を抑え、供給を呼び込み、マッチング摩擦を緩和することで、市場を「回す」役割を持ち得る価格でもある。価格を動かさなければ超過需要が残り、多くの客が長く待つか、配車を受けられない可能性が高まる。もちろん、サージ価格が実際にどこまで「市場を回す」役に立っているか、どこからが単なる超過利潤の徴収になっているかは、実証的に検証すべき問いであり、規制当局や研究者の関心が今も続いている論点である。

補足3:アルゴリズムによる価格競争

Calvano, Calzolari, Denicolò and Pastorello (2020) は、反復価格競争のシミュレーションで、独立に学習するQ-learningのエージェントが競争的な水準を上回る価格を学ぶ場合を示した。これは、今回の一企業による在庫管理とは異なり、複数の企業が相手の反応を学習する問題である。企業ごとの価格設定を分析した先には、そうした相互作用も残っている。

参考文献

  • Su, X. (2007). “Intertemporal Pricing with Strategic Customer Behavior.” Management Science, 53(5), 726–741. https://doi.org/10.1287/mnsc.1060.0667
  • Steiner, P. O. (1957). “Peak Loads and Efficient Pricing.” Quarterly Journal of Economics, 71(4), 585–610. https://doi.org/10.2307/1885712
  • Borenstein, S. (2005). “The Long-Run Efficiency of Real-Time Electricity Pricing.” Energy Journal, 26(3), 93–116.
  • Gallego, G., and van Ryzin, G. (1994). “Optimal Dynamic Pricing of Inventories with Stochastic Demand over Finite Horizons.” Management Science, 40(8), 999–1020. https://doi.org/10.1287/mnsc.40.8.999
  • Galichon, A., and Hsieh, Y.-W. (2017). “A Theory of Decentralized Matching Markets without Transfers, with an Application to Surge Pricing.” USC-INET Research Paper 17-03. https://doi.org/10.2139/ssrn.2908532
  • Williams, K. R. (2022). “The Welfare Effects of Dynamic Pricing: Evidence from Airline Markets.” Econometrica, 90(2), 831–858. https://doi.org/10.3982/ECTA16180
  • Castillo, J. C., Knoepfle, D., and Weyl, E. G. (2024). “Matching and Pricing in Ride Hailing: Wild Goose Chases and How to Solve Them.” Management Science, 71(5), 4377–4395. https://doi.org/10.1287/mnsc.2022.00096 (previous drafts were titled “Surge Pricing Solves the Wild Goose Chase.”)
  • Calvano, E., Calzolari, G., Denicolò, V., and Pastorello, S. (2020). “Artificial Intelligence, Algorithmic Pricing, and Collusion.” American Economic Review, 110(10), 3267–3297.