Cartographie interactive, cartographie web : Folium¶
Folium est une bibliothèque Python permettant de créer des cartes interactives basées sur Leaflet.js. Elle est particulièrement appréciée pour sa simplicité d'utilisation et sa capacité à intégrer facilement dans un navigateur web des données géospatiales provenant de fichiers GeoJSON, de DataFrames Pandas. Folium propose un large éventail de visualisations, notamment des cartes choroplèthes, des marqueurs, des cercles, des polygones et des tracés linéaires. Grâce à sa compatibilité avec des palettes de couleurs variées et ses options de personnalisation poussées, Folium est un outil pertinent pour produire des cartes dynamiques et esthétiques dans un environnement Jupyter Notebook ou en tant qu'application web.
Ce notebook est conçu pour être exécuté dans Jupyter. En effet, comme Folium produit des résultats dans le langage de programmation html/javascript, il parait logique que celui-ci soit directement lu et interprété par un navigateur conçu pour lire/interpréter ce langage. Ainsi, avec Jupyter, on peut facilement visualiser le résultat des cartes interactives dans les cellules résultats. En revanche, avec un outil comme Spyder, cela est moins pratique, car Spyder ne gère pas l'affichage du code html/javascript. Il faut donc enregistrer le résultat en .html, puis ouvrir ce fichier avec un navigateur.
Généralement, son installation est relativement simple :
conda install folium
import folium
Pour réaliser sa première carte, on peut s'appuyer sur les fonds de carte des API de cartographie web classiques comme OpenStreetMap. On utilise pour cela la fonction Map(). A l'intérieur de cette fonction, on pourra simplement spécifier le centrage de la carte avec l'argument location. Ici, pas de doute à avoir sur le système de coordonnées, tout passe par les coordonnées géographiques latitude/longitude du système WGS84.
m = folium.Map(location=(48.78829944638303,2.443630894415632))
m
On peut choisir son niveau de zoom de départ avec l'argument zoom_start. On peut rajouter des markers (folium.Marker()) pour positionner des points d'intérêt et jouer sur les arguments pour personnaliser l'icône du marker, rajouter une information au survol ou au clic...
m = folium.Map(location=(48.78829944638303,2.443630894415632), zoom_start=20)
folium.Marker(location=(48.78829944638303,2.443630894415632), tooltip="Cliquer", popup="UPEC", icon=folium.Icon(color="green")).add_to(m)
m
Par la suite, plutôt que d'afficher la carte sur toute la largeur de la page, je vais utiliser une fonction affiche_jupyter() pour afficher une carte plus petite. En théorie, pour gérer la largeur de la carte, c'est très simple il suffit d'utiliser l'argument width, mais dans une cellule Jupyter cela produit une sortie étrange. Vous pouvez de votre côté vous contenter d'afficher la carte m comme dans les deux exemples ci-dessus.
def affiche_jupyter(nom):
from IPython.display import IFrame, display
m.save(nom+".html")
iframe = IFrame(src=nom+".html", width=600, height=500)
display(iframe)
m = folium.Map(location=(48.78829944638303,2.443630894415632), zoom_start=20)
folium.Marker(location=(48.78829944638303,2.443630894415632), tooltip="Cliquer", popup="UPEC", icon=folium.Icon(color="green")).add_to(m)
affiche_jupyter(nom='m')
Pour changer de fond de carte, il faut utiliser l'argument tiles et connaitre le nom des fonds de carte disponibles. Il y en a beaucoup : https://leaflet-extras.github.io/leaflet-providers/preview/
m = folium.Map(location=(48.78829944638303,2.443630894415632), zoom_start=15, tiles="CartoDB positron")
affiche_jupyter(nom='m2')
Si vous ne voulez pas utiliser les fonds de carte des API, il est possible de mettre un tiles vide... Cela peut permettre de diffuser (d'afficher) uniquement ses propres données.
m = folium.Map(location=(48.78829944638303,2.443630894415632), tiles="", zoom_start=20)
folium.Marker(location=(48.78829944638303,2.443630894415632), tooltip="Cliquer", popup="UPEC").add_to(m)
affiche_jupyter(nom='m3')
Plusieurs fonds de carte, plus généralement plusieurs couches, peuvent être affichés. Il convient alors de rajouter un gestionnaire de couches (map.LayerControl()).
m = folium.Map(location=(48.78829944638303,2.443630894415632), zoom_start=20)
folium.TileLayer("CartoDB positron").add_to(m)
m.add_child(folium.map.LayerControl())
affiche_jupyter(nom='m4')
Il est possible d'utiliser des fonctions (par exemple PolyLine()) pour dessiner des objets sur le fond de carte.
m = folium.Map(location=(48.78829944638303,2.443630894415632), zoom_start=20)
folium.Marker(location=(48.78829944638303,2.443630894415632), tooltip="Cliquer", popup="UPEC", icon=folium.Icon(color="green")).add_to(m)
ligne = [(48.78845,2.44363),(48.78829,2.44463)]
folium.PolyLine(ligne).add_to(m)
affiche_jupyter(nom='m5')
On pourra alors placer les objets dans des groupes (FeatureGroup()) afin d'avoir des couches se superposant à un (ou plusieurs) fond de carte.
m = folium.Map(location=(48.78829944638303,2.443630894415632), zoom_start=20)
group1 = folium.FeatureGroup("Marker").add_to(m)
folium.Marker(location=(48.78829944638303,2.443630894415632)).add_to(group1)
group2 = folium.FeatureGroup("Ligne").add_to(m)
ligne = [(48.78845,2.44363),(48.78829,2.44463)]
folium.PolyLine(ligne).add_to(group2)
folium.LayerControl().add_to(m)
affiche_jupyter(nom='m6')
Afficher une couche Shapefile¶
Désormais, il est possible de chercher à afficher des données géographiques avec Folium. Nous allons prendre ici l'exemple d'un shapefile, celui des départements de France métropolitaine, présent dans ce dossier zippé : https://sergelhomme.fr/data/Donnees.zip
import geopandas as gpd
gdf = gpd.read_file('C:/Users/Serge/Desktop/Donnees/Departements.shp')
gdf.plot()
<Axes: >
Pour afficher ce shapefile avec Folium, il faut premièrement le transformer en WGS84, ce qui n'est pas le cas ici.
gdf.crs
<Projected CRS: EPSG:2154> Name: RGF93 v1 / Lambert-93 Axis Info [cartesian]: - [east]: Easting (metre) - [north]: Northing (metre) Area of Use: - undefined Coordinate Operation: - name: unnamed - method: Lambert Conic Conformal (2SP) Datum: Reseau Geodesique Francais 1993 v1 - Ellipsoid: GRS 1980 - Prime Meridian: Greenwich
gdf = gdf.to_crs(epsg=4326)
gdf.plot()
<Axes: >
Puis, il faut transformer ce shapefile dans un format geojson.
gdf_json = gdf.to_json()
Le tour est joué, on peut ajouter notre ancien fichier shapefile à un fond de carte à l'aide de la fonction GeoJson().
m = folium.Map(location=(47,2), zoom_start=5)
folium.GeoJson(gdf_json).add_to(m)
affiche_jupyter(nom='m7')
Pour plus d'interactivité, cette fonction GeoJson() possède un argument popup permettant d'appeler la fonction GeoJsonPopup(). Cela permet d'afficher simplement les champs souhaités par un simple clic sur l'objet. L’argument style_function permet de personnaliser le style de la couche.
m = folium.Map(location=(47,2), zoom_start=5)
folium.GeoJson(gdf_json,
popup=folium.GeoJsonPopup(fields=["CODE_DEPT","Tx_Etrange","NOM_DEPT"]),
style_function= lambda feature: {"color": "red"}).add_to(m)
affiche_jupyter(nom='m8')
A noter que l'objet gdf_json est un simple objet texte. Pour le manipuler comme un json structuré, on peut utiliser la bibliothhèque json et sa fonction loads(). Ensuite, on peut parcourir les objets géographiques (features) comme ci-dessous :
import json
data = json.loads(gdf_json)
for feature in data["features"][0:10]:
print(feature["properties"]["NOM_DEPT"])
AIN AISNE ALLIER ALPES-DE-HAUTE-PROVENCE HAUTES-ALPES ALPES-MARITIMES ARDECHE ARDENNES ARIEGE AUBE
Il est possible créer des cartes choroplèthes avec la fonction Choropleth(). On pourra y ajouter une couche pour mettre des popups comme ci-dessus, sauf que pour voir la carte choroplèthe, il faudra rendre cette couche invisible.
m = folium.Map(location=(47,2), zoom_start=5)
folium.Choropleth(
geo_data=gdf_json,
data=gdf,
columns=["CODE_DEPT","Tx_Etrange"],
fill_color="YlGn",
fill_opacity=0.7,
line_opacity=0.2,
legend_name="Tx_Etrange",
key_on = 'feature.properties.CODE_DEPT',
).add_to(m)
folium.GeoJson(
gdf_json,
style_function= lambda feature: {
"fillOpacity": 0, # Transparence totale
"color": "transparent", # Bordure transparente
},
popup=folium.GeoJsonPopup(fields=["CODE_DEPT","Tx_Etrange","NOM_DEPT"]),
).add_to(m)
affiche_jupyter(nom='m9')
Dans notre cas, on préférera s'appuyer sur des classes personnalisées.
# Définir les bornes pour 5 classes
bins = [0, .02, .04, .06, .1, .25]
m = folium.Map(location=(47,2), zoom_start=5)
folium.Choropleth(
geo_data=gdf_json,
data=gdf,
columns=["CODE_DEPT","Tx_Etrange"],
fill_color="YlGn",
fill_opacity=0.7,
line_opacity=0.2,
legend_name="Tx_Etrange",
key_on = 'feature.properties.CODE_DEPT',
bins=bins,
).add_to(m)
affiche_jupyter(nom='m10')
Bonus : encore plus d'interactivité avec ipywidgets¶
Compte-tenu des différents choix possibles pour effectuer une discrétisation, il peut sembler pertinent de faire varier les paramètres de manière dynamique pour identifier la meilleure représentation. En combinant Folium et ipywidgets il est possible d'obtenir un résultat intéressant dans Jupyter. Ci-dessous un code qui permet à l'aide d'une liste déroulante de changer de méthode de discrétisation :
Vérifier que ipywidgets est bien à jour : pip install --upgrade ipywidgets
import folium
import geopandas as gpd
import pandas as pd
import mapclassify
import ipywidgets as widgets
from IPython.display import display, clear_output
# Charger les données des départements français
gd = gpd.read_file('C:/Users/Serge/Desktop/Donnees/Departements.shp')
gd = gd.to_crs(epsg=4326)
gdf_json = gd.to_json()
# Liste des méthodes de discrétisation disponibles
methods = ["Quantiles", "EqualInterval", "NaturalBreaks"]
# Création du widget pour choisir la méthode de discrétisation
dropdown = widgets.Dropdown(
options=methods,
value="Quantiles",
description="Méthode :",
style={'description_width': 'initial'}
)
# Zone de sortie pour afficher la carte
output = widgets.Output()
# Fonction pour mettre à jour la carte
def update_map(change):
with output:
clear_output(wait=True) # Effacer l'ancienne carte
classifier = getattr(mapclassify, dropdown.value)(gd["Tx_Etrange"], k=5)
thresholds = [min(gd["Tx_Etrange"])] + list(classifier.bins)
m = folium.Map(location=[46.6031, 1.8883], zoom_start=6)
folium.Choropleth(
geo_data=gdf_json,
data=gd,
columns=["CODE_DEPT","Tx_Etrange"],
fill_color="YlGn",
fill_opacity=0.7,
line_opacity=0.2,
legend_name="Tx_Etrange",
key_on = 'feature.properties.CODE_DEPT',
bins = thresholds,
).add_to(m)
display(m)
# Lier la mise à jour au changement de sélection
dropdown.observe(update_map, names="value")
# Afficher les widgets et la carte initiale
display(dropdown,output)
update_map(None)
Cela peut sembler un peu poussé de faire cela pour un tel résultat. Mais au final, une des forces de Python est ici. En offrant par exemple assez facilement l'accès à des widgets, on peut créer des affichages très personnalisés et dynamiques pour explorer des données. C'est sans doute pour cela que Python est très apprécié des datascientists, alors-même que des langages comme R semblent plus spécialisés dans le domaine de la statistique. Ci-dessous on fait varier les principaux paramètres de la régession et ce n'est pas si long en ligne de codes (et encore j'aurai plus faire plus simple je pense) :
import folium
import geopandas as gpd
import mapclassify
import ipywidgets as widgets
from IPython.display import display, clear_output
# Charger les données des départements français
gd = gpd.read_file('C:/Users/Serge/Desktop/Donnees/Departements.shp')
gd = gd.to_crs(epsg=4326)
gdf_json = gd.to_json()
# Liste des méthodes de discrétisation disponibles
methods = ["Quantiles", "EqualInterval", "NaturalBreaks"]
palette = ["YlGn", "YlOrRd", "Purples"]
# Création du widget pour choisir la méthode
dropdown = widgets.Dropdown(
options=methods,
value="Quantiles",
description="Méthode :",
style={'description_width': 'initial'}
)
# Création du widget pour choisir la palette
dropdown2 = widgets.Dropdown(
options=palette,
value="YlGn",
description="Palette :",
style={'description_width': 'initial'}
)
# Création du widget pour choisir le nombre de classes
dropdown3 = widgets.IntText(
value=5,
description="Nombre de classes :",
style={'description_width': 'initial'}
)
# Zone de sortie pour afficher la carte
output = widgets.Output()
# Fonction pour mettre à jour la carte
def update_map(change):
with output:
clear_output(wait=True) # Effacer l'ancienne carte
classifier = getattr(mapclassify, dropdown.value)(gd["Tx_Etrange"], k=dropdown3.value)
thresholds = [min(gd["Tx_Etrange"])] + list(classifier.bins)
m = folium.Map(location=[46.6031, 1.8883], zoom_start=6)
folium.Choropleth(
geo_data=gdf_json,
data=gd,
columns=["CODE_DEPT","Tx_Etrange"],
fill_color= dropdown2.value,
fill_opacity=0.7,
line_opacity=0.2,
legend_name="Tx_Etrange",
key_on = 'feature.properties.CODE_DEPT',
bins = thresholds,
).add_to(m)
display(m)
# Lier la mise à jour au changement de sélection
dropdown.observe(update_map, names="value")
dropdown2.observe(update_map, names="value")
dropdown3.observe(update_map, names="value")
# Afficher les widgets et la carte initiale
display(dropdown,dropdown2,dropdown3,output)
update_map(None)
Pour obtenir le même résultat en HTML/Javascript, utiliser Python avec Folium n'est pas très pertinent selon moi. Pour cela, il faut simplement rajouter du HMTL et du javascript au code généré par Folium. Sur le papier, cela se fait très bien. Dans la réalité, le code généré par Folium est un peu complexe, avec par exemple des noms de variable générés avec des parties aléatoires. Bref, à ce niveau-là, sachant qu'il faudra écrire dans Python le code HTML et du javascript, autant faire le code HTML/javascript tout seul et apprendre leaflet : https://sergelhomme.fr/notebook/fincolorplusieurs.html