Ne récupérer que les communes de plus de 2 000 habitants avec cartiflette

Auteur·rice

Lino Galiana

Ce tutoriel illustre un besoin fréquent : ne garder qu’une partie des géométries, soit selon leurs attributs, soit par des filtres géographiques.

AstucePourquoi filtrer avec cartiflette ?
  • Moins de données téléchargées : cartiflette ne rapatrie que les parties du fichier publié utiles à la requête, et le filtre est appliqué pendant la lecture ;
  • Une seule ligne de code : la condition s’ajoute aux paramètres habituels de carti_download ;
  • Le même filtre partout : l’API de cartiflette accepte le même paramètre where, utilisable depuis R, JavaScript ou un navigateur.

Avec cartiflette, ce filtre se fait au moment du téléchargement, grâce à l’argument where, une condition écrite en SQL. Seules les communes retenues sont récupérées, avec tous leurs attributs, prêtes à être cartographiées :

Commençons d’abord par ne garder que les communes de plus de 2 000 habitants de la région Occitanie. Cela nous donnera, in fine, cette carte:

Nous verrons ensuite comment récupérer des données par le biais de requêtes géographiques.

1 Récupérer toute la région, la démarche habituelle

La démarche habituelle consiste à demander les communes (borders="COMMUNE") d’une région (filter_by="REGION"), identifiée par son code Insee : 76 pour l’Occitanie.

from cartiflette import carti_download

occitanie = carti_download(
    values="76",
    borders="COMMUNE",
    filter_by="REGION",
    year=2026,
)

On obtient les 4 446 communes de la région, avec leur population (POPULATION), leur département, leur bassin de vie, etc.

INSEE_COM NOM POPULATION LIBELLE_DEPARTEMENT
0 09007 Allières 76 Ariège
1 09009 Alzen 262 Ariège
2 09013 Arabaux 79 Ariège
ax = occitanie.plot(color="#c6d7e8", edgecolor="white", linewidth=0.1)
occitanie.dissolve("INSEE_DEP").boundary.plot(ax=ax, color="black", linewidth=0.4)
ax.set_axis_off()

Il serait possible de ne garder les communes de plus de 2 000 habitants qu’ensuite, avec Pandas (occitanie.loc[occitanie["POPULATION"] > 2000]). Mais il aurait fallu télécharger toutes les communes, et la plupart d’entre elles sont aussitôt écartées.

2 Ne récupérer que les communes de plus de 2 000 habitants

L’argument where applique ce filtre dès la lecture :

Listing 1: Filtrer les communes avec where
grandes_communes = carti_download(
    values="76",
    borders="COMMUNE",
    filter_by="REGION",
    year=2026,
    where="POPULATION > 2000",
)
1
Une condition SQL, comme dans une clause WHERE, sur n’importe quelle colonne : "POPULATION > 2000 AND INSEE_DEP = '31'", "NOM ILIKE 'saint%'", etc.

Il ne reste que 542 communes, qui regroupent 73 % de la population de la région.

INSEE_COM NOM POPULATION
251 31555 Toulouse 514819
353 34172 Montpellier 310240
115 30189 Nîmes 151839
446 66136 Perpignan 121616
292 34032 Béziers 81545

3 Faire la carte

Les communes de toute la région, récupérées à la première étape, servent de fond de carte, et les limites des départements s’en déduisent. Les communes de plus de 2 000 habitants sont colorées selon leur population, sur une échelle logarithmique : Toulouse compte 250 fois plus d’habitants que les plus petites d’entre elles.

import matplotlib.pyplot as plt

departements = occitanie.dissolve("INSEE_DEP")

fig, ax = plt.subplots(figsize=(8, 7))
occitanie.plot(ax=ax, color="#eeeeee", edgecolor="white", linewidth=0.1)
grandes_communes.plot(
    ax=ax,
    column="POPULATION",
    scheme="NaturalBreaks",
    k=5,
    cmap="YlOrRd",
    edgecolor="white",
    linewidth=0.1,
    legend=True,
    legend_kwds={
        "title": "Population",
        "loc": "upper center",
        "bbox_to_anchor": (0.5, 0.02),
        "ncol": 3,
        "fmt": "{:.0f}",
        "frameon": False,
    },
)
departements.boundary.plot(ax=ax, color="black", linewidth=0.4)
ax.set_axis_off()
ax.set_title("Communes de plus de 2 000 habitants en Occitanie")
plt.figtext(
    0.5,
    0.02,
    "Source : IGN, ADMIN EXPRESS COG CARTO 2026",
    horizontalalignment="center",
    fontsize=8,
    style="italic",
)
plt.show()

Les communes de plus de 2 000 habitants en Occitanie

Les cartes de cette page sont faites avec Observable Plot, par le biais de Quarto. Le code source de cette page est disponible ici.

Les données passent de Python à Javascript avec la fonction ojs_define. Elle ne sait pas transmettre un GeoDataFrame, mais elle transmet un dictionnaire : celui d’un GeoJSON, ou d’un TopoJSON pour la région entière. Ce dernier format ne décrit qu’une fois chaque frontière entre deux communes : il pèse deux fois moins lourd, et il permet de tracer les limites des départements sans autre fichier.

import json

import shapely
import topojson as tp

colonnes = ["INSEE_COM", "NOM", "POPULATION", "INSEE_DEP", "LIBELLE_DEPARTEMENT", "geometry"]


def pour_javascript(gdf):
    """Polygones orientés comme l'attend Javascript (d3), seulement les colonnes utiles."""
    gdf = gdf[colonnes].copy()
    gdf["geometry"] = shapely.orient_polygons(gdf.geometry.values, exterior_cw=True)
    return gdf


ojs_define(
    occitanie_topo=json.loads(
        tp.Topology(pour_javascript(occitanie), prequantize=1e5, toposimplify=False).to_json()
    ),
    grandes_communes_geo=json.loads(pour_javascript(grandes_communes).to_json(drop_id=True)),
)
1
Le contour extérieur des polygones est décrit dans le sens inverse des aiguilles d’une montre, comme le demande la norme GeoJSON. d3, sur lequel repose Observable Plot, attend le sens inverse : sans cette ligne, chaque commune serait remplie comme « tout le globe sauf la commune ».

Côté Javascript, on reconstitue les communes et les limites des départements à partir du TopoJSON. La conversion en TopoJSON inverse le sens de quelques communes en plusieurs morceaux : redresser remet à l’endroit tout polygone qui couvrirait plus d’un hémisphère. La projection est une conique conforme proche du Lambert 93, la projection officielle de la France métropolitaine :

function redresser(collection) {
    const polygone = (anneaux) =>
        d3.geoArea({type: "Polygon", coordinates: anneaux}) > 2 * Math.PI
            ? anneaux.map((anneau) => anneau.slice().reverse())
            : anneaux
    for (const d of collection.features) {
        const g = d.geometry
        if (g.type === "Polygon") g.coordinates = polygone(g.coordinates)
        if (g.type === "MultiPolygon") g.coordinates = g.coordinates.map(polygone)
    }
    return collection
}

communes = redresser(topojson.feature(occitanie_topo, occitanie_topo.objects.data))

limites_departements = topojson.mesh(
    occitanie_topo, occitanie_topo.objects.data,
    (a, b) => a.properties.INSEE_DEP !== b.properties.INSEE_DEP
)

contour_region = topojson.mesh(
    occitanie_topo, occitanie_topo.objects.data, (a, b) => a === b
)

projection = ({type: "conic-conformal", parallels: [44, 49], rotate: [-3, 0], domain: communes})

infobulle = (d) => `${d.properties.NOM} (${d.properties.LIBELLE_DEPARTEMENT})
${d.properties.POPULATION.toLocaleString("fr-FR")} habitants`

La carte de toute la région montre chaque commune, avec les limites des départements :

function carteOccitanie() {
    return Plot.plot({
        projection,
        width: 640,
        marks: [
            Plot.geo(communes, Plot.centroid({
                fill: "#c6d7e8",
                stroke: "white",
                strokeWidth: 0.2,
                title: infobulle,
                tip: true
            })),
            Plot.geo(limites_departements, {stroke: "black", strokeWidth: 0.5}),
            Plot.geo(contour_region, {stroke: "black", strokeWidth: 0.8})
        ]
    })
}

La carte des communes de plus de 2 000 habitants les superpose au fond de carte de la région :

function carteGrandesCommunes() {
    return Plot.plot({
        projection,
        width: 640,
        color: {
            type: "log",
            scheme: "ylorrd",
            legend: true,
            label: "Population",
            ticks: [2000, 5000, 20000, 100000, 500000],
            tickFormat: (d) => d.toLocaleString("fr-FR"),
            marginLeft: 16,
            width: 360
        },
        marks: [
            Plot.geo(communes, {fill: "#eeeeee", stroke: "white", strokeWidth: 0.2}),
            Plot.geo(grandes_communes_geo, Plot.centroid({
                fill: (d) => d.properties.POPULATION,
                stroke: "white",
                strokeWidth: 0.2,
                title: infobulle,
                tip: true
            })),
            Plot.geo(limites_departements, {stroke: "black", strokeWidth: 0.5}),
            Plot.geo(contour_region, {stroke: "black", strokeWidth: 0.8})
        ]
    })
}

4 Aller plus loin : filtrer selon la géométrie

where accepte aussi les fonctions spatiales de DuckDB, sur la colonne geometry (coordonnées en longitude, latitude). Par exemple, les communes qui touchent un rectangle, ici la Camargue, à cheval sur l’Occitanie et la Provence-Alpes-Côte d’Azur :

camargue = carti_download(
    values="France",
    borders="COMMUNE",
    filter_by="FRANCE_ENTIERE",
    year=2026,
    where="ST_Intersects(geometry, ST_MakeEnvelope(4.1, 43.3, 4.9, 43.75))",
)
1
ST_MakeEnvelope(longitude min, latitude min, longitude max, latitude max). Les communes à cheval sur le bord du rectangle sont gardées entières.
import geopandas as gpd
from shapely.geometry import box

ax = camargue.plot(
    column="LIBELLE_DEPARTEMENT",
    categorical=True,
    edgecolor="white",
    linewidth=0.2,
    legend=True,
    legend_kwds={"loc": "upper left", "bbox_to_anchor": (1, 1), "frameon": False},
)
gpd.GeoSeries([box(4.1, 43.3, 4.9, 43.75)]).boundary.plot(
    ax=ax, color="black", linestyle="--", linewidth=1
)
ax.set_axis_off()
1
Le rectangle du filtre, en pointillés.

Les communes de la Camargue sont transmises comme celles de l’Occitanie :

ojs_define(camargue_geo=json.loads(pour_javascript(camargue).to_json(drop_id=True)))

Le rectangle du filtre est tracé comme une ligne (LineString) plutôt que comme un polygone, ce qui évite la question du sens de parcours :

rectangle = ({
    type: "LineString",
    coordinates: [[4.1, 43.3], [4.9, 43.3], [4.9, 43.75], [4.1, 43.75], [4.1, 43.3]]
})

function carteCamargue() {
    return Plot.plot({
        projection: {
            type: "conic-conformal", parallels: [44, 49], rotate: [-3, 0],
            domain: {type: "GeometryCollection", geometries: [rectangle, ...camargue_geo.features.map((d) => d.geometry)]}
        },
        width: 640,
        color: {legend: true, scheme: "set2"},
        marks: [
            Plot.geo(camargue_geo, Plot.centroid({
                fill: (d) => d.properties.LIBELLE_DEPARTEMENT,
                stroke: "white",
                strokeWidth: 0.4,
                title: infobulle,
                tip: true
            })),
            Plot.geo(rectangle, {stroke: "black", strokeDasharray: "4,3", strokeWidth: 1.2})
        ]
    })
}

D’autres exemples (distance à un point, commune contenant un point) sont dans la documentation du client Python.