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.csv—customer_id, lat, lon, weekly_demandfor every store.candidates.csv—candidate_id, lat, lonfor the five leased sites (D1…D5).
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_kmfor all five, so you can eyeball the ranking.
Run it
python data/make_data.py # once — generates data/ (synthetic, seeded)python starter.py # your working file; it starts naive and wrongpytest test_solution.py -qThe 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_kmshould 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_choiceandnaive_unweighted_choiceshould each be a different site fromchosen_depot_id. If any of them equals your answer, you haven’t separated the two traps.- Re-running
python data/make_data.pynever changes the data (it’s seeded), so your numbers shouldn’t move between runs.