Introduction à Python pour la Data Science¶
TP 4 — Application Pandas : cartographier les revenus en France¶
Problématique¶
Comment récupérer, nettoyer, analyser et représenter sur une carte des données de revenus à l’échelle communale en France ?
Dans ce TP, on construit une carte choroplèthe interactive de la médiane du revenu disponible par unité de consommation, commune par commune.
Le travail mobilise principalement :
pandaspour l’import, le nettoyage, les agrégations et les jointures ;requestspour interroger l’API dedata.gouv.fr;geopandaspour manipuler les contours géographiques ;foliumpour produire une carte interactive.
Le code INSEE de la commune sera la clé de jointure principale.
Objectifs¶
À l’issue du TP, vous devrez être capable de :
- récupérer automatiquement une ressource depuis l’API de
data.gouv.fr; - auditer un jeu de données réel ;
- nettoyer des colonnes numériques et des codes géographiques ;
- distinguer code postal et code INSEE ;
- contrôler la cardinalité d’une jointure ;
- combiner données statistiques et données géographiques ;
- construire une carte choroplèthe ;
- interpréter les limites statistiques et cartographiques d’un résultat.
Sources utilisées¶
Données de revenus
Jeu de données Revenus à la commune en France, diffusé sur data.gouv.fr. Il contient notamment le nombre de ménages, le nombre de personnes, des déciles, des quartiles et une médiane de revenu.
Contours communaux
Jeu de données Contours administratifs, diffusé sur data.gouv.fr et utilisé par l’API Découpage administratif.
Les ressources de ces jeux de données peuvent évoluer. Le TP utilise l’API de
data.gouv.frpour retrouver automatiquement les URL plutôt que de figer une URL de téléchargement.
1. Préparation de l’environnement¶
Les librairies geopandas, folium, mapclassify et openpyxl peuvent ne pas être installées dans tous les environnements.
Sur Google Colab, décommenter la cellule suivante.
# À exécuter uniquement si nécessaire.
%pip install geopandas folium mapclassify openpyxl requests
from pathlib import Path
import re
import unicodedata
import numpy as np
import pandas as pd
import requests
import matplotlib.pyplot as plt
import geopandas as gpd
import folium
from branca.colormap import linear
pd.set_option("display.max_columns", 100)
pd.set_option("display.float_format", lambda x: f"{x:,.2f}")
print("pandas :", pd.__version__)
print("geopandas :", gpd.__version__)
print("folium :", folium.__version__)
2. Organisation du projet¶
On crée un dossier data/ pour les fichiers téléchargés et un dossier outputs/ pour les résultats.
Cette organisation évite de mélanger :
- les données brutes ;
- les données nettoyées ;
- les cartes et exports produits.
DATA_DIR = Path("data")
OUTPUT_DIR = Path("outputs")
DATA_DIR.mkdir(exist_ok=True)
OUTPUT_DIR.mkdir(exist_ok=True)
DATA_DIR, OUTPUT_DIR
3. Récupérer les ressources avec l’API de data.gouv.fr¶
L’API renvoie les métadonnées du jeu de données au format JSON. Parmi ces métadonnées, la clé resources contient les fichiers disponibles.
On écrit une fonction générique pour :
- interroger l’API ;
- afficher les ressources ;
- sélectionner un fichier selon son format ou son nom.
DATA_GOUV_API = "https://www.data.gouv.fr/api/1/datasets"
def get_dataset_metadata(dataset_slug: str) -> dict:
"""Récupère les métadonnées d'un jeu de données data.gouv.fr."""
url = f"{DATA_GOUV_API}/{dataset_slug}/"
response = requests.get(url, timeout=60)
response.raise_for_status()
return response.json()
def resources_table(metadata: dict) -> pd.DataFrame:
"""Transforme la liste des ressources data.gouv.fr en DataFrame."""
columns = [
"id",
"title",
"format",
"url",
"filesize",
"last_modified",
]
resources = pd.DataFrame(metadata.get("resources", []))
for column in columns:
if column not in resources.columns:
resources[column] = pd.NA
return resources[columns]
3.1 Métadonnées du jeu de données de revenus¶
REVENUE_DATASET_SLUG = "revenus-a-la-commune-en-france"
revenue_metadata = get_dataset_metadata(REVENUE_DATASET_SLUG)
print(revenue_metadata["title"])
print(revenue_metadata.get("description", "")[:500])
revenue_resources = resources_table(revenue_metadata)
revenue_resources
Question¶
À partir du tableau précédent :
- quels formats sont disponibles ?
- quel fichier paraît le plus simple à lire avec Pandas ?
- quelle est sa taille ?
- quand a-t-il été modifié ?
3.2 Sélection automatique de la ressource Excel¶
On privilégie ici le fichier .xlsx, car il est de taille raisonnable et directement lisible avec pd.read_excel.
xlsx_candidates = revenue_resources.loc[
revenue_resources["format"].astype("string").str.lower().eq("xlsx")
].copy()
if xlsx_candidates.empty:
xlsx_candidates = revenue_resources.loc[
revenue_resources["url"].astype("string").str.lower().str.endswith(".xlsx")
].copy()
assert not xlsx_candidates.empty, "Aucune ressource XLSX trouvée."
revenue_resource = xlsx_candidates.iloc[0]
revenue_url = revenue_resource["url"]
revenue_resource
4. Télécharger et lire les données de revenus¶
Le fichier est enregistré localement pour éviter de le télécharger à chaque exécution.
revenue_path = DATA_DIR / "revenus_communes.xlsx"
if not revenue_path.exists():
response = requests.get(revenue_url, timeout=180)
response.raise_for_status()
revenue_path.write_bytes(response.content)
print(revenue_path)
print(f"Taille : {revenue_path.stat().st_size / 1_000_000:.1f} Mo")
Un fichier Excel peut contenir plusieurs feuilles. On commence par les lister.
excel_file = pd.ExcelFile(revenue_path)
excel_file.sheet_names
# Examiner les premières lignes sans imposer de header.
preview = pd.read_excel(
revenue_path,
sheet_name=excel_file.sheet_names[0],
header=None,
nrows=15,
)
preview
# Adapter sheet_name et header après observation du preview.
SHEET_NAME = excel_file.sheet_names[0]
HEADER_ROW = 0
revenus_raw = pd.read_excel(
revenue_path,
sheet_name=SHEET_NAME,
header=HEADER_ROW,
)
revenus_raw.head()
5. Audit initial¶
Avant tout nettoyage, documenter :
- la granularité (ce que représente) d’une ligne ;
- le nombre de lignes et de colonnes ;
- les noms de colonnes ;
- les
dtype; - les valeurs manquantes ;
- les doublons ;
- les colonnes susceptibles de contenir le code INSEE et la médiane de revenu.
print("Shape :", revenus_raw.shape)
revenus_raw.info()
audit_revenus = pd.DataFrame({
"dtype": revenus_raw.dtypes.astype("string"),
"n_unique": revenus_raw.nunique(dropna=False),
"n_missing": revenus_raw.isna().sum(),
"missing_rate": revenus_raw.isna().mean(),
})
audit_revenus.sort_values("missing_rate", ascending=False)
list(enumerate(revenus_raw.columns))
Question¶
Identifier les colonnes correspondant probablement à :
- la commune ;
- son code INSEE ;
- la médiane du revenu ;
- le nombre de ménages ;
- le nombre de personnes ;
- le premier décile ;
- le neuvième décile.
Les noms exacts peuvent varier selon la version du fichier.
6. Normaliser les noms de colonnes¶
Les fichiers réels contiennent souvent :
- des accents ;
- des espaces ;
- des parenthèses ;
- des unités ;
- des caractères spéciaux.
On crée une fonction de normalisation afin d’obtenir des noms faciles à manipuler.
def normalize_column_name(name: object) -> str:
"""Convertit un nom de colonne en snake_case ASCII."""
text = str(name).strip().lower()
text = unicodedata.normalize("NFKD", text)
text = "".join(char for char in text if not unicodedata.combining(char))
text = re.sub(r"[^a-z0-9]+", "_", text)
return text.strip("_")
revenus = revenus_raw.copy()
revenus.columns = [normalize_column_name(column) for column in revenus.columns]
revenus.columns.tolist()
Recherche assistée des colonnes¶
La fonction suivante aide à repérer les noms contenant certains mots-clés.
def find_columns(df: pd.DataFrame, *keywords: str) -> list[str]:
keywords = tuple(keyword.lower() for keyword in keywords)
return [
column
for column in df.columns
if all(keyword in column.lower() for keyword in keywords)
]
print("Code / commune :", find_columns(revenus, "geo"))
print("Médiane :", find_columns(revenus, "median"))
print("Ménages :", find_columns(revenus, "menag"))
print("Décile 1 :", find_columns(revenus, "1"))
print("Décile 9 :", find_columns(revenus, "9"))
7. Construire une table communale propre¶
Renseigner les noms détectés dans les variables ci-dessous.
Ne pas inventer les noms : les copier depuis
revenus.columns.
# À adapter à la structure réellement observée.
COL_CODE = None
COL_COMMUNE = None
COL_MEDIANE = None
COL_MENAGES = None
COL_PERSONNES = None
COL_D1 = None
COL_D9 = None
{
"COL_CODE": COL_CODE,
"COL_COMMUNE": COL_COMMUNE,
"COL_MEDIANE": COL_MEDIANE,
"COL_MENAGES": COL_MENAGES,
"COL_PERSONNES": COL_PERSONNES,
"COL_D1": COL_D1,
"COL_D9": COL_D9,
}
assert COL_CODE in revenus.columns
assert COL_COMMUNE in revenus.columns
assert COL_MEDIANE in revenus.columns
print("Les colonnes obligatoires ont été identifiées.")
Lorsque les noms sont renseignés, construire un sous-ensemble comprenant les variables utiles.
Les colonnes optionnelles sont ajoutées uniquement lorsqu’elles existent.
column_mapping = {
COL_CODE: "code_insee",
COL_COMMUNE: "commune",
COL_MEDIANE: "revenu_median",
COL_MENAGES: "menages_fiscaux",
COL_PERSONNES: "personnes_menages",
COL_D1: "d1",
COL_D9: "d9",
}
revenus_clean = (
revenus[list(column_mapping)]
.rename(columns=column_mapping)
.copy()
)
revenus_clean.head()
8. Le code INSEE¶
Le code INSEE :
- est une clé administrative ;
- comporte généralement cinq caractères ;
- peut commencer par
0; - peut contenir des lettres pour certaines communes corses ;
- ne doit pas être converti naïvement en entier.
Le code postal n’est pas une clé communale fiable :
- une commune peut avoir plusieurs codes postaux ;
- plusieurs communes peuvent partager un code postal ;
- ses limites ne correspondent pas nécessairement aux limites administratives.
Contrôles¶
- afficher la distribution des longueurs ;
- identifier les codes ne respectant pas le format attendu ;
- vérifier l’unicité ;
- vérifier les valeurs manquantes.
revenus_clean["code_insee"].str.len().value_counts(dropna=False)
print("Clé unique :", revenus_clean["code_insee"].is_unique)
print("Codes manquants :", revenus_clean["code_insee"].isna().sum())
print("Codes dupliqués :", revenus_clean["code_insee"].duplicated().sum())
9. Valeurs manquantes¶
Affichez les valeurs numériques manquantes :
numeric_columns = [
column
for column in [
"revenu_median",
"menages_fiscaux",
"personnes_menages",
"d1",
"d9",
]
if column in revenus_clean.columns
]
revenus_clean[numeric_columns].isna().sum()
Secret statistique¶
Les données de revenus ne sont pas diffusées pour toutes les petites communes. Une valeur manquante n’est donc pas nécessairement une erreur.
Il faut distinguer :
- une donnée réellement absente ;
- une valeur non diffusée pour préserver le secret statistique ;
- une erreur de conversion ;
- une commune non couverte par le champ statistique.
10. Analyse descriptive des revenus¶
Produire une description des variables numériques et examiner la distribution de la médiane.
#Votre codee
Questions¶
- la distribution est-elle symétrique ?
- observe-t-on des valeurs extrêmes ?
- combien de communes ont une médiane non renseignée ?
- faut-il remplacer les valeurs manquantes par
0? - une moyenne communale non pondérée représente-t-elle correctement la France ?
revenus_clean["revenu_median"].agg([
"count",
"mean",
"median",
"std",
"min",
"max",
])
Classement exploratoire¶
Afficher les dix communes aux revenus médians les plus élevés et les dix plus faibles.
Interpréter avec prudence :
- les valeurs peuvent être absentes dans les très petites communes ;
- une commune n’est pas pondérée par sa population ;
- la médiane par unité de consommation n’est pas le revenu moyen du ménage.
# Votre Code
11. Construire des indicateurs supplémentaires¶
Lorsque les premier et neuvième déciles sont présents, on peut calculer :
$$ \text{rapport interdécile} = \frac{D9}{D1} $$ Cet indicateur décrit l’écart entre le haut et le bas de la distribution.
Un rapport élevé indique une dispersion importante, mais ne suffit pas à décrire toute la distribution.
if {"d1", "d9"}.issubset(revenus_clean.columns):
revenus_clean["rapport_interdecile"] = (
revenus_clean["d9"] / revenus_clean["d1"]
).where(revenus_clean["d1"] > 0)
revenus_clean.head()
12. Récupérer les contours des communes¶
Le fond de carte utilisé est le fichier GeoJSON communal simplifié à 1000 mètres.
Cette simplification :
- réduit le temps de téléchargement ;
- accélère la jointure et l’affichage ;
- est suffisante pour une carte nationale ;
- n’est pas adaptée à une analyse cadastrale précise.
CONTOURS_DATASET_SLUG = "contours-administratifs"
contours_metadata = get_dataset_metadata(CONTOURS_DATASET_SLUG)
contours_resources = resources_table(contours_metadata)
contours_resources.head(5)
On sélectionne une ressource dont le nom contient communes-1000m et dont le format est GeoJSON ou GeoJSON compressé.
resource_text = (
contours_resources["title"].fillna("")
+ " "
+ contours_resources["url"].fillna("")
).str.lower()
contour_candidates = contours_resources.loc[
resource_text.str.contains("communes-1000m", regex=False)
& ~resource_text.str.contains("associees", regex=False)
].copy()
contour_candidates
# Priorité au GeoJSON non compressé s'il est disponible.
plain_geojson = contour_candidates.loc[
contour_candidates["url"]
.astype("string")
.str.lower()
.str.endswith(".geojson")
]
if not plain_geojson.empty:
contour_resource = plain_geojson.iloc[0]
else:
contour_resource = contour_candidates.iloc[0]
contour_url = contour_resource["url"]
contour_resource
GeoPandas peut généralement lire directement l’URL.
communes_geo = gpd.read_file(contour_url)
communes_geo.head()
print("Shape :", communes_geo.shape)
print("CRS :", communes_geo.crs)
communes_geo.info()
13. Audit du fond géographique¶
Identifier :
- la colonne du code INSEE ;
- la colonne du nom de commune ;
- le type de géométrie ;
- l’unicité de la clé ;
- le système de coordonnées.
communes_geo.columns.tolist()
communes_geo.geometry.geom_type.value_counts(dropna=False)
Renseigner les noms des colonnes géographiques.
GEO_CODE_COLUMN = None
GEO_NAME_COLUMN = None
communes_geo = (
communes_geo
.rename(columns={
GEO_CODE_COLUMN: "code_insee",
GEO_NAME_COLUMN: "commune_geo",
})
[["code_insee", "commune_geo", "geometry"]]
.copy()
)
communes_geo.head()
print("Codes uniques :", communes_geo["code_insee"].is_unique)
print("Codes manquants :", communes_geo["code_insee"].isna().sum())
print("Géométries manquantes :", communes_geo.geometry.isna().sum())
print("Géométries valides :", communes_geo.geometry.is_valid.mean())
14. Comparer les clés avant la jointure¶
Une jointure ne doit jamais être exécutée sans vérifier les clés.
Comparer :
- les codes présents uniquement dans les revenus ;
- les codes présents uniquement dans le fond géographique ;
- le nombre de codes communs ;
- les éventuels doublons.
revenue_codes = set(
revenus_clean["code_insee"].dropna()
)
geo_codes = set(
communes_geo["code_insee"].dropna()
)
only_revenue = revenue_codes - geo_codes
only_geo = geo_codes - revenue_codes
common_codes = revenue_codes & geo_codes
print("Codes revenus :", len(revenue_codes))
print("Codes géographiques :", len(geo_codes))
print("Codes communs :", len(common_codes))
print("Revenus seulement :", len(only_revenue))
print("Géographie seulement:", len(only_geo))
sorted(only_revenue)[:30]
sorted(only_geo)[:30]
Question¶
Proposer des explications possibles aux écarts :
- changement de géographie communale ;
- fusion ou création de communes ;
- arrondissements municipaux ;
- champ statistique incomplet ;
- secret statistique ;
- différence de millésime entre les deux sources.
15. Effectuer la jointure¶
On conserve toutes les géométries avec une jointure left.
La relation attendue est one-to-one si les deux clés sont uniques.
carte = communes_geo.merge(
revenus_clean,
on="code_insee",
how="left",
indicator=True,
)
carte.head()
carte["_merge"].value_counts(dropna=False)
match_rate = carte["revenu_median"].notna().mean()
print(f"Taux de communes avec revenu renseigné : {match_rate:.1%}")
Attention¶
Une commune peut être correctement appariée tout en ayant une valeur de revenu manquante.
La colonne _merge renseigne la réussite de la jointure, pas la disponibilité de l’indicateur.
pd.crosstab(
carte["_merge"],
carte["revenu_median"].notna(),
margins=True,
)
16. Première carte statique avec GeoPandas¶
Avant de créer une carte interactive, produire une carte statique permet de vérifier rapidement :
- que la jointure est correcte ;
- que les valeurs sont plausibles ;
- que le système de coordonnées convient ;
- que la variable est correctement associée aux communes.
ax = carte.plot(
column="revenu_median",
figsize=(12, 12),
legend=True,
scheme="quantiles",
k=7,
missing_kwds={
"color": "lightgrey",
"label": "Valeur non diffusée",
},
)
ax.set_title(
"Médiane du revenu disponible par unité de consommation"
)
ax.set_axis_off()
plt.show()
Comme vous pouvez le constater, la carte est difficilement lisible, car elle représente l’ensemble du territoire français, y compris les territoires ultramarins. Pour cette première visualisation, nous allons donc nous concentrer uniquement sur la France métropolitaine.
carte_metropole = carte.cx[-6:10, 41:52] # longitude_min:longitude_max, latitude_min:latitude_max
ax = carte_metropole.plot(
column="revenu_median",
figsize=(12, 12),
legend=True,
scheme="quantiles",
k=7,
missing_kwds={
"color": "lightgrey",
"label": "Valeur non diffusée",
},
)
ax.set_title("Médiane du revenu disponible par unité de consommation")
ax.set_axis_off()
plt.show()
Quantiles ou intervalles réguliers ?¶
Une carte par quantiles contient approximativement le même nombre de communes dans chaque classe.
Avantage :
- les contrastes spatiaux sont visibles.
Limite :
- deux communes ayant des revenus proches peuvent appartenir à deux classes différentes ;
- les amplitudes des classes ne sont pas constantes.
Comparer avec une classification en intervalles réguliers ou naturels.
# Votre code
17. Construire une carte interactive avec Folium¶
Folium attend des coordonnées géographiques en WGS84 (EPSG:4326).
carte.crs
17.1 Préparer les données¶
Les infobulles doivent rester lisibles. On prépare des chaînes formatées.
carte["revenu_median_label"] = (
carte["revenu_median"]
.map(lambda value: f"{value:,.0f} €" if pd.notna(value) else "Non diffusé")
)
if "rapport_interdecile" in carte.columns:
carte["rapport_interdecile_label"] = (
carte["rapport_interdecile"]
.map(lambda value: f"{value:.2f}" if pd.notna(value) else "Non diffusé")
)
17.2 Choisir une échelle de couleurs¶
Pour limiter l’effet des valeurs extrêmes, on peut utiliser les quantiles 2 % et 98 % comme bornes visuelles.
valid_revenues = carte["revenu_median"].dropna()
vmin = valid_revenues.quantile(0.02)
vmax = valid_revenues.quantile(0.98)
colormap = linear.YlOrRd_09.scale(vmin, vmax)
colormap.caption = (
"Médiane du revenu disponible par unité de consommation (€)"
)
vmin, vmax
17.3 Fonction de style¶
def style_commune(feature):
value = feature["properties"].get("revenu_median")
if value is None or pd.isna(value):
fill_color = "#d9d9d9"
else:
clipped_value = min(max(value, vmin), vmax)
fill_color = colormap(clipped_value)
return {
"fillColor": fill_color,
"color": "#555555",
"weight": 0.25,
"fillOpacity": 0.75,
}
17.4 Générer la carte¶
Pour des raisons de performance, on utilise les contours simplifiés.
La carte peut malgré tout être lourde, car la France compte plusieurs dizaines de milliers de communes.
france_map = folium.Map(
location=[46.6, 2.4],
zoom_start=6,
tiles="CartoDB positron",
)
tooltip_fields = [
"commune_geo",
"code_insee",
"revenu_median_label",
]
tooltip_aliases = [
"Commune :",
"Code INSEE :",
"Revenu médian :",
]
folium.GeoJson(
carte,
name="Revenu médian",
style_function=style_commune,
highlight_function=lambda feature: {
"weight": 1.5,
"color": "black",
"fillOpacity": 0.9,
},
tooltip=folium.GeoJsonTooltip(
fields=tooltip_fields,
aliases=tooltip_aliases,
localize=True,
sticky=False,
),
).add_to(france_map)
colormap.add_to(france_map)
folium.LayerControl().add_to(france_map)
france_map
18. Exporter la carte¶
map_path = OUTPUT_DIR / "carte_revenus_communes.html"
france_map.save(map_path)
print(f"Carte enregistrée dans : {map_path.resolve()}")
19. Exporter les données nettoyées¶
On exporte une table sans géométrie ainsi qu’un fichier géographique.
revenus_clean.to_csv(
OUTPUT_DIR / "revenus_communes_nettoyes.csv",
index=False,
)
carte.to_file(
OUTPUT_DIR / "revenus_communes.geojson",
driver="GeoJSON",
)
20. Analyse départementale¶
Le code département peut être dérivé du code INSEE, mais les départements corses nécessitent un traitement particulier.
On écrit une fonction explicite.
def department_from_insee(code: object) -> object:
if pd.isna(code):
return pd.NA
code = str(code)
if code.startswith(("2A", "2B")):
return code[:2]
if code.startswith(("97", "98")):
return code[:3]
return code[:2]
revenus_clean["departement"] = (
revenus_clean["code_insee"]
.map(department_from_insee)
.astype("string")
)
revenus_clean[["code_insee", "departement"]].head()
Moyenne simple ou moyenne pondérée ?¶
La moyenne simple des médianes communales donne le même poids à chaque commune.
Une moyenne pondérée par le nombre de personnes donne davantage de poids aux communes peuplées, mais une moyenne de médianes ne devient pas pour autant la médiane départementale.
Cette agrégation doit être présentée comme un indicateur exploratoire, et non comme une statistique officielle départementale.
departement_summary = (
revenus_clean
.groupby("departement", as_index=False)
.agg(
n_communes=("code_insee", "size"),
n_revenus_disponibles=("revenu_median", "count"),
revenu_median_communes=("revenu_median", "median"),
revenu_moyen_communes=("revenu_median", "mean"),
)
)
departement_summary.head()
Extension pondérée¶
Lorsque personnes_menages est disponible, construire une moyenne pondérée :
$$ \bar{x}_w = \frac{\sum_i w_i x_i}{\sum_i w_i} $$
où (w_i) représente ici le nombre de personnes dans les ménages fiscaux.
def weighted_mean(group, value_column, weight_column):
#Votre Code
return
if "personnes_menages" in revenus_clean.columns:
weighted_by_department = (
revenus_clean
.groupby("departement")
.apply(
weighted_mean,
value_column="revenu_median",
weight_column="personnes_menages",
include_groups=False,
)
.rename("revenu_moyen_pondere_exploratoire")
.reset_index()
)
departement_summary = departement_summary.merge(
weighted_by_department,
on="departement",
how="left",
validate="one_to_one",
)
departement_summary.head()
21. Analyse spatiale exploratoire¶
Répondre aux questions suivantes :
- quelles zones concentrent les revenus médians les plus élevés ?
- observe-t-on un contraste entre espaces urbains, périurbains et ruraux ?
- quelles communes apparaissent comme des valeurs extrêmes locales ?
- les zones sans données sont-elles réparties aléatoirement ?
- une carte brute des revenus suffit-elle pour comparer le coût de la vie ?
- quelles autres variables seraient nécessaires pour interpréter les différences ?
22. Limites méthodologiques¶
Discuter des limites méthodologiques induite par les points suivant :
22.1 Revenu médian et revenu moyen¶
...
22.2 Unité de consommation¶
...
22.3 Secret statistique¶
...
22.4 Effet de taille des communes¶
...
22.5 Millésimes géographiques¶
...
22.6 Corrélation spatiale¶
...
22.7 Interprétation causale¶
...
Exercice 1 — Créer une carte du rapport interdécile¶
Lorsque D1 et D9 sont disponibles :
- calculer le rapport interdécile ;
- analyser les valeurs manquantes et infinies ;
- produire un histogramme ;
- produire une carte statique ;
- produire une couche Folium supplémentaire ;
- ajouter un
LayerControlpermettant d’alterner entre revenu médian et rapport interdécile ; - commenter les différences spatiales entre niveau de revenu et dispersion des revenus.
# Votre code
Exercice 3 — Comparer les méthodes de classification¶
Produire quatre cartes du revenu médian avec :
- des intervalles réguliers ;
- des quantiles ;
- des ruptures naturelles ;
- une échelle continue.
Pour chaque méthode :
- afficher les bornes des classes ;
- compter le nombre de communes dans chaque classe ;
- identifier les changements de perception visuelle ;
- expliquer quelle méthode paraît la plus adaptée à une carte nationale.
# Votre code
Exercice 4 — Construire une fonction réutilisable¶
Écrire une fonction :
def build_choropleth(
geo_df,
value_column,
name_column,
legend_title,
):
...
La fonction doit :
- contrôler le CRS ;
- gérer les valeurs manquantes ;
- calculer automatiquement des bornes robustes ;
- créer les tooltips ;
- retourner un objet
folium.Map; - ne pas modifier le
GeoDataFramed’entrée ; - contenir une docstring et des contrôles d’entrée.
# Votre code
Exercice 5 — Mini rapport territorial¶
Choisir un département et produire automatiquement :
- une carte des revenus communaux ;
- les cinq communes aux revenus les plus élevés ;
- les cinq communes aux revenus les plus faibles ;
- le taux de données manquantes ;
- un histogramme ;
- une comparaison avec l’ensemble de la France ;
- un commentaire de dix lignes maximum.
Le code du département doit être un paramètre unique facilement modifiable.
# Votre code
Exercice 6 — Question de recul¶
Répondre de manière argumentée :
Une carte commune par commune des revenus permet-elle d’identifier les communes « riches » et les communes « pauvres » ?
La réponse devra discuter au minimum :
- la définition de l’indicateur ;
- le coût de la vie ;
- les inégalités internes ;
- le secret statistique ;
- les mobilités domicile-travail ;
- la taille des communes ;
- le choix des classes et de l’échelle de couleurs ;
- le risque d’erreur écologique.