Test the commitment-sizing rule
Simulate a modelNeeds Python with NumPy
Problem
A commitment (reserved capacity, a savings plan) buys a lower unit price in exchange for paying whether you use it or not. Committing too little leaves demand on the expensive on-demand price; committing too much pays for idle units. The two errors are not symmetric: an unused committed unit wastes its whole price, while an uncovered unit costs only the difference between on-demand and committed. So the best commitment sits below the mean of demand, at the level that demand exceeds often enough to justify paying for it every day.
Generate 12 months of daily demand with a mean of 60 units and a peak of 100 (any skewed distribution). For commitment levels from 20 to 100, compute total cost, where committed units cost 0.6 each whether used or not, and the rest cost 1.0. Plot cost against commitment level. Try a different distribution and a 20% yearly price fall in years 2 and 3, and see how the best level moves.
The script tries a right-skewed (gamma) and a left-skewed (beta) demand shape.
The question. At which commitment level is the year cheapest, where does that sit in the distribution of daily demand, and how does a falling on-demand price move it?
Predict
Expected result: the minimum sits below the mean, near the percentile equal to the discount (the P40 here, where a committed unit costs 0.6 of list).
Run it
1Install uv once. It fetches Python and the lab's packages for you.
Terminal curl -LsSf https://astral.sh/uv/install.sh | sh2Get the script. It is the whole lab: read it, then download it.
View the codeHide the code
test-the-commitment-sizing-rule.py # /// script # requires-python = ">=3.10" # dependencies = ["numpy"] # /// # Copyright (c) 2026 Aayush Sharma · SPDX-License-Identifier: MIT # https://thedataengineering.com/lab-code/LICENSE.txt """Test the commitment sizing rule. A year of skewed daily demand (mean 60, peak 100). Committed units cost 0.6 each whether used or not; the rest cost 1.0 on demand. Total cost for every commitment level, against the demand distribution's percentiles; then a different shape, and a 20% yearly price fall over a 3-year commitment. """ import argparse, json import numpy as np p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) p.add_argument("--mean", type=float, default=60, help="Mean daily demand.") p.add_argument("--peak", type=float, default=100, help="Peak daily demand.") p.add_argument("--committed-price", type=float, default=0.6, help="Price of a committed unit, used or not.") p.add_argument("--price-fall", type=float, default=0.2, help="Yearly fall of the on-demand price, for the 3-year case.") p.add_argument("--seed", type=int, default=0, help="Random seed.") p.add_argument("--json", help="Also write the results to this JSON file.") args = p.parse_args() rng = np.random.default_rng(args.seed) def shaped(kind, days=365): x = rng.gamma(3, 1, days) if kind == "right-skewed (gamma)" else rng.beta(5, 2, days) x = (x - x.min()) / (x.max() - x.min()) # scale to [0, 1] lo = (args.mean - args.peak * x.mean()) / (1 - x.mean()) # then to [lo, peak] with the requested mean return lo + (args.peak - lo) * x def cost(demand, c, years=1): total = 0.0 for y in range(years): on_demand = (1 - args.price_fall) ** y if years > 1 else 1.0 total += c * args.committed_price * len(demand) + np.maximum(demand - c, 0).sum() * on_demand return total rows, best = [], [] levels = list(range(20, 101, 2)) for kind in ("right-skewed (gamma)", "left-skewed (beta)"): d = shaped(kind) for years, label in ((1, "1 year"), (3, f"3 years, on-demand price -{args.price_fall:.0%}/yr")): costs = [cost(d, c, years) for c in levels] for c, v in zip(levels, costs): rows.append({"demand": kind, "term": label, "commit": c, "cost": v / years}) bc = levels[int(np.argmin(costs))] pct = float(np.mean(d < bc) * 100) best.append({"demand": kind, "term": label, "best_commit": bc, "percentile": pct, "mean": float(d.mean())}) print(f"{kind:<22} {label:<32} best commitment {bc:>3} = P{pct:.0f} of demand (mean {d.mean():.0f})") if args.json: with open(args.json, "w") as f: json.dump({"tables": {"costs": rows, "best": best}}, f)3Run it in the folder you saved it to, with the defaults first. Then change a knob from the table below and run it again.
Terminal uv run test-the-commitment-sizing-rule.py uv run test-the-commitment-sizing-rule.py --help # every knob
| Knob · default | What it changes |
|---|---|
| --mean60 | Mean daily demand. |
| --peak100 | Peak daily demand. |
| --committed-price0.6 | Price of a committed unit, used or not. |
| --price-fall0.2 | Yearly fall of the on-demand price, for the 3-year case. |
| --seed0 | Random seed. |
Our run
Data table
| Committed units | series | Cost per year |
|---|---|---|
| 20 | 1 year | 19K |
| 22 | 1 year | 18.7K |
| 24 | 1 year | 18.4K |
| 26 | 1 year | 18.1K |
| 28 | 1 year | 17.8K |
| 30 | 1 year | 17.5K |
| 32 | 1 year | 17.2K |
| 34 | 1 year | 16.9K |
| 36 | 1 year | 16.6K |
| 38 | 1 year | 16.4K |
| 40 | 1 year | 16.1K |
| 42 | 1 year | 15.8K |
| 44 | 1 year | 15.5K |
| 46 | 1 year | 15.2K |
| 48 | 1 year | 14.9K |
| 50 | 1 year | 14.7K |
| 52 | 1 year | 14.5K |
| 54 | 1 year | 14.3K |
| 56 | 1 year | 14.3K |
| 58 | 1 year | 14.3K |
| 60 | 1 year | 14.4K |
| 62 | 1 year | 14.5K |
| 64 | 1 year | 14.8K |
| 66 | 1 year | 15K |
| 68 | 1 year | 15.3K |
| 70 | 1 year | 15.7K |
| 72 | 1 year | 16K |
| 74 | 1 year | 16.4K |
| 76 | 1 year | 16.8K |
| 78 | 1 year | 17.2K |
| 80 | 1 year | 17.6K |
| 82 | 1 year | 18K |
| 84 | 1 year | 18.4K |
| 86 | 1 year | 18.9K |
| 88 | 1 year | 19.3K |
| 90 | 1 year | 19.7K |
| 92 | 1 year | 20.2K |
| 94 | 1 year | 20.6K |
| 96 | 1 year | 21K |
| 98 | 1 year | 21.5K |
| 100 | 1 year | 21.9K |
| 20 | 3 years, on-demand price -20%/yr | 16.3K |
| 22 | 3 years, on-demand price -20%/yr | 16.1K |
| 24 | 3 years, on-demand price -20%/yr | 15.9K |
| 26 | 3 years, on-demand price -20%/yr | 15.8K |
| 28 | 3 years, on-demand price -20%/yr | 15.6K |
| 30 | 3 years, on-demand price -20%/yr | 15.5K |
| 32 | 3 years, on-demand price -20%/yr | 15.3K |
| 34 | 3 years, on-demand price -20%/yr | 15.2K |
| 36 | 3 years, on-demand price -20%/yr | 15K |
| 38 | 3 years, on-demand price -20%/yr | 14.9K |
| 40 | 3 years, on-demand price -20%/yr | 14.7K |
| 42 | 3 years, on-demand price -20%/yr | 14.5K |
| 44 | 3 years, on-demand price -20%/yr | 14.4K |
| 46 | 3 years, on-demand price -20%/yr | 14.2K |
| 48 | 3 years, on-demand price -20%/yr | 14.1K |
| 50 | 3 years, on-demand price -20%/yr | 14K |
| 52 | 3 years, on-demand price -20%/yr | 13.9K |
| 54 | 3 years, on-demand price -20%/yr | 13.9K |
| 56 | 3 years, on-demand price -20%/yr | 13.9K |
| 58 | 3 years, on-demand price -20%/yr | 14K |
| 60 | 3 years, on-demand price -20%/yr | 14.1K |
| 62 | 3 years, on-demand price -20%/yr | 14.4K |
| 64 | 3 years, on-demand price -20%/yr | 14.6K |
| 66 | 3 years, on-demand price -20%/yr | 14.9K |
| 68 | 3 years, on-demand price -20%/yr | 15.2K |
| 70 | 3 years, on-demand price -20%/yr | 15.6K |
| 72 | 3 years, on-demand price -20%/yr | 16K |
| 74 | 3 years, on-demand price -20%/yr | 16.3K |
| 76 | 3 years, on-demand price -20%/yr | 16.7K |
| 78 | 3 years, on-demand price -20%/yr | 17.2K |
| 80 | 3 years, on-demand price -20%/yr | 17.6K |
| 82 | 3 years, on-demand price -20%/yr | 18K |
| 84 | 3 years, on-demand price -20%/yr | 18.4K |
| 86 | 3 years, on-demand price -20%/yr | 18.9K |
| 88 | 3 years, on-demand price -20%/yr | 19.3K |
| 90 | 3 years, on-demand price -20%/yr | 19.7K |
| 92 | 3 years, on-demand price -20%/yr | 20.2K |
| 94 | 3 years, on-demand price -20%/yr | 20.6K |
| 96 | 3 years, on-demand price -20%/yr | 21K |
| 98 | 3 years, on-demand price -20%/yr | 21.5K |
| 100 | 3 years, on-demand price -20%/yr | 21.9K |
The cheapest commitment
| Demand | Term | Best commitment | Percentile of demand | Mean demand |
|---|---|---|---|---|
| right-skewed (gamma) | 1 year | 56 | 39.5 | 60 |
| right-skewed (gamma) | 3 years, on-demand price -20%/yr | 54 | 28.2 | 60 |
| left-skewed (beta) | 1 year | 58 | 40 | 60 |
| left-skewed (beta) | 3 years, on-demand price -20%/yr | 46 | 25.8 | 60 |
right-skewed (gamma) 1 year best commitment 56 = P39 of demand (mean 60)
right-skewed (gamma) 3 years, on-demand price -20%/yr best commitment 54 = P28 of demand (mean 60)
left-skewed (beta) 1 year best commitment 58 = P40 of demand (mean 60)
left-skewed (beta) 3 years, on-demand price -20%/yr best commitment 46 = P26 of demand (mean 60)What we found
For one year the cheapest commitment was 56 units under right-skewed demand, at P39.5 of daily demand, and 58 units (P40) under left-skewed demand: below the mean of 60 in both, at the expected P40. That is the newsvendor answer for these prices: a committed unit pays for itself only on days when demand exceeds it, and at 0.6 against 1.0 it needs to be used more than 60% of the time, which puts the optimum at the 40th percentile.
A falling on-demand price lowered the best commitment further, to 46 units (P25.8) under left-skewed demand over three years: every year the discount is worth less, so less of the demand is worth locking in.
Read it
The commitment level is a percentile, not a forecast: the price ratio sets it. With committed price c and on-demand price 1, commit up to the level that demand exceeds a share c of the time. Most teams commit at the mean or at last quarter's peak, which pays for idle units on most days.
A commitment also inverts the marginal incentive inside its period: usage below the cap is free at the margin, so optimizing below the cap is worth nothing and pulling work into the period is worth a lot. Same engineer, same workload, opposite advice, decided only by a contract.