Skip to content

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.5
naive plan — stock the average, Q = 80 -> average daily profit 203.77
critical fractile 0.667 -> Q = 89 (above the average 80) -> average daily profit 206.98
profit sweep over every Q peaks at Q = 89 -> exactly the fractile's pick

Stocking 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 once python -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):

Terminal window
python data/make_data.py

That 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:

Terminal window
python data/make_data.py # once
python part_a/optimize_stock.py

Prefer 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)

FunctionWhat it does hereDocs
pd.read_csvLoad the demand historyread_csv
np.quantileThe critical-fractile quantity from the demand distributionnp.quantile
np.minimum / np.maximumSplit each day into units sold vs units left overnp.minimum
ConceptThe newsvendor model and the critical fractileNewsvendor 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 # TODO it 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:

Terminal window
python part_b/starter.py # writes part_b/out/
pytest part_b/test_solution.py -q

If pytest reports everything as skipped, it just means the output files don’t exist yet — run python part_b/starter.py first (once your solution fills in the TODOs), then re-run.

Your solution writes into part_b/out/:

  • report.jsonorder_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.

FieldValue
DatasetsSynthetic — croissant demand (Part A); sandwich demand (Part B)
Data typeSynthetic (seeded, reproducible)
Generated bydata/make_data.pynumpy.random.default_rng (seeds 20260718 / 555 / 999)
Source / licenseNot applicable — synthetic, generated in-repo; no third-party rights
Access / verification date2026-07-18
RedistributionNot applicable — generator committed, CSV output never committed
robots.txt / ToSNot 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