Python pour un data scientist/economist

Lino Galiana

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 sur seaborn 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 :

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.

  1. 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’option compression. Nommer cet objet comptages
  2. 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
  3. 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
  4. 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.

  1. 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’argument figsize = (10,10)
  2. 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.

  1. 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’argument crs. Avec les versions anciennes de l’ENSAE, il faut utiliser .to_string sur un objet CRS pour qu’il soit reconnu par contextily. Sur des versions récentes, la valeur numérique du code EPSG est suffisante. Pour ne pas afficher les axes, vous pouvez utiliser ax.set_axis_off()
  2. 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

  1. 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 nommant df2, pour le trafic entre 17 et 20 heures (bornes incluses)
  2. Essayer de comprendre ce que fait la fonction expand_points
  3. 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 de radius_sd à 100.
  • Reconvertit l’output au format WGS 84 (epsg: 4326)
  1. Appliquer cette fonction à df1 et df2

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,

  1. Appliquer la fonction geoplot.kdeplot avec comme consigne:
    • d’utiliser les arguments shade=True et shade_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 de geoplot.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
  2. Ne pas oublier d’ajouter les arrondissements. Avec geoplot, il faut utiliser geoplot.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:

  1. Déterminer le point central en construisant des colonnes longitudes et latitudes et en prenant la moyenne de celles-ci
  2. Utiliser la méthode fit_bounds qui cale la carte sur les coins sud-ouest et nord-est. En supposant que la carte s’appelle m, on fera m.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 avec color = '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:

Last updated on 8 Oct 2020
Published on 6 Oct 2020
Edit on GitHub