Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

ce TP vise à vous faire découvrir quelques possibilités de manipulation et d’affichage de données géographiques

le résultat final sera de produire une carte interactive; pour des raisons techniques on ne peut pas l’afficher directement ici, mais pour avoir un aperçu, cherchez dans le zip téléchargé le fichier media/addresses-final.html et ouvrez-le dans un navigateur web

l’API de géolocalisation

pour obtenir des coordonnées latitude/longitude à partir d’une adresse, en France on peut utiliser un service public et gratuit:

vous pouvez charger le fichier data/addresses.csv; toutes ces adresses sont situées à PARIS

Loading...

et la première chose qu’on va faire, c’est naturellement d’utiliser cette API pour géolocaliser ces adresses

un petit extrait

mais avant cela, je vous recommande de produire un fichier addresses-small.csv qui contient un petit extrait, disons les 10 ou 20 premières lignes; ce sera très utile pour débugger

une par une

c’est très pratique de pouvoir faire une recherche des adresses ‘une par une’; voici comment ça on s’y prendrait:

fort de toutes ces observations, allez chercher sur Internet la doc d’une librairie Python qui s’appelle requests
et notamment vous devriez tomber sur une page qui commence avec cet exemple

>>> r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>> r.status_code
200
>>> r.headers['content-type']
'application/json; charset=utf8'
>>> r.encoding
'utf-8'
>>> r.text
'{"type":"User"...'
>>> r.json()
{'private_gists': 419, 'total_private_repos': 77, ...}

qui devrait vous éclairer sur l’usage de base de la librairie
(sachant que dans notre cas c’est encore plus simple puisque nous n’avons pas besoin de nous authentifier)

{'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [2.350728, 48.850345]}, 'properties': {'label': '18 Rue des Bernardins 75005 Paris', 'score': 0.7544495104895104, 'housenumber': '18', 'id': '75105_0896_00018', 'banId': 'acbebc10-eac0-475f-9466-e7dbe9446b02', 'name': '18 Rue des Bernardins', 'postcode': '75005', 'citycode': '75105', 'x': 652355.3, 'y': 6861340.65, 'city': 'Paris', 'district': 'Paris 5e Arrondissement', 'context': '75, Paris, Île-de-France', 'type': 'housenumber', 'importance': 0.68356, 'depcode': '75', 'street': 'Rue des Bernardins', '_type': 'address'}}], 'query': '18 rue BERNARDINS,Paris'}

en un seul coup

si vous avez bien lu la page qui décrit l’API, vous devez avoir remarqué qu’il y a une autre façon de lui soumettre une recherche
c’est ce qui est indiqué ici (cherchez search/csv dans la page de l’API)

curl -X POST -F data=@path/to/file.csv -F columns=voie -F columns=ville https://data.geopf.fr/geocodage/search/csv/

en Python

sauf que nous, on ne veut pas utiliser curl, on veut faire cette requête en Python; voici comment on va s’y prendre

  1. en une seule requête à l’API, on va envoyer tout le fichier csv, en lui indiquant juste quelles sont les colonnes qui contiennent les morceaux de l’adresse

  2. le résultat - toujours au format csv - pourra être également transformé en dataframe

  3. qu’il ne restera plus qu’à merge (ou join si vous préférez) avec la dataframe de départ, pour ajouter les résultats de la géolocalisation dans les données de départ pour cette étape on peut envisager de ne garder que certaines colonnes de la géolocalisation (assez bavarde par ailleurs), je vous recommande de conserver uniquement:

    • latitude, longitude - of course

    • result_city pour pouvoir vérifier la validité des résultats - ici on devrait toujours avoir Paris

    • result_type qui devrait toujours renvoyer housenumber, ça permet à nouveau de vérifier qu’on a bien une adresse connue

je vous donne ma solution; si vous essayez de la chercher par vous-même, je vous recommande d’y aller pas à pas, commencez par juste l’étape 1, puis 1 et 2, et enfin de 1 à 3
c’est utile aussi de commencer par une toute petite dataframe pour ne pas attendre des heures pendant la mise au point...

Loading...

afficher sur une carte

à présent qu’on a une position, on va pouvoir afficher ces adresses sur une carte

et pour ça il y a plein de libs disponibles, on va choisir folium

si nécessaire, il faut l’installer (comment on fait déjà ?)

pour commencer, allez chercher la documentation; le plus simple c’est de demander à google folium python

Question subsidiaire: comment j’ai fait d’après vous pour embarquer cette recherche dans un lien hypertexte ?

et surtout (regardez les deux premiers pour l’instant):

le fond de carte

on ajoute les adresses

pareil mais vous ajoutez les adresses qui se trouvent dans la dataframe
éventuellement, vous pouvez comme sur l’exemple du Getting Started ajouter un tooltip avec l’adresse complète

on sauve la carte

une fonction très sympatique de folium, c’est qu’on peut sauver cette carte sous la forme d’un fichier html, on dit standalone, c’est-à-dire autosuffisant, pas besoin de Python ni de Jupyter pour la regarder

et maintenant pour voir le résultat, ouvrez le fichier dans un autre tab du navigateur

les quartiers de Paris

maintenant on va ranger les adresses par quartier; pour cela nous avons dans le dossier data/ le fichier quartier_paris.zip qui contiennent la définition des 80 quartiers qui recouvrent Paris intra-muros

ça se présente comme ceci:

geopandas.geodataframe.GeoDataFrame
Loading...
<Figure size 640x480 with 1 Axes>

pour afficher cela sur la carte, il faut se livrer à une petite gymnastique

  1. on construit la map comme on l’a vu jusqu’ici

  2. on passe à folium.GeoJson() la geo-dataframe, pour créer un objet

  3. qu’on ajoute dans la map

ce qui nous donne ceci:

Loading...

un peu de couleurs

maintenant c’est à vous: il s’agit d’améliorer cela pour ajouter des couleurs aux quartiers:

  1. écrivez une fonction random_color qui renvoie une couleur au hasard, i.e. une chaine comme #12f285

  2. ajoutez dans la dataframe quartiers une colonne contenant une couleur aléatoire

  3. écrivez map_addresses_in_quartiers_2, une variante qui affiche les quartiers avec chacun une couleur

couleurs: étape 1

'0a'
('#12f285', '#12f285')

couleurs: étape 2

Loading...

couleurs: étape 3

spatial join

ce qu’on aimerait bien faire à présent, c’est de trouver le quartier de chaque adresse
c’est-à-dire en pratique de rajouter dans la dataframe des adresses une ou des colonnes indiquant le quartier

et pour faire cela il existe un outil très intéressant dans geopandas qui s’appelle le spatial join et qui est décrit ici

en deux mots, l’idée c’est de faire comme un JOIN usuel (ou un pd.merge() si vous préférez)
mais pour décider si on doit aligner deux lignes (une dans la df de gauche et l’autre dans la df de droite):

ce qui signifie, en pratique, qu’on va faire ceci

  1. transformer la dataframe d’adresses en une GeoDataFrame et remplacer les colonnes latitude et longitude par une colonne position, cette fois dans un format connu de geopandas

  2. et ainsi on pourra appliquer un spatial join entre la (géo)dataframe d’adresses et la (géo)dataframe des quartiers, en choisissant ce prédicat contains

  3. et du coup modifier la carte pour afficher les adresses dans la bonne couleur - celle du quartier - pour vérifier qu’on a bien fait correctement le classement en quartiers

on doit donc obtenir autant de ligne que d’adresses, mais avec une ou des colonnes en plus (couleur, nom du quartier, ...) qui caractérisent le quartier dans lequel se trouve l’adresse

spatial join: étape 1

je vous donne le code; ce qu’il faut savoir notamment c’est qu’en geopandas il y a la notion de ‘colonne active’, celle qui contient les informations géographiques; ça n’est pas forcément hyper-intuitif la première fois...

https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.set_geometry.html

spatial join: étape 2

Il ne nous reste plus qu’à faire ce fameux spatial join, je vous laisse trouver le code pour faire ça

Loading...
(20, 9)

spatial join: étape 3

on va donc maintenant récrire map_addresses; la logique reste la même mais on veut afficher chaque adresse avec une couleur qui provient de son quartier

comme vous allez le voir, l’objet folium.Marker ne peut pas s’afficher avec une couleur arbitraire - il semble qu’il y ait seulement une petite liste de couleurs supportées

pour contourner ça, utilisez à la place un objet de type folium.CircleMarker

on sauve la carte

c’est sans doute un bon moment pour sauver tout ce qu’on a fait:

on clusterise

pour pouvoir passer à l’échelle, il est indispensable de clusteriser; c’est-à-dire de grouper les points en fonction du niveau de zoom; voyons ce que ça donne

ne perdez pas de temps à chercher le code vous-même, c’est un peu hacky comme on dit, voici comment il faut faire

sur l’échantillon

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[44], line 3
      1 # let's first test it on the small extract
      2 
----> 3 map_addresses(geoaddresses_small_extended)

Cell In[43], line 33, in map_addresses(gdf)
     29 """
     30 
     31     # compute an extract with fewer columns
     32     # not strictly necessary but easier to deal with column names
---> 33     extract = gdf[['latitude', 'longitude', 'human', 'color']]
     34     cluster = plugins.FastMarkerCluster(
     35         extract, callback=callback,
     36         # nicer behaviour

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/geopandas/geodataframe.py:1933, in GeoDataFrame.__getitem__(self, key)
   1927 def __getitem__(self, key):
   1928     """
   1929     If the result is a column containing only 'geometry', return a
   1930     GeoSeries. If it's a DataFrame with any columns of GeometryDtype,
   1931     return a GeoDataFrame.
   1932     """
-> 1933     result = super().__getitem__(key)
   1934     # Custom logic to avoid waiting for pandas GH51895
   1935     # result is not geometry dtype for multi-indexes
   1936     if (
   1937         pd.api.types.is_scalar(key)
   1938         and key == ""
   (...)   1941         and not is_geometry_type(result)
   1942     ):

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/frame.py:4384, in DataFrame.__getitem__(self, key)
   4380                 indexer = [indexer]
   4381         else:
   4382             if is_iterator(key):
   4383                 key = list(key)
-> 4384             indexer = self.columns._get_indexer_strict(key, "columns")[1]
   4385 
   4386         # take() does not accept boolean indexers
   4387         if getattr(indexer, "dtype", None) == bool:

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/indexes/base.py:6302, in Index._get_indexer_strict(self, key, axis_name)
   6299 else:
   6300     keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
-> 6302 self._raise_if_missing(keyarr, indexer, axis_name)
   6304 keyarr = self.take(indexer)
   6305 if isinstance(key, Index):
   6306     # GH 42790 - Preserve name from an Index

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/indexes/base.py:6355, in Index._raise_if_missing(self, key, indexer, axis_name)
   6352     raise KeyError(f"None of [{key}] are in the [{axis_name}]")
   6354 not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
-> 6355 raise KeyError(f"{not_found} not in index")

KeyError: "['color'] not in index"

sur le dataset entier

---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
Cell In[45], line 7
      3 # first prepare the full dataset
      4 geoaddresses = add_quartiers(convert_lat_lon(addresses))
      5 
      6 # and create the map from that
----> 7 final_map = map_addresses(geoaddresses)
      8 
      9 # display the map
     10 final_map

Cell In[43], line 33, in map_addresses(gdf)
     29 """
     30 
     31     # compute an extract with fewer columns
     32     # not strictly necessary but easier to deal with column names
---> 33     extract = gdf[['latitude', 'longitude', 'human', 'color']]
     34     cluster = plugins.FastMarkerCluster(
     35         extract, callback=callback,
     36         # nicer behaviour

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/geopandas/geodataframe.py:1933, in GeoDataFrame.__getitem__(self, key)
   1927 def __getitem__(self, key):
   1928     """
   1929     If the result is a column containing only 'geometry', return a
   1930     GeoSeries. If it's a DataFrame with any columns of GeometryDtype,
   1931     return a GeoDataFrame.
   1932     """
-> 1933     result = super().__getitem__(key)
   1934     # Custom logic to avoid waiting for pandas GH51895
   1935     # result is not geometry dtype for multi-indexes
   1936     if (
   1937         pd.api.types.is_scalar(key)
   1938         and key == ""
   (...)   1941         and not is_geometry_type(result)
   1942     ):

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/frame.py:4384, in DataFrame.__getitem__(self, key)
   4380                 indexer = [indexer]
   4381         else:
   4382             if is_iterator(key):
   4383                 key = list(key)
-> 4384             indexer = self.columns._get_indexer_strict(key, "columns")[1]
   4385 
   4386         # take() does not accept boolean indexers
   4387         if getattr(indexer, "dtype", None) == bool:

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/indexes/base.py:6302, in Index._get_indexer_strict(self, key, axis_name)
   6299 else:
   6300     keyarr, indexer, new_indexer = self._reindex_non_unique(keyarr)
-> 6302 self._raise_if_missing(keyarr, indexer, axis_name)
   6304 keyarr = self.take(indexer)
   6305 if isinstance(key, Index):
   6306     # GH 42790 - Preserve name from an Index

File /__w/exos-ds/exos-ds/venv/lib/python3.14/site-packages/pandas/core/indexes/base.py:6355, in Index._raise_if_missing(self, key, indexer, axis_name)
   6352     raise KeyError(f"None of [{key}] are in the [{axis_name}]")
   6354 not_found = list(ensure_index(key)[missing_mask.nonzero()[0]].unique())
-> 6355 raise KeyError(f"{not_found} not in index")

KeyError: "['color'] not in index"

références

le jeu de données utilisé ici provient à l’origine de https://nakala.fr/10.34847/nkl.3038f62v#e6f991e37ab1fadb388262b57472a703ba379d48, légèrement retravaillé pour les besoins du TP