Skip to content

About

Zones de chalandise et choix d'un 5e magasin avec un modèle de Huff. Villes réelles (GeoNames), ventes simulées. Python.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

🗺️ Catchment Area Analysis — Where to open the next store

A geospatial analysis of a retail chain's customers that answers the question every multi-site business eventually asks: "Where are my customers, and where should the next store go?" — mapped store catchment areas, quantified how local the business really is, and a gravity (Huff-style) model that ranks candidate cities for expansion.

Objectif : from customer + sales data, map each store's catchment area, measure how much revenue is truly local, and recommend the location of the 5th store.


📦 The data

Geography is real, sales are synthetic but realistic — and 100 % reproducible (fixed seed).

Source What Detail
Real — GeoNames (geonamescache) 692 French cities name, population, latitude, longitude
Synthetic — spatial gravity model 12,000 customers · 78,221 orders · €2.93M demand drawn ∝ population × (0.12 + e^(−dist/18km))

A fictional chain, Comptoir & Co, runs 4 stores: Paris, Lyon, Marseille, Lille. Each customer is anchored on a real city; each order carries a basket amount (log-normal, ~€38 median). Because demand is generated with a distance-decay rule, the customer map behaves like a real physical retailer — and the analysis then has to recover that structure from the data.

Why synthetic? There is no open French customer-level dataset at postal-code granularity. Anchoring simulated sales on real geography keeps the maps credible and the method identical to a real engagement.


🔍 Exploratory analysis — where the customers are

  • A hyper-concentrated footprint. The median customer lives just 7.9 km from their nearest store.
  • Paris dominates. Customers whose nearest store is Paris generate 65 % of revenue (Marseille 15 %, Lyon 13 %, Lille 6 %).
  • Demand tracks the shopfronts. Colour each customer by nearest store and the four stores light up as dense clusters — with large empty stretches in the West and South-West.

Customer map Sales EDA


🧭 The analysis

1. How local is the business? (catchment radius)

Sort customers by distance to their nearest store, accumulate revenue:

Within radius Share of revenue
≤ 10 km 61.7 %
≤ 20 km 80.8 %
≤ 30 km 86.5 %
≤ 100 km 91.0 %

80 % of all revenue comes from customers living within 19 km of a store. This is a local business: a store's power is its immediate surroundings, and beyond ~30 km the brand barely exists.

Revenue vs distance

2. White zones & the next-store recommendation

For every city we compute an underserved potential = population × (1 − access), where access = e^(−distance/18km) is the pull of the nearest store. Then a Huff-style gravity model scores each candidate city by the demand a new store would capture — crucially counting only the incremental reach (max(0, new_pull − current_access)), so it never double-counts demand already served by an existing store (no cannibalisation).

Rank Candidate Population Extra revenue / yr People within 40 km
🥇 Toulouse 511,684 +€111,701 670,194
🥈 Bordeaux 265,328 +€104,819 709,684
🥉 Nice 342,669 +€102,121 890,316
4 Nantes 325,070 +€91,458 561,540
5 Strasbourg 274,845 +€66,455 407,834

White zones

Recommendation: open store #5 in Toulouse. The whole South-West is the blind spot — Toulouse and Bordeaux, France's 4th and 9th largest cities, both sit 300+ km from the nearest existing store. Toulouse alone represents ~€112k/yr of currently-unserved demand.


💡 The business lesson

You don't grow a local business by squeezing the zones you already own — 80 % of the revenue is already locked inside a 19 km ring around each store. You grow it by planting a flag where the demand exists and you're simply absent. A map turns "we should expand someday" into "open in Toulouse, here's the number."


🗂️ Repo structure

.
├── data.py            # real cities (GeoNames) + synthetic sales (gravity model)
├── common.py          # palette, data loading, France backdrop, haversine
├── analyse.py         # EDA + customer map           -> carte_clients.png, eda_ventes.png
├── chalandise.py      # catchment radius + Huff model -> distance_revenu.png, zones_blanches.png
├── data/              # generated CSVs (not versioned)
├── requirements.txt
└── README.md

▶️ Run it

python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\Activate.ps1
pip install -r requirements.txt

python data.py                   # build the dataset (seeded, reproducible)
python analyse.py                # EDA + customer map
python chalandise.py             # catchment analysis + next-store recommendation

🚀 Next steps

  • Drive-time isochrones (road network) instead of straight-line distance.
  • Cannibalisation check: revenue an existing store loses when a neighbour opens.
  • Enrich with INSEE income / spend data to weight demand by purchasing power.
  • An interactive Folium map for exploration.

🧰 Stack

Python · pandas · numpy · matplotlib · geonamescache

Geography from the open GeoNames database. Sales are synthetic (seeded) and anchored on real cities — same method, real or simulated data.

About

Zones de chalandise et choix d'un 5e magasin avec un modèle de Huff. Villes réelles (GeoNames), ventes simulées. Python.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages