[Étude de cas IA & SIG] Quel réseau de transports en commun dessert le mieux sa population : Lyon ou Paris ?

[Étude de cas IA & SIG] Quel réseau de transports en commun dessert le mieux sa population : Lyon ou Paris ?

Isochrone de 60 minutes de transports en communs depuis Châtelet, en Ile-de-France, visualisé sous QGIS

Objectif : Comparer la desserte réelle des deux réseaux en analysant la population accessible à 15, 30 et 60 minutes, et voir dans quelle mesure une IA peut nous aider ! 

Pour beaucoup d'usagers, les transports en communs sont avant tout un accès à un espace-temps : que puis-je atteindre en 15, 30 ou 60 minutes depuis mon point de départ ? Pour comparer objectivement la performance des réseaux de Lyon (TCL + TER) et Paris/Île-de-France (RATP + RER + Transilien), on s'est posé une question simple : quelle est la population que je peux atteindre en transports en commun à durée égale ?

Méthodologie : comparer l'accessibilité à la population à la maille IRIS

Pour réaliser cette analyse, on a croisé trois types de données :

  1. Les isochrones : zones accessibles depuis un point donné en X minutes, en transports en commun, aux heures de pointe du soir (18h).

  2. La population INSEE à la maille IRIS (découpage infracommunal), millésime 2021 (publié en 2024), pour être le plus précis possible dans ces zones urbaines denses.

  3. La géographie des IRIS (fournie par l’IGN), millésime 2023, pour que les données soient géographiquement compatibles avec la publication INSEE.

La question : est-ce qu'un grand modèle de language (LLM) peut nous aider à réaliser cette analyse géospatiale et accélerer nos routines quotidiennes en tant que SIGiste ? Pour cette étude de cas, nous utiliserons le modèle 4o de ChatGPT de l'entreprise OpenAI. Néanmoins, il est possible d'utiliser des IA souveraines et européennes - on peut même comparer leurs performances objectives et leurs capacités à trouver la réponse du premier coup (zero-shot reasoning).

1. ETL : Utiliser l'IA pour automatiser l'import

La donnée INSEE sur les populations est plutôt bien formattée et ne comporte pas de pièges évidents pour un import direct. Voici le prompt que j'ai utilisé pour voir si notre IA est capable de créer une table dans notre base de données Postgres + Postgis pour stocker le dernier millésime des données de population IRIS de l'INSEE, et la commande pour charger ce CSV directement depuis un lien internet : 

"En utilisant l'onglet Dictionnaire, propose-moi une requête SQL pour créer une table Postgres pour stocker les données du CSV "Population en 2021 - IRIS - France hors Mayotte" https://www.insee.fr/fr/statistiques/8268806#dictionnaire" ainsi que la commande \copy pour le copier directement depuis un lien public sur lequel j'hébergerai ce CSV non zippé."

Voici comment créer la table PostgreSQL et importer les données depuis un lien public, 
en respectant le dictionnaire de l'INSEE pour le fichier "Population en 2021 - IRIS - France hors Mayotte".

🔧 Étape 1 : Création de la table SQL
sql
Copier
Modifier
CREATE TABLE population_iris_2021 (
    CODGEO VARCHAR(9),              -- Code de l'IRIS
    NOM_IRIS VARCHAR(255),          -- Nom de l'IRIS
    TRIRIS VARCHAR(1),              -- Type d’IRIS
    COM VARCHAR(5),                 -- Code commune
    NOM_COM VARCHAR(255),           -- Nom de la commune
    REG VARCHAR(2),                 -- Code région
    DEP VARCHAR(3),                 -- Code département
    ARM VARCHAR(5),                 -- Code arrondissement
    POPULATION INTEGER              -- Population municipale 2021
);

Raté. ChatGPT n'est pas très efficace pour crawler correctement une page via une URL et perd souvent du contexte.

Le mieux reste de fournir le HEAD du fichier CSV réel de l'INSEE. En effet, l'INSEE ne fournit pas la documentation des variables dans le même ordre que les colonnes du CSV, ce qui va nous embêter pour un import direct - il est toujours plus efficace de fournir un extrait réel de la donnée (headers et premières lignes).

// Après quelques itérations ...

Voici une version entièrement corrigée avec des types NUMERIC pour les colonnes à virgule :

sql
Copier
Modifier
CREATE TABLE population_iris_2021 (
    IRIS VARCHAR(9),
    COM VARCHAR(5),
    TYP_IRIS CHAR(1),
    LAB_IRIS TEXT,

    P21_POP NUMERIC,
    P21_POP0002 NUMERIC,
    P21_POP0305 NUMERIC,
    P21_POP0610 NUMERIC,
    P21_POP1117 NUMERIC,
    P21_POP1824 NUMERIC,
    P21_POP2539 NUMERIC,
    P21_POP4054 NUMERIC,
    P21_POP5564 NUMERIC,
    P21_POP6579 NUMERIC,
    P21_POP80P NUMERIC,
    P21_POP0014 NUMERIC,
    P21_POP1529 NUMERIC,
    P21_POP3044 NUMERIC,
    P21_POP4559 NUMERIC,
    P21_POP6074 NUMERIC,
    P21_POP75P NUMERIC,
    P21_POP0019 NUMERIC,
    P21_POP2064 NUMERIC,
    P21_POP65P NUMERIC,

    P21_POPH NUMERIC,
    P21_H0014 NUMERIC,
    P21_H1529 NUMERIC,
    P21_H3044 NUMERIC,
    P21_H4559 NUMERIC,
    P21_H6074 NUMERIC,
    P21_H75P NUMERIC,
    P21_H0019 NUMERIC,
    P21_H2064 NUMERIC,
    P21_H65P NUMERIC,

    P21_POPF NUMERIC,
    P21_F0014 NUMERIC,
    P21_F1529 NUMERIC,
    P21_F3044 NUMERIC,
    P21_F4559 NUMERIC,
    P21_F6074 NUMERIC,
    P21_F75P NUMERIC,
    P21_F0019 NUMERIC,
    P21_F2064 NUMERIC,
    P21_F65P NUMERIC,

    C21_POP15P NUMERIC,
    C21_POP15P_CS1 NUMERIC,
    C21_POP15P_CS2 NUMERIC,
    C21_POP15P_CS3 NUMERIC,
    C21_POP15P_CS4 NUMERIC,
    C21_POP15P_CS5 NUMERIC,
    C21_POP15P_CS6 NUMERIC,
    C21_POP15P_CS7 NUMERIC,
    C21_POP15P_CS8 NUMERIC,

    C21_H15P NUMERIC,
    C21_H15P_CS1 NUMERIC,
    C21_H15P_CS2 NUMERIC,
    C21_H15P_CS3 NUMERIC,
    C21_H15P_CS4 NUMERIC,
    C21_H15P_CS5 NUMERIC,
    C21_H15P_CS6 NUMERIC,
    C21_H15P_CS7 NUMERIC,
    C21_H15P_CS8 NUMERIC,

    C21_F15P NUMERIC,
    C21_F15P_CS1 NUMERIC,
    C21_F15P_CS2 NUMERIC,
    C21_F15P_CS3 NUMERIC,
    C21_F15P_CS4 NUMERIC,
    C21_F15P_CS5 NUMERIC,
    C21_F15P_CS6 NUMERIC,
    C21_F15P_CS7 NUMERIC,
    C21_F15P_CS8 NUMERIC,

    P21_POP_FR NUMERIC,
    P21_POP_ETR NUMERIC,
    P21_POP_IMM NUMERIC,
    P21_PMEN NUMERIC,
    P21_PHORMEN NUMERIC
);

Jusque là c'est correct : je peux créer la table dans ma BDD postgres avec psql en copiant/collant cette instruction.

📥 Étape 2 : Import avec \copy depuis un lien HTTP
Assumons que vous avez hébergé le fichier CSV à cette URL :

https://mon-domaine.fr/population_iris_2021.csv
PostgreSQL \copy ne sait pas importer directement depuis une URL. 
Il faut donc d'abord télécharger le fichier localement, puis utiliser \copy depuis le chemin local.

1. Téléchargement :
bash
curl -o population_iris_2021.csv https://mon-domaine.fr/population_iris_2021.csv
2. Import avec \copy dans psql :
sql
\copy population_iris_2021 FROM 'population_iris_2021.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')

Ensuite, le chatbot m'explique que je ne peux pas utiliser \copy pour importer depuis un lien web et me propose de télécharger le fichier avec curl et la commande \copy pour le charger dans la base.

Encore un piège : je sais que c'est faux, donc je peux corriger le tir avec un nouveau prompt et "forcer" le modèle de langage à me donner la bonne réponse : 

"C'est faux, \copy peut importer un lien public avec FROM PROGRAM et wget."

Tu as absolument raison 👏 — et merci de la correction !

En effet, \copy de psql peut lire directement depuis un flux téléchargé 
via un programme externe en utilisant FROM PROGRAM, 
ce qui permet un import direct depuis une URL sans passer par un téléchargement manuel préalable.

✅ Méthode correcte avec \copy FROM PROGRAM
Supposons que ton fichier CSV est hébergé ici :
https://mon-domaine.fr/population_iris_2021.csv

Voici comment faire en une commande SQL dans psql :

sql
\copy population_iris_2021 FROM PROGRAM 
'wget -q -O - https://mon-domaine.fr/population_iris_2021.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8')

La commande fonctionne. Cependant, un utilisateur novice aurait été "trompé" par ChatGPT (mais serait tout de même arrivé au bon résultat, en plus d'étapes). On voit qu'une IA augmente mieux les compétences des experts ou initiés, même sur des tâches plutôt basiques d'administration de base de données.

En revanche, l'IA est très proactive sur les étapes suivantes : 

"Souhaites-tu aussi que je t'aide à :

  • vérifier la validité des IRIS avec des géométries ?
  • ajouter une clé primaire ou index ?
  • partitionner par région ou département ?
  • ajouter une contrainte de validité sur les codes INSEE ?"

2. Analyse géospatiale complexe en quelques secondes : la magie opère 

Supposons que l'on souhaite estimer la population exacte desservie en transports en communs : on pourrait utiliser une approximation en utilisant le pourcentage de l'iris intersecté par l'isochrone, et faire un ratio avec la population contenue dans cet iris. Voyons si ChatGPT propose spontanément cette méthodologie.

 

Isochrone 60 minutes de transports en communs à Lyon à partir de Bellecour, visualisé sur QGIS

En prenant en compte la table geo_data.population_iris_2021 que tu as créé, 
la table geo_data.iris_histories qui stocke les géométries des iris français dans le temps ("
Table "geo_data.iris_histories"
        Column         |            Type             | Collation | Nullable |                       Default                       
-----------------------+-----------------------------+-----------+----------+-----------------------------------------------------
 id                    | bigint                      |           | not null | nextval('geo_data.iris_histories_id_seq'::regclass)
 name                  | text                        |           | not null | 
 national_code         | character varying           |           | not null | 
 iris_type             | integer                     |           |          | 
 commune_name          | character varying           |           |          | 
 commune_national_code | character varying           |           |          | 
 population            | integer                     |           |          | 
 year                  | timestamp without time zone |           | not null | 
 geom                  | geometry(MultiPolygon,4326) |           |          | 
 created_at            | timestamp without time zone |           | not null | 
 updated_at            | timestamp without time zone |           | not null | 
 hide                  | boolean                     |           | not null | false
Indexes:
    "iris_histories_pkey" PRIMARY KEY, btree (id)
    "index_iris_histories_on_geom" gist (geom)
    "index_iris_histories_year" btree (EXTRACT(year FROM year))"), 


la table "geo_data.isochrones" qui stocke des isochrones (dont voici la description de la table
 et un exemple de données :     Table "geo_data.isochrones"
      Column       |              Type              | Collation | Nullable |                     Default                     
-------------------+--------------------------------+-----------+----------+-------------------------------------------------
 id                | integer                        |           | not null | nextval('geo_data.isochrones_id_seq'::regclass)
 transport_mode    | integer                        |           | not null | 0
 duration          | integer                        |           | not null | 
 isochronable_type | character varying              |           |          | 
 isochronable_id   | integer                        |           |          | 
 geom              | geometry(MultiPolygon,4326)    |           |          | 
 created_at        | timestamp without time zone    |           | not null | 
 updated_at        | timestamp without time zone    |           | not null | 
 direction         | integer                        |           | not null | 0
 label             | character varying              |           |          | 
 date              | timestamp(6) without time zone |           |          | 
 date_description  | character varying              |           |          | 
 metadata          | jsonb                          |           | not null | '{}'::jsonb
Indexes:
    "isochrones_pkey" PRIMARY KEY, btree (id)
    "index_isochrones_on_geom" gist (geom)

horizon-api::DATABASE=> SELECT id, transport_mode, duration, direction, label, date, date_description, metadata FROM geo_data.isochrones LIMIT 1;
-[ RECORD 1 ]----+------------------------------------
id               | 1
transport_mode   | 1
duration         | 900
direction        | 1
label            | ia_access_tc_2025_paris_chatelet
date             | 2025-03-19 08:00:00
date_description | 
metadata         | {"date": "20250319", "time": 28800})


1. Écris une requête SQL postgis qui détermine la surface couverte par l'isochrone 
de 60mn en transports en communs depuis Châtelet à 18h.
2. Écris une requête SQL postgis qui détermine une approximation de la population exacte 
desservie par ce même isochrone grâce aux données IRIS de l'INSEE.
ChatGPT a dit :
Parfait, tu m’as donné toutes les infos nécessaires. 
Voici les deux requêtes SQL PostGIS demandées, bien contextualisées pour ta base de données.

✅ 1. Surface couverte par l'isochrone de 60 minutes en TC depuis Châtelet à 18h
On part de la table geo_data.isochrones, on filtre sur :

transport_mode = 1 (Transports en commun)

duration = 3600 secondes (60 minutes)

direction = 1 (accès)

label ILIKE '%chatelet%' (point de départ = Châtelet)

date::time = '18:00:00' (heure exacte)

sql

SELECT
  id,
  ST_Area(geom::geography) / 1000000 AS area_km2
FROM geo_data.isochrones
WHERE
  transport_mode = 1
  AND duration = 3600
  AND direction = 1
  AND label ILIKE '%chatelet%'
  AND date::time = '18:00:00';
👉 La colonne area_km2 renvoie la surface de l’isochrone en kilomètres carrés (ST_Area sur géographie = en m²).

✅ 2. Population approximative desservie par cet isochrone (à 18h)
On va :

Récupérer l'isochrone cible ;

Joindre avec geo_data.iris_histories via ST_Intersects ;

Filtrer iris_histories sur year = '2021-01-01' (proche du millésime INSEE 2021) ;

Joindre avec la table geo_data.population_iris_2021 via national_code = IRIS ;

Estimer la population desservie par intersection spatiale.

🔍 On fait une approximation proportionnelle par ST_Area(ST_Intersection(...)) / ST_Area(...).

sql
WITH target_isochrone AS (
  SELECT geom
  FROM geo_data.isochrones
  WHERE
    transport_mode = 1
    AND duration = 3600
    AND direction = 1
    AND label ILIKE '%chatelet%'
    AND date::time = '18:00:00'
  LIMIT 1
),
iris_intersected AS (
  SELECT
    i.id AS iris_id,
    i.national_code,
    ST_Intersection(i.geom, iso.geom) AS geom_inter,
    ST_Area(i.geom::geography) AS iris_area_m2,
    ST_Area(ST_Intersection(i.geom, iso.geom)::geography) AS inter_area_m2
  FROM geo_data.iris_histories i
  CROSS JOIN target_isochrone iso
  WHERE
    i.year = '2021-01-01'
    AND ST_Intersects(i.geom, iso.geom)
),
pop_estimation AS (
  SELECT
    p.*,
    i.inter_area_m2,
    i.iris_area_m2,
    COALESCE(p.P21_POP, 0) * (i.inter_area_m2 / NULLIF(i.iris_area_m2, 0)) AS estimated_pop
  FROM iris_intersected i
  JOIN geo_data.population_iris_2021 p
    ON p.IRIS = i.national_code
)
SELECT
  ROUND(SUM(estimated_pop))::INTEGER AS total_population_estimated
FROM pop_estimation;

La partie clé de ce (long) prompt :

  1. Écris une requête SQL postgis qui détermine la surface couverte par l'isochrone de 60 mn en transports en communs depuis Châtelet à 18h.
  2. Écris une requête SQL postgis qui détermine une approximation de la population exacte desservie par ce même isochrone grâce aux données IRIS de l'INSEE.

Et voilà ! En fournissant la structure SQL des tables et un exemple de donnée pour les isochrones, les requêtes proposées par l'IA fonctionnent du premier coup. Et elle a "pensé" toute seule au ratio d'intersection comme approximation de la population desservie pour un IRIS, même si cette requête est assez lente - ce qui est normal.

Résultats : population accessible en TC par durée

VilleDurée TCSurface accessible (km²)Population accessible (nombre d'habitants)
Lyon15mn11,03176 498
Paris & IDF15mn15,93289 997
Lyon30mn115,94881 481
Paris & IDF30mn166,913 190 374
Lyon60mn707,141 592 333
Paris & IDF60mn6799 726 699

Analyse : Paris plus efficace pour atteindre les masses, Lyon plus compact

  1. Sur les courtes distances (15 min) : Paris couvre une surface 45% plus grande et touche 64% de population en plus. Son réseau métropolitain très dense (métro, RER, bus) y est pour beaucoup.

  2. En 30 minutes : l'effet du RER et des liaisons intermodales franciliennes se fait sentir. Paris atteint plus de 3 millions d'habitants, contre 880 000 pour Lyon.

  3. En 60 minutes : le poids de l'Ile-de-France devient évident. Paris accède à 6 fois plus d'habitants pour 2,4× plus de surface.

Conclusion provisoire : Paris est ultra performant pour accéder à une très grande population dans un temps donné. Mais cette performance repose sur un réseau massivement subventionné, qui couvre des échelles de distance bien plus grandes que celui de Lyon.

❌ Limite de l'analyse : le biais TER / Transilien

L'échelle de temps de 60 minutes fait apparaître des lignes express (TER pour Lyon, Transilien ou RER pour Paris) qui ne sont pas toutes équivalentes en termes d'accès tarifaire ni de billet utilisable. Une analyse fondée uniquement sur l'accessibilité spatiale ne rend pas compte de ces disparités. De plus, le surinvestissement évident dans le transport en communs Francilien à l'échelle nationale rend la conclusion de cette étude évidente, pour le moment. Dans le prochain article, nous compléterons cette étude avec une analyse de la desserte de l'emploi et d'un équipement public.

Conclusion sur l'IA

En évitant les pièges basiques (perte de contexte, pas de modèle de données fourni, pas d'exemple de donnée fourni ou fichier de donnée attaché en pièce jointe directement), l'IA est performante pour assister l'expert SIG dans des tâches simples et complexes d'administration de base de données géospatiale de type PostgreSQL + PostGIS, mais aussi dans l'écriture de requêtes SQL d'analyse que je jugerais comme avancées. Un utilisateur novice ou intermédiaire mettrait en effet beaucoup plus de temps à produire une telle requête fonctionnelle.

Dans le prochain article de cette étude de cas, nous essaierons d'aller encore plus loin et de tester les limites de la génération de SQL d'analyse en poussant ChatGPT dans ses retranchements - en croisant plus de données de types différents pour compléter notre étude.

Nous analyserons également la performance comparative d'IA souveraines avec des modèles comme Mistral.

Vous avez un accès à Albert, l'IA au service de la fonction publique ? Prenez rendez-vous pour un benchmark gratuit de ce modèle de langage dans vos opérations quotidiennes.

Commentez sur LinkedIn cette publication

Abonnez-vous :