← Back to projects

Data Science · Case study · 2026

Mapping transit deserts in Île-de-France

Île-de-France has 1,297 communes served by 75 transit operators — yet behind that density, some towns are still out of comfortable reach of public transport. This case study quantifies that gap: building a composite mobility score from open GTFS and INSEE data, then isolating the towns where the policy question is sharpest.

PythonGeoPandasShapelySciPy (cKDTree)FoliumGTFSGBFS

01

The problem

Île-de-France is an extraordinarily dense transport region — 1,297 communes, 75 operators, hundreds of train, metro, tram and bus lines. But aggregate density hides local reality. In some communes, the nearest stop is a long walk, frequency is thin, and a car is still the daily default. These are the transit deserts: not zero coverage, but coverage too weak to be a real alternative.

The policy question is harder than it looks. Public transit investment is finite, so the relevant target isn't every under-served pixel — it's communes where enough people live with too little service. This project frames that question quantitatively and produces a short, actionable list.

02

The data

Four open data sources, all stitched at the commune level:

DatasetSourceFormat
Transit timetables and stopsÎle-de-France Mobilités via transport.data.gouv.frGTFS
Population by communeINSEE via geo.api.gouv.frJSON
Commune boundariesfrance-geojsonGeoJSON
Bike-share docksVélib' Métropole (Smovengo)GBFS

A small but instructive data pitfall: INSEE's default API returns Paris as one single commune (75056), while france-geojson splits Paris into its 20 arrondissements (75101-75120). Joining naively would silently drop Paris from the map. Fix: hit INSEE's arrondissement endpoint as a second pass and concatenate.

03

From timetables to a usable grid

GTFS data ships as raw schedules: thousands of stops, hundreds of routes, millions of timetabled departures. To turn that into per-commune accessibility, the pipeline lays a regular 500-meter grid over the region and, for each grid cell, computes the distance to the nearest transit stop.

Naive distance-to-each-stop would be O(cells × stops) — too slow at this scale. The solution is a SciPy cKDTree built on stop coordinates: O(cells × log(stops)) lookups, sub-second on the full region. Cell-level distances are then aggregated up to commune level (mean distance to nearest stop), along with daily passages (summed) and the count of distinct transport modes served.

04

A composite mobility score

Three signals matter for whether transit is a real option: how close a stop is, how often something passes, and how many modes are available. They are blended into a single 0-100 score:

mobility_score = 0.40 × (1 − normalized_distance_to_stop)
               + 0.40 × log(normalized_daily_passages)
               + 0.20 × normalized_modal_diversity

Each component is min-max normalized to [0, 1] before weighting, and the final result is scaled to [0, 100]. Two design choices stand out:

  • Log-transform on frequency. Without it, hubs like Châtelet (≈10,000 passages/day) would flatten every other commune to near zero. The log makes the score sensitive at the low end, where the policy question lives.
  • 40/40/20 weights. Distance and frequency get equal weight (both equally veto a line as a real option); modal diversity matters but is the weakest single signal — a single high-frequency train station often beats five poorly-served bus stops.

05

Distribution across the region

Applied to all 1,297 communes, the score gives a clear regional picture: a strong central mass around 45-55, a long right tail of well-served urban communes, and a thin left tail of severely under-served towns.

Distribution of mobility scores across 1,297 IDF communes
Score distribution across Île-de-France. Median = 53.9. The bulk sits between 40 and 60; the highest scores (80+) are central Paris and inner-ring hubs, the lowest (<40) are rural outer communes.

The regional median (53.9) becomes the natural cut-off for what counts as “below average”.

06

Identifying priority transit deserts

A low score alone doesn't make a public policy priority — a village of 200 with no train is a different problem from a town of 10,000 with no train. The definition combines scale and deficit:

Priority transit desert = commune with ≥ 3,000 inhabitants AND mobility score ≤ regional median (53.9).

Plotting population (log scale) against score makes the filter visual — only the bottom-right quadrant qualifies:

Population vs mobility score scatter, with priority deserts highlighted
17 communes qualify as priority transit deserts. They cluster tightly: enough people to matter, but a mobility score stuck around the regional median or below.

07

Where they are

The 17 priority deserts aren't scattered randomly. They concentrate in three departments that share one structural feature: no direct heavy-rail backbone.

Priority deserts by department
10 of the 17 priority deserts are in Seine-et-Marne (77). Yvelines (78), well covered by Transilien lines N and U, has zero. The map reads like a transit-investment heatmap.
Top 10 priority deserts by population
The largest under-served town is L'Isle-Adam (12,493 inhabitants, score 51.0). Several Seine-et-Marne towns follow (Fontenay-Trésigny, La Ferté-Gaucher, Jouarre, Châtelet-en-Brie). Milly-la-Forêt has the lowest score (43.1) of the group.

08

The pipeline and the interactive map

The end-to-end pipeline is four numbered scripts, runnable in order from a clean checkout — no proprietary GIS software (no ArcGIS, no QGIS):

src/
├── 01_transit_network.py    GTFS ingestion + stop mapping by mode
├── 02_accessibility_grid.py 500m grid, nearest-stop via cKDTree, commune aggregation
├── 03_mobility_score.py     Composite score + priority deserts
└── 04_dashboard.py          Interactive Folium dashboard

The output is a Folium-based interactive map: choropleth of the mobility score over commune boundaries, transit stops overlaid by mode, and the priority deserts called out by name. Try it inline below — pan around the region and click any commune for its raw score components.

09

Takeaways

StepSkill demonstrated
Ingesting GTFS + INSEE + GeoJSON + GBFSReal-world open-data plumbing, with the Paris arrondissement gotcha caught
500m grid + cKDTree nearest-stopSpatial scaling beyond a naive O(n²)
Composite score with log-transformScore design with explicit policy intent
Threshold rule (population × median)Translating an analytical question into a decision rule
Folium interactive map + GitHub PagesShipping the work to a non-technical reader
100% open-source stackReproducibility — anyone with Python can run the whole pipeline

What I take away: the hardest part of this kind of work isn't the geospatial code — it's deciding what counts as “a problem worth flagging”. The 40/40/20 weights and the (population × median) cut-off are opinions, not facts. Stating them clearly is what turns an analysis into a decision tool.