Overbooking limit optimization in Python

Selling exactly to capacity leaves money on the table, because a predictable fraction of bookings cancel or no-show. Selling too far past capacity triggers walk costs and reputation damage. Overbooking limit optimization finds the authorization level that maximizes expected net revenue by trading the marginal revenue of one more sold room against the expected cost of having to walk a guest. This how-to sits under Yield Optimization Algorithms in the Dynamic Pricing Rule Engines & Optimization pillar and consumes the cancellation signal produced by the forecasting pillar.

Prerequisites

  • Python 3.11+
  • Standard library math, logging, dataclasses; statistics for the normal approximation
  • A cancellation/no-show probability from Lead-Time & Cancellation Forecasting
  • Physical capacity per room type, and estimates of average rate and per-walk cost
  • Rates and costs in integer minor units

Step 1 — Frame the trade-off

The decision variable is the authorization limit — how many rooms to sell against a physical capacity. Each extra authorization earns the room rate if the guest shows and no walk is needed, but costs the walk expense if realized demand-after-cancellations exceeds capacity. The optimum is the largest authorization where the marginal expected revenue still exceeds the marginal expected walk cost.

python
from __future__ import annotations

import logging
import math
from dataclasses import dataclass

logger = logging.getLogger("pricing.overbooking")


@dataclass(frozen=True)
class OverbookingInputs:
    capacity: int
    show_prob: float        # 1 - cancellation/no-show rate, in (0, 1]
    avg_rate_cents: int
    walk_cost_cents: int     # cost of walking one guest

Step 2 — Model shows with a normal approximation

If each of n authorized bookings shows independently with probability p, the number who show is Binomial(n, p), well-approximated by a normal for realistic n. The probability of an oversell at authorization n is the chance shows exceed capacity, which we get from the normal survival function.

python
def oversell_probability(n: int, capacity: int, p: float) -> float:
    mean = n * p
    var = n * p * (1.0 - p)
    if var <= 0:
        return 1.0 if mean > capacity else 0.0
    z = (capacity + 0.5 - mean) / math.sqrt(var)   # continuity-corrected
    # survival of standard normal = P(shows > capacity)
    return 0.5 * math.erfc(z / math.sqrt(2))

Step 3 — Search for the revenue-maximizing authorization

Walk authorization upward while the marginal booking’s expected gain beats its expected walk cost. The marginal gain is the rate times the show probability; the marginal cost is the walk cost times the incremental oversell probability.

python
def optimal_authorization(inp: OverbookingInputs, max_overbook: int = 20) -> int:
    if not 0.0 < inp.show_prob <= 1.0:
        raise ValueError("show_prob must be in (0, 1]")

    best_n = inp.capacity
    for n in range(inp.capacity, inp.capacity + max_overbook + 1):
        marginal_gain = inp.avg_rate_cents * inp.show_prob
        marginal_cost = inp.walk_cost_cents * oversell_probability(n + 1, inp.capacity,
                                                                   inp.show_prob)
        if marginal_gain <= marginal_cost:
            best_n = n
            break
        best_n = n + 1
    logger.info("optimal authorization=%d (capacity=%d, show_prob=%.3f)",
                best_n, inp.capacity, inp.show_prob)
    return best_n

The loop stops the moment an extra room’s expected walk cost overtakes its expected revenue — the economically correct authorization, not a fixed “sell 10% over” heuristic that ignores how cancellation risk changes by date.

Verification and testing

python
def test_higher_walk_cost_reduces_overbooking() -> None:
    base = OverbookingInputs(capacity=100, show_prob=0.85,
                             avg_rate_cents=18000, walk_cost_cents=25000)
    expensive = OverbookingInputs(capacity=100, show_prob=0.85,
                                  avg_rate_cents=18000, walk_cost_cents=200000)
    assert optimal_authorization(expensive) <= optimal_authorization(base)
    assert optimal_authorization(base) >= 100

A passing test captures the intuition and the math agreeing: when walking a guest is expensive, the model authorizes fewer overbooked rooms.

Common pitfalls and edge cases

  • Static show probability. Cancellation risk varies by lead time and segment; feed a date-specific show probability, not a portfolio average.
  • Independence assumption. Group bookings cancel together, breaking the Binomial model; treat large groups separately.
  • Ignoring walk severity. Not all walks cost the same; a sold-out city on a peak night has a far higher effective walk cost.
  • No cap on authorization. Bound the search so a degenerate input cannot authorize an absurd oversell.
  • Continuity correction omitted. Dropping the +0.5 term biases the oversell probability for small capacities.