De belles cartes avec python: mise en pratique
La pratique de la cartographie se fera, dans ce cours, en répliquant des cartes qu’on peut trouver sur la page de l’open-data de la ville de Paris ici.
Note
Produire de belles cartes demande du temps mais aussi du bon sens. En fonction de la structure des données, certaines représentations sont à éviter voire à exclure. L’excellent guide disponible ici propose quelques règles et évoque les erreurs à éviter lorsqu’on désire effectuer des représentations spatiales.
Ce TP vise à initier:
- Au module graphique de geopandas ainsi qu’aux packages geoplot et
contextily pour la construction de cartes figées.
geoplot
est construit surseaborn
et constitue ainsi une extension des graphiques de base. - Au package folium qui est un point d’accès vers la librairie JavaScript leaflet permettant de produire des cartes interactives
Les données utilisées sont :
- Un sous-ensemble des données de paris open data a été mis à disposition sur pour faciliter l’import (élimination des colonnes qui ne nous servirons pas mais ralentissent l’import)
- La localisation précise des stations
- Arrondissements parisiens
Dans la première partie, nous allons utiliser les packages suivants:
import pandas as pd
import geopandas as gpd
import contextily as ctx
import geoplot
import matplotlib.pyplot as plt
import folium
Warning
Certaines librairies géographiques dépendent de rtree
qui est parfois difficile à installer. Pour installer rtree
, le mieux est d’utiliser anaconda
:
conda install rtree --yes
Première carte avec l’API matplotlib
de geopandas
Exercice 1: Importer les données
Importer les données de compteurs de vélos en deux temps.
- D’abord, les comptages peuvent être trouvés à l’adresse https://github.com/linogaliana/python-datascientist/raw/master/data/bike.csv. ⚠ Il s’agit de données
compressées au format
gzip
, il faut donc utiliser l’optioncompression
. Nommer cet objetcomptages
- Importer les données de localisation des compteurs à partir de l’url https://parisdata.opendatasoft.com/explore/dataset/comptage-velo-compteurs/download/?format=geojson&timezone=Europe/Berlin&lang=fr. Nommer cet objet
compteurs
- On va également utiliser les données d’arrondissements de la ville de Paris. Importer ces données depuis https://opendata.paris.fr/explore/dataset/arrondissements/download/?format=geojson&timezone=Europe/Berlin&lang=fr
- Utiliser la méthode
plot
pour représenter les localisations des compteurs dans l’espace. C’est, on peut l’avouer, peu informatif sans apport extérieur. Il va donc falloir travailler un peu l’esthétique
Warning
On serait tenté de faire un merge de la base compteurs et comptages. En l’occurrence, il s’agirait d’un produit cartésien puisqu’il s’agit de faire exploser la base spatiale. Avec des données spatiales, c’est souvent une très mauvaise idée. Cela duplique les points, créant des difficultés à représenter les données mais aussi ralentit les calculs. Sauf à utiliser la méthode dissolve
(qui va agréger k fois la même géométrie…), les géométries sont perdues lorsqu’on effectuer des groupby
.
3.
Maintenant, tout est prêt pour une première carte. matplotlib
fonctionne selon
le principe des couches. On va de la couche la plus lointaine à celle le plus
en surface. L’exception est lorsqu’on ajoute un fond de carte contextily
via
ctx.add_basemap
: on met cet appel en dernier.
Exercice 2: première carte
Représenter une carte avec le fonds de carte des arrondissements.
- Faire attention à avoir des arrondissements dont l’intérieur est transparent (argument à utiliser:
facecolor
). Faire des bordures d’arrondissements noir. Pour obtenir un graphique plus grand, vous pouvez utiliser l’argumentfigsize = (10,10)
- Pour les localisations, les points doivent être rouges en étant plus transparent au centre (argument à utiliser:
alpha
)
Vous devriez obtenir cette carte:

Exercice 3: Ajouter un fonds de carte avec contextily
Repartir de la carte précédente.
- Utiliser
ctx.add_basemap
pour ajouter un fonds de carte. ⚠ Par défaut,contextily
désire un système de projection (crs) qui est le Web Mercator (epsg: 3857). Il faut changer la valeur de l’argumentcrs
. Avec les versions anciennes de l’ENSAE, il faut utiliser.to_string
sur un objet CRS pour qu’il soit reconnu parcontextily
. Sur des versions récentes, la valeur numérique du code EPSG est suffisante. Pour ne pas afficher les axes, vous pouvez utiliserax.set_axis_off()
- Trouver un fonds de carte plus esthétique, qui permette de visualiser les grands axes, parmi ceux possibles. Pour tester l’esthétique, vous pouvez utiliser cet url. La documentation de référence sur les tuiles disponibles est ici
## <AxesSubplot:>

Pour le moment, la fonction geoplot.kdeplot
n’incorpore pas toutes les fonctionalités de seaborn.kdeplot
. Pour être en mesure de construire une heatmap
avec des données pondérées (cf. cette issue dans le dépôt seaborn), il y a une astuce. Il faut simuler k points de valeur 1 autour de la localisation observée. La fonction ci-dessous, qui m’a été bien utile, est pratique
def expand_points(shapefile,
index_var = "grid_id",
weight_var = 'prop',
radius_sd = 100,
crs = 2154):
"""
Multiply number of points to be able to have a weighted heatmap
:param shapefile: Shapefile to consider
:param index_var: Variable name to set index
:param weight_var: Variable that should be used
:param radius_sd: Standard deviation for the radius of the jitter
:param crs: Projection system that should be used. Recommended option
is Lambert 93 because points will be jitterized using meters
:return:
A geopandas point object with as many points by index as weight
"""
shpcopy = shapefile
shpcopy = shpcopy.set_index(index_var)
shpcopy['npoints'] = np.ceil(shpcopy[weight_var])
shpcopy['geometry'] = shpcopy['geometry'].centroid
shpcopy['x'] = shpcopy.geometry.x
shpcopy['y'] = shpcopy.geometry.y
shpcopy = shpcopy.to_crs(crs)
shpcopy = shpcopy.loc[np.repeat(shpcopy.index.values, shpcopy.npoints)]
shpcopy['x'] = shpcopy['x'] + np.random.normal(0, radius_sd, shpcopy.shape[0])
shpcopy['y'] = shpcopy['y'] + np.random.normal(0, radius_sd, shpcopy.shape[0])
gdf = gpd.GeoDataFrame(
shpcopy,
geometry = gpd.points_from_xy(shpcopy.x, shpcopy.y),
crs = crs)
return gdf
Exercice 4: Data cleaning avant de pouvoir faire une heatmap
- Calculer le trafic moyen, pour chaque station, entre 7 heures et 10 heures (bornes incluses) et nommer cet objet
df1
. Faire la même chose, en nommantdf2
, pour le trafic entre 17 et 20 heures (bornes incluses) - Essayer de comprendre ce que fait la fonction
expand_points
- Créer une fonction qui suive les étapes suivantes:
- Convertit un DataFrame dans le système de projection Lambert 93 (epsg: 2154)
- Applique la fonction
expand_points
avec les noms de variable adéquats. Vous pouvez fixer la valeur deradius_sd
à100
. - Reconvertit l’output au format WGS 84 (epsg: 4326)
- Appliquer cette fonction à
df1
etdf2
Le principe de la heatmap est de construire, à partir d’un nuage de point bidimensionnel, une distribution 2D lissée. La méthode repose sur les estimateurs à noyaux qui sont des méthodes de lissage local.
Exercice 5: Heatmap, enfin
Représenter, pour ces deux moments de la journée, la heatmap
du trafic de vélo avec geoplot.kdeplot
. Pour cela,
- Appliquer la fonction
geoplot.kdeplot
avec comme consigne:- d’utiliser les arguments
shade=True
etshade_lowest=True
pour colorer l’intérieur des courbes de niveaux obtenues - d’utiliser une palette de couleur rouge avec une transparence modérée (
alpha = 0.6
) - d’utiliser l’argument
clip
pour ne pas déborder hors de Paris (en cas de doute, se référer à l’aide degeoplot.kdeplot
) - L’argument bandwidth détermine le plus ou moins fort lissage spatial. Vous pouvez partir d’un bandwidth égal à 0.01 et le faire varier pour voir l’effet sur le résultat
- d’utiliser les arguments
- Ne pas oublier d’ajouter les arrondissements. Avec
geoplot
, il faut utilisergeoplot.polyplot
## C:\Users\W3CRK9\AppData\Local\R-MINI~1\envs\R-RETI~1\lib\site-packages\seaborn\_decorators.py:36: FutureWarning: Pass the following variable as a keyword arg: y. From version 0.12, the only valid positional argument will be `data`, and passing other arguments without an explicit keyword will result in an error or misinterpretation.
## warnings.warn(
## C:\Users\W3CRK9\AppData\Local\R-MINI~1\envs\R-RETI~1\lib\site-packages\seaborn\distributions.py:1659: FutureWarning: The `bw` parameter is deprecated in favor of `bw_method` and `bw_adjust`. Using 0.35 for `bw_method`, but please see the docs for the new parameters and update your code.
## warnings.warn(msg, FutureWarning)
## C:\Users\W3CRK9\AppData\Local\R-MINI~1\envs\R-RETI~1\lib\site-packages\seaborn\distributions.py:1678: UserWarning: `shade_lowest` is now deprecated in favor of `thresh`. Setting `thresh=0`, but please update your code.
## warnings.warn(msg, UserWarning)

Des cartes réactives grâce à folium
De plus en plus de données de visualisation reposent sur la cartographie réactive. Que ce soit dans l’exploration des données ou dans la représentation finale de résultats, la cartographie réactive est très appréciable.
folium
offre une interface très flexible et très facile à prendre à main. Les cartes sont construites grâce à la librairie JavaScript Leaflet.js
mais, sauf si on désire aller loin dans la customisation du résultat, il n’est pas nécessaire d’avoir des notions dans le domaine.
Un objet folium se construit par couche. La première est l’initialisation de la carte. Les couches suivantes sont les éléments à mettre en valeur. L’initialisation de la carte nécessite la définition d’un point central (paramètre location
) et d’un zoom de départ (zoom_start
). Plutôt que de fournir manuellement le point central et le zoom on peut:
- Déterminer le point central en construisant des colonnes longitudes et latitudes et en prenant la moyenne de celles-ci
- Utiliser la méthode
fit_bounds
qui cale la carte sur les coins sud-ouest et nord-est. En supposant que la carte s’appellem
, on feram.fit_bounds([sw, ne])
Le bout de code suivant permet de calculer le centre de la carte
df['lon'] = df.geometry.x
df['lat'] = df.geometry.y
center = compteurs[['lat', 'lon']].mean().values.tolist()
Alors que le code suivant permet de calculer les coins:
sw = compteurs[['lat', 'lon']].min().values.tolist()
ne = compteurs[['lat', 'lon']].max().values.tolist()
Hint
Si un fond gris s’affiche, c’est qu’il y a un problème de localisation. Cela provient généralement d’un problème de projection ou d’une inversion des longitudes et latitudes.
Les longitudes représentent les x (axe ouest-nord) et les latitudes y (axe sud-nord). folium
attend qu’on lui fournisse les données sous la forme [latitude, longitude]
donc [y,x]
Exercice 6: Visualiser la localisation des stations
A partir des données compteurs
, représenter la localisation des stations. Les consignes sont:
- le centre de la carte s’obtient avec le morceau de code ci-dessous qui agrège l’ensemble des géométries, calcule le centroid et récupère la valeur sous forme de liste
- un zoom optimal
La carte obtenue doit ressembler à la suivante:
Exercice 7: Représenter les stations
Faire une carte avec des ronds proportionnels au nombre de comptages:
- Pour le rayon de chaque cercle, en notant vous pouvez faire
500*x/max(x)
(règle au doigt mouillé) - Vous pouvez réduire la taille des bordures de cercle avec l’option
weight = 1
et fixer la couleur aveccolor = 'grey'
- (Optionnel) Colorer les 10 plus grosses stations. L’opacité étant, par défaut, un peu faible, le paramètre
fill_opacity = 0.4
améliore le rendu. - (Optionnel) Afficher, en supplément du nom du compteur lorsqu’on clique, la valeur du comptage en revenant à la ligne
La carte obtenue devrait ressembler à la suivante: