Skip to content

Site the Depot: pick the one depot that's actually closest to the demand

This is a Challenge, not a tutorial. There’s no guided walkthrough — just a realistic mess, a question, and a test that tells you when you’ve nailed it. You bring the approach. You don’t need to have done any course here first; if the assumed skills below sound doable, dive in.

The situation

A regional distributor around the North Sea is opening one new depot to serve all of its retail stores, and has five candidate sites under lease negotiation. Ops wants the site that minimises how far, on average, a delivery has to travel — but “on average” has to mean weighted by how much each store actually orders, because a depot exists to move volume, not to be equidistant from dots on a map.

You’re handed two files in data/:

  • customers.csvcustomer_id, lat, lon, weekly_demand for every store.
  • candidates.csvcandidate_id, lat, lon for the five leased sites (D1D5).

Two things make this easy to get confidently wrong:

  • The map is a sphere, not a grid. Around 53° N a degree of longitude covers only ~0.6 of the ground distance of a degree of latitude. Rank the sites by a straight-line distance across the raw lat/lon numbers and a depot that’s mostly east of the demand looks far when it’s really the closest. The distance you rank by has to be true ground distance — figuring out how to get that is part of the challenge.
  • Most stores are not most of the demand. The big northern cluster is a crowd of small stores; a handful of southern stores each order enormous weekly volume. Count every store the same and the depot drifts north to the crowd; account for how much each store actually orders (weekly_demand) and it belongs in the south-west.

Fix only one of these and you’ll still choose the wrong site.

Your deliverable

Write into out/:

  • report.json — with:
    • chosen_depot_id — the candidate that is genuinely closest to where the demand is,
    • weighted_avg_distance_km — that winning depot’s average distance to the stores in km, counting each store by how much it orders,
    • naive_euclidean_choice — the site you’d pick if you kept the demand-weighting but measured distance the crude way (straight-line across the degree grid) — proof you know it differs,
    • naive_unweighted_choice — the site you’d pick with the correct distance but ignoring demand.
  • depot_scores.csv (optional, recommended)candidate_id, weighted_avg_distance_km for all five, so you can eyeball the ranking.

Run it

Terminal window
python data/make_data.py # once — generates data/ (synthetic, seeded)
python starter.py # your working file; it starts naive and wrong
pytest test_solution.py -q

The starter.py already produces the right output shape with the wrong numbers — it ranks the sites by Euclidean-on-degrees and treats every store equally. Fix the three # TODOs. You’re done when all three tests pass: the chosen depot is the true best, its demand-weighted haversine distance matches, and you’ve correctly named the site the Euclidean shortcut would have picked.

Why this is a cross-domain challenge

It’s deliberately not solvable from one skill set:

  • Geospatial — get the distance right. Latitude/longitude are angles on a sphere; a degree of longitude shrinks with latitude, so Euclidean-on-degrees mis-ranks nearby sites. Haversine (or an equivalent projected metric) is the fix.
  • Supply-chain — get the objective right. Facility siting minimises demand-weighted travel, not average distance to locations. The depot follows the volume.

Correct haversine but unweighted, and you pick the depot nearest the crowd of small stores. Demand-weighted but Euclidean-on-degrees, and the flat-grid distortion picks the wrong nearby site. Only both together land the right depot — and the acceptance test checks both the depot and the weighted-distance value.

Self-check (before you trust it)

  • Your weighted_avg_distance_km should be in the hundreds of km — if it’s a single-digit number you’re reporting degrees, not kilometres (you didn’t switch to haversine).
  • naive_euclidean_choice and naive_unweighted_choice should each be a different site from chosen_depot_id. If any of them equals your answer, you haven’t separated the two traps.
  • Re-running python data/make_data.py never changes the data (it’s seeded), so your numbers shouldn’t move between runs.