Neighbourhood Fit Score

Un moteur de score de quartiers conçu pour les expatriés UE qui s’installent à Bruxelles — 724 secteurs statistiques, 3 scénarios de personas, un routage réel sur le réseau piéton, et la couche qu’aucun concurrent n’a encore livrée : des suggestions d’amélioration concrètes qui montrent précisément quelle infrastructure, à elle seule, ferait grimper le score d’un secteur, et de combien de points

Rôle
En solo — stratégie produit, data engineering, moteur de score, frontend, déploiement
Disciplines
Product DesignUXLocation IntelligenceData EngineeringFront-end
Liens
En bref
  • Problème — 37,2 % des 1,25 million d’habitants de Bruxelles ne sont pas belges ; ils choisissent parmi 19 communes avec des outils qui donnent un chiffre, jamais une raison
  • Insight — Les concurrents se battent sur le score lui-même ; le terrain libre, c’est le pour qui, le pourquoi et le comment l’améliorer — aucun outil européen n’y touche
  • Solution — J’ai bâti seule un moteur de score sur 724 secteurs × 3 personas : il explique le pourquoi, propose des améliorations et compare deux secteurs
  • Résultat — MVP en bêta publique sur cityscore.ontwrpn.com, 20 premiers utilisateurs, 2 172 scores précalculés, API <5 ms, 0 € de coût LLM

Une ville d’arrivées sans la moindre intelligence géospatiale utile

Bruxelles est, dans les faits, une ville d’arrivées : 37,2 % de ses 1,25 million d’habitants ne sont pas belges, attirés par les institutions de l’UE, l’OTAN, les ONG internationales et une diaspora croissante de télétravailleurs. La seule Commission européenne emploie 21 705 agents, et quelque 1 900 nouveaux stagiaires Blue Book arrivent chaque mars et chaque octobre. Ce sont des arrivants très qualifiés, bien rémunérés, dont l’anglais est la langue par défaut, et qui doivent trancher avec très peu d’informations : laquelle des 19 communes colle à leur situation de vie précise — une famille avec de jeunes enfants, un télétravailleur en quête de cafés, un senior qui a besoin de transports fréquents et d’un médecin généraliste à proximité ? Les outils existants ne répondent pas à cette question. Ils donnent un chiffre.

Un audit concurrentiel de 8 outils (Walk Score, Niche, AreaVibes, Local Logic, Rightmove, Novad, mylocationscore, AARP Livability Index) a mis au jour trois lacunes structurelles : chaque concurrent sérieux est centré sur les États-Unis ou le Canada ; les scores existent sans explication — un 72/100 ne dit rien de ce qui l’a déterminé ; et la couche « comment l’améliorer » est entièrement vacante. Aucun concurrent n’a jamais livré « ajoutez une pharmacie ici → le score grimpe de 9 points ».

Les concurrents se battent sur le score lui-même. Le terrain vacant, c’est le pour qui, le pourquoi et le comment l’améliorer — autant de questions que le marché européen de l’intelligence géospatiale a laissées entièrement sans réponse

Données ouvertes, vraies rues — pas de distance à vol d’oiseau

Le moteur de score s’appuie sur quatre sources de données ouvertes : 724 secteurs statistiques Statbel (~100–200 habitants chacun, l’unité du recensement belge), l’extrait OpenStreetMap Geofabrik Belgium PBF sur 18 catégories d’aménités, les flux GTFS de la STIB/MIVB avec des arrêts pondérés par la fréquence, et les couches SIG UrbIS de Bruxelles. La décision la plus importante a été de calculer les trajets sur de vraies rues piétonnes via un graphe de marche OSMnx (92 950 nœuds, 264 936 arêtes) plutôt que sur la distance à vol d’oiseau — qui surestime systématiquement l’accessibilité de 20 à 40 % environ.

La décroissance avec la distance suit une fonction plateau+gaussienne : note maximale en deçà d’un temps seuil, décroissance douce jusqu’à zéro au maximum, sans rupture brutale. Les sous-scores de transport multiplient cette décroissance par un facteur de fréquence — un arrêt desservi toutes les 5 minutes obtient la note maximale ; un arrêt desservi toutes les 30 minutes obtient proportionnellement moins, mais conserve 40 % de sa décroissance plutôt que rien. Le moteur a tourné ~10 minutes sur un ordinateur portable pour précalculer les 2 172 scores (724 secteurs × 3 scénarios). En production, les réponses de l’API sont <5 ms — une consultation, pas un calcul.

Choroplèthe MapLibre GL sur OpenFreeMap (sans clé). Les marqueurs jaunes indiquent où une seule nouvelle aménité produirait le plus gros gain de score

Expliquer, changer, améliorer, comparer — les quatre verbes que les concurrents ont ignorés

Chaque score s’accompagne d’une décomposition par catégorie, de points forts et de lacunes codés par couleur, et — c’est la couche qui fait la différence — de suggestions d’amélioration concrètes. Pour chaque secteur × scénario, le moteur repère les 3 catégories où une seule aménité supplémentaire produit le plus gros gain de score, avec un marqueur sur la carte à l’emplacement suggéré. Résultat : « +1 pharmacie dans un rayon de 400 m → score Senior 62 → 71 (+9 points). » Trois scénarios de personas étaient livrés au lancement — Famille, Senior, Télétravail — avec les pondérations de chaque scénario affichées dans l’interface, par choix de transparence : on invite à l’examen plutôt que de se cacher derrière une boîte noire.

Le panneau « Demander » diffuse en streaming une réponse de 2 à 4 phrases fondée uniquement sur les données propres au secteur, via Groq (Llama 3.3 70B, niveau gratuit) en server-sent events — avec la consigne de n’utiliser aucune connaissance extérieure et d’éviter les données démographiques. Le panneau Comparer met deux secteurs côte à côte avec les écarts par catégorie, un gagnant par ligne et un verdict en français clair, appuyé sur /api/compare. Le filtre « Trouver par aménité » permet de sélectionner plusieurs puces d’aménités (parcs à chiens compris) avec un seuil de score minimal pour faire ressortir les secteurs correspondants.

Panneau de score pour Vieille Halle aux Blés (scénario Famille, score 86). Chaque section, de l’anneau aux Score Boosters, est générée à partir de données précalculées en <5 ms

cityscore.ontwrpn.com — aperçu en ligne
Démo en ligne
cityscore.ontwrpn.com
Ouvrir

Bêta live — cherchez n’importe quelle adresse bruxelloise, choisissez un scénario, explorez les améliorations

La contrainte la plus rude : faire tourner du calcul géo lourd dans un budget d’hébergement serré

L’insight architectural fondamental : séparer le calcul hors ligne du service en production. Les lourdes bibliothèques géospatiales (osmnx, geopandas, NetworkX Dijkstra) ne peuvent pas tourner dans un serveur web de production sous le plafond de 512 Mo de RAM du niveau gratuit de Render. La solution est une séparation stricte en deux couches : un pipeline hors ligne (OSMnx, geopandas, NetworkX, Statbel, STIB GTFS) tourne en local ou chaque mois dans GitHub Actions pour rafraîchir les données, sérialise 2 172 scores en CSV et les commite dans le dépôt. L’API de production (FastAPI + shapely + numpy + SQLModel) se charge depuis ce CSV au démarrage du conteneur et répond aux requêtes par de simples consultations.

Décisions techniques clés : Python FastAPI plutôt que Node Express (les bibliothèques géospatiales sont de première classe en Python ; un seul langage pour le pipeline et le runtime) ; shapely point-dans-polygone plutôt que PostGIS (les 724 secteurs tiennent entièrement en mémoire — aucune extension Postgres nécessaire) ; secteurs statistiques Statbel plutôt que quartiers UrbIS (granularité plus fine, défendable face au MAUP) ; OpenFreeMap plutôt qu’un fond de carte commercial (pas de clé API, pas de limites de débit, hébergé dans l’UE, coût nul) ; géocodage Nominatim avec cache en base (la limite de 1 req/s amortie par un cache SQLite agressif). En juin 2026, l’application a quitté Render + Supabase pour une instance Coolify auto-hébergée sur Hetzner avec Postgres interne, ce qui a fait disparaître l’expiration de Render Postgres à 30 jours.

pyrosm était le mauvais pari de dépendance — incompatible avec Python 3.14 à cause d’un build pyrobuf cassé. Basculer vers la CLI osmium-tool (C++, sans dépendance à une version de Python) a coûté deux jours. Leçon : valider la compatibilité de ses dépendances géospatiales avant de s’engager dans une stratégie de parsing

Durcissement de production, accessibilité et design system en une seule passe

Le sprint de durcissement de juin 2026 a couvert : gzip + ETag/Cache-Control + choroplèthe mise en cache en mémoire ; arrondi des coordonnées (−66 % de charge utile) ; limitation de débit slowapi sur /api/explain et /api/score ; CSP + en-têtes de sécurité ; validation des scénarios et des catégories (422/400) ; aucune fuite de chaînes d’erreur ; et un nouvel endpoint /api/weights faisant office de source de vérité unique, qui a supprimé une table de pondérations de scénarios dupliquée entre la config du pipeline, le script de seed et le frontend. MapLibre a été isolé dans un chunk vendor mis en cache longue durée.

Passe d’accessibilité WCAG 2.2 AA vérifiée avec axe-core (0 violation) : listbox au clavier + sélecteur de secteur hors carte, erreurs en role=alert, focus-visible, prefers-reduced-motion, cibles tactiles ≥24 px, redistribution responsive à 320 px. Finitions visuelles — révélations décalées, compteur de score en rAF, barres et anneau en dégradé — toutes respectueuses de reduced-motion. La passe design system a remplacé une vingtaine de tailles de police ad hoc et un espacement de sections irrégulier par une échelle typographique unifiée et une échelle d’espacement de 4 px. Deux bogues latents corrigés : un plantage de migration SQLite et des POI de carte injectés en double.

MVP livré, 20 bêta-testeurs, lucide sur ce qu’un v1 mono-ville peut prétendre

MVP livré en bêta publique sur cityscore.ontwrpn.com. Les métriques au lancement : 724 secteurs statistiques bruxellois scorés, 3 scénarios de personas, 2 172 scores précalculés, 4 533 suggestions d’amélioration (jusqu’à 3 par secteur × scénario), réponse de l’API <5 ms, empreinte totale en base <10 Mo, 0 € d’hébergement sur un VPS Hetzner auto-hébergé et partagé. Les 20 premiers bêta-testeurs ont été recrutés dans les canaux d’expatriés bruxellois. L’infrastructure multi-ville pour Anvers est déjà en place (pipeline paramétré par ville, migration de la colonne city) — il ne reste plus qu’à lancer le pipeline de données.

Ce que je ferais autrement : commencer par la couche d’amélioration. C’est le différenciateur le plus fort, et pourtant je ne l’ai construite qu’en semaine 5. Pour les projets à venir : commencer par ce que personne d’autre ne fait, et le valider en premier. Le pivot de pyrosm vers osmium a coûté deux jours, qu’une vérification de compatibilité des dépendances de 30 minutes aurait évités. L’expiration de Render Postgres à 30 jours (et non 90, comme je le supposais) a confirmé la leçon : toujours tester la reprise, pas seulement le déploiement — c’est exactement pourquoi les 2 172 scores sont tous commités dans le dépôt comme données de seed.

La choroplèthe de Bruxelles avec le panneau de score ouvert. Le produit est en ligne sur cityscore.ontwrpn.com

Sources

Projet suivant
Knowledge Graph Explorer