Skip to content

The Bullwhip Effect: steady demand, wild orders, and finding who amplifies

The data in this project is synthetic — generated locally by data/make_data.py, with no external source. That is deliberate: the bullwhip effect is a property of ordering policies, so a seeded simulator makes the amplification exact and reproducible, and lets the grader recover the true per-stage ratios — which real, confounded supply-chain data never cleanly gives. No signup, no downloads, no GPU.

Before you start

New to the bullwhip effect? Start here.

A supply chain is a line of stages — a retailer buys from a wholesaler, who buys from a distributor, who buys from the factory. Each stage orders from the one above it to restock what it sold.

The bullwhip effect is a famous, well-documented surprise: even when the end customer’s demand is steady, the orders passed up the chain swing more and more wildly at each stage. The factory ends up seeing violent order swings for a product whose actual demand barely moved. It’s not irrationality — it falls out of ordinary forecasting and restocking. Each stage forecasts the (already jumpy) orders it receives, adds safety stock, and refills a lead-time pipeline, and in doing so amplifies the swing it passes on.

The way to measure it is the bullwhip ratio at a stage:

bullwhip ratio = variance of the orders this stage PLACES
-------------------------------------------
variance of the demand this stage RECEIVES

A ratio above 1 means this stage amplified. The crucial subtlety: you measure it per stage — each echelon against the demand it saw (the stage just below it) — not everyone against the end customer. Measured cumulatively, the number just grows toward the top and hides which stage is the real amplifier.

What you’ll learn: simulate a chain and watch the ratio compound stage by stage, then diagnose a chain from its order log and pinpoint the echelon amplifying most. Section 3 lists exactly what you’ll be able to do.

New to Python too? See Start Here — it starts from zero, and shows how to run this in your browser with nothing to install.

1. The Brief

Your factory keeps whipsawing between overtime and idle lines, but the sales team swears customer demand is flat. You’ll reproduce the mechanism — simulate a four-stage chain and show steady demand turning into ~100× more variable factory orders — then, handed the order log of a different chain, find the single echelon that’s amplifying the most so it can be fixed.

The bullwhip effect is one of the most cited results in operations (Procter & Gamble’s diapers, the MIT Beer Game) precisely because it’s counterintuitive and expensive: it drives excess inventory, stockouts, and capacity thrash all at once. Being able to measure it per stage — and name the culprit — is the first step any supply-chain analyst takes before proposing a fix.

2. Difficulty & time

Difficulty 5 / 10. A short simulation and a variance ratio — but with real moving parts: an order-up-to policy (forecast + safety stock + lead-time pipeline), a chain of echelons, and the per-stage-vs-cumulative distinction that’s easy to get subtly wrong. Calibrated against the ledger (G10): a peer of reorder-point-trap (5) and analytics-with-duckdb (5) — an operations model with a genuine trap, more involved than the single-mechanic 3s but not a full algorithm. It sits alongside mtouch-attribution (5) in needing a couple of correct modelling choices, and below vendor-dedupe (6), which layers blocking and fuzzy matching. The trap (measuring amplification cumulatively and fingering the wrong stage) is one you can walk straight into.

Time is separate from difficulty: Part A is about an hour; Part B is 2–3 hours. The scripts run in under a second — the time is understanding the policy and the ratio.

3. What you’ll be able to do after

  • Simulate an order-up-to echelon — forecast, safety stock, lead-time pipeline — and chain several together.
  • Compute the bullwhip ratio and explain why an order-up-to policy amplifies demand variance.
  • Attribute amplification per stage — and explain why a cumulative ratio hides the culprit.
  • Diagnose a chain from its order log — find the echelon amplifying most and reason about why (here, order batching).

The finished result

By the end of Part A you’ll have watched steady customer demand turn into wildly swinging factory orders — and measured the amplification stage by stage:

end-customer demand: mean 99, variance 139 (calm)
per-stage bullwhip: retailer 2.79 wholesaler 3.94 distributor 3.71 factory 2.62
cumulative amplification: 2.8 -> 11.0 -> 40.8 -> 107x (variance 139 -> 14,856 at the factory)
longer lead time 1->2->4: retailer bullwhip 2.05 -> 2.79 -> 4.71

Steady demand in, ~107× wilder orders out — and it all falls out of an ordinary order-up-to policy, not anyone behaving irrationally.

4. Prereqs & time box

You can write a function and a loop, and use numpy/pandas (var, groupby). No supply-chain background needed — the primer above covers the vocabulary. A little comfort with the idea of variance helps.

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: 60–90 minutes. Hard cap.
  • Part B — Your Turn: 2–4 hours. Hard cap.

5. Setup & data

The data is synthetic and generated locally — see the banner above and SOURCES.md for full provenance. One command creates both parts’ data:

Terminal window
python data/make_data.py

That writes two CSVs into data/ (git-ignored; regenerate anytime, byte-for-byte identical because the seed is fixed):

  • Part Ademand_a.csv (300 periods: t, demand). Steady end-customer demand your simulation is driven by.
  • Part Bchain_b.csv (stage, t, order). The order log of a 5-echelon chain: stage 0 is the customer demand, stages 1–5 are each echelon’s orders. The first 30 warm-up periods are already dropped, so it’s at steady state.

6. Part A — Guided Build

You’ll build the chain and measure the amplification. Run the whole thing, or (better) one # %% cell at a time — see Start Here:

Terminal window
python part_a/simulate_chain.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/simulate_chain.py. The spine:

Step 1 — The customer demand. Steady, low variance.

Checkpoint: mean ≈ 100, variance ≈ 139. The customer is calm.

Step 2 — One order-up-to echelon. Simulate the retailer; measure its bullwhip ratio.

Checkpoint: ratio ≈ 2.8 — the retailer’s orders already swing ~3× as much as demand.

Step 3 — Four echelons. Feed each stage’s orders as the next stage’s demand.

Checkpoint: per-stage ratios ~2.8, 3.9, 3.7, 2.6; cumulative ≈ 107× by the factory (variance 139 → ~14,900).

Step 4 — A driver: lead time.

Checkpoint: retailer bullwhip climbs 2.0 → 2.8 → 4.7 as lead time goes 1 → 2 → 4. Writes part_a/bullwhip_a.csv.

Why this and not that

  • Why order-up-to amplifies. Each period the stage re-forecasts (from jumpy incoming orders) and adjusts its target stock; the order it places is demand plus the change in target. That extra term is the amplification — it reacts to the forecast moving, not just to demand.
  • Why it compounds up the chain. Each stage’s orders — already more variable than its demand — become the next stage’s demand. Amplification multiplies stage over stage, which is why the factory sees the worst of it.
  • Why lead time makes it worse. A longer pipeline means each order must cover more future demand, so any forecast error is scaled up by a bigger factor. Shortening lead times (and steadying forecasts) is a real lever against the bullwhip.

Functions you’ll meet (and where to read more)

FunctionWhat it does hereDocs
collections.dequeModel the lead-time pipeline of in-transit ordersdeque
np.mean / np.stdMoving-average forecast and safety stockmean
np.varVariance, for the bullwhip ratiovar
df.groupby("stage")Split the Part B log into per-stage seriesgroupby

If something looks off

  • The per-stage ratios rise then fall (2.79, 3.94, 3.71, 2.62) — did the factory stop amplifying? No, that’s the expected shape. Each per-stage ratio compares a stage only to the demand it received, and every stage roughly triples it — so what compounds is the cumulative ratio (2.8 → 107×). By the factory, orders swing ~107× more than the customer’s demand (variance 139 → 14,856) even though the factory’s own multiplier (2.62) is the smallest. Per-stage tells you which echelon amplifies; cumulative tells you how bad it has gotten — Part B turns on keeping them apart.
  • 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.

Same core skill, a chain you didn’t build. chain_b.csv is the order log of a 5-echelon chain — stage 0 is the customer, stages 1–5 are the echelons’ orders. Measure the per-stage bullwhip ratio for each echelon and name the one amplifying most. One echelon behaves very differently from the smooth order-up-to of Part A, and it’s the culprit. The starter (part_b/starter.py) measures each stage against the end customer — a cumulative ratio — and so reconstructs only 1 of 5 stages and fingers the wrong echelon.

The transfer: in Part A you knew the policy and drove the sim. Here you only have the log, the stages are heterogeneous, and you must compare each echelon to the demand it saw (the stage below it) — not to the customer.

Acceptance criteria (checkable)

Your solution writes into part_b/out/:

  • bullwhip.csv — columns stage, ratio for stages 1–5 (per-stage bullwhip).
  • report.json — with keys stages, worst_stage, achieved_metric (you can leave achieved_metric null; the grader scores you).

Then:

Terminal window
python part_b/starter.py
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.

You’re done when both tests pass: you reconstruct ≥ 90% of the five per-stage ratios within 2% (the reference gets all five), and your worst_stage matches the true worst amplifier. The cumulative shortcut gets 1 of 5 and names the wrong stage.

Constraints — what must be true of your solution, not how to get there

  • Measure each stage against the stage directly below it, not against the end customer.
  • Report a ratio for every echelon (stages 1–5).
  • Identify worst_stage as the echelon with the highest per-stage ratio.
  • Don’t reorder or drop stages — the log is already at steady state.

Hints

Hint 1 — pair each stage with the one below

Split the log into a series per stage. The bullwhip for stage s is var(orders[s]) / var(orders[s-1]) — where stage 0 is the customer demand.

Hint 2 — why the starter is wrong

The starter divides by var(orders[0]) (the customer) for every stage. That’s the cumulative amplification, which only equals the per-stage ratio for stage 1 and always grows toward the top.

Hint 3 — the culprit (still not the code)

Once you have per-stage ratios, worst_stage is simply the stage with the largest one. It won’t be the top of the chain — look for the echelon whose orders are spiky (it batches its orders instead of ordering smoothly).

8. Self-check

You don’t need the answer key. You’re done when all of these hold:

  • pytest reports both green — all five ratios within 2% and the right worst_stage.
  • Your worst stage is in the middle of the chain, not the top — if you picked the last stage, you’re measuring cumulatively.
  • Re-running from python data/make_data.py reproduces the same numbers — the data is seeded.

And the tell-tale: the culprit stage’s per-stage ratio should tower over its neighbours (roughly 18× vs 2–4×). If all your ratios look similar and grow monotonically, you computed the cumulative ratio, not the per-stage one.

9. Stretch

  • Explain the culprit. Plot the culprit echelon’s orders against its incoming demand. The spikes (orders every few periods, zero in between) are order batching — quantify how much of its variance comes from the batching interval.
  • A second cause. Add price promotions to the customer demand (occasional spikes as buyers forward-buy) and measure how much extra bullwhip they inject — the other classic driver.
  • (Genuinely hard) Dampen it. Re-simulate the Part A chain with order smoothing (order only a fraction of the inventory gap each period) and measure the bullwhip reduction — then find where service (fill rate) starts to suffer. The variance-vs-service tradeoff is the real design problem.

10. Ship it

Put this in a portfolio repo and it counts. In that repo’s README:

  • State the finding in one sentence an ops director gets: “customer demand varied by X, but factory orders varied by 100× that — and one echelon (order batching) is responsible for most of the amplification.”
  • Show the per-stage bullwhip ratios and call out the culprit, next to the cumulative curve so the reader sees why per-stage matters.
  • Include the order-up-to simulator and one paragraph on why steady demand still produces wild orders. That paragraph is the part that signals you understand the mechanism, not just np.var.

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
DatasetSynthetic multi-echelon supply-chain order flows (Part A 300-period demand; Part B 5-echelon order log)
TypeSynthetic
Generated bydata/make_data.py (this repo), seeds 20260730 (A) and 20260731 (B)
Source URLNot applicable — generated, not fetched
LicenseNot applicable — synthetic, no third-party rights
Access / generation date2026-07-18
RedistributionPermitted — synthetic; output regenerated from the seeded script, never committed
robots.txt checkedNot applicable — no scraping
ToS URLNot applicable — no external source

Synthetic data teaches the mechanic; it does not prove a finding about the real world. The amplification and the batching echelon were generated on purpose — recovering them demonstrates the technique, not a claim about any real supply chain.

Next up

Finished this one? Continue the Supply Chain track:

The reorder-point trap: why ordering to average demand runs you out of stock  ·  Browse all courses