Faire une carte avec les DROM rapprochés grâce à cartiflette
Ce tutoriel vise à illustrer un cas d’usage classique de cartiflette : récupérer de manière flexible un fonds de carte dont les DROM sont rapprochés et où on voit un zoom sur l’Ile de France.
Ce besoin classique est illustré en répliquant la carte faite par Jules Grandin pour son podcast “La Minute Cartographique” L’objectif final de ce tutoriel est de faire cette carte:
Voir l’épisode en question de “La Minute Cartographique”
- Beaucoup moins de ligne de code à écrire :
- Réduit le temps nécessaire avant d’obtenir une carte exploratoire, ce qui permet de se concentrer sur la construction de celle-ci plutôt que les étapes antérieures
- Réduit la difficulté à mettre à jour le code ;
- Moins de bande passante et d’espace disque utilisé car seule la donnée nécessaire est téléchargée ;
- Moindre besoin d’expertise en SIG car la librairie fournit un
GeoDataFrameprêt à l’emploi ce qui ne nécessite pas une connaissance pointue dans le domaine (système de projection, format shapefile, etc.) ; - Moins de risque d’erreur que lorsqu’on fait soi-même la combinaison de sources à des niveaux administratifs différents (accoler le masque des arrondissements à celui des communes limitrophes nécessite beaucoup de précautions) ;
- Bénéficier de métadonnées supplémentaires sur les communes que les fonds de carte
AdminExpress
1 Préliminaire: récupération des cheptels français
La vidéo de Jules Grandin évoque une ouverture et préparation des données dans Excel 😱. Néanmois, en quelques lignes de code, on peut render ces données propres à l’analyse statistique et cartographique avec Python 😌
Préparation des données avec Pandas
import pandas as pd
def import_cheptel():
url_cheptel = "https://www.insee.fr/fr/statistiques/fichier/2012795/TCRD_073.xlsx"
cheptel2022 = pd.read_excel(url_cheptel, header=3, na_values="nd")
type_cheptel = cheptel2022.columns[~cheptel2022.columns.str.startswith("Unnamed")]
cheptel2022.loc[:, type_cheptel] = cheptel2022.loc[:, type_cheptel].mul(1000)
cheptel2022 = cheptel2022.rename(
{
"Unnamed: 0": "code",
"Unnamed: 1": "departement",
"Volailles gallus": "Volailles",
},
axis="columns",
)
type_cheptel = type_cheptel.str.replace(" gallus", "")
return cheptel2022, type_cheptel
cheptel = import_cheptel()
cheptel[0].head(4)- 1
- Les premières lignes sont de la documentation, les données ne commencent réellement que ligne 4
- 2
- Extraction du type de bêtes, cela sera plus utile ultérieurement
- 3
- Les données sont en milliers de tête, on ajuste pour pouvoir comparer à la population
- 4
- La ligne correspondant aux noms de variable n’est pas complète dans le Excel. Il faut donc retravailler les noms de colonnes.
- 5
- On renvoie la donnée et la liste des bêtes
| code | departement | Bovins | Porcins | Ovins | Caprins | Volailles | |
|---|---|---|---|---|---|---|---|
| 0 | 01 | Ain | 155400.0 | 140900.0 | 20800.0 | 6500.0 | NaN |
| 1 | 02 | Aisne | 171600.0 | 47300.0 | 30000.0 | 700.0 | NaN |
| 2 | 03 | Allier | 478100.0 | 88600.0 | 127000.0 | 8600.0 | NaN |
| 3 | 04 | Alpes-de-Haute-Provence | 14900.0 | 2400.0 | 188800.0 | 9500.0 | NaN |
En ce qui concerne la population, qui nous servira au dénominateur de notre indicateur, nous allons utiliser la variable fournie dans les données récupérées par le biais de cartiflette1.
2 Récupérer les contours modifiés via cartiflette
2.1 La récupération des données utilisant cartiflette
Notre objectif est de faire une carte aux caractéristiques suivantes:
- Les DROM sont rapprochés de la France hexagonale en étant placés sous celle-ci ;
- Un cartogramme zoomé de la petite couronne parisienne (départements de la Seine, Hauts-de-Seine, Seine-Saint-Denis et Val de Marne) est disponible en haut à gauche de celle-ci.
Construire soi-même ce fonds de carte est fastidieux, comme l’illustre la partie Comment faire sans cartiflette. Avec cartiflette, ce fonds de carte est préconstruit à partir des découpages officiels de l’IGN. Il suffit de remplacer le niveau FRANCE_ENTIERE par FRANCE_ENTIERE_DROM_RAPPROCHES dans l’appel à carti_download.
Les données de cheptel portent sur 2022 : on prend donc les contours de la même année. Le millésime 2022 a été publié avant que cartiflette ne diffuse ses fonds de carte au format GeoParquet, sous la forme d’un fichier GeoJSON par valeur. Le code est pourtant le même que pour un millésime récent : carti_download choisit lui-même le format disponible.
- 1
-
Millésime publié en GeoJSON, avant la diffusion en GeoParquet :
carti_downloadlit les fichiers GeoJSON. À partir de 2025, il lit un fichier GeoParquet dont il ne télécharge que les parties utiles.
departements est un GeoDataFrame classique proposant les métadonnées usuelles de cartiflette avec des géographies modifiées pour rapprocher la France des DROM2.
departements.boundary.plot(edgecolor = "black", linewidth=0.2)
2.2 Association du fonds de carte cartiflette au jeu de données initial
La suite du travail n’est pas intrinsèquement lié à cartiflette mais correspond à de la manipulation de données spatiales nécessaires à la finalisation de la carte.
Comme nous avons besoin de localiser les décomptes départements de bétail, nous faisons une jointure spatiale entre notre fonds de carte et nos cheptels
cheptel_geospatial = departements.merge(
cheptel[0], left_on="INSEE_DEP", right_on = "code"
)A partir de là, on finalise notre travail sur le jeu de données pour calculer le ratio entre le nombre de têtes et la population:
Dérouler pour voir la finalisation du travail de données
def ratio_cheptel(type_cheptel, data_departement):
"""
Fonction permettant de rapporter, pour un type de bête donné,
le nombre de têtes par habitant d'un département
"""
data_departement = data_departement.copy()
data_departement["ratio_" + type_cheptel] = (
data_departement.loc[:, type_cheptel]
.div(data_departement["POPULATION"], axis=0)
)
data_departement["more_" + type_cheptel] = (
data_departement["ratio_" + type_cheptel] > 1
)
return data_departement.loc[:, "more_" + type_cheptel]
eval_ratio = pd.concat(
[ratio_cheptel(bete, cheptel_geospatial) for bete in cheptel[1]],
axis = 1
)
cheptel_geospatial = pd.concat(
[cheptel_geospatial, eval_ratio],
axis = 1
)- 1
- Un DataFrame temporaire permettant de définir, par le biais d’une boucle, nos indicateurs de population.
- 2
- On associe ces nouvelles colonnes à nos données initiales
Le jeu de données prêt à l’emploi prend la forme suivante:
cheptel_geospatial.head(2)| INSEE_DEP | PAYS | LIBELLE_DEPARTEMENT | POPULATION | SOURCE | geometry | code | departement | Bovins | Porcins | Ovins | Caprins | Volailles | more_Bovins | more_Porcins | more_Ovins | more_Caprins | more_Volailles | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 43 | France | Haute-Loire | 227570 | IGN:EXPRESS-COG-CARTO-TERRITOIRE | POLYGON ((3.89745 45.35699, 3.89731 45.35658, ... | 43 | Haute-Loire | 206400.0 | 60800.0 | 124100.0 | 13800.0 | NaN | False | False | False | False | False |
| 1 | 65 | France | Hautes-Pyrénées | 229567 | IGN:EXPRESS-COG-CARTO-TERRITOIRE | MULTIPOLYGON (((-0.09705 43.5822, -0.09642 43.... | 65 | Hautes-Pyrénées | 89700.0 | 41900.0 | 127400.0 | 6400.0 | NaN | False | False | False | False | False |
2.3 Production de la carte interactive
On peut maintenant se concentrer sur la carte. Pour la rendre plus informative, il est utile de contextualiser les départements en permettant aux spectateurs de la carte de connaître le nom des départements ayant un cheptel plus important que sa population.
Cela peut être fait comme sur la carte d’origine de Jules Grandin en inscrivant les noms des départements concernés sur celle-ci. Cela nécessite tout de même un travail manuel important pour qui n’utilise pas une interface comme QGIS.
Il est plus simple, pour les utilisateurs de logiciels comme Python, R ou Javascript de produire une carte interactive qui permet d’afficher des valeurs lorsqu’on passe la souris dessus. Il existe plusieurs écosystèmes pour faire cela, plus ou moins directement reliés à Javascript, le langage consacré pour ce type d’applications réactives.
Les exemples ci-dessous proposent un code en Python pour faire une carte interactive par le biais de la librairie Folium. Cette manière de procéder étant un peu fastidieuse, car Folium n’est pas initialement conçu pour ce type de cartes, il est recommandé de privilégier la solution Observable qui est un framework Javascript ressemblant beaucoup à Python mais plus adapté que ce dernier pour la dataviz.
En premier lieu, on définit une fonction générique qui permet de construire, pour un type de cheptel donné, la carte qui sera créée:
Définition d’une fonction générique pour créer la carte
import folium
import branca
palettes = {
"Bovins": {True: "#f9b439", False: "#d8d4d3"},
"Porcins": {True: "#ec6569", False: "#d8d4d3"},
"Ovins": {True: "#5eb04e", False: "#d8d4d3"},
"Caprins": {True: "#5eb04e", False: "#d8d4d3"}
}
# Function to apply color based on the 'more_XXXXX' values
def style_function(feature, bete):
more_bovins = feature['properties'][f'more_{bete}']
return {
'fillColor': palettes[bete][more_bovins],
'color': 'black',
'weight': 1,
'fillOpacity': 0.7,
}
def map_repartition(cheptel_geospatial, bete):
white_tile = branca.utilities.image_to_url([[1, 1], [1, 1]])
# Initialize a folium map
m = folium.Map(
location=[
cheptel_geospatial.geometry.centroid.y.mean(), cheptel_geospatial.geometry.centroid.x.mean()
],
zoom_start=5,
tiles=white_tile, attr="white tile"
)
# Add GeoJson layer to the map
folium.GeoJson(
cheptel_geospatial,
style_function=lambda x: style_function(x, bete),
tooltip=folium.GeoJsonTooltip(
fields=['LIBELLE_DEPARTEMENT', 'POPULATION', bete],
aliases=['Department:', 'Population:', f'{bete}:'],
localize=True
)
).add_to(m)
return m- 1
-
Foliumest une librairie qui est pensée, avant tout, pour proposer un fonds de carte typeOpenStreetMapen arrière-plan. Celui-ci ne nous intéresse pas ici car nous avons créé des éléments sans contrepartie sur la Terre (les DROM sous la France, le zoom de l’Ile de France au nord ouest de la Normandie). Nous créons donc un fond blanc qui est utilisé de manière détournée par Folium.
Pour créer celles-ci, il suffit dès lors d’appliquer la fonction sur le type d’élevage désiré:
Visualiser par le biais de Folium
map_repartition(cheptel_geospatial, "Bovins")
map_repartition(cheptel_geospatial, "Porcins")
map_repartition(cheptel_geospatial, "Ovins")Néanmoins, ce n’est pas la méthode adoptée pour créer les cartes ci-dessous:
Pour cela, il vaut mieux utiliser Javascript par le biais d’Observable dont le code est disponible dans la boite déroulable ci-dessous:
Javascript ?
Ce tutoriel utilise le framework Observable par le biais de Quarto, l’outil de construction de site web développé par Posit. Le code source de cette page est disponible ici3.
Pour se simplifier la tâche, on va créer une fonction graphique qui prend en input un Array et un type d’élevage (par exemple “Bovins”) et renvoie une carte:
import {addTooltips} from "@mkfreeman/plot-tooltip"
function produceMap(cheptel, bete){
const carte = addTooltips(
Plot.plot({
marginLeft: 0,
marginRight: 0,
width: 400,
height: 400,
color: {
legend: true,
value: [
labels_widgets.filter(d => d.type == bete)[0].true,
labels_widgets.filter(d => d.type == bete)[0].false
],
range: [
palettes.filter(d => d.name == bete)[0].trueColor,
palettes.filter(d => d.name == bete)[0].falseColor
],
label: "Alors ?"
},
y: {axis: null},
x: {axis: null},
marks: [
Plot.geo(cheptel, {
fill: (d) => labels_widgets.filter(d => d.type == bete)[0][d.properties[bete] > d.properties.POPULATION],
sort: {
color: "fill",
reverse: true
},
title: (d) => `${d.properties.LIBELLE_DEPARTEMENT} (${d.properties.INSEE_DEP}) \nCheptel: ${d.properties[bete]} \nPopulation: ${d.properties.POPULATION} \n Ratio: ${(d.properties[bete]/d.properties.POPULATION).toLocaleString(undefined, {maximumSignificantDigits: 2})}`,
})
]
})
)
return carte
}Celle-ci utilise un certain nombre d’ingrédients. En premier lieu, des arrays permettant de changer les valeurs affichées en fonction de la valeur du widget sélectionné :
labels_widgets = [
{type: "Bovins", true: "Plus de vaches 🐮", false: "Plus d'habitants"},
{type: "Porcins", true: "Plus de cochons 🐷", false: "Plus d'habitants"},
{type: "Ovins", true: "Plus de moutons 🐑", false: "Plus d'habitants"},
{type: "Caprins", true: "Plus de chèvres 🐐", false: "Plus d'habitants"}
]
title = [
{type: "Bovins", title: "Plus de vaches 🐮 ou d'habitants ?"},
{type: "Porcins", title: "Plus de cochons 🐷 ou d'habitants ?"},
{type: "Ovins", title: "Plus de moutons 🐑 ou d'habitants ?"},
{type: "Caprins", title: "Plus de chèvres 🐐 ou d'habitants ?"}
]
palettes = [
{
name: "Bovins",
trueColor: "#f9b439",
falseColor: "#d8d4d3"
},
{
name: "Porcins",
trueColor: "#ec6569",
falseColor: "#d8d4d3"
},
{
name: "Ovins",
trueColor: "#5eb04e",
falseColor: "#d8d4d3"
},
{
name: "Caprins",
trueColor: "#00b5ed",
falseColor: "#d8d4d3"
}
]
title_chosen = title.filter(d => d.type == bete)[0].titleCe widget est un radio button qui peut être créé par le code suivant, permettant à la fois d’exposer celui-ci sur l’interface et de stocker sa valeur active dans la variable bete
viewof bete = Inputs.radio(
new Map([["Vaches 🐮", "Bovins"], ["Cochons 🐷", "Porcins"], ["Moutons 🐑", "Ovins"]]), {value: "Bovins"}
)Il ne manque qu’à importer les données nécessaires à la carte dans Javascript. La fonction ojs_define permettant la transmission d’un objet Python à R ne fonctionnant pas pour un GeoPandas, le plus simple est de créer un GeoJSON en Python :
cheptel_geospatial.to_file(
"cheptel.json", driver="GeoJSON"
)Il suffit ensuite d’importer celui-ci avec la fonction FileAttachment d’Observable
cheptel = FileAttachment("cheptel.json").json()4 Bilan
Si le fait qu’il suffise que le code de cartiflette se réduise à Listing 1 contre Listing 2, Listing 3, Listing 4 et Listing 5 pour obtenir un résultat équivalent ne suffit pas, si le fait que le GeoDataFrame obtenu avec cartiflette comporte plus de métadonnées que celui créé sans ce package n’est pas non plus suffisant, la suite déroule quelques arguments supplémentaires de l’intérêt d’utiliser cartiflette.
4.1 Volume de données téléchargées
Fonction convert_size pour avoir les tailles de fichiers dans un format lisible
import math
def convert_size(size_bytes):
if size_bytes == 0:
return "0 o"
size_name = ("o", "Ko", "Mo", "Go", "To")
i = int(math.floor(math.log(size_bytes, 1024)))
s = round(size_bytes / math.pow(1024, i), 1)
return f"{s} {size_name[i]}"Taille des couches téléchargées sur le site de l’IGN
poids_ign_bytes = sum(os.path.getsize(f) for f in fichiers_ign.values())
poids_ign = convert_size(poids_ign_bytes)Taille des mêmes données issues de cartiflette
writedir = "cartiflette-data/usecase2"
os.makedirs(writedir, exist_ok=True)
departements.to_parquet(f"{writedir}/contours.parquet")
poids_cartiflette_bytes = os.path.getsize(f"{writedir}/contours.parquet")
poids_cartiflette = convert_size(poids_cartiflette_bytes)- 1
-
On enregistre le
GeoDataFrameobtenu aveccartifletteau format GeoParquet, compact et lisible directement parGeoPandas.
Les deux approches portent sur la France entière, à la même résolution (carti_download ne simplifie pas les contours par défaut). Mais l’IGN ne diffuse pour 2022 qu’une archive regroupant toutes les couches (communes, arrondissements, EPCI…) :
- les données
cartiflettereprésenteraient 11.8 Mo si elles étaient stockées sur disque ; - sans passer par
cartiflette, il faut télécharger 142.9 Mo pour n’en utiliser que la couche des départements.
4.2 Nombre de lignes de code
En ce qui concerne le temps de développement de ce code, on est sur quelques secondes pour le code avec cartiflette en utilisant la documentation interactive d’exemples contre plusieurs dizaines de minutes pour le code sans cartiflette.
4.3 Temps de traitement
Le code cartiflette permet de récupérer les éléments contextuels nécessaires en 7.9 s. Sans cartiflette, il faut 9.8 s, soit un rapport de 1 à 1 pour cette tâche.
Notes de bas de page
Dans l’idéal, nous utiliserions les populations légales mesurées par l’Insee. Les fonds de carte de l’IGN proposent néanmoins déjà une variable
POPULATIONqui est celle qui sera utilisée ici. Des travaux ultérieurs surcartifletteviseront à remplacer cette variable par les populations légales pour assurer que les métadonnées utilisées par les fonds de carte correspondent, au même titre que les découpages géométriques, aux données officielles.↩︎Ce fonds de carte est en géométrie WGS84 (CRS: 4326). Ce n’est néanmoins qu’une apparence car les géométries nécessaires pour représenter les DROM et l’Ile de France sur cette carte ne correspondent à aucune réalité terrestre. Il ne faut donc considérer ce fonds de carte que pour la cartographie finale, pas la manipulation de données géographique (par exemple pour un calcul de distance terrestre). Pour celles-ci, le fonds de carte normal à utiliser est
FRANCE_ENTIERE.↩︎Javascriptest un langage asynchrone donc l’exécution du code ne dépend pas de l’ordre de définition des objets dans le code, contrairement àPython. Il est donc normal de voir dans les codes ci-dessous une utilisation d’une variable, par exemplebete, qui n’est pas définie en amont. Celle-ci peut être définie plus loin dans le code, c’estJavascriptqui va réordonner de manière logique l’exécution du code.↩︎cartifletteconstruit ce fond de carte avecmapshaper, en s’inspirant d’un tutoriel d’Eric Mauvière.↩︎
3 Comment faire sans
cartiflette?L’approche est nettement plus fastidieuse sans
cartifletteet nécessite une expertise assez avancée en géomatique pour manipuler les géométries des objetsPython.Pour obtenir les mêmes données, prêtes à l’emploi, cela passe par quatre étapes principales :
Python, dans une projection adaptée aux transformations géométriques.La première étape consiste donc à télécharger les données depuis le site de mise à disposition de l’IGN. En 2022, ADMIN EXPRESS COG CARTO n’est diffusé que sous la forme d’une archive
.7zregroupant toutes les couches au format shapefile : il faut la télécharger en entier pour n’en garder que les départements.1️⃣ Code pour télécharger l’archive de l’IGN
La deuxième étape consiste à importer les contours administratifs dans
Python, en projection Web Mercator (EPSG:3857) : les déplacements sont exprimés en mètres dans cette projection.2️⃣ Importer les départements de l’IGN
La troisième étape est celle faisant appel à une expertise dans le domaine géomatique. Il faut trouver, pour chaque DROM, une mise à l’échelle et une translation qui le placent sous la France hexagonale, et faire de même pour un zoom sur la petite couronne parisienne. Les paramètres ci-dessous sont ceux qu’utilise
cartifletteaujourd’hui : le résultat est très proche de son fond de carte.3️⃣ Déplacer les DROM et zoomer sur la petite couronne
Enfin, la dernière étape consiste à consolider notre fonds de carte en réunissant les géométries4. On obtient un fonds de carte équivalent à celui de
cartiflette, qui pourrait être utilisé de la même manière que précédemment ☝️, à ceci près qu’il manque les métadonnées de l’Insee. Les noms de colonnes de l’IGN ont en outre changé avec l’édition 2025 (code_inseeau lieu deINSEE_DEP…) : ce code ne fonctionnerait pas tel quel sur un millésime récent, alors que celui utilisantcartiflettene change que par l’année.Code