The newsvendor problem: how much to stock when running out and over-ordering cost different amounts
Data: both parts use synthetic data, generated locally by
data/make_data.py(seeded, reproducible). Nothing is downloaded; no signup, no GPU. Synthetic is essential: to score a stocking decision you must be able to simulate its profit against demand you know, including demand the decision didn’t get to see.
Before you start
New to this? Two minutes of vocabulary, because the whole project turns on it.
You have to commit to a quantity before you know demand — bake the croissants this morning, print the newspapers tonight — and whatever you pick, demand will be higher or lower. Two different mistakes, with two different price tags:
- Underage cost — you stocked one too few: you lose the profit you’d have made on
that sale,
price − cost. - Overage cost — you stocked one too many: the leftover is marked down or dumped,
losing
cost − salvage. - Critical fractile — the newsvendor rule. Stock up to the point where
P(demand ≤ Q) = underage / (underage + overage). In words: cover demand up to the quantile set by how the two mistakes compare. If running out hurts more, that fraction is above 0.5 and you stock above average; if over-ordering hurts more, it’s below 0.5 and you stock below average.
A tiny example — if selling one more earns 3 but dumping one costs 1.5, running out is twice as painful, the fractile is 3/(3+1.5) = 0.67, and you deliberately stock past the average to protect the fat margin. The average demand never enters that reasoning.
What you’ll learn: to turn the two costs of being wrong into a single stocking quantity, to see why “stock the average” is almost never right, and to know which direction to lean.
New to Python? Section 4 (“Where to write and run code”) starts from zero.
1. The Brief
You’ll take a year of a bakery’s daily croissant demand and find the number to bake each morning. The obvious answer — bake the average — leaves money on the table, because running out forfeits a bigger margin than over-baking wastes, so the profit-maximising quantity sits above the mean; a simulation confirms the critical-fractile pick lands right on the peak. Then, in Part B, you’ll face a product where the maths flips — fresh sandwiches that are expensive to make and worthless unsold — and the right move is to stock below average, a decision you’ll be judged on against a held-out year of demand.
This is one of the oldest results in operations, and it’s everywhere a perishable or time-boxed decision lives: bakeries, fashion buys, vaccine doses, cloud capacity, a restaurant’s prep list. The trap is universal too — people anchor on the forecast (the average) and adjust by gut. The newsvendor rule replaces the gut with the one number that matters: the ratio of the two costs of being wrong. Getting the direction right — stock above or below the forecast — is the difference between a plan and a guess.
2. Difficulty & time
Difficulty 4 / 10. It sits just above the difficulty-3 builds (#silent-row-loss, #idempotent-loads, #benford-fraud): beyond a single mechanic, it asks you to reason about two asymmetric costs, derive a quantity from them, and confirm it against a simulation — and Part B flips the cost structure so the answer moves to the other side of the mean, a genuine transfer rather than a re-run. It stays below the difficulty-5 group (#reorder-point-trap, #accuracy-is-lying, #hidden-communities): the core is one formula (the critical fractile) and Part A’s path is fully specified. It’s a different problem from #reorder-point-trap despite both being inventory — that one sets safety stock for a service level across many cycles; this one maximises profit for a single period. Anchored to the rubric’s 3–4 band and those named ledger projects (G10).
Time is separate from difficulty: the scripts run in under a second (measured Part A wall-clock 0.66s). Part A is about 45 minutes; Part B is 2 hours of your own work.
3. What you’ll be able to do after
- Turn the two costs of being wrong (underage, overage) into the critical fractile and the profit-maximising order quantity.
- Explain why “stock the average” is usually wrong, and predict which side of the average the right answer is on from the cost structure alone.
- Simulate the profit of any stocking policy against a demand series, and confirm the formula’s pick matches the simulated optimum.
- Make the decision from history and stand by it against demand you haven’t seen.
The finished result
By the end of Part A you’ll have turned the two lopsided costs of being wrong into one stocking quantity — and watched a full profit sweep land on that exact number:
underage (stock one too few) 3.0 vs overage (stock one too many) 1.5naive plan — stock the average, Q = 80 -> average daily profit 203.77critical fractile 0.667 -> Q = 89 (above the average 80) -> average daily profit 206.98profit sweep over every Q peaks at Q = 89 -> exactly the fractile's pickStocking 89 rather than the average 80 wins because a stockout forfeits twice what an over-bake wastes — and in Part B the costs flip, so the same rule stocks below the mean.
4. Prereqs & time box
You can write a function and use a numpy array or a pandas column. No operations or statistics background is assumed beyond “a quantile is the value below which a given fraction of the data falls,” which is all the fractile needs.
Where to write and run code
Two ways to run this project — pick either:
- In your browser (no install). If the project page shows a ▶ Run button, click it to run Part A right here — nothing to set up. Part A uses only pandas and numpy, which run in the browser.
- On your computer. The one-time setup — installing Python, downloading the code from the
course site (no GitHub, no
git), opening a terminal, creating a virtual environment, installing the requirements, and running Part A step by step — is covered once in Start Here. Come back here oncepython -c "import pandas"runs without error. Part B (writing your own solution) needs this local setup.
Time box
- Part A — Guided Build: 30–60 minutes. Hard cap 90.
- Part B — Your Turn: 1.5–2.5 hours. Hard cap 4.
5. Setup & data
Both datasets are synthetic, generated by one seeded command (byte-for-byte reproducible, git-ignored, never committed):
python data/make_data.pyThat writes data/demand_a.csv (a year of daily croissant demand) and
data/demand_b.csv (a history of sandwich demand). The held-out future demand your Part
B decision is scored against is documented in data/make_data.py and used by the
grader — not handed to you.
6. Part A — Guided Build
Run the full script and read along:
python data/make_data.py # oncepython part_a/optimize_stock.pyPrefer the browser? If this page has a ▶ Run button, click it instead of the command above — Part A runs in your browser with nothing to install. The STEP output and every checkpoint below are identical either way.
The full, commented script is in part_a/optimize_stock.py.
Croissant economics: price 5, cost 2, salvage 0.5 (leftovers marked down).
Step 1 — The two mistakes cost different amounts.
Checkpoint: underage (one too few) costs 3.0; overage (one too many) costs 1.5. Running out is twice as painful, so lean toward baking more than average.
Step 2 — The naive plan: bake the average.
Checkpoint: stock the mean (80) → average daily profit ≈ 203.8. Stocking the mean runs you out about half the days, and each stockout forfeits the fat 3.0 margin.
Step 3 — The critical-fractile plan.
Checkpoint: fractile = 3 / (3 + 1.5) = 0.667 → stock the 66.7th percentile = 89 (above the mean) → profit ≈ 207.0.
Step 4 — Sweep every Q to confirm.
Checkpoint: simulated profit peaks at Q = 89 — exactly the fractile’s pick. The formula lands you on the optimum without sweeping, and both clear the naive-mean profit.
Why this and not that
- Why not just stock the average (the forecast)? The average minimises the expected miss, but you’re not paid on the size of the miss — you’re paid on profit, and the two directions of miss have different price tags. When they’re unequal, the profit peak moves off the average, toward whichever mistake is cheaper.
- Why the fractile, not “add a safety margin by feel”? The fractile is the exact
break-even: at quantity Q, one more unit is worth stocking only if the chance of
selling it (
P(demand > Q)) times the underage cost beats the chance of dumping it times the overage cost. Solving that gives the fractile — no guessing. - Why simulate as well as apply the formula? The formula assumes you know the demand distribution; the simulation checks the decision against the actual demand you have. Here they agree, which is the reassurance — but when demand is skewed or lumpy, simulating guards against a distribution assumption that doesn’t hold.
Functions you’ll meet (and where to read more)
| Function | What it does here | Docs |
|---|---|---|
pd.read_csv | Load the demand history | read_csv |
np.quantile | The critical-fractile quantity from the demand distribution | np.quantile |
np.minimum / np.maximum | Split each day into units sold vs units left over | np.minimum |
| Concept | The newsvendor model and the critical fractile | Newsvendor model (Wikipedia) |
If something looks off
- My Part B test fails with a profit around 55.7, below the 59.0 floor — what did I miss? That’s
the starter still stocking the mean (its placeholder is
order_quantity = round(hist.mean())). These sandwiches cost 7 to make and salvage nothing, so over-making is the expensive mistake: fill in the TODOs to use the critical fractile (here 0.30), which stocks below the mean, and the profit clears the floor. - A checkpoint number doesn’t match. The data is seeded, so the numbers are exact. Re-run
python data/make_data.py(you may have skipped or edited it), then re-run Part A. - Setup problems (Python not found,
pip,FileNotFoundError, re-activating the venv): see the troubleshooting list in Start Here.
7. Part B — Your Turn
Part B is the optional “your turn” half — a guided fill-in-the-blank, not a blank page. You get a working starter (
part_b/starter.py) that already handles the easy version; your job is to finish the# TODOit marks out. More open than Part A’s step-by-step, but you are never staring at an empty file. If you finished Part A and want to prove the skill, this is where it happens. New to Python? It’s completely fine to stop after Part A and come back later.
Different product, flipped maths. A cafe makes fresh sandwiches: price 10, cost 7,
salvage 0 — they’re expensive to make and unsold ones are thrown away. You have a year
of demand history in data/demand_b.csv. Decide how many to make.
Same core skill as Part A (the critical fractile), but now over-making is the expensive mistake, so the fractile is below 0.5 and the right quantity is below average demand. And this time you’re judged the way reality judges you: your single decision is scored against a held-out year of demand you never see.
Acceptance criteria (checkable)
Fill in part_b/starter.py, then:
python part_b/starter.py # writes part_b/out/pytest part_b/test_solution.py -qIf
pytestreports everything asskipped, it just means the output files don’t exist yet — runpython part_b/starter.pyfirst (once your solution fills in the TODOs), then re-run.
Your solution writes into part_b/out/:
report.json—order_quantity,profit_on_future,achieved_metric.
You’re done when the tests pass. The grader simulates your order quantity against a held-out year of demand (recovered from the seed, never shown to you) and scores the average daily profit. It requires profit ≥ 59.0. The reference (critical fractile) earns 60.8; stocking the mean earns only ~55.7 and fails, because over-making these costly sandwiches sinks it.
Constraints — what must be true, not how to get there
- Decide from the history, then live with it against unseen demand — don’t peek at the
future in
make_data.py. - Use the cost structure, not a gut margin. Here overage (7) dwarfs underage (3), so the fractile is 0.3 and the quantity is below the mean.
- Your reported
profit_on_future(if you fill it) must match what your order actually earns (the grader recomputes it).
Hints
Hint 1 — the direction
Work out underage (price − cost) and overage (cost − salvage). Overage is much larger here, so the fractile underage/(underage+overage) is well below 0.5 — meaning the right quantity is below average demand, not above. That’s the opposite of Part A.
Hint 2 — the quantity
Once you have the fractile, the order quantity is that quantile of the demand history:
np.quantile(history, fractile), rounded to a whole number.
Hint 3 — sanity check (still not the code)
If your quantity is above the mean, re-check which cost is bigger — for these sandwiches you should be stocking noticeably below average. You can also simulate your candidate Q against the history (as in Part A) to see the profit before you commit; the held-out year behaves like the history because they’re drawn the same way.
8. Self-check
You don’t need the answer key. You’re done when:
- You can state, in one sentence, why stocking the average is not the profit-maximising choice.
- You can predict the direction (above or below the mean) from the two costs, and yours matches for both products.
- Your Part B order clears the profit floor on the held-out demand, and you can explain why over-stocking would have cost you.
9. Stretch
- Service level from the fractile. The critical fractile is the probability you don’t run out (the “in-stock” service level). Compute it for both products and connect it to how #reorder-point-trap sets safety stock for a target service level.
- Skewed demand. Regenerate demand from a lognormal (a long right tail, common for sales) instead of a normal, and show the fractile still works while “mean plus a margin” misleads.
- (Genuinely hard) Price the salvage decision. Let salvage value be a choice (a markdown you set), and jointly pick the order quantity and the markdown that maximise expected profit. Now two decisions interact.
10. Ship it
Put this in a portfolio repo and it counts. In that repo’s README:
- Lead with the one-liner anyone gets: “‘Stock the forecast’ is almost always wrong — here’s how the ratio of two costs tells you exactly how far above or below to stock, and the profit it wins.”
- Show both products side by side: same method, opposite decisions (bake above average vs. below), with the profit each earns vs. stocking the mean.
- State the rule in one sentence: the critical fractile turns “how much do the two mistakes cost?” into the order quantity. That’s the judgment that signals you can plan under uncertainty.
Keep make_data.py and the tests in the repo so anyone can reproduce your numbers from
zero.
11. Sources
Rendered from SOURCES.md.
| Field | Value |
|---|---|
| Datasets | Synthetic — croissant demand (Part A); sandwich demand (Part B) |
| Data type | Synthetic (seeded, reproducible) |
| Generated by | data/make_data.py — numpy.random.default_rng (seeds 20260718 / 555 / 999) |
| Source / license | Not applicable — synthetic, generated in-repo; no third-party rights |
| Access / verification date | 2026-07-18 |
| Redistribution | Not applicable — generator committed, CSV output never committed |
| robots.txt / ToS | Not applicable — nothing is scraped or fetched |
The data is synthetic and teaches the mechanic (single-period stocking via the critical
fractile); the numbers describe a generated world, not a real bakery. The generative
process is documented in data/make_data.py.
Next up
Finished this one? Continue the Supply Chain track:
The Bullwhip Effect: steady demand, wild orders, and finding who amplifies → · Browse all courses