Mímisbrunnr知恵の泉

← オペレーションズマネジメント 一覧

🎓 レベル:応用 | 重要度:A(必須)

📎 前提:確率的在庫モデル(新聞売り子問題)(臨界比 CR=Cu/(Cu+Co)・Q*=F^{-1}(CR)。リトルウッドもオーバーブッキングもこの親戚) | 確率の土台:正規分布(標準正規・標準化)(分位点 Φ^{-1}) | 次:離散事象シミュレーション最適化

要点(BLUF)

1. 収益管理とは:消滅する容量を、安売りしすぎず・空席も出さず

航空便の座席は、出発した瞬間に売れ残りの価値がゼロになります。在庫として翌日に繰り越せない——この「消滅する容量(perishable capacity)」が RM の出発点です。しかも顧客には早割で安く買う観光客と、直前に高く買う出張客がいる。座席は同じでも支払い意思(WTP)が違うわけです。

このとき航空会社のジレンマは明確です。安い予約を受けすぎると、後から来る高運賃の出張客に売る席がなくなる(安売りで容量を食い潰す)。かといって高運賃を待ちすぎると、出張客が思ったより来ずに空席のまま出発してしまう(消滅損)。RM は「いくつの席を高運賃のために取っておくか(保護水準)」を、需要の確率分布とコストから決める問題です。

flowchart LR
  REQ["安い予約(低運賃 pL)が来た"] --> CHK{"残席 > 保護水準 Q ?"}
  CHK -->|"はい(席に余裕)"| ACC["受ける(pL を確定)"]
  CHK -->|"いいえ(残り Q 席)"| REJ["断る(高運賃 pH のために保護)"]

RM が成り立つ前提は3つ。(1) 容量が固定で消滅する、(2) 需要をセグメントに分けられる(運賃クラス・購入時期・キャンセル条件などのフェンスで囲い込める)、(3) 予約が時間差で入る(安い客が先・高い客が後)。この3条件が揃う航空・ホテル・レンタカー・興行で RM は威力を発揮します。

2. リトルウッドの法則:保護水準の導出

座席 CC 席、運賃は高 pHp_H と低 pLp_LpL<pHp_L<p_H)の2クラス。低運賃の客が先に予約し、高運賃の客が後から来るとします。決めるのは高運賃のための保護水準 QQ——「残り QQ 席になったら、もう低運賃は断って高運賃のために取っておく」その QQ です。

限界分析で最適 QQ を出します。いま目の前に低運賃客が1人いて、残席がちょうど QQ 席だとする。この**QQ 席目**を「いま pLp_L で売る」か「保護して高運賃を待つ」か。

保護したほうが得なのは pHP(DHQ)pLp_H\,P(D_H\ge Q)\ge p_L のとき。QQ を増やすほど「QQ 席目まで高運賃が来る確率」P(DHQ)P(D_H\ge Q) は下がるので、保護を増やす価値があるのは

P(DHQ)pLpHP(D_H\ge Q)\ge \frac{p_L}{p_H}

が成り立つ間だけ。境目、すなわち P(DH>Q)=pLpHP(D_H>Q^*)=\dfrac{p_L}{p_H} が最適保護水準です。

P(DH>Q)=pLpHQ=FH1 ⁣(1pLpH)\boxed{\,P(D_H>Q^*)=\frac{p_L}{p_H}\quad\Longleftrightarrow\quad Q^*=F_H^{-1}\!\left(1-\frac{p_L}{p_H}\right)\,}

直感も明快です。運賃比 pL/pHp_L/p_H が小さい(安席が高席よりずっと安い)ほど、P(DH>Q)P(D_H>Q^*) を小さく=QQ^* を大きくしてたくさん保護する。逆に2クラスの値段が近ければ保護は薄くてよい。「安く売る機会費用」と「高く売れる確率」の釣り合いです。

3. 新聞売り子の親戚であること

リトルウッドの法則は、保護水準 QQ を「在庫」、高運賃需要 DHD_H を「需要」とみなした新聞売り子問題確率的在庫モデル(新聞売り子問題))に他なりません。保護水準を1つ動かしたときの2つの後悔を考えます。

新聞売り子の臨界比に入れると

CR=CuCu+Co=pHpL(pHpL)+pL=pHpLpH=1pLpHCR=\frac{C_u}{C_u+C_o}=\frac{p_H-p_L}{(p_H-p_L)+p_L}=\frac{p_H-p_L}{p_H}=1-\frac{p_L}{p_H}

FH(Q)=1pLpHF_H(Q^*)=1-\dfrac{p_L}{p_H}、つまり P(DH>Q)=pLpHP(D_H>Q^*)=\dfrac{p_L}{p_H}——リトルウッドと完全に一致します。RM は「席という消滅在庫を、どの運賃クラスにどれだけ取っておくか」の新聞売り子だ、と見抜けるのが勘所です。

4. 保護水準が期待収益を最大化することを実証(コード)

C=100C=100 席、pH=400,pL=150p_H=400,p_L=150、高運賃需要 DHN(50,182)D_H\sim N(50,18^2)。リトルウッドの保護水準 QQ^* を計算し、保護水準 QQ のグリッド上で期待収益を200万サンプルでモンテカルロして、最大化点が QQ^* に一致するかを確かめます(低運賃需要は豊富で CQC-Q 席は必ず埋まると仮定)。

import numpy as np
from scipy.stats import norm

rng = np.random.default_rng(20260627)

# 2運賃クラスの座席:容量 C、高運賃 pH、低運賃 pL(pL < pH)
C = 100        # 総座席数
pH = 400.0     # 高運賃(円)
pL = 150.0     # 低運賃(円)

# 高運賃需要 D_H ~ N(muH, sigmaH)(低運賃需要は豊富で C-Q 席は必ず埋まると仮定)
muH, sigmaH = 50.0, 18.0

# リトルウッドの法則:保護水準 Q* で P(D_H > Q*) = pL/pH
# => F_H(Q*) = 1 - pL/pH、Q* = muH + norm.ppf(1 - pL/pH)*sigmaH
ratio = pL / pH
crit = 1 - ratio          # = 新聞売り子の臨界比 Cu/(Cu+Co), Cu=pH-pL, Co=pL
z = norm.ppf(crit)
Q_star = muH + z * sigmaH
print(f"運賃比 pL/pH = {ratio:.4f}")
print(f"保護の臨界比 1 - pL/pH = (pH-pL)/pH = {crit:.4f}")
print(f"  新聞売り子で言えば Cu=pH-pL={pH-pL:.0f}, Co=pL={pL:.0f}, CR={crit:.4f}")
print(f"z = norm.ppf(1-pL/pH) = {z:.4f}")
print(f"高運賃の保護水準 Q* = muH + z*sigmaH = {Q_star:.2f} 席")
print(f"  => 低運賃に開放するのは C - Q* = {C - Q_star:.2f} 席まで")
print()

# --- モンテカルロ:保護水準 Q のグリッドで期待収益を評価し、最大化点を確認 ---
# 収益(Q) = pL*(C-Q)(低運賃で確実に埋まる) + pH*min(D_H, Q)(保護席を高運賃が埋める)
N = 2_000_000
DH = rng.normal(muH, sigmaH, size=N)

def expected_revenue(Q):
    high = np.minimum(DH, Q)                  # 保護席を埋めた高運賃客
    rev = pL * (C - Q) + pH * high
    return rev.mean()

Q_grid = np.arange(20.0, 90.0 + 0.5, 0.5)
revs = np.array([expected_revenue(Q) for Q in Q_grid])
Q_best = Q_grid[np.argmax(revs)]
print(f"MC 期待収益を最大化する保護水準 Q (グリッド) = {Q_best:.2f}")
print(f"リトルウッドの法則による Q*                  = {Q_star:.2f}")
print(f"Q* での期待収益 = {expected_revenue(Q_star):.2f} 円/便")

# 「保護しすぎ(全席保護)」「保護せず(全部低運賃)」との比較
print(f"  参考: 全席低運賃(Q=0)   の期待収益 = {expected_revenue(0):.2f}")
print(f"  参考: 全席保護(Q=C)     の期待収益 = {expected_revenue(C):.2f}")

出力:

運賃比 pL/pH = 0.3750
保護の臨界比 1 - pL/pH = (pH-pL)/pH = 0.6250
  新聞売り子で言えば Cu=pH-pL=250, Co=pL=150, CR=0.6250
z = norm.ppf(1-pL/pH) = 0.3186
高運賃の保護水準 Q* = muH + z*sigmaH = 55.74 席
  => 低運賃に開放するのは C - Q* = 44.26 席まで

MC 期待収益を最大化する保護水準 Q (グリッド) = 55.50
リトルウッドの法則による Q*                  = 55.74
Q* での期待収益 = 24768.51 円/便
  参考: 全席低運賃(Q=0)   の期待収益 = 14994.10
  参考: 全席保護(Q=C)     の期待収益 = 19992.97

出力の意味:運賃比 pL/pH=0.375p_L/p_H=0.375 なので保護の臨界比は 10.375=0.6251-0.375=0.625。新聞売り子で言えば Cu=pHpL=250C_u=p_H-p_L=250Co=pL=150C_o=p_L=150CR=0.625CR=0.625 と一致します。z=Φ1(0.625)=0.3186z=\Phi^{-1}(0.625)=0.3186、保護水準 Q=50+0.3186×18=55.74Q^*=50+0.3186\times18=55.74 席——100席のうち約56席を高運賃に取っておき、低運賃に開放するのは約44席まで。コードはこの QQ^* を使わずに保護水準を20〜90で動かしただけですが、期待収益の最大化点 55.50 がリトルウッドの 55.74 に一致しました。そして保護戦略の価値が数字に出ています——全席を安く売ると 14,994 円(容量を食い潰す)、全席を高運賃に取っておくと 19,993 円(出張客が来ず空席)、最適保護で 24,768 円。どちらの極端より2〜6割多い収益で、「安席で容量を埋めつつ、高席のぶんだけ賢く取っておく」のリトルウッドが効いていると分かります。

5. オーバーブッキング:空席損と搭乗拒否のトレードオフ(コード)

予約客の一部は当日現れません(ノーショー)。その空席を埋めるには容量を超えて予約を取る(オーバーブッキング)しかありませんが、取りすぎると全員現れたとき搭乗拒否が出ます。これも新聞売り子です——**空席損 ff(過小コスト CuC_u搭乗拒否コスト DD(過剰コスト CoC_o)**のトレードオフで、最適予約上限の臨界比は ff+D\dfrac{f}{f+D}。容量 C=100C=100、ノーショー率 r=0.15r=0.15、純収益 f=300f=300、搭乗拒否補償 D=800D=800 で、最適予約上限を求めます。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from scipy.stats import binom

rng = np.random.default_rng(20260627)

# オーバーブッキング:容量 C、ノーショー率 r、運賃 f、搭乗拒否コスト D
C = 100        # 座席数
r = 0.15       # ノーショー率(予約1件が現れない確率)
q = 1 - r      # 搭乗率
f = 300.0      # 1席あたり純収益(空席は機会損失 f)
Dcost = 800.0  # 搭乗拒否1件あたりの補償コスト

# 新聞売り子型の臨界比:限界の1予約を足すと、現れる確率 q で
#   満席なら拒否(コスト Dcost)・空席なら収益 f。
#   P(現れた客数 S >= C) <= f/(f+Dcost) の間は予約を増やすべき
CR = f / (f + Dcost)
print(f"臨界比 f/(f+Dcost) = {CR:.4f}")
print(f"  (空席損 f={f:.0f} が過小コスト Cu、搭乗拒否 D={Dcost:.0f} が過剰コスト Co)")

# 限界条件から最適予約上限 b* を求める:P(S_b>=C)<=CR の間は1席増やす
b_pred = C
while binom.sf(C - 1, b_pred, q) <= CR:        # sf(C-1,n,q)=P(S_b>=C)
    b_pred += 1
print(f"限界条件 P(S_b>=C)<=CR から予測する最適予約上限 b* = {b_pred}(超過 {b_pred - C} 席)")
print(f"  b*-1={b_pred - 1} での P(S>=C) = {binom.sf(C - 1, b_pred - 1, q):.4f} (<= {CR:.4f}:もう1席増やす価値あり)")
print(f"  b*  ={b_pred} での P(S>=C) = {binom.sf(C - 1, b_pred, q):.4f} (>  {CR:.4f}:これ以上は搭乗拒否が勝つ)")
print()

# --- モンテカルロ:予約上限 b のグリッドで期待利益を評価し、最大化点を確認 ---
# 利益(b) = f*min(S,C) - Dcost*max(S-C,0),  S=現れた客数 ~ Binomial(b, q)
N = 1_000_000
b_grid = np.arange(C, C + 36)
exp_profit = []
for b in b_grid:
    S = rng.binomial(b, q, size=N)
    profit = f * np.minimum(S, C) - Dcost * np.maximum(S - C, 0)
    exp_profit.append(profit.mean())
exp_profit = np.array(exp_profit)
b_best = b_grid[np.argmax(exp_profit)]
print(f"MC 期待利益を最大化する予約上限 b (グリッド) = {b_best}")
print(f"限界条件による予測 b*                         = {b_pred}")
print(f"  b*={b_pred} の期待利益 = {exp_profit[b_grid == b_pred][0]:.2f} 円/便")
print(f"  超過なし b=C={C} の期待利益 = {exp_profit[0]:.2f} 円/便(ノーショーで空席)")
print(f"  オーバーブッキングの利得 = {exp_profit[b_grid == b_pred][0] - exp_profit[0]:.2f} 円/便")

# --- 図:予約上限 vs 期待利益 ---
plt.figure(figsize=(10, 5.5))
plt.plot(b_grid, exp_profit, "o-", color="#1f77b4", ms=4, label="期待利益(MC)")
plt.axvline(C, color="gray", ls="--", label=f"容量 C={C}(超過なし)")
plt.scatter([b_best], [exp_profit[b_grid == b_best][0]], color="#d62728",
            zorder=5, s=80, label=f"最適 b*={b_best}{b_best - C}席超過)")
plt.xlabel("予約上限 b(受け付ける予約数)")
plt.ylabel("期待利益(円/便)")
plt.title("オーバーブッキング:空席損と搭乗拒否のトレードオフ=新聞売り子型の最適予約上限")
plt.legend(); plt.tight_layout(); plt.show()

出力:

臨界比 f/(f+Dcost) = 0.2727
  (空席損 f=300 が過小コスト Cu、搭乗拒否 D=800 が過剰コスト Co)
限界条件 P(S_b>=C)<=CR から予測する最適予約上限 b* = 115(超過 15 席)
  b*-1=114 での P(S>=C) = 0.2530 (<= 0.2727:もう1席増やす価値あり)
  b*  =115 での P(S>=C) = 0.3328 (>  0.2727:これ以上は搭乗拒否が勝つ)

MC 期待利益を最大化する予約上限 b (グリッド) = 115
限界条件による予測 b*                         = 115
  b*=115 の期待利益 = 28634.53 円/便
  超過なし b=C=100 の期待利益 = 25499.54 円/便(ノーショーで空席)
  オーバーブッキングの利得 = 3134.98 円/便

出力の意味:搭乗拒否(800円)が空席損(300円)より重いので、臨界比は 300300+800=0.2727\dfrac{300}{300+800}=0.2727 と0.5より小さく、「満席〜超過になる確率が27%に達するまで」予約を取り増やすのが最適です。限界条件は b=114b=114P(SC)=0.2530P(S\ge C)=0.2530(まだ <0.2727<0.2727 なので1席増やす価値あり)、b=115b=1150.33280.3328>0.2727>0.2727 なので打ち止め)と判定し、最適予約上限 b=115b^*=115(容量より15席多く受ける)。これが MC の期待利益最大化点 115 とぴたり一致しました。利得も明確で、容量ぴったり100席しか受けないと、ノーショーで空席が出て 25,500 円。15席オーバーブッキングすると 28,635 円で、+3,135 円/便。図は期待利益が bb について上に凸で、b=115b^*=115(赤)で頂点になる様子を示します。左に寄れば空席損で取りこぼし、右に行きすぎれば搭乗拒否補償で削られる——典型的な新聞売り子の山型です。

⚠️ よくある誤解

関連ノート