Faire une carte avec les DROM rapprochés grâce à cartiflette

Auteur·rice

Lino Galiana

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”
AstucePourquoi utiliser cartiflette pour ce type de besoins ?
  • 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 GeoDataFrame prê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.

Listing 1: Example d’utilisation de cartiflette
from cartiflette import carti_download

departements = carti_download(
    values="France",
    borders="DEPARTEMENT",
    filter_by="FRANCE_ENTIERE_DROM_RAPPROCHES",
    year=2022,
)
1
Millésime publié en GeoJSON, avant la diffusion en GeoParquet : carti_download lit 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
Folium est une librairie qui est pensée, avant tout, pour proposer un fonds de carte type OpenStreetMap en 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:

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].title

Ce 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()

3 Comment faire sans cartiflette ?

L’approche est nettement plus fastidieuse sans cartiflette et nécessite une expertise assez avancée en géomatique pour manipuler les géométries des objets Python.

Pour obtenir les mêmes données, prêtes à l’emploi, cela passe par quatre étapes principales :

  • 1️⃣ Télécharger l’archive de l’IGN, pour toute la France, et en extraire la couche des départements.
  • 2️⃣ L’importer dans Python, dans une projection adaptée aux transformations géométriques.
  • 3️⃣ Pour chaque DROM et pour les départements de la petite couronne, modifier la géométrie par une mise à l’échelle et une translation.
  • 4️⃣ Consolider le fond de carte à partir des différents éléments obtenus.

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 .7z regroupant toutes les couches au format shapefile : il faut la télécharger en entier pour n’en garder que les départements.

Listing 2: 1️⃣ Code pour télécharger les données
1️⃣ Code pour télécharger l’archive de l’IGN
import os
import requests
import py7zr

edition = "ADMIN-EXPRESS-COG-CARTO_3-1__SHP_WGS84G_FRA_2022-04-15"
url_ign = (
    "https://data.geopf.fr/chunk/telechargement/download/"
    f"ADMIN-EXPRESS-COG-CARTO/{edition}/{edition}.7z"
)
dossier_ign = "adminexpress-2022"
archive_ign = f"{dossier_ign}/{edition}.7z"
os.makedirs(dossier_ign, exist_ok=True)

# Toutes les couches, pour toute la France, téléchargées une seule fois
if not os.path.exists(archive_ign):
    with requests.get(url_ign, stream=True) as r:
        r.raise_for_status()
        with open(archive_ign, "wb") as f:
            for bloc in r.iter_content(chunk_size=1 << 20):
                f.write(bloc)

# On n'extrait que le shapefile des départements (.shp, .dbf, .shx...)
with py7zr.SevenZipFile(archive_ign, mode="r") as z:
    membres = [
        nom for nom in z.getnames()
        if os.path.basename(nom).split(".")[0] == "DEPARTEMENT"
    ]
    z.extract(path=dossier_ign, targets=membres)
shp_departements = os.path.join(
    dossier_ign, next(nom for nom in membres if nom.endswith(".shp"))
)

fichiers_ign = {"archive": archive_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.

Listing 3: 2️⃣ Importer les données
2️⃣ Importer les départements de l’IGN
import geopandas as gpd

france = gpd.read_file(shp_departements).to_crs(3857)

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 cartiflette aujourd’hui : le résultat est très proche de son fond de carte.

Listing 4: 3️⃣ Déplacer les DROM et zoomer sur la petite couronne
3️⃣ Déplacer les DROM et zoomer sur la petite couronne
# Translation (en mètres, EPSG:3857) et facteur d'échelle de chaque élément
DEPLACEMENTS = {
    "971": ((6_355_000, 3_330_000), 1.5),   # Guadeloupe
    "972": ((6_480_000, 3_505_000), 1.5),   # Martinique
    "973": ((5_760_000, 4_720_000), 0.35),  # Guyane
    "974": ((-6_170_000, 7_560_000), 1.5),  # La Réunion
    "976": ((-4_885_000, 6_590_000), 1.5),  # Mayotte
}
PETITE_COURONNE = ["75", "92", "93", "94"]


def deplacer(gdf, translation, echelle):
    """Agrandit (ou réduit) autour du centre de l'emprise, puis translate."""
    xmin, ymin, xmax, ymax = gdf.total_bounds
    centre = ((xmin + xmax) / 2, (ymin + ymax) / 2)
    geometrie = gdf.geometry.scale(echelle, echelle, origin=centre)
    return gdf.set_geometry(geometrie.translate(*translation))


drom_deplaces = [
    deplacer(france.loc[france["INSEE_DEP"] == code], translation, echelle)
    for code, (translation, echelle) in DEPLACEMENTS.items()
]
zoom_idf = deplacer(
    france.loc[france["INSEE_DEP"].isin(PETITE_COURONNE)],
    (-650_000, 275_000),
    4,
)

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_insee au lieu de INSEE_DEP…) : ce code ne fonctionnerait pas tel quel sur un millésime récent, alors que celui utilisant cartiflette ne change que par l’année.

Listing 5: 4️⃣ Réunir toutes les géométries
Code
import pandas as pd

hexagone = france.loc[~france["INSEE_DEP"].isin(DEPLACEMENTS)]
france_modif = pd.concat([hexagone, *drom_deplaces, zoom_idf]).to_crs(4326)

france_modif.boundary.plot(edgecolor="black", linewidth=0.2)

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 GeoDataFrame obtenu avec cartiflette au format GeoParquet, compact et lisible directement par GeoPandas.

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 cartiflette repré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

  1. 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 POPULATION qui est celle qui sera utilisée ici. Des travaux ultérieurs sur cartiflette viseront à 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.↩︎

  2. 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.↩︎

  3. Javascript est 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 exemple bete, qui n’est pas définie en amont. Celle-ci peut être définie plus loin dans le code, c’est Javascript qui va réordonner de manière logique l’exécution du code.↩︎

  4. cartiflette construit ce fond de carte avec mapshaper, en s’inspirant d’un tutoriel d’Eric Mauvière.↩︎