Une partie essentielle du travail du data scientist est d’être en mesure de synthétiser une information dans des représentations graphiques percutantes. Ce chapitre permet de découvrir les enjeux de la représentation de données avec Python, l’écosystème pour faire ceci. Il ouvre également à la représentation interactive de données avec Plotly.
Pour essayer les exemples présents dans ce tutoriel :
Compétences à l’issue de ce chapitre
Découvrir l’écosystème matplotlib et
seaborn pour la construction de graphiques par enrichissement successif de couches.
Découvrir le récent écosystème plotnine,
qui est une implémentation en Python du packageRggplot2
pour ce type de représentation et qui, grâce à sa grammaire des graphiques, offre une syntaxe puissante pour construire des visualisations de données.
Découvrir le principe des représentations interactives HTML (format web) grâce aux packages plotly et altair.
Apprendre les enjeux de la représentation graphique, les compromis nécessaires pour construire un message clair et les limites de certaines représentations classiques.
La pratique de la data visualisation se fera, dans ce cours, en répliquant des graphiques qu’on peut trouver sur
la page de l’open data de la ville de Paris
ici ou en proposant des alternatives à ceux-ci sur les mêmes données.
L’objectif de ce chapitre n’est pas de faire un inventaire complet des graphiques pouvant être fait avec Python, ce serait long, assez insipide et peu pertinent car des sites le font déjà très bien à partir d’une grande variété d’exemple, notamment le site python-graph-gallery.com/. L’objectif est plutôt d’illustrer, par la pratique, quelques enjeux liés à l’utilisation des principales librairies graphiques de Python.
On peut distinguer quelques grandes familles de représentations graphiques: les représentations de distributions propres à une variable, les représentations de relations entre plusieurs variables, les cartes qui permettent de représenter dans l’espace une ou plusieurs variables…
Ces familles se ramifient elles-mêmes en de multiples types de figures. Par exemple, selon la nature du phénomène, les représentations de relations peuvent prendre la forme d’une série temporelle (évolution d’une variable dans le temps), d’un nuage de point (corrélation entre deux variables), d’un diagramme en barre (pour souligner le rapport relatif entre les valeurs d’une variable en fonction d’une autre), etc.
Plutôt qu’un inventaire à la Prévert des types de visualisations possibles, ce chapitre et le suivant vont plutôt proposer quelques visualisations qui pourraient donner envie d’aller plus loin dans l’analyse avant la mise en oeuvre d’une forme de modélisation. Ce chapitre est consacré aux visualisations traditionnelles, le suivant est dédié à la cartographie. Ces deux chapitres font partie d’un tout visant à offrir les premiers éléments pour synthétiser l’information présente dans un jeu de données.
Le pas suivant est d’approfondir le travail de communication et de synthèse par le biais de communications pouvant prendre des formes aussi diverses que des rapports, des publications scientifiques ou articles, des présentations, une application interactive, un site web ou des notebooks comme ceux proposés par ce cours. Le principe général est identique quelle que soit le medium utilisé et intéresse particulièrement les data scientists lorsqu’ils font appel à de l’exploitation intensive de données. Ce sera l’objet d’un chapitre futur de ce cours1.
Important
Être capable de construire des visualisations de données intéressantes est une compétence nécessaire à tout data scientist ou chercheur. Pour améliorer la qualité de ces visualisations, il est recommandé de suivre certains conseils donnés par des spécialistes de la dataviz sur la sémiologie graphique.
Les bonnes visualisations de données, comme celles du New York Times, reposent certes sur des outils adaptés (des librairies JavaScript) mais aussi sur certaines règles de représentation qui permettent de comprendre en quelques secondes le message d’une visualisation.
Ce post de blog est une ressource qu’il est utile de consulter régulièrement. Ce post de blog d’Albert Rapp montre bien comment construire graduellement une bonne visualisation de données.
Ce chapitre s’appuie sur les données de comptage des passages de vélo dans les points de mesure parisiens diffusés sur le site de l’open data de la ville de Paris.
L’exploitation de l’historique récent a été grandement facilité par la diffusion des données au format Parquet, un format moderne plus pratique que le CSV. Pour en savoir plus sur ce format, vous pouvez consulter les ressources évoquées dans le paragraphe consacré à ce format dans le chapitre d’approfondissement.
Code pour importer les données à partir du format Parquet
import osimport requestsfrom tqdm import tqdmimport pandas as pdimport duckdburl ="https://minio.lab.sspcloud.fr/lgaliana/data/python-ENSAE/comptage-velo-donnees-compteurs.parquet"# problem with https://opendata.paris.fr/api/explore/v2.1/catalog/datasets/comptage-velo-donnees-compteurs/exports/parquet?lang=fr&timezone=Europe%2FParisfilename ="comptage_velo_donnees_compteurs.parquet"# DOWNLOAD FILE --------------------------------# Perform the HTTP request and stream the downloadresponse = requests.get(url, stream=True)ifnot os.path.exists(filename):# Perform the HTTP request and stream the download response = requests.get(url, stream=True)# Check if the request was successfulif response.status_code ==200:# Get the total size of the file from the headers total_size =int(response.headers.get("content-length", 0))# Open the file in write-binary mode and use tqdm to show progresswith (open(filename, "wb") asfile, tqdm( desc=filename, total=total_size, unit="B", unit_scale=True, unit_divisor=1024, ) as bar, ):# Write the file in chunksfor chunk in response.iter_content(chunk_size=1024):if chunk: # filter out keep-alive chunksfile.write(chunk) bar.update(len(chunk))else:print(f"Failed to download the file. Status code: {response.status_code}")else:print(f"The file '{filename}' already exists.")# READ FILE AND CONVERT TO PANDAS --------------------------query ="""SELECT id_compteur, nom_compteur, id, sum_counts, dateFROM read_parquet('comptage_velo_donnees_compteurs.parquet')"""# READ WITH DUCKDB AND CONVERT TO PANDASdf = duckdb.sql(query).df()df.head(3)
2 Premières productions graphiques avec l’API Matplotlib de Pandas
Chercher à produire une visualisation parfaite du premier coup est illusoire. Il est beaucoup plus réaliste d’améliorer graduellement une représentation graphique afin, petit à petit, de mettre en avant les effets de structure dans un jeu de données.
Nous allons donc commencer par nous représenter la distribution des passages aux principales stations de mesure. Pour cela nous allons produire rapidement un barplot puis l’améliorer graduellement.
Dans cette partie, nous allons ainsi reproduire les deux premiers graphiques de la page d’analyse des données : Les 10 compteurs avec la moyenne horaire la plus élevée et Les 10 compteurs ayant comptabilisé le plus de vélos. Les valeurs chiffrées des graphiques peuvent être différentes de celles de la page en ligne, c’est normal, car nous ne travaillons pas systématiquement sur les données ayant la même fraîcheur que celles en ligne.
Pour importer les librairies graphiques que nous utiliserons dans ce chapitre, il faut faire
import matplotlib.pyplot as pltimport seaborn as sns1from plotnine import*
1
Importer des librairies sous la forme from package import * n’est pas une très bonne pratique. Néanmoins, pour un package comme plotnine, dont nous allons utiliser de nombreuses fonctions, ce serait un peu fastidieux d’importer les fonctions au cas par cas. De plus, cela permet de réutiliser presque tels quels les exemples de code de la librairie Rggplot, nombreux sur internet avec démonstrations visuelles. from package import * est l’équivalent Python de la pratique library(package) en R.
2.1 Comprendre, en quelques mots, le principe de matplotlib
matplotlib date du début des années 2000 et a émergé pour proposer une alternative en Python à la création de graphiques sous Matlab, un logiciel propriétaire de calcul numérique. matplotlib est donc une librairie assez ancienne, antérieure à l’émergence de Python dans l’écosystème du traitement de données. Cela s’en ressent sur la logique de construction de matplotlib qui n’est pas toujours intuitive lorsqu’on est familier de l’écosystème moderne de la data science. Heureusement, il existe de nombreuses librairies qui s’appuient sur matplotlib mais qui visent à fournir une syntaxe plus familière aux data scientists.
matplotlib propose principalement deux niveaux d’abstraction: la figure et les axes. La figure est, en quelque sorte, la “toile” globale qui contient un ou plusieurs axes dans lesquels s’inséreront des graphiques. Selon les cas, il faudra jouer avec les paramètres de figure ou d’axe, ce qui rend très flexible la construction d’un graphique mais peut également être déroutant car on ne sait jamais trop quel niveau d’abstraction il faut modifier pour mettre à jour sa figure2. Comme le montre la Figure 2.1, tous les éléments d’une figure sont paramétrables.
Figure 2.1: Comprendre l’architecture d’une figure matplotlib (Source: documentation officielle)
En pratique, il existe deux manières de créer et mettre à jour sa figure selon qu’on préfère passer par:
l’approche explicite, héritière d’une logique de programmation orientée objet, où on crée des objets Figure et Axes et met à jour ceux-ci.
l’approche implicite, basée sur l’interface pyplot qui utilise une succession de fonctions pour mettre à jour les objets créés implicitement.
import numpy as npimport matplotlib.pyplot as pltx = np.linspace(0, 2, 100) # Sample data.# Note that even in the OO-style, we use `.pyplot.figure` to create the Figure.fig, ax = plt.subplots(figsize=(5, 2.7), layout="constrained")ax.plot(x, x, label="linear") # Plot some data on the Axes.ax.plot(x, x**2, label="quadratic") # Plot more data on the Axes...ax.plot(x, x**3, label="cubic") # ... and some more.ax.set_xlabel("x label") # Add an x-label to the Axes.ax.set_ylabel("y label") # Add a y-label to the Axes.ax.set_title("Simple Plot") # Add a title to the Axes.ax.legend() # Add a legend.
Ces éléments constituent le minimum pour comprendre la logique de matplotlib. Pour être plus à l’aise avec ces concepts, la pratique répétée est indispensable.
2.2 Découvrir matplotlib par l’intermédiaire de Pandas
Exercice 1 : Produire un premier graphique
Les données comportent plusieurs dimensions pouvant faire l’objet d’une analyse statistique. Nous allons commencer par nous focaliser sur le volume de passage à tel ou tel compteur.
Puisque nous avons comme objectif de synthétiser l’information présente dans notre jeu de données, nous devons d’abord mettre en œuvre quelques agrégations ad hoc pour produire un graphique lisible.
Garder les dix bornes à la moyenne la plus élevée. Comme pour obtenir un graphique ordonné du plus grand au plus petit avec les méthodes plot de Pandas, il faut avoir les données ordonnées du plus petit au plus grand (oui c’est bizarre mais c’est comme ça…), réordonner les données.
En premier lieu, sans se préoccuper des éléments de style ni de la beauté
du graphique, créer la structure du barplot (diagramme en bâtons) de la
page d’analyse des données.
Pour préparer le travail sur la deuxième figure, ne conserver
que les 10 compteurs ayant comptabilisé le plus de vélos.
Comme pour la question 2, créer un barplot
pour reproduire la figure 2 de l’open data parisien.
Les 10 principales stations à l’issue de la question 1 représentent celles ayant la moyenne la plus élevée pour le volume de passages de vélos. Ces données réordonnées permettent de créer un graphique lisible et de mettre en avant les stations les plus fréquentées.
Figure 1, sans travail sur le style, présente les données sous forme de barplot basique. Bien qu’elle montre les informations essentielles, elle manque de mise en page esthétique, de couleurs harmonieuses et d’annotations claires, nécessaires pour améliorer la lisibilité et l’impact visuel.
Figure 2 sans travail sur le style:
On commence à avoir quelque chose qui commence à transmettre un message synthétique sur la nature des données. On peut néanmoins remarquer plusieurs éléments problématiques (par exemple les labels) mais aussi des éléments ne correspondant pas (les titres des axes, etc.) ou manquants (le nom du graphique…).
Comme les graphiques produits par Pandas suivent la logique très flexible de matplotlib, il est possible de les customiser. Cependant, cela demande généralement beaucoup de travail et la grammaire matplotlib n’est pas aussi normalisée que celle de ggplot en R. Si on désire rester dans l’écosystème matplotlib, il est préférable de directement utiliser seaborn, qui offre quelques arguments prêts à l’emploi. Sinon on peut basculer sur l’écosystème plotnine qui offrira une syntaxe normalisée pour modifier les différents
3 Utiliser directement seaborn
3.1 Comprendre seaborn en quelques lignes
seaborn est une interface haut niveau au dessus de matplotlib. Ce package offre un ensemble de fonctionnalités pour créer des figures ou des axes matplotlib directement depuis une fonction admettant de nombreux arguments et, si besoin d’aller plus loin dans la customisation, d’utiliser les fonctionnalités de matplotlib pour mettre à jour la figure, que ce soit par le biais de l’approche implicite ou explicite décrites précédemment.
Comme pour matplotlib, seaborn permet de faire la même figure de multiples manières. seaborn hérite de la dualité axes-figures de matplotlib et il faudra souvent jouer avec un niveau ou l’autre. La principale caractéristique de seaborn est d’offrir quelques points d’entrée standardisés, par exemple seaborn.relplot ou seaborn.catplot, et une logique d’inputs basée sur le DataFrame là où matplotlib est structurée autour du arrayNumpy.
La figure comporte maintenant un message mais il est encore peu lisible. Il y a plusieurs manières de faire un barplot en seaborn. Les deux principales sont :
sns.catplot ;
sns.barplot.
On propose d’utiliser sns.catplot pour cet exercice. Il s’agit d’un point d’entrée assez fréquent pour faire des graphiques d’une variable discrétisée.
3.2 Le diagramme en barre (barplot)
Exercice 2: reproduire la première figure avec seaborn
Réinitialiser l’index des dataframesdf1 et df2
pour avoir une colonne ‘Nom du compteur’. Réordonner les données
de manière décroissante pour obtenir un graphique ordonné dans
le bon sens avec seaborn.
Refaire le graphique précédent avec la fonction catplot de seaborn. Pour
contrôler la taille du graphique vous pouvez utiliser les arguments height et
aspect.
Ajouter les titres des axes et le titre du graphique pour le premier graphique
Essayez de colorer en rouge l’axe des x. Vous pouvez pré-définir un
style avec sns.set_style("ticks", {"xtick.color": "red"})
À l’issue de la question 2, c’est-à-dire en utilisant seaborn pour reproduire de manière minimale un barplot, on obtient :
Après quelques réglages esthétiques, à l’issue des questions 3 et 4, on obtient une figure proche de celle du portail open data parisien.
Les paramètres supplémentaires proposés à la question 4 permettent finalement d’obtenir la figure
On comprend ainsi que le boulevard de Sébastopol est le plus emprunté, ce qui ne vous surprendra pas si vous faites du vélo à Paris. Néanmoins, si vous n’êtes pas familiers avec la géographie parisienne, cela sera peu informatif pour vous, vous allez avoir besoin d’une représentation graphique supplémentaire : une carte ! Nous verrons ceci lors d’un prochain chapitre.
Exercice 2bis : reproduire la figure “Les 10 compteurs ayant comptabilisé le plus de vélos”
En suivant l’approche graduelle de l’exercice 2, refaire le graphique Les 10 compteurs ayant comptabilisé le plus de vélos avec seaborn.
3.3 Un exemple d’alternative au barplot, le lollipop chart
Les diagrammes en bâtons (barplot) sont extrêmement communs, sans doute à cause de l’héritage d’Excel où ces graphiques sont faisables en deux clics. Néanmoins, en ce qui concerne le message à transmettre, ils sont loin d’être parfaits. Par exemple, les barres prennent beaucoup d’espace visuel, ce qui peut brouiller le message à transmettre sur le rapport entre les observations.
Sur le plan sémiologique, c’est-à-dire sur le plan de l’efficacité du message à transmettre, les lollipop charts sont préférables : ils transmettent la même information mais avec moins de signes visuels pouvant brouiller sa compréhension.
Les lollipop charts ne sont pas parfaits non plus mais sont un peu plus efficaces pour transmettre le message. Pour en savoir plus sur les alternatives au barplot, la conférence d’Eric Mauvière pour le réseau des data scientists de la statistique publique, dont le message principal est “Désempilez vos figures”, mérite le détour (disponible sur le site ssphub.netlify.app/).
Exercice 3 (optionnel) : reproduire la figure 2 avec un lollipop chart
En suivant l’approche graduelle de l’exercice 2,
refaire le graphique Les 10 compteurs ayant comptabilisé le plus de vélos.
plotnine est le nouveau venu dans l’écosystème de la visualisation en Python. Cette librairie est développée par Posit, l’entreprise à l’origine de l’éditeur RStudio et de l’écosystème du tidyverse si central dans le langage R. Cette librairie vise à importer la logique de ggplot en Python, c’est-à-dire une grammaire des graphiques normalisée, lisible et flexible héritée de Wilkinson (2012).
L’état d’esprit des habitués de ggplot2 quand ils découvrent plotnine
Dans cette approche, un graphique est vu comme une succession de couches qui, une fois superposées, donneront la figure suivante. En soi, ce principe n’est pas différent de celui de matplotlib. Néanmoins, la grammaire utilisée par plotnine est beaucoup plus intuitive et normalisée, ce qui offrira beaucoup plus d’autonomie pour modifier sa figure.
La logique de ggplot (et plotnine) par Lisa (2021), image elle-même empruntée à Field (2012)
Avec plotnine, il n’y a plus de point d’entrée dual figure-axe. Comme l’illustrent les slides ci-dessous :
On initialise une figure
On met à jour les couches (layers), un niveau d’abstraction très général concernant aussi bien les données représentées que les échelles des axes ou la couleur
À la fin, on peut jouer sur l’esthétique en modifiant les labels des axes, de la légende, les titres, etc.
Exercice 4: reproduire la première figure avec plotnine
Ceci est le même exercice que l’exercice 2. L’objectif est de faire cette figure avec plotnine
5 Premières agrégations temporelles
On va maintenant se concentrer sur la dimension temporelle de notre jeu de données à travers deux approches :
Un diagramme en barre synthétisant l’information de notre jeu de données de manière mensuelle ;
Des séries instructives sur la dynamique temporelle. Cela sera l’objet de la prochaine partie.
Avant cela, nous allons enrichir ces données pour bénéficier d’un historique plus long, permettant notamment d’avoir la période Covid dans nos données, ce qui présente un intérêt du fait de la dynamique particulière du trafic dans cette période (arrêt brutal, reprise très forte…).
Voir le code pour bénéficier d’un historique plus long de données
import requestsimport zipfileimport ioimport osfrom pathlib import Pathimport pandas as pdimport geopandas as gpdlist_useful_columns = ["Identifiant du compteur","Nom du compteur","Identifiant du site de comptage","Nom du site de comptage","Comptage horaire","Date et heure de comptage",]# GENERIC FUNCTION TO RETRIEVE DATA -------------------------def download_unzip_and_read( url, extract_to=".", list_useful_columns=list_useful_columns):""" Downloads a zip file from the specified URL, extracts its contents, and reads the CSV file based on the filename pattern in the URL. Parameters: - url (str): The URL of the zip file to download. - extract_to (str): The directory where the contents of the zip file should be extracted. Returns: - df (DataFrame): The loaded pandas DataFrame from the extracted CSV file. """try:# Extract the file pattern from the URL (filename without the extension) file_pattern = url.split("/")[-1].replace("_zip/", "")# Send a GET request to the specified URL to download the file response = requests.get(url) response.raise_for_status() # Ensure we get a successful response# Create a ZipFile object from the downloaded byteswith zipfile.ZipFile(io.BytesIO(response.content)) as z:# Extract all the contents to the specified directory z.extractall(path=extract_to)print(f"Extracted all files to {os.path.abspath(extract_to)}") dir_extract_to = Path(extract_to)# dir_extract_to = Path(f"./{file_pattern}/")# Look for the file matching the pattern csv_filename = [f.name for f in dir_extract_to.iterdir() if f.suffix ==".csv"]ifnot csv_filename:print(f"No file matching pattern '{file_pattern}' found.")returnNone# Read the first matching CSV file into a pandas DataFrame csv_path = os.path.join(dir_extract_to.name, csv_filename[0])print(f"Reading file: {csv_path}") df = pd.read_csv(csv_path, sep=";")# CONVERT TO GEOPANDAS df[["latitude", "longitude"]] = df["Coordonnées géographiques"].str.split(",", expand=True ) df["latitude"] = pd.to_numeric(df["latitude"]) df["longitude"] = pd.to_numeric(df["longitude"]) gdf = gpd.GeoDataFrame( df, geometry=gpd.points_from_xy(df.longitude, df.latitude) )# CONVERT TO TIMESTAMP df["Date et heure de comptage"] = ( df["Date et heure de comptage"] .astype(str) .str.replace(r"\+.*", "", regex=True) ) df["Date et heure de comptage"] = pd.to_datetime( df["Date et heure de comptage"], format="%Y-%m-%dT%H:%M:%S", errors="coerce" ) gdf = df.loc[:, list_useful_columns]return gdfexcept requests.exceptions.RequestException as e:print(f"Error: The downloaded file has not been found: {e}")returnNoneexcept zipfile.BadZipFile as e:print(f"Error: The downloaded file is not a valid zip file: {e}")returnNoneexceptExceptionas e:print(f"An error occurred: {e}")returnNonedef read_historical_bike_data(year): dataset ="comptage_velo_donnees_compteurs" url_comptage =f"https://opendata.paris.fr/api/datasets/1.0/comptage-velo-historique-donnees-compteurs/attachments/{year}_{dataset}_csv_zip/" df_comptage = download_unzip_and_read( url_comptage, extract_to=f"./extracted_files_{year}" )if df_comptage isNone: url_comptage_alternative = url_comptage.replace("_csv_zip", "_zip") df_comptage = download_unzip_and_read( url_comptage_alternative, extract_to=f"./extracted_files_{year}" )return df_comptage# IMPORT HISTORICAL DATA -----------------------------historical_bike_data = pd.concat( [read_historical_bike_data(year) for year inrange(2018, 2024)])rename_columns_dict = {"Identifiant du compteur": "id_compteur","Nom du compteur": "nom_compteur","Identifiant du site de comptage": "id","Nom du site de comptage": "nom_site","Comptage horaire": "sum_counts","Date et heure de comptage": "date",}historical_bike_data = historical_bike_data.rename(columns=rename_columns_dict)# IMPORT LATEST MONTHS ----------------import osimport requestsfrom tqdm import tqdmimport pandas as pdimport duckdburl ="https://opendata.paris.fr/api/explore/v2.1/catalog/datasets/comptage-velo-donnees-compteurs/exports/parquet?lang=fr&timezone=Europe%2FParis"filename ="comptage_velo_donnees_compteurs.parquet"# DOWNLOAD FILE --------------------------------# Perform the HTTP request and stream the downloadresponse = requests.get(url, stream=True)ifnot os.path.exists(filename):# Perform the HTTP request and stream the download response = requests.get(url, stream=True)# Check if the request was successfulif response.status_code ==200:# Get the total size of the file from the headers total_size =int(response.headers.get("content-length", 0))# Open the file in write-binary mode and use tqdm to show progresswith (open(filename, "wb") asfile, tqdm( desc=filename, total=total_size, unit="B", unit_scale=True, unit_divisor=1024, ) as bar, ):# Write the file in chunksfor chunk in response.iter_content(chunk_size=1024):if chunk: # filter out keep-alive chunksfile.write(chunk) bar.update(len(chunk))else:print(f"Failed to download the file. Status code: {response.status_code}")else:print(f"The file '{filename}' already exists.")# READ FILE AND CONVERT TO PANDASquery ="""SELECT id_compteur, nom_compteur, id, sum_counts, dateFROM read_parquet('comptage_velo_donnees_compteurs.parquet')"""# READ WITH DUCKDB AND CONVERT TO PANDASdf = duckdb.sql(query).df()df.head(3)# PUT THEM TOGETHER ----------------------------historical_bike_data["date"] = historical_bike_data["date"].dt.tz_localize(None)df["date"] = df["date"].dt.tz_localize(None)historical_bike_data = historical_bike_data.loc[ historical_bike_data["date"] < df["date"].min()]df = pd.concat([historical_bike_data, df])
Pour commencer, reproduisons la troisième figure qui est, encore une fois, un barplot. Ici, sur le plan sémiologique, ce n’est pas justifié d’utiliser un barplot, une simple série suffirait à fournir une information similaire.
La première question du prochain exercice implique une première rencontre avec une donnée temporelle à travers une opération assez classique en séries temporelles : changer le format d’une date pour pouvoir faire une agrégation à un pas de temps plus large.
Exercice 5: barplot des comptages mensuels
Créer une variable month dont le format respecte, par exemple, le schéma 2019-08 grâce à la bonne option de la méthode dt.to_period
Appliquer les conseils précédents pour construire et améliorer graduellement un graphique afin d’obtenir une figure similaire à la 3e production sur la page de l’open data parisien. Faire cette figure d’abord depuis début 2022 puis sur toute la période de notre historique
Question optionnelle: représenter la même information sous forme de lollipop
La figure avec les données depuis début 2022 aura cet aspect si elle a été construite avec plotnine :
Avec seaborn, elle ressemblera plutôt à ceci :
Si vous préférez représenter cela sous forme de lollipop3:
Enfin, sur l’ensemble de la période, la série prendra plutôt cette forme :
6 Première série temporelle
Il est plus commun de représenter les données ayant une dimension temporelle sous la forme de série que de barres empilées.
Exercice 5: barplot des comptages mensuels
Créer une variable day qui transforme l’horodatage en format journalier du type 2021-05-01 avec dt.day.
Reproduire la figure de la page d’open data.
7 Des graphiques réactifs grâce aux librairies Javascript
7.1 L’écosystème disponible depuis Python
Les figures figées construites avec matplotlib ou plotnine sont figées et présentent ainsi l’inconvénient de ne pas permettre d’interaction avec le lecteur. Toute l’information doit donc être contenue dans la figure, ce qui peut la rendre difficile à lire. Si la figure est bien faite, avec différents niveaux d’information, cela peut bien fonctionner.
Il est néanmoins plus simple, grâce aux technologies web, de proposer des visualisations à plusieurs niveaux. Un premier niveau d’information, celui du coup d’œil, peut suffire à assimiler les principaux messages de la visualisation. Ensuite, un comportement plus volontaire de recherche d’information secondaire peut permettre d’en savoir plus. Les visualisations réactives, qui sont maintenant la norme dans le monde de la dataviz, permettent ce type d’approche : le lecteur d’une visualisation peut passer sa souris à la recherche d’informations complémentaires (par exemple, les valeurs exactes) ou cliquer pour faire apparaître des informations complémentaires sur la visualisation ou autour.
Ces visualisations reposent sur le même triptyque que l’ensemble de l’écosystème web : HTML, CSS et JavaScript. Les utilisateurs de Python ne vont jamais manipuler directement ces langages, qui demandent une certaine expertise, mais vont utiliser des librairies au niveau de R qui génèreront automatiquement tout le code HTML, CSS et JavaScript permettant de créer la figure.
Il existe plusieurs écosystèmes Javascript mis à disposition des développeurs.euses par le biais de Python. Les deux principales librairies sont Plotly, associée à l’écosystème Javascript du même nom, et Altair, associée à l’écosystème Vega et Altair en Javascript4. Pour permettre aux pythonistes de découvrir la librairie Javascript émergente Observable Plot, l’ingénieur de recherche français Julien Barnier a développé pyobsplot une librairie Python permettant d’utiliser cet écosystème depuis Python.
L’interactivité ne doit pas juste être un gadget n’apportant pas de lisibilité supplémentaire, voire la détériorant. Il est rare de pouvoir se contenter de la figure produite sans avoir à fournir un travail supplémentaire pour la rendre efficace.
7.1.1 La librairie Plotly
Le package Plotly est une surcouche à la librairie JavascriptPlotly.js qui permet de créer et manipuler des objets graphiques de manière très flexible afin de produire des objets réactifs sans avoir à recourir à Javascript.
Le point d’entrée recommandé est le module plotly.express (documentation ici) qui offre une approche intuitive pour construire des graphiques pouvant être modifiés a posteriori si besoin (par exemple pour customiser les axes).
Visualiser les figures produites par Plotly
Dans un notebookJupyter classique, les lignes suivantes de code permettent d’afficher le résultat d’une commande Plotly sous un bloc de code :
Pour JupyterLab, l’extension jupyterlab-plotly s’avère nécessaire:
!jupyter labextension install jupyterlab-plotly
7.2 Réplication de l’exemple précédent avec Plotly
Les modules suivants seront nécessaires pour construire des graphiques avec plotly:
import plotlyimport plotly.express as px
Exercice 7: un barplot avec Plotly
L’objectif est de reconstruire le premier diagramme en barre rouge avec Plotly.
Réalisez le graphique en utilisant la fonction adéquate avec plotly.express et…
Ne pas prendre le thème par défaut mais un à fond blanc, pour avoir un résultat ressemblant à celui proposé sur le site de l’open-data.
Pour la couleur rouge, vous pouvez utiliser l’argument color_discrete_sequence.
Ne pas oublier de nommer les axes.
Pensez à la couleur du texte de l’axe inférieur.
Tester un autre thème, à fond sombre. Pour les couleurs, faire un groupe stockant les trois plus fortes valeurs puis les autres.
La première question permet de construire le graphique suivant :
Alors qu’avec le thème sombre (question 2), on obtient :
7.3 La librairie altair
Pour cet exemple, nous allons reconstruire notre figure précédente.
Comme ggplot/plotnine, Vega est un écosystème graphique visant à proposer une implémentation de la grammaire des graphiques de Wilkinson (2012). La syntaxe de Vega est donc basée sur un principe déclaratif : on déclare une construction par couches et transformations de données progressives.
À l’origine, Vega est basée sur une syntaxe JSON, d’où son lien fort avec Javascript. Néanmoins, il existe une API Python qui permet de faire ce type de figures interactives nativement en Python. Pour comprendre la logique de construction d’un code altair, voici comment répliquer la figure précédente avec :
Voir l’architecture d’une figure altair
import altair as altcolor_scale = alt.Scale(domain=[True, False], range=["green", "red"])fig2 = ( alt.Chart(df1)1 .mark_bar()2 .encode( x=alt.X("average(sum_counts):Q", title="Moyenne du comptage par heure sur la période sélectionnée",3 ), y=alt.Y("nom_compteur:N", sort="-x", title=""), color=alt.Color("top:N", scale=color_scale, legend=alt.Legend(title="Top")), tooltip=[ alt.Tooltip("nom_compteur:N", title="Nom du compteur"), alt.Tooltip("sum_counts:Q", title="Moyenne horaire"),4 ], )5 .properties(title="Les 10 compteurs avec la moyenne horaire la plus élevée") .configure_view(strokeOpacity=0))fig2.interactive()
1
On déclare d’abord le dataframe qui sera utilisé, comme nous le faisions avec ggplot(df) avec plotnine. Puis le type de figure désirée (ici un diagramme en barre, mark_bar dans la grammaire d’altair).
2
On définit notre couche principale avec encode. Celle-ci peut accepter simplement des noms de colonnes ou des constructeurs plus complexes, comme ici.
3
On définit un constructeur pour notre axe des x, à la fois pour gérer l’échelle des valeurs mais aussi les paramètres de celle-ci (labels, etc.). Ici, on définit l’axe des x comme une valeur continue (:Q), moyenne de sum_counts pour chaque valeur de \(y\). Cette moyenne n’est pas indispensable, on aurait pu se contenter d’écrire sum_counts:Q voire même sum_counts mais c’est pour illustrer la gestion des transformations de données dans altair.
4
Le tooltip nous permet de gérer l’interactivité de notre figure.
5
Les propriétés viennent à la fin de notre déclaration pour finaliser la figure.
Informations additionnelles
environment files have been tested on.
Python version used:
Package
Version
affine
2.4.0
aiobotocore
2.15.1
aiohappyeyeballs
2.4.3
aiohttp
3.10.8
aioitertools
0.12.0
aiosignal
1.3.1
alembic
1.13.3
altair
5.4.1
aniso8601
9.0.1
annotated-types
0.7.0
anyio
4.8.0
appdirs
1.4.4
archspec
0.2.3
asttokens
2.4.1
attrs
24.2.0
babel
2.17.0
bcrypt
4.2.0
beautifulsoup4
4.12.3
black
24.8.0
blinker
1.8.2
blis
1.2.0
bokeh
3.5.2
boltons
24.0.0
boto3
1.35.23
botocore
1.35.23
branca
0.7.2
Brotli
1.1.0
cachetools
5.5.0
cartiflette
0.0.2
Cartopy
0.24.1
catalogue
2.0.10
cattrs
24.1.2
certifi
2024.8.30
cffi
1.17.1
charset-normalizer
3.3.2
click
8.1.7
click-plugins
1.1.1
cligj
0.7.2
cloudpathlib
0.20.0
cloudpickle
3.0.0
colorama
0.4.6
comm
0.2.2
commonmark
0.9.1
conda
24.9.1
conda-libmamba-solver
24.7.0
conda-package-handling
2.3.0
conda_package_streaming
0.10.0
confection
0.1.5
contextily
1.6.2
contourpy
1.3.0
cryptography
43.0.1
cycler
0.12.1
cymem
2.0.11
cytoolz
1.0.0
dask
2024.9.1
dask-expr
1.1.15
databricks-sdk
0.33.0
dataclasses-json
0.6.7
debugpy
1.8.6
decorator
5.1.1
Deprecated
1.2.14
diskcache
5.6.3
distributed
2024.9.1
distro
1.9.0
docker
7.1.0
duckdb
0.10.1
en_core_web_sm
3.8.0
entrypoints
0.4
et_xmlfile
2.0.0
exceptiongroup
1.2.2
executing
2.1.0
fastexcel
0.11.6
fastjsonschema
2.21.1
fiona
1.10.1
Flask
3.0.3
folium
0.17.0
fontawesomefree
6.6.0
fonttools
4.54.1
fr_core_news_sm
3.8.0
frozendict
2.4.4
frozenlist
1.4.1
fsspec
2023.12.2
geographiclib
2.0
geopandas
1.0.1
geoplot
0.5.1
geopy
2.4.1
gitdb
4.0.11
GitPython
3.1.43
google-auth
2.35.0
graphene
3.3
graphql-core
3.2.4
graphql-relay
3.2.0
graphviz
0.20.3
great-tables
0.12.0
greenlet
3.1.1
gunicorn
22.0.0
h11
0.14.0
h2
4.1.0
hpack
4.0.0
htmltools
0.6.0
httpcore
1.0.7
httpx
0.28.1
httpx-sse
0.4.0
hyperframe
6.0.1
idna
3.10
imageio
2.37.0
importlib_metadata
8.5.0
importlib_resources
6.4.5
inflate64
1.0.1
ipykernel
6.29.5
ipython
8.28.0
itsdangerous
2.2.0
jedi
0.19.1
Jinja2
3.1.4
jmespath
1.0.1
joblib
1.4.2
jsonpatch
1.33
jsonpointer
3.0.0
jsonschema
4.23.0
jsonschema-specifications
2024.10.1
jupyter-cache
1.0.0
jupyter_client
8.6.3
jupyter_core
5.7.2
kaleido
0.2.1
kiwisolver
1.4.7
langchain
0.3.18
langchain-community
0.3.9
langchain-core
0.3.34
langchain-text-splitters
0.3.6
langcodes
3.5.0
langsmith
0.1.147
language_data
1.3.0
lazy_loader
0.4
libmambapy
1.5.9
locket
1.0.0
loguru
0.7.3
lxml
5.3.1
lz4
4.3.3
Mako
1.3.5
mamba
1.5.9
mapclassify
2.8.1
marisa-trie
1.2.1
Markdown
3.6
markdown-it-py
3.0.0
MarkupSafe
2.1.5
marshmallow
3.26.1
matplotlib
3.9.2
matplotlib-inline
0.1.7
mdurl
0.1.2
menuinst
2.1.2
mercantile
1.2.1
mizani
0.11.4
mlflow
2.16.2
mlflow-skinny
2.16.2
msgpack
1.1.0
multidict
6.1.0
multivolumefile
0.2.3
munkres
1.1.4
murmurhash
1.0.12
mypy-extensions
1.0.0
narwhals
1.26.0
nbclient
0.10.0
nbformat
5.10.4
nest_asyncio
1.6.0
networkx
3.3
nltk
3.9.1
numpy
2.1.2
opencv-python-headless
4.10.0.84
openpyxl
3.1.5
opentelemetry-api
1.16.0
opentelemetry-sdk
1.16.0
opentelemetry-semantic-conventions
0.37b0
orjson
3.10.15
OWSLib
0.28.1
packaging
24.1
pandas
2.2.3
paramiko
3.5.0
parso
0.8.4
partd
1.4.2
pathspec
0.12.1
patsy
0.5.6
Pebble
5.1.0
pexpect
4.9.0
pickleshare
0.7.5
pillow
10.4.0
pip
24.2
platformdirs
4.3.6
plotly
5.24.1
plotnine
0.13.6
pluggy
1.5.0
polars
1.8.2
preshed
3.0.9
prometheus_client
0.21.0
prometheus_flask_exporter
0.23.1
prompt_toolkit
3.0.48
protobuf
4.25.3
psutil
6.0.0
ptyprocess
0.7.0
pure_eval
0.2.3
py7zr
0.20.8
pyarrow
17.0.0
pyarrow-hotfix
0.6
pyasn1
0.6.1
pyasn1_modules
0.4.1
pybcj
1.0.3
pycosat
0.6.6
pycparser
2.22
pycryptodomex
3.21.0
pydantic
2.10.6
pydantic_core
2.27.2
pydantic-settings
2.7.1
Pygments
2.18.0
PyNaCl
1.5.0
pynsee
0.1.8
pyogrio
0.10.0
pyOpenSSL
24.2.1
pyparsing
3.1.4
pyppmd
1.1.1
pyproj
3.7.0
pyshp
2.3.1
PySocks
1.7.1
python-dateutil
2.9.0
python-dotenv
1.0.1
python-magic
0.4.27
pytz
2024.1
pyu2f
0.1.5
pywaffle
1.1.1
PyYAML
6.0.2
pyzmq
26.2.0
pyzstd
0.16.2
querystring_parser
1.2.4
rasterio
1.4.3
referencing
0.36.2
regex
2024.9.11
requests
2.32.3
requests-cache
1.2.1
requests-toolbelt
1.0.0
retrying
1.3.4
rich
13.9.4
rpds-py
0.22.3
rsa
4.9
Rtree
1.3.0
ruamel.yaml
0.18.6
ruamel.yaml.clib
0.2.8
s3fs
2023.12.2
s3transfer
0.10.2
scikit-image
0.24.0
scikit-learn
1.5.2
scipy
1.13.0
seaborn
0.13.2
setuptools
74.1.2
shapely
2.0.6
shellingham
1.5.4
six
1.16.0
smart-open
7.1.0
smmap
5.0.0
sniffio
1.3.1
sortedcontainers
2.4.0
soupsieve
2.5
spacy
3.8.4
spacy-legacy
3.0.12
spacy-loggers
1.0.5
SQLAlchemy
2.0.35
sqlparse
0.5.1
srsly
2.5.1
stack-data
0.6.2
statsmodels
0.14.4
tabulate
0.9.0
tblib
3.0.0
tenacity
9.0.0
texttable
1.7.0
thinc
8.3.4
threadpoolctl
3.5.0
tifffile
2025.1.10
toolz
1.0.0
topojson
1.9
tornado
6.4.1
tqdm
4.66.5
traitlets
5.14.3
truststore
0.9.2
typer
0.15.1
typing_extensions
4.12.2
typing-inspect
0.9.0
tzdata
2024.2
Unidecode
1.3.8
url-normalize
1.4.3
urllib3
1.26.20
uv
0.5.30
wasabi
1.1.3
wcwidth
0.2.13
weasel
0.4.1
webdriver-manager
4.0.2
websocket-client
1.8.0
Werkzeug
3.0.4
wheel
0.44.0
wordcloud
1.9.3
wrapt
1.16.0
xgboost
2.1.1
xlrd
2.0.1
xyzservices
2024.9.0
yarl
1.13.1
yellowbrick
1.5
zict
3.0.0
zipp
3.20.2
zstandard
0.23.0
View file history
md`Ce fichier a été modifié __${table_commit.length}__ fois depuis sa création le ${creation_string} (dernière modification le ${last_modification_string})`
functionreplacePullRequestPattern(inputString, githubRepo) {// Use a regular expression to match the pattern #digitvar pattern =/#(\d+)/g;// Replace the pattern with ${github_repo}/pull/#digitvar replacedString = inputString.replace(pattern,'[#$1]('+ githubRepo +'/pull/$1)');return replacedString;}
table_commit = {// Get the HTML table by its class namevar table =document.querySelector('.commit-table');// Check if the table existsif (table) {// Initialize an array to store the table datavar dataArray = [];// Extract headers from the first rowvar headers = [];for (var i =0; i < table.rows[0].cells.length; i++) { headers.push(table.rows[0].cells[i].textContent.trim()); }// Iterate through the rows, starting from the second rowfor (var i =1; i < table.rows.length; i++) {var row = table.rows[i];var rowData = {};// Iterate through the cells in the rowfor (var j =0; j < row.cells.length; j++) {// Use headers as keys and cell content as values rowData[headers[j]] = row.cells[j].textContent.trim(); }// Push the rowData object to the dataArray dataArray.push(rowData); } }return dataArray}
// Get the element with class 'git-details'{var gitDetails =document.querySelector('.commit-table');// Check if the element existsif (gitDetails) {// Hide the element gitDetails.style.display='none'; }}
Wilkinson, Leland. 2012. The grammar of graphics. Springer.
Notes de bas de page
This chapter will be built around the Quarto ecosystem. In the meantime, you can consult the excellent documentation of this ecosystem and practice, which is the best way to learn.↩︎
Thankfully, with a vast amount of online code using matplotlib, code assistants like ChatGPT or Github Copilot are invaluable for creating charts based on instructions.↩︎
I removed the color on the y-axis as I find it adds little to the figure and may even degrade the clarity of the message.↩︎
The names of these libraries are inspired by the Summer Triangle constellation, of which Vega and Altair are two members.↩︎
Citation
BibTeX
@book{galiana2023,
author = {Galiana, Lino},
title = {Python pour la data science},
date = {2023},
url = {https://pythonds.linogaliana.fr/},
doi = {10.5281/zenodo.8229676},
langid = {fr}
}