Cette page, associée au livre Programmation Python avancée – Guide pour une pratique élégante et efficace aux éditions Dunod (ISBN 978-2-10-081598-2), contient les ressources complémentaires et le code source des exemples présentés.
View the Project on GitHub xoolive/python
« RetourLa place croissante que prend Jupyter dans l’écosystème Python et des technologies du web fait la part belle à des outils de visualisation interactive. Nous présentons ici deux de ces bibliothèques:
la grammaire de visualisation (grammar of graphics) Altair complète la bibliothèque Matplotlib, avec une syntaxe plus naturelle, qui traite séparément les données de la spécification de la visualisation. Elle est basée sur les bibliothèques Javascript d3js et Vega, couramment utilisées par les journalistes qui produisent des infographies;
la bibliothèque ipyleaflet propose quant à elle d’enrichir des fenêtres interactives de visualisation de cartes, sur le modèle de Google Maps ou OpenStreetMap, avec des données géographiques.
À l’instar de Pandas, une présentation complète d’Altair en quelques pages relève de la gageure. Elle ne remplace pas la riche documentation de la bibliothèque accessible sur le site https://altair-viz.org. Ce chapitre propose une simple introduction des possibilités de cette bibliothèque, basée sur un jeu de données1 rendu célèbre par le chercheur suédois Hans Rosling2. Ce fichier comprend, par année et par pays, des données de population, d’espérance de vie et de PIB par habitant (rapportées en équivalent en dollars de 2011).
Altair est une grammaire graphique, c’est-à-dire un langage qui décrit
une visualisation de données avant de l’appliquer à un jeu de données
particulier. Elle est construite autour de la bibliothèque Pandas, prend
en paramètre des pd.DataFrame
et produit, à l’aide des bibliothèques
Javascript Vega Lite et D3.js, une visualisation de données sur le web.
Elle peut également prendre en paramètre des URL vers des données
ordonnées au format JSON, accessibles sur le Net.
Le point de départ de la bibliothèque sera alors un jeu de données caractérisé par le mot-clé anglais tidy (rangé): cela signifie que les données brutes ont déjà été prétraitées, filtrées, ordonnées pour produire des points qui s’approchent au plus près de la définition de la visualisation. On manipulera alors:
pd.DataFrame
qui sera intégré automatiquement à la
visualisation, et où les types de données seront inférés;Il convient de garder en mémoire les limitations classiques actuelles des moteurs de rendus Javascript: à l’heure actuelle (2021), il faudra certainement se limiter à des visualisations qui manipulent un ordre de grandeur de 100 000 points.
Le fichier fourni sur la page web précédente comprend quelques
incohérences, des valeurs manquantes (on reconstruit notamment la
colonne continent
), et on ne s’intéressera qu’aux points situés entre
1950 et 2015, avec des valeurs présentes de population: le code Pandas
qui construit les données utilisées pour les visualisations de cette
page est fourni ci-dessous:
data = pd.read_csv(
"life-expectancy-vs-gdp-per-capita.csv",
header=1,
names=[
"country", "country_code", "year", "population", "continent",
"life_expectancy", "GDP_per_capita"
],
parse_dates=["year"],
)
continents = data.query("continent == continent").groupby("country").agg({"continent": "max"})
data = (
data.drop(columns="continent")
.merge(continents.reset_index(), on="country")
.query("'1950' <= year <= '2015' and population == population")
)
data.head()
country | country_code | year | population | life_expectancy | GDP_per_capita | continent | |
---|---|---|---|---|---|---|---|
150 | Afghanistan | AFG | 1950 | 7752000.0 | 27.638 | 2392.0 | Asia |
151 | Afghanistan | AFG | 1951 | 7840000.0 | 27.878 | 2422.0 | Asia |
152 | Afghanistan | AFG | 1952 | 7936000.0 | 28.361 | 2462.0 | Asia |
153 | Afghanistan | AFG | 1953 | 8040000.0 | 28.852 | 2568.0 | Asia |
154 | Afghanistan | AFG | 1954 | 8151000.0 | 29.350 | 2576.0 | Asia |
L’usage est d’importer la bibliothèque Altair sous l’alias alt
:
>>> import altair as alt
Les visualisations Altair sont basées sur trois types de données:
alt.Chart
contiennent la donnée (sous forme de pd.DataFrame
ou de chemin vers un fichier);.mark_()
) décrit le
type de visualisation voulu (nuage de points, courbe, etc.);.encode()
) est associé
à une feature pour distribuer les points sur une caractéristique
(l’encodage) de la visualisation.Dans l’exemple suivant, un nuage de points .mark_point()
sur les
données réduites à l’année 2015, on associe l’abscisse x
, l’ordonnée
y
et la couleur color
chacune à une caractéristique (le PIB par
habitant, l’espérance de vie et le continent). C’est la bibliothèque qui
se charge d’interpréter la description pour fournir une visualisation
conforme.
data_2015 = data.query('year == "2015"')
alt.Chart(data_2015).encode(
x="GDP_per_capita",
y="life_expectancy",
color="continent"
).mark_point()
Les marques les plus fréquentes ont toutes un nom explicite, qui n’appelle pas nécessairement d’explication approfondie:
mark_point() |
mark_circle() |
mark_square() |
mark_line() |
mark_area() |
mark_bar() |
mark_tick() |
Les canaux d’encodage les plus fréquents sont:
x |
abscisse |
y |
ordonnée |
couleur |
couleur de la marque |
opacity |
transparence/opacité de la marque |
shape |
forme de la marque |
size |
taille de la marque |
facet |
répétition du canal |
Il est possible d’utiliser des arguments nommés pour les canaux sur le modèle
x="x_data"
, ou d’utiliser les constructeurs Altair associés
alt.X("x_data")
en paramètres nommés ou non, qui permettent également de
passer des arguments supplémentaires:
title
différent pour annoter l’axe des ordonnées;scale
qui ne comprend pas la valeur 0;format
pour compter les populations en
millions. Le formatage est défini par la bibliothèque web d3js:
https://github.com/d3/d3-formatAltair utilise le type de chacune des features à partir des dtype
Pandas. Il est possible de les spécifier néanmoins, et cette étape est
nécessaire si les données sont passées par fichier:
Q
pour quantitative: des données numériques continues, comme une
altitude, une température;N
pour nominal: des données textuelles, comme un nom de pays;O
pour ordinal: des données numériques entières pour des
classements;T
pour temporal: des données temporelles.data_france = data.query('country == "France"')
alt.Chart(data_france).encode(
alt.X("year:T", title="année"), # ①
alt.Y(
"population:Q",
scale=alt.Scale(zero=False), # ②
axis=alt.Axis(format="~s") # ③
),
).mark_line()
Un nuage de points sans encodage affiche un simple point. Bien que cette entrée ne renvoie pas d’erreur, elle n’est pas pertinente en soi.
alt.Chart(data).mark_point()
Pour un encodage de données nominales, une coordonnée est attribuée à chaque élément unique de la feature. Les plages de couleurs sont également choisies en fonction, pour distinguer clairement une catégorie d’une autre.
alt.Chart(data).encode(
alt.Y("continent:N"),
alt.Color("continent:N")
).mark_square()
L’agrégation de données correspond à l’opération groupby()
en Pandas.
Altair permet de définir ce type d’opération à calculer sur les données
préparées passées en paramètre. Le calcul est alors effectué par la
bibliothèque Javascript de visualisation au lieu de l’être par Pandas.
L’avantage principal est que le volume des données produites pour créer
toutes les visualisations est réduit.
Dans l’exemple ci-dessous, la préparation de données équivalente avant visualisation serait, pour un calcul de valeur médiane:
data_2015.groupby("continent").agg({"GDP_per_capita": "median"})
continent | GDP_per_capita |
---|---|
Africa | 2954.0 |
Asia | 11738.0 |
Europe | 26240.0 |
North America | 10358.5 |
Oceania | 38890.5 |
South America | 14117.5 |
alt.Chart(data_2015).encode(
alt.X(
"median(GDP_per_capita):Q",
title="PIB par habitant médian en 2015", axis=alt.Axis(format="~s"),
),
alt.Y("continent:N"),
alt.Color("continent:N"),
).mark_bar(size=10)
D’autres opérateurs d’agrégation sont disponibles, notamment pour la
somme sum
, le produit product
, la moyenne mean
, le minimum min
,
le maximum max
, le nombre d’éléments vides missing
, ou le nombre
d’éléments distincts distinct
.
L’exemple suivant affiche le nombre de pays par continent. Chaque pays
est représenté de nombreuses fois dans le fichier (une fois par année)
mais l’opérateur distinct
comprend cette nuance.
alt.Chart(data).encode(
alt.X("distinct(country):N", title="Nombre de pays"),
alt.Y("continent:N"),
alt.Color("continent:N"),
).mark_bar(size=10)
Il est possible de produire des agrégations quel que soit le canal d’encodage. Dans la visualisation suivante, l’écart-type est encodé dans la couleur des barres. Comme le PIB par habitant est annoté comme type de données quantitatif, Altair choisit une table de couleur adaptée qui fait varier la saturation de la couleur, par opposition au type de données nominatif qui fournit une table de couleur faisant varier la teinte.
Cet exemple est aussi l’occasion de préciser deux autres options:
L’attribut sort
① permet ici de trier les catégories de l’axe Y
suivant un critère qui peut être arbitraire, croissant ou
décroissant (par rapport à l’ordre alphabétique pour les variables
nominatives), ou suivant l’ordre associé à un autre canal
d’encodage. Le signe -
dans l’exemple ci-dessous indique un ordre
décroissant. Cette option permet d’ordonner visuellement les barres
par longueur décroissante plutôt que par ordre alphabétique sur le
nom des continents.
L’attribut scale
② fonctionne également pour le canal d’encodage de
couleur: il permet ici de calibrer les bornes inférieures et
supérieures de la table de couleurs. Par défaut, ces bornes sont
assignées aux valeurs minimales et maximales trouvées dans les
données.
alt.Chart(data_2015).encode(
alt.X(
"mean(GDP_per_capita):Q",
axis=alt.Axis(format="~s"), title="Moyenne du PIB par habitant",
),
alt.Y("continent:N", sort="-x"), # ①
alt.Color(
"stdev(GDP_per_capita):Q", title="Écart-type",
scale=alt.Scale(domain=(0, 30e3)) # ②
),
).mark_bar(size=10)
L’Asie semble être le continent avec le plus d’inégalités de richesse.
Un diagramme en boîte permet de visualiser différemment les données: les
éléments atypiques sortent des boîtes à moustache et le canal d’encodage
tooltip
permet d’afficher le nom du pays quand on passe la souris sur
le point. Le Qatar en Asie et la Norvège en Europe par exemple sont des
pays au PIB par habitant très supérieur à celui des voisins.
alt.Chart(data_2015).encode(
alt.X("GDP_per_capita:Q", axis=alt.Axis(format="~s"), title="PIB par habitant",),
alt.Y("continent:N"),
alt.Tooltip("country:N"),
).mark_boxplot(size=10)
Le tracé d’histogrammes est vu par Altair du point de vue d’une
agrégation particulière où les échantillons sont répartis en classes (le
mot-clé bin en anglais, déjà vu avec Matplotlib, puis la méthode
d’agrégation sans argument count()
): il faut donc préciser cette
agrégation pour visualiser des distributions.
Une autre fonctionnalité permise par Altair est la composition de graphes:
+
associe plusieurs couches (layers) sur la même
visualisation;|
et &
concatènent deux visualisations côte à côte
(hconcat
pour horizontal) ou l’une au-dessus l’autre (vconcat
pour
vertical).Lors de composition de graphes, il est possible de factoriser des
spécifications. Dans l’exemple suivant, le même graphe est affiché
deux fois, la visualisation de droite ajoute un canal d’encodage de
couleur ①. On notera également l’utilisation de la fonction
.properties
② qui permet entre autres de spécifier la taille de la
fenêtre.
base = (
alt.Chart(data_2015)
.encode(
alt.X(
"GDP_per_capita", bin=alt.Bin(maxbins=30),
title="PIB par habitant", axis=alt.Axis(format="$~s"),
),
alt.Y("count()", title="Nombre de pays"),
)
.mark_bar()
.properties(width=280, height=200) # ②
)
base | base.encode(alt.Color("continent")) # ①
Il est également possible de changer de marque entre deux visualisations factorisées, ou de surcharger des encodages ou personnalisations précédemment spécifiées.
# Définition de la partie commune aux visualisations
base = (
alt.Chart(data_2015)
.encode(
alt.X(
"sum(population):Q",
title="Population totale en 2015", axis=alt.Axis(format="~s"),
),
alt.Color("continent:N", legend=None),
)
.mark_bar(size=10)
.properties(width=280)
)
(
base.encode(alt.Y("continent:N", title=None))
| base.encode(alt.X("population:Q"), alt.Y("continent:N")).mark_point()
) & base.properties(width=680)
Nous avons vu avec les méthodes d’agrégation que les visualisations peuvent appeler des calculs intermédiaires sur les données d’origine. Ces calculs peuvent être faits via Pandas avant de programmer une visualisation, ou au sein de la visualisation à l’aide de nombreuses fonctions de transformation Altair spécifiques.
Les fonctions de transformation (les mots-clés suivant le modèle
.transform_
) changent la structure des données d’entrée pour y ajouter
de nouvelles colonnes, ou features, filtrer ou trier des lignes
suivant un critère, ou opérer des jointures sur d’autres tables.
Les principales fonctions de transformation sont:
transform_aggregate() |
agrégation d’une colonne avec écrasement |
transform_joinaggregate() |
agrégation d’une colonne dans une nouvelle colonne |
transform_calculate() |
calcul d’une nouvelle grandeur |
transform_density() |
calcul d’une estimation de densité |
transform_window() |
calcul d’un critère par fenêtre (sous-ensemble de lignes) |
Dans l’exemple suivant, on crée une nouvelle feature avec la
population moyenne par pays dans l’intervalle d’années considéré, afin
d’ordonner les pays avec le plus peuplé en moyenne en bas de l’affichage
et le moins peuplé en haut. La transformation joinaggregate
permet de
conserver la feature population
malgré le calcul de sa version
agrégée.
La deuxième transformation filter
permet de ne sélectionner que les
pays d’Europe avec plus de 50 millions d’habitants en moyenne. Le
paramètre datum
fait référence au jeu de données embarqués dans le
constructeur alt.Chart
.
(
alt.Chart(data)
.encode(
alt.X("year:T", title="année"),
alt.Y("population:Q", axis=alt.Axis(format="~s")),
alt.Color("country:N", title="pays"),
alt.Order("mean_pop:Q", sort="descending"),
)
.mark_area()
.transform_joinaggregate(mean_pop="mean(population)", groupby=["country"])
.transform_filter({"and": ["datum.continent == 'Europe'", "datum.mean_pop > 50e6"]})
.properties(width=400, height=200)
)
L’exemple suivant met en application toutes les notions vues jusqu’ici. On cherche à afficher les dix premiers pays suivant un critère donné sur le même jeu de données. Ici aucun prétraitement Pandas n’a été réalisé. Tout est spécifié dans Altair:
une spécification est factorisée entre les deux visualisations ①. La
différence réside dans la feature
attribuée au canal d’encodage
x
;
le critère est évalué sur la dernière donnée (en fonction de
l’année) présente par pays: les deux colonnes population
et
GDP_per_capita
sont remplacées par la valeur correspondant à
l’année la plus récente. C’est l’agrégation argmax
② qui retrouve
la dernière donnée associée à chaque pays, la transformation
calculate
③ sélectionne les données de population en se basant sur
les indices produits par argmax
.
On notera l’utilisation du mot-clé datum
qui rappelle le jeu de
données manipulé;
le tri des pays par ordre décroissant est spécifié dans l’encodage
du canal y
. En revanche la coupe après les 10 premiers pays
nécessite l’application d’un critère basé sur le rang de chaque
valeur en fonction des valeurs décroissantes de population
et de
GDP_per_capita
④. In fine, c’est un transform_filter()
qui se
charge de sélectionner les lignes en fonction du rang ⑤.
base = (
alt.Chart(data)
.mark_bar(size=10)
.encode(alt.Y("country:N", sort="-x", title="pays"), alt.Color("continent:N"),) # ①
.transform_aggregate( # ②
most_recent_year="argmax(year)", groupby=["country", "continent"]
)
.transform_calculate( # ③
population="datum.most_recent_year.population",
GDP_per_capita="datum.most_recent_year.GDP_per_capita",
)
.transform_window( # ④
rank_pop="rank(population)",
sort=[alt.SortField("population", order="descending")],
)
.transform_window(
rank_gdp="rank(GDP_per_capita)",
sort=[alt.SortField("GDP_per_capita", order="descending")],
)
.properties(width=300, height=200)
)
(
base.encode(alt.X("population:Q", axis=alt.Axis(format="~s"))).transform_filter(
alt.datum.rank_pop <= 10 # ⑤
)
| base.encode(
alt.X("GDP_per_capita:Q", axis=alt.Axis(format="$~s"), title="PIB par habitant")
).transform_filter(
alt.datum.rank_gdp <= 10 # ⑤
)
)
L’interactivité la plus simple est celle qui est induite par l’encodage
de canal tooltip
: au passage de la souris sur un point donné, un
pop-up apparaît avec les informations spécifiées. La documentation
montre de nombreux exemples d’interactivité, basés sur les mouvements de
la souris, la sélection d’intervalles, ou d’autres.
L’exemple ci-dessous reprend le type de visualisation avec lequel Hans Rosling s’est illustré lors de plusieurs conférences TED: un point correspond à un pays, sa taille à sa population. Ici, on place en $x$ le PIB par habitant et en $y$ l’espérance de vie dans le pays.
On souhaite animer la visualisation par année pour pouvoir suivre la
trajectoire de chacun de ces pays dans cette espace. Cette sélection se
fait au moyen d’un widget où la poignée sélectionne l’année ①. La
méthode selection_single
réagit en attribuant au champ year
la
valeur positionnée sur le widget: la visualisation est alors mise à jour
quand la valeur du champ change.
L’encodage est basique ②; le nom du pays s’affiche quand on passe la
souris sur un point; le canal text
servira à annoter certains points
de manière permanente, directement sur la visualisation; l’axe des
abscisses est choisi logarithmique. Au lieu de choisir la dernière année
qui contient des données, on choisit les données de l’année sélectionnée
par le widget. ③
La visualisation est alors constituée de deux couches: les cercles de couleur (par continent) à la taille proportionnelle à la population du pays (en échelle logarithmique); et une annotation textuelle pour certains pays dont la trajectoire reflète le cours de l’histoire (chute de l’URSS, Khmers rouges au Cambodge, fin de l’Apartheid en Afrique du Sud, essor économique spectaculaire de la Corée du Sud).
annotate_countries = [
"South Africa", "United States", "France", "China", "Russia", "Nigeria",
"Brazil", "South Korea", "Japan", "India", "Cambodia",
]
year_slider = alt.binding_range(min=1950, max=2015, step=1, name="year:") # ①
year_selector = alt.selection_single(
name="year_selection", fields=["year"], bind=year_slider, init={"year": 2000}
)
base = (
alt.Chart(data)
.encode( # ②
alt.X(
"GDP_per_capita:Q", axis=alt.Axis(format="k"),
scale=alt.Scale(type="log", domain=(100, 1e5)), title="PIB par habitant",
),
alt.Y(
"life_expectancy:Q",
scale=alt.Scale(domain=(20, 90)), title="Espérance de vie",
),
alt.Text("country:N"),
alt.Tooltip("country:N"),
)
.transform_filter("datum.year == year_selection.year") # ③
.properties(width=600, height=400)
)
(
base.mark_circle()
.encode(
alt.Color("continent:N"),
alt.Size("population:Q", scale=alt.Scale(domain=(5e6, 1e9), type="log")),
)
.add_selection(year_selector)
+ base.transform_filter(
{"field": "country", "oneOf": annotate_countries}
).mark_text(size=14, align="right", xOffset=-10, font="Ubuntu")
)
Il est possible de personnaliser l’affichage d’une visualisation Altair à trois niveaux:
.configure_()
.La dernière méthode est souvent la manière privilégiée de procéder à des microajustements, par exemple sur la taille des polices, le positionnement des étiquettes.
base = (
alt.Chart(data_france)
.encode(
alt.X("year:T", title="année"),
alt.Y("population:Q", scale=alt.Scale(zero=False), axis=alt.Axis(format="~s")),
)
.mark_line()
)
(
base.properties(title="Population française", height=200, width=300)
.configure_axis(labelFontSize=12, titleFontSize=0, labelAngle=-30)
.configure_line(size=3, color="#008f6b")
.configure_title(anchor="start", fontSize=16, font="Fira Sans", color="#008f6b")
.configure_view(stroke=None)
)
Le support pour les structures de données géographiques dans Altair est encore jeune à l’heure où ces lignes sont écrites. Il est néanmoins possible de produire des cartes à partir de fichiers au format standardisé pour décrire des informations géographiques, les formats les plus courants étant GeoJSON et TopoJSON.
Altair fournit alors:
.mark_geoshape()
;geojson
(G
);mercator
, orthographic
, conicConformal
, etc.La difficulté consiste alors ici à avoir accès à des fonds de carte pour
créer des visualisations de qualité. La bibliothèque fournit parmi les
jeux de données officiels vega_datasets
une carte du monde de haut
niveau et une carte des États-Unis à bonne résolution.
Pour un public francophone, on trouvera à l’heure de l’écriture de ces lignes:
L’utilisation de la bibliothèque geopandas3 (qui ne sera pas
détaillée dans cet ouvrage) facilite la manipulation des fichiers
GeoJSON et TopoJSON sous forme de tableau Pandas dont les colonnes sont
les métadonnées, et une colonne particulière, généralement nommée
geometry
, contient une structure qui représente la forme de l’objet en
question.
import geopandas as gpd
github_url = "https://raw.githubusercontent.com/{user}/{repo}/master/{path}"
regions_fr = gpd.GeoDataFrame.from_file(
github_url.format(
user="gregoiredavid", repo="france-geojson",
path="regions-version-simplifiee.geojson",
)
)
departements_fr = gpd.GeoDataFrame.from_file(
github_url.format(
user="gregoiredavid", repo="france-geojson",
path="departements-version-simplifiee.geojson",
)
)
belgique = gpd.GeoDataFrame.from_file(
github_url.format(user="arneh61", repo="Belgium-Map", path="Provincies.json",)
).assign(
centroid_lon=lambda df: df.geometry.centroid.x,
centroid_lat=lambda df: df.geometry.centroid.y,
)
La structure geopandas peut être passée en argument de alt.Chart
et il
est alors possible d’utiliser la marque géographique. Ici, on choisit le
nom de la région en encodage de la couleur. Dans l’exemple de la carte
de la Belgique, on ajoute le nom de chaque province au centroïde de la
forme géométrique. Le nom de Bruxelles est enlevé (transform_filter
)
pour ne pas se chevaucher avec celui du Brabant Flamand. Enfin, la
coordonnée en latitude du texte est volontairement bruitée pour éviter
les chevauchements (transform_calculate
); des méthodes de placement
d’étiquettes textuelles sans chevauchement plus complexes existent mais
sortent du cadre de cet ouvrage.
base = alt.Chart(belgique)
(
base.mark_geoshape(stroke="white").encode(alt.Color("NAME_1", title="Région"))
+ (
base.encode(
alt.Longitude("centroid_lon"),
alt.Latitude("centroid_lat"),
alt.Text("NAME_2"),
)
.mark_text(color="black", font="Ubuntu", fontSize=12)
.transform_filter("datum.NAME_2 != 'Bruxelles'")
.transform_calculate(centroid_lat="datum.centroid_lat + .1 * (random() - .5)")
)
)
Contrairement à Matplotlib, la projection par défaut choisie pour les
cartes est la projection de Mercator. L’utilisation de la projection
plate carrée qui associe la latitude à la coordonnée y
et la longitude
à la coordonnée x
est moins directe. La figure suivante compare à ce
titre les trois projections plate carrée, inadaptée pour la plupart des
usages, Mercator, qui fonctionne par défaut dans la plupart des régions
du monde, et Lambert 93, définie ici manuellement, qui est la projection
standard en France. Un graticule (une grille de lignes isolatitudes et
isolongitudes) est ajouté ① pour donner une meilleure perception des
opérations de projection.
base = (
alt.Chart(
alt.graticule(step=[2, 2], extentMajor=([-5, 41], [11, 52])) # ①
).mark_geoshape(fill="None", stroke="#008f6b")
+ alt.Chart(regions_fr).mark_geoshape(
stroke="white", fill="#008f6b", strokeWidth=1.2
)
).properties(width=250, height=250)
(
base.project("identity", reflectY=True).properties(title="Plate Carrée")
| base.project("mercator").properties(title="Mercator (par défaut)")
| (
base.project("conicConformal", rotate=[-3, -46.5], parallels=[49, 44])
.properties(title="Lambert 93")
)
).configure_title(font="Ubuntu", fontSize=15, anchor="start")
L’affichage de fonds de carte est rarement une fin en soi. L’intérêt est
de pouvoir afficher des informations supplémentaires. On peut ajouter
une couche (layer) à notre fond de carte, et utiliser les canaux
d’encodage latitude
et longitude
. Nous illustrons ici cette
utilisation avec l’exemple des toponymes au suffixe -acum : une colonne
est ajoutée pour catégoriser les villes par suffixe, l’encodage color
se charge ensuite de la visualisation.
(
(
alt.Chart(regions_fr)
.mark_geoshape(stroke="grey", fill="white")
.project("conicConformal", rotate=[-3, -46.5], parallels=[49, 44]) # Lambert 93
+ alt.Chart(
pd.concat(
[
villes.query(
f"nom.str.contains('.{fin}$')", engine="python"
).assign(suffixe=f"-{fin}")
for fin in ["ac", "ach", "acq", "ay", "az", "ecques"]
]
)
)
.mark_circle(opacity=0.5)
.encode(
alt.Latitude("latitude"),
alt.Longitude("longitude"),
alt.Color("suffixe"),
alt.Tooltip("nom"),
)
)
.properties(title="Le suffixe -acum dans les toponymes en France")
.configure_legend(
labelFont="Ubuntu", titleFont="Ubuntu", labelFontSize=13, titleFontSize=14
)
.configure_title(font="Ubuntu", fontSize=16, anchor="start")
.configure_view(stroke=None)
)
L’interaction entre les données et les fonds de carte peut être plus marquée, comme dans les cartes choroplèthes qui associent une couleur à une région géographique. On peut ici reprendre les statistiques de population par département français pour associer chaque mesure à un polygone affiché sur la carte.
Cette association se fait ici sur la base du numéro du département (dans
la colonne departement
) qui est présent dans le pd.DataFrame
et dans
le fichier GeoJSON des départements (dans la propriété code
) à l’aide
de la fonction .transform_lookup
.
feature_list = ["altitude_max", "population"]
chart = (
alt.Chart(departements_fr)
.mark_geoshape(stroke="white")
.encode(alt.Tooltip(["code", "nom"]))
.transform_lookup(lookup="code", from_=alt.LookupData(stats, "departement", feature_list))
.project("conicConformal", rotate=[-3, -46.5], parallels=[49, 44])
.properties(width=220, height=200)
)
(
chart.encode(alt.Color("altitude_max:Q")) | chart.encode(alt.Color("population:Q"))
).configure_view(stroke=None).resolve_scale(color="independent")
ipyleaflet4 est une bibliothèque Python spécifiquement conçue pour
l’environnement Jupyter. Elle permet un portage de l’application
Javascript Leaflet qui propose d’enrichir des widgets de cartes
interactives sur le modèle de Google Maps ou OpenStreetMap. La
bibliothèque met à disposition un widget particulier Map
, à l’image
des autres structures ipywidgets
qu’il est possible d’enrichir et
d’équiper de fonctions callbacks (des fonctions déclenchées
automatiquement lors d’événements prédéfinis) pour l’interaction.
Une carte est alors initialisée sur des coordonnées géographiques avec
un niveau de zoom. Dans l’exemple ci-dessous, on affiche les vingt
communes les plus peuplées au bout d’un Marker
. Ces éléments sont par
défaut interactifs : un clic dessus ouvre une fenêtre où l’on peut
insérer un nouveau widget au choix, ici un simple contenu enrichi avec
le nom de la ville et sa population.
Dans cet exemple, on ajoute également un menu déroulant Dropdown
avec
la liste des villes affichées: la fonction de rappel récupère ici les
coordonnées de la ville sélectionnée pour centrer la carte dessus.
Enfin, il est également possible d’enrichir une carte avec des
informations issues de fichiers au format GeoJSON, à l’image de ceux
utilisés pour Altair. Le paramètre hover_style
permet ici de
configurer un comportement interactif simple où le style est mis à jour
quand la souris passe au-dessus d’un élément.
from ipyleaflet import Map, Marker, GeoData
from ipywidgets import HTML, Layout, Dropdown
top_20 = villes.sort_values("population", ascending=False).head(20)
map_ = Map(center=(46.5, 3), zoom=5, layout=Layout(max_width="400px")) # ①
for _, data in top_20.iterrows():
marker = Marker(location=(data.latitude, data.longitude), draggable=False) # ②
marker.popup = HTML(f"<b>{data.nom}</b>: {data.population:_} habitants") # ③
map_.add_layer(marker)
def on_click(info):
ville = top_20.set_index("nom").loc[info["new"]]
map_.center = (ville.latitude, ville.longitude)
map_.zoom = 8
dropdown = Dropdown(description="Ville:", options=sorted(top_20.nom))
dropdown.observe(on_click, names="value")
geodata = GeoData(
geo_dataframe=departements_fr,
style={"color": "#008f6b", "opacity": 1, "fillOpacity": 0.1, "weight": 1},
hover_style={"color": "white", "fillOpacity": 0.4, "weight": 3, "zorder": 2},
)
map_.add_layer(geodata)
display(dropdown, map_)
L’environnement Jupyter intégré aux navigateurs web permet d’importer les facilités d’interactivité développées dans les bibliothèques Javascript modernes pour produire des visualisations et environnements d’exploration des données tout en restant dans l’ecosystème Python.
Matplotlib et Altair abordent la visualisation de données de deux
points de vue différents. Matplotlib propose un langage bas niveau
et des structures de données qui permettent de configurer tous les
éléments d’une présentation graphique. Altair expose une grammar of
graphics (grammaire de visualisation) qui permet de spécifier une
visualisation pour la décliner ensuite sur les données. C’est
probablement l’équivalent le plus proche des bibliothèques de
visualisation avancées d’autres langages, comme ggplot2
en R.
Pour aller plus loin
The Grammar of Graphics, Leland Wilkinson, 2012
Springer, ISBN 978-1-4419-2033-1
Fundamentals of Data Visualization, Claus O. Wilke, 2018
O’Reilly, ISBN 978-1-4920-3108-6
https://clauswilke.com/dataviz/