{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"---\n",
"title : \"🏭 Émissions de CO₂ des communes françaises\"\n",
"---"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Les données\n",
"\n",
"Le CITEPA, pour le compte du ministère chargé de l'Environnement, publie un\n",
"**inventaire des émissions de gaz à effet de serre commune par commune**, ventilé\n",
"par secteur d'activité : agriculture, industrie, transport routier, résidentiel,\n",
"tertiaire, déchets.\n",
"\n",
"| | |\n",
"|---|---|\n",
"| Intitulé | Inventaire de gaz à effet de serre territorialisé |\n",
"| Producteur | CITEPA, pour la Direction générale de l'énergie et du climat |\n",
"| **Année de référence** | **2016** |\n",
"| Couverture | 35 798 communes |\n",
"| Unité | tonnes équivalent CO₂ par an |\n",
"| Licence | Licence Ouverte (Etalab) |\n",
"| Source | [data.ademe.fr](https://data.ademe.fr/) |\n",
"\n",
"Le producteur précise que cet inventaire « a vocation à donner des **ordres de\n",
"grandeur** pertinents » et qu'il « n'a pas le niveau de finesse et de précision des\n",
"travaux d'inventaire réalisés au niveau territorial ». Autrement dit, ces chiffres\n",
"servent à comparer des territoires entre eux, pas à établir le bilan exact d'une\n",
"commune donnée. C'est une limite qu'il faut connaître avant de conclure quoi que ce\n",
"soit, et que nous rappellerons en fin d'exercice.\n",
"\n",
"Nous croiserons ces émissions avec la **population communale** déjà utilisée dans\n",
"l'exercice précédent. Les émissions datant de 2016, nous prendrons la population de\n",
"cette même année, et non la plus récente : diviser des émissions de 2016 par une\n",
"population de 2021 n'aurait pas de sens.\n",
"\n",
"La question posée est simple à formuler et moins simple à trancher : **quels\n",
"territoires émettent le plus ?**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"
\n",
"\n",
"## Exercice\n",
"\n",
"### 1. Découvrir les données\n",
"- Charger `emissions-communes.csv`. Combien de communes, combien de colonnes ?\n",
"- Quelles sont les colonnes de secteurs ? Afficher leurs statistiques descriptives.\n",
"- La colonne `INSEE commune` est-elle un identifiant unique ? Et la colonne `Commune` ?\n",
"\n",
"### 2. Les valeurs manquantes\n",
"- Quelle proportion de valeurs manquantes dans chaque colonne ?\n",
"- Deux colonnes sont très lacunaires. Afficher les communes où `Autres transports\n",
" international` est **renseigné et le plus élevé**. Que reconnaissez-vous ? Qu'en\n",
" déduire sur la signification du `NaN` ici ?\n",
"- Créer une colonne `total` donnant les émissions totales de chaque commune.\n",
"\n",
"### 3. Joindre à la population\n",
"- Charger `base-pop-historiques-1876-2021.csv` et en extraire le code, le nom, le\n",
" département et la **population de 2016** de chaque commune. Attention au format\n",
" des nombres.\n",
"- Joindre les deux tables. Combien de communes se raccordent ? Combien restent\n",
" orphelines ?\n",
"\n",
"### 4. Émissions par habitant\n",
"- Écarter les communes sans habitant, puis créer `par_habitant`.\n",
"- Quelles sont les 5 communes les plus émettrices **au total** ? Et **par habitant** ?\n",
"- Agréger par département, des deux façons. Comparer les classements et **commenter**.\n",
"\n",
"### 5. Cartographier\n",
"- Représenter sur une carte de France les émissions **totales** par département,\n",
" puis les émissions **par habitant**. Commenter la différence entre les deux cartes.\n",
"\n",
"
"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:00.814865Z",
"iopub.status.busy": "2026-10-30T13:11:00.814139Z",
"iopub.status.idle": "2026-10-30T13:11:01.894436Z",
"shell.execute_reply": "2026-10-30T13:11:01.893759Z"
}
},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"import seaborn as sns\n",
"import json # pour lire le fond de carte, qui est au format GeoJSON\n",
"\n",
"from IPython.display import display, Markdown\n",
"\n",
"import plotly.express as px\n",
"\n",
"# Pour la génération automatique de graphiques interactifs dans le polycopié\n",
"import plotly.io as pio\n",
"import os\n",
"if os.getenv(\"QUARTO_OUTPUT_FORMAT\") is not None:\n",
" # Export Quarto HTML\n",
" pio.renderers.default = \"iframe_connected\"\n",
"else:\n",
" # Notebook interactif\n",
" pio.renderers.default = \"notebook_connected\"\n",
"\n",
"sns.set_theme(context=\"notebook\", style=\"whitegrid\", palette=\"pastel\")\n",
"plt.rcParams['figure.figsize'] = (11, 5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 1. Découvrir les données"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Le code INSEE commence par un zéro pour les départements 01 à 09. Si on laisse\n",
"pandas deviner le type, il le lira comme un entier et `01001` deviendra `1001` : la\n",
"jointure échouerait plus loin. On impose donc le type texte dès la lecture."
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:01.896756Z",
"iopub.status.busy": "2026-10-30T13:11:01.896481Z",
"iopub.status.idle": "2026-10-30T13:11:02.012655Z",
"shell.execute_reply": "2026-10-30T13:11:02.012158Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"**35798** communes, **12** colonnes"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" INSEE commune | \n",
" Commune | \n",
" Agriculture | \n",
" Autres transports | \n",
" Autres transports international | \n",
" CO2 biomasse hors-total | \n",
" Déchets | \n",
" Energie | \n",
" Industrie hors-énergie | \n",
" Résidentiel | \n",
" Routier | \n",
" Tertiaire | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 01001 | \n",
" L'ABERGEMENT-CLEMENCIAT | \n",
" 3711.425991 | \n",
" NaN | \n",
" NaN | \n",
" 432.751835 | \n",
" 101.430476 | \n",
" 2.354558 | \n",
" 6.911213 | \n",
" 309.358195 | \n",
" 793.156501 | \n",
" 367.036172 | \n",
"
\n",
" \n",
" | 1 | \n",
" 01002 | \n",
" L'ABERGEMENT-DE-VAREY | \n",
" 475.330205 | \n",
" NaN | \n",
" NaN | \n",
" 140.741660 | \n",
" 140.675439 | \n",
" 2.354558 | \n",
" 6.911213 | \n",
" 104.866444 | \n",
" 348.997893 | \n",
" 112.934207 | \n",
"
\n",
" \n",
" | 2 | \n",
" 01004 | \n",
" AMBERIEU-EN-BUGEY | \n",
" 499.043526 | \n",
" 212.577908 | \n",
" NaN | \n",
" 10313.446515 | \n",
" 5314.314445 | \n",
" 998.332482 | \n",
" 2930.354461 | \n",
" 16616.822534 | \n",
" 15642.420313 | \n",
" 10732.376934 | \n",
"
\n",
" \n",
" | 3 | \n",
" 01005 | \n",
" AMBERIEUX-EN-DOMBES | \n",
" 1859.160954 | \n",
" NaN | \n",
" NaN | \n",
" 1144.429311 | \n",
" 216.217508 | \n",
" 94.182310 | \n",
" 276.448534 | \n",
" 663.683146 | \n",
" 1756.341319 | \n",
" 782.404357 | \n",
"
\n",
" \n",
" | 4 | \n",
" 01006 | \n",
" AMBLEON | \n",
" 448.966808 | \n",
" NaN | \n",
" NaN | \n",
" 77.033834 | \n",
" 48.401549 | \n",
" NaN | \n",
" NaN | \n",
" 43.714019 | \n",
" 398.786800 | \n",
" 51.681756 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" INSEE commune Commune Agriculture Autres transports \\\n",
"0 01001 L'ABERGEMENT-CLEMENCIAT 3711.425991 NaN \n",
"1 01002 L'ABERGEMENT-DE-VAREY 475.330205 NaN \n",
"2 01004 AMBERIEU-EN-BUGEY 499.043526 212.577908 \n",
"3 01005 AMBERIEUX-EN-DOMBES 1859.160954 NaN \n",
"4 01006 AMBLEON 448.966808 NaN \n",
"\n",
" Autres transports international CO2 biomasse hors-total Déchets \\\n",
"0 NaN 432.751835 101.430476 \n",
"1 NaN 140.741660 140.675439 \n",
"2 NaN 10313.446515 5314.314445 \n",
"3 NaN 1144.429311 216.217508 \n",
"4 NaN 77.033834 48.401549 \n",
"\n",
" Energie Industrie hors-énergie Résidentiel Routier \\\n",
"0 2.354558 6.911213 309.358195 793.156501 \n",
"1 2.354558 6.911213 104.866444 348.997893 \n",
"2 998.332482 2930.354461 16616.822534 15642.420313 \n",
"3 94.182310 276.448534 663.683146 1756.341319 \n",
"4 NaN NaN 43.714019 398.786800 \n",
"\n",
" Tertiaire \n",
"0 367.036172 \n",
"1 112.934207 \n",
"2 10732.376934 \n",
"3 782.404357 \n",
"4 51.681756 "
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# dtype={'INSEE commune': str} force la lecture de cette colonne en texte,\n",
"# ce qui préserve le zéro initial des codes 01xxx à 09xxx\n",
"emissions = pd.read_csv('emissions-communes.csv', dtype={'INSEE commune': str})\n",
"\n",
"# .shape renvoie le couple (nombre de lignes, nombre de colonnes)\n",
"display(Markdown(f\"**{emissions.shape[0]}** communes, **{emissions.shape[1]}** colonnes\"))\n",
"\n",
"# .head() affiche les cinq premières lignes\n",
"emissions.head()"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.045113Z",
"iopub.status.busy": "2026-10-30T13:11:02.044926Z",
"iopub.status.idle": "2026-10-30T13:11:02.048884Z",
"shell.execute_reply": "2026-10-30T13:11:02.048443Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"**10 secteurs** : `Agriculture`, `Autres transports`, `Autres transports international`, `CO2 biomasse hors-total`, `Déchets`, `Energie`, `Industrie hors-énergie`, `Résidentiel`, `Routier`, `Tertiaire`"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# on construit la liste des colonnes de secteurs, c'est-à-dire\n",
"# toutes les colonnes sauf les deux colonnes d'identification\n",
"secteurs = [c for c in emissions.columns if c not in ('INSEE commune', 'Commune')]\n",
"\n",
"display(Markdown(f\"**{len(secteurs)} secteurs** : \" + \", \".join(f\"`{s}`\" for s in secteurs)))"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.050678Z",
"iopub.status.busy": "2026-10-30T13:11:02.050523Z",
"iopub.status.idle": "2026-10-30T13:11:02.084116Z",
"shell.execute_reply": "2026-10-30T13:11:02.083622Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" count | \n",
" mean | \n",
" std | \n",
" min | \n",
" 25% | \n",
" 50% | \n",
" 75% | \n",
" max | \n",
"
\n",
" \n",
" \n",
" \n",
" | Agriculture | \n",
" 35736.0 | \n",
" 2460.0 | \n",
" 2927.0 | \n",
" 0.0 | \n",
" 798.0 | \n",
" 1559.0 | \n",
" 3008.0 | \n",
" 98949.0 | \n",
"
\n",
" \n",
" | Autres transports | \n",
" 9979.0 | \n",
" 655.0 | \n",
" 9233.0 | \n",
" 0.0 | \n",
" 53.0 | \n",
" 107.0 | \n",
" 237.0 | \n",
" 513141.0 | \n",
"
\n",
" \n",
" | Autres transports international | \n",
" 2891.0 | \n",
" 7692.0 | \n",
" 113764.0 | \n",
" 0.0 | \n",
" 10.0 | \n",
" 20.0 | \n",
" 33.0 | \n",
" 3303394.0 | \n",
"
\n",
" \n",
" | CO2 biomasse hors-total | \n",
" 35798.0 | \n",
" 1774.0 | \n",
" 7871.0 | \n",
" 4.0 | \n",
" 198.0 | \n",
" 425.0 | \n",
" 1095.0 | \n",
" 576394.0 | \n",
"
\n",
" \n",
" | Déchets | \n",
" 35792.0 | \n",
" 411.0 | \n",
" 4122.0 | \n",
" 0.0 | \n",
" 26.0 | \n",
" 55.0 | \n",
" 111.0 | \n",
" 275500.0 | \n",
"
\n",
" \n",
" | Energie | \n",
" 34490.0 | \n",
" 663.0 | \n",
" 26456.0 | \n",
" 2.0 | \n",
" 2.0 | \n",
" 5.0 | \n",
" 52.0 | \n",
" 2535858.0 | \n",
"
\n",
" \n",
" | Industrie hors-énergie | \n",
" 34490.0 | \n",
" 2423.0 | \n",
" 56704.0 | \n",
" 1.0 | \n",
" 7.0 | \n",
" 14.0 | \n",
" 152.0 | \n",
" 6765119.0 | \n",
"
\n",
" \n",
" | Résidentiel | \n",
" 35792.0 | \n",
" 1784.0 | \n",
" 8916.0 | \n",
" 1.0 | \n",
" 96.0 | \n",
" 227.0 | \n",
" 749.0 | \n",
" 410676.0 | \n",
"
\n",
" \n",
" | Routier | \n",
" 35778.0 | \n",
" 3536.0 | \n",
" 9663.0 | \n",
" 1.0 | \n",
" 420.0 | \n",
" 1071.0 | \n",
" 3099.0 | \n",
" 586055.0 | \n",
"
\n",
" \n",
" | Tertiaire | \n",
" 35798.0 | \n",
" 1105.0 | \n",
" 5164.0 | \n",
" 0.0 | \n",
" 95.0 | \n",
" 216.0 | \n",
" 576.0 | \n",
" 288175.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" count mean std min 25% \\\n",
"Agriculture 35736.0 2460.0 2927.0 0.0 798.0 \n",
"Autres transports 9979.0 655.0 9233.0 0.0 53.0 \n",
"Autres transports international 2891.0 7692.0 113764.0 0.0 10.0 \n",
"CO2 biomasse hors-total 35798.0 1774.0 7871.0 4.0 198.0 \n",
"Déchets 35792.0 411.0 4122.0 0.0 26.0 \n",
"Energie 34490.0 663.0 26456.0 2.0 2.0 \n",
"Industrie hors-énergie 34490.0 2423.0 56704.0 1.0 7.0 \n",
"Résidentiel 35792.0 1784.0 8916.0 1.0 96.0 \n",
"Routier 35778.0 3536.0 9663.0 1.0 420.0 \n",
"Tertiaire 35798.0 1105.0 5164.0 0.0 95.0 \n",
"\n",
" 50% 75% max \n",
"Agriculture 1559.0 3008.0 98949.0 \n",
"Autres transports 107.0 237.0 513141.0 \n",
"Autres transports international 20.0 33.0 3303394.0 \n",
"CO2 biomasse hors-total 425.0 1095.0 576394.0 \n",
"Déchets 55.0 111.0 275500.0 \n",
"Energie 5.0 52.0 2535858.0 \n",
"Industrie hors-énergie 14.0 152.0 6765119.0 \n",
"Résidentiel 227.0 749.0 410676.0 \n",
"Routier 1071.0 3099.0 586055.0 \n",
"Tertiaire 216.0 576.0 288175.0 "
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# .describe() calcule les statistiques descriptives de chaque colonne numérique\n",
"# .T transpose le résultat : un secteur par ligne, plus lisible quand ils sont nombreux\n",
"emissions[secteurs].describe().round(0).T"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Les ordres de grandeur sont très étalés : certaines communes émettent quelques\n",
"tonnes, d'autres plusieurs millions. C'est attendu pour 35 000 communes allant du\n",
"hameau à la métropole."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**L'identifiant est-il unique ?**"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.085904Z",
"iopub.status.busy": "2026-10-30T13:11:02.085752Z",
"iopub.status.idle": "2026-10-30T13:11:02.098791Z",
"shell.execute_reply": "2026-10-30T13:11:02.098254Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"codes INSEE distincts : **35798** sur 35798 \n",
"noms de communes distincts : **33338** sur 35798"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# .nunique() compte les valeurs distinctes, len() compte les lignes :\n",
"# si les deux coïncident, la colonne est un identifiant\n",
"display(Markdown(\n",
" f\"codes INSEE distincts : **{emissions['INSEE commune'].nunique()}** sur {len(emissions)} \\n\"\n",
" f\"noms de communes distincts : **{emissions['Commune'].nunique()}** sur {len(emissions)}\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Le **code INSEE** est unique, le **nom** ne l'est pas : plus de deux mille communes\n",
"françaises portent le même nom qu'une autre. C'est pourquoi toute jointure entre\n",
"fichiers communaux se fait sur le code, jamais sur le libellé."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 2. Les valeurs manquantes"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.100601Z",
"iopub.status.busy": "2026-10-30T13:11:02.100345Z",
"iopub.status.idle": "2026-10-30T13:11:02.107774Z",
"shell.execute_reply": "2026-10-30T13:11:02.107290Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" % manquant | \n",
"
\n",
" \n",
" \n",
" \n",
" | Autres transports international | \n",
" 91.9 | \n",
"
\n",
" \n",
" | Autres transports | \n",
" 72.1 | \n",
"
\n",
" \n",
" | Energie | \n",
" 3.7 | \n",
"
\n",
" \n",
" | Industrie hors-énergie | \n",
" 3.7 | \n",
"
\n",
" \n",
" | Agriculture | \n",
" 0.2 | \n",
"
\n",
" \n",
" | Routier | \n",
" 0.1 | \n",
"
\n",
" \n",
" | CO2 biomasse hors-total | \n",
" 0.0 | \n",
"
\n",
" \n",
" | Déchets | \n",
" 0.0 | \n",
"
\n",
" \n",
" | Résidentiel | \n",
" 0.0 | \n",
"
\n",
" \n",
" | Tertiaire | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" % manquant\n",
"Autres transports international 91.9\n",
"Autres transports 72.1\n",
"Energie 3.7\n",
"Industrie hors-énergie 3.7\n",
"Agriculture 0.2\n",
"Routier 0.1\n",
"CO2 biomasse hors-total 0.0\n",
"Déchets 0.0\n",
"Résidentiel 0.0\n",
"Tertiaire 0.0"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# .isnull() renvoie un tableau de booléens de même forme que le DataFrame ;\n",
"# .mean() sur des booléens donne la proportion de True, que l'on convertit en pourcentage\n",
"(emissions[secteurs].isnull().mean() * 100).round(1).sort_values(ascending=False).to_frame('% manquant')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Deux colonnes sortent du lot : `Autres transports international` est vide à près\n",
"de **92 %**, `Autres transports` à **72 %**. Avant de décider quoi en faire, regardons\n",
"**où** elles sont renseignées."
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.109600Z",
"iopub.status.busy": "2026-10-30T13:11:02.109399Z",
"iopub.status.idle": "2026-10-30T13:11:02.119408Z",
"shell.execute_reply": "2026-10-30T13:11:02.118833Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"renseignée pour **2891** communes sur 35798"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Commune | \n",
" Autres transports international | \n",
"
\n",
" \n",
" \n",
" \n",
" | 31099 | \n",
" MAUREGARD | \n",
" 3303394.0 | \n",
"
\n",
" \n",
" | 31108 | \n",
" LE MESNIL-AMELOT | \n",
" 3303394.0 | \n",
"
\n",
" \n",
" | 31111 | \n",
" MITRY-MORY | \n",
" 2202275.0 | \n",
"
\n",
" \n",
" | 30438 | \n",
" LE HAVRE | \n",
" 2109460.0 | \n",
"
\n",
" \n",
" | 35561 | \n",
" TREMBLAY-EN-FRANCE | \n",
" 1101146.0 | \n",
"
\n",
" \n",
" | 35757 | \n",
" ROISSY-EN-FRANCE | \n",
" 1101131.0 | \n",
"
\n",
" \n",
" | 1987 | \n",
" NICE | \n",
" 1003572.0 | \n",
"
\n",
" \n",
" | 28017 | \n",
" COLOMBIER-SAUGNIEU | \n",
" 865809.0 | \n",
"
\n",
" \n",
" | 35426 | \n",
" PARAY-VIEILLE-POSTE | \n",
" 785878.0 | \n",
"
\n",
" \n",
" | 27580 | \n",
" MULHOUSE | \n",
" 615461.0 | \n",
"
\n",
" \n",
" | 4397 | \n",
" MARIGNANE | \n",
" 527361.0 | \n",
"
\n",
" \n",
" | 35487 | \n",
" WISSOUS | \n",
" 471527.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Commune Autres transports international\n",
"31099 MAUREGARD 3303394.0\n",
"31108 LE MESNIL-AMELOT 3303394.0\n",
"31111 MITRY-MORY 2202275.0\n",
"30438 LE HAVRE 2109460.0\n",
"35561 TREMBLAY-EN-FRANCE 1101146.0\n",
"35757 ROISSY-EN-FRANCE 1101131.0\n",
"1987 NICE 1003572.0\n",
"28017 COLOMBIER-SAUGNIEU 865809.0\n",
"35426 PARAY-VIEILLE-POSTE 785878.0\n",
"27580 MULHOUSE 615461.0\n",
"4397 MARIGNANE 527361.0\n",
"35487 WISSOUS 471527.0"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"colonne = 'Autres transports international'\n",
"\n",
"# .notna() sélectionne les lignes où la valeur est présente\n",
"renseignees = emissions[ emissions[colonne].notna() ]\n",
"\n",
"display(Markdown(f\"renseignée pour **{len(renseignees)}** communes sur {len(emissions)}\"))\n",
"\n",
"# .nlargest(12, colonne) renvoie les 12 lignes aux plus grandes valeurs de cette colonne\n",
"renseignees.nlargest(12, colonne)[['Commune', colonne]].round(0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"**Ces noms vous disent quelque chose.**\n",
"\n",
"Mauregard, Le Mesnil-Amelot, Mitry-Mory, Tremblay-en-France, Roissy-en-France : ce sont\n",
"les communes de **l'aéroport Charles-de-Gaulle**. Paray-Vieille-Poste et Wissous bordent\n",
"**Orly**, Colombier-Saugnieu est **Lyon-Saint-Exupéry**, Marignane **Marseille-Provence**,\n",
"Blagnac **Toulouse**. Le Havre est un **port**.\n",
"\n",
"La valeur manquante ne signifie donc pas « donnée perdue » : elle signifie **« il n'y a\n",
"ni aéroport ni port dans cette commune »**. L'absence d'information *est* l'information.\n",
"\n",
"Les conséquences sont directes :\n",
"\n",
"- **remplacer par la moyenne** serait absurde, puisqu'on attribuerait un trafic aérien\n",
" à des villages ;\n",
"- **supprimer les lignes incomplètes** effacerait 92 % de la France ;\n",
"- **remplacer par zéro** est ici le seul choix défendable, et c'est exactement ce que\n",
" fait `sum()` par défaut en ignorant les `NaN`.\n",
"\n",
"
"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.121172Z",
"iopub.status.busy": "2026-10-30T13:11:02.120978Z",
"iopub.status.idle": "2026-10-30T13:11:02.135555Z",
"shell.execute_reply": "2026-10-30T13:11:02.135131Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Commune | \n",
" total | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" L'ABERGEMENT-CLEMENCIAT | \n",
" 5724.424941 | \n",
"
\n",
" \n",
" | 1 | \n",
" L'ABERGEMENT-DE-VAREY | \n",
" 1332.811619 | \n",
"
\n",
" \n",
" | 2 | \n",
" AMBERIEU-EN-BUGEY | \n",
" 63259.689119 | \n",
"
\n",
" \n",
" | 3 | \n",
" AMBERIEUX-EN-DOMBES | \n",
" 6792.867439 | \n",
"
\n",
" \n",
" | 4 | \n",
" AMBLEON | \n",
" 1068.584766 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Commune total\n",
"0 L'ABERGEMENT-CLEMENCIAT 5724.424941\n",
"1 L'ABERGEMENT-DE-VAREY 1332.811619\n",
"2 AMBERIEU-EN-BUGEY 63259.689119\n",
"3 AMBERIEUX-EN-DOMBES 6792.867439\n",
"4 AMBLEON 1068.584766"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# .sum(axis=1) additionne sur les colonnes, donc ligne par ligne.\n",
"# Par défaut les NaN sont ignorés, ce qui revient ici à les compter pour zéro.\n",
"emissions['total'] = emissions[secteurs].sum(axis=1)\n",
"\n",
"emissions[['Commune', 'total']].head()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 3. Joindre à la population"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Le fichier de populations sépare les milliers par un **espace insécable**\n",
"(`1 000`), que `read_csv` ne sait pas interpréter seul, comme nous l'avons vu dans le\n",
"cours. On nettoie donc la colonne avant de la convertir.\n",
"\n",
"Nous prenons la colonne `PMUN2016`, population municipale de 2016, pour la faire\n",
"correspondre à l'année des émissions."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.137394Z",
"iopub.status.busy": "2026-10-30T13:11:02.137210Z",
"iopub.status.idle": "2026-10-30T13:11:02.282564Z",
"shell.execute_reply": "2026-10-30T13:11:02.282071Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"avant nettoyage : `'1\\xa0030'`"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/markdown": [
"après nettoyage : `1030`"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" CODGEO | \n",
" REG | \n",
" DEP | \n",
" LIBGEO | \n",
" PMUN2016 | \n",
"
\n",
" \n",
" \n",
" \n",
" | 0 | \n",
" 55039 | \n",
" 44 | \n",
" 55 | \n",
" Beaumont-en-Verdunois | \n",
" 0 | \n",
"
\n",
" \n",
" | 1 | \n",
" 55050 | \n",
" 44 | \n",
" 55 | \n",
" Bezonvaux | \n",
" 0 | \n",
"
\n",
" \n",
" | 2 | \n",
" 55139 | \n",
" 44 | \n",
" 55 | \n",
" Cumières-le-Mort-Homme | \n",
" 0 | \n",
"
\n",
" \n",
" | 3 | \n",
" 55189 | \n",
" 44 | \n",
" 55 | \n",
" Fleury-devant-Douaumont | \n",
" 0 | \n",
"
\n",
" \n",
" | 4 | \n",
" 55239 | \n",
" 44 | \n",
" 55 | \n",
" Haumont-près-Samogneux | \n",
" 0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" CODGEO REG DEP LIBGEO PMUN2016\n",
"0 55039 44 55 Beaumont-en-Verdunois 0\n",
"1 55050 44 55 Bezonvaux 0\n",
"2 55139 44 55 Cumières-le-Mort-Homme 0\n",
"3 55189 44 55 Fleury-devant-Douaumont 0\n",
"4 55239 44 55 Haumont-près-Samogneux 0"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# usecols limite la lecture aux colonnes utiles : le fichier en compte une\n",
"# par recensement depuis 1876, inutile de tout charger en mémoire\n",
"colonnes_pop = ['CODGEO', 'LIBGEO', 'DEP', 'REG', 'PMUN2016']\n",
"\n",
"population = pd.read_csv('base-pop-historiques-1876-2021.csv', sep=';', low_memory=False,\n",
" dtype={'CODGEO': str, 'DEP': str, 'REG': str},\n",
" usecols=colonnes_pop)\n",
"\n",
"# le séparateur n'apparaît que sur les valeurs supérieures à mille :\n",
"# on repère une telle ligne pour pouvoir l'afficher avant et après conversion\n",
"grandes = population['PMUN2016'].astype(str).str.len() > 4\n",
"display(Markdown(\"avant nettoyage : `\"\n",
" + repr(population.loc[grandes, 'PMUN2016'].iloc[0]) + \"`\"))\n",
"\n",
"# chr(160) est l'espace insécable ; regex=False car on remplace un caractère littéral\n",
"population['PMUN2016'] = (population['PMUN2016'].astype(str)\n",
" .str.replace(chr(160), '', regex=False)\n",
" .astype(int))\n",
"\n",
"display(Markdown(\"après nettoyage : `\"\n",
" + repr(population.loc[grandes, 'PMUN2016'].iloc[0]) + \"`\"))\n",
"population.head()"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.284440Z",
"iopub.status.busy": "2026-10-30T13:11:02.284250Z",
"iopub.status.idle": "2026-10-30T13:11:02.358307Z",
"shell.execute_reply": "2026-10-30T13:11:02.357732Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"communes dans l'inventaire : **35798** \n",
"après la jointure : **35798** \n",
"sans correspondance : **966**"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# how='left' conserve toutes les lignes de l'inventaire, même sans correspondance ;\n",
"# validate='one_to_one' fait échouer la jointure si une clé était dupliquée d'un côté\n",
"df = emissions.merge(population,\n",
" left_on='INSEE commune', right_on='CODGEO',\n",
" how='left', validate='one_to_one')\n",
"\n",
"display(Markdown(\n",
" f\"communes dans l'inventaire : **{len(emissions)}** \\n\"\n",
" f\"après la jointure : **{len(df)}** \\n\"\n",
" f\"sans correspondance : **{df['CODGEO'].isna().sum()}**\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Environ **un millier de communes** n'ont pas de correspondance. Les deux fichiers\n",
"ne datent pas du même millésime, et le découpage communal bouge sans cesse : fusions,\n",
"communes nouvelles. Nous les écartons, en sachant ce que nous écartons."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.360257Z",
"iopub.status.busy": "2026-10-30T13:11:02.360084Z",
"iopub.status.idle": "2026-10-30T13:11:02.371837Z",
"shell.execute_reply": "2026-10-30T13:11:02.371213Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"**34832** communes conservées"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# .notna() garde les lignes où la jointure a trouvé une correspondance\n",
"df = df[ df['CODGEO'].notna() ]\n",
"\n",
"display(Markdown(f\"**{len(df)}** communes conservées\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 4. Émissions par habitant"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Quelques communes ont une population nulle, des villages détruits pendant la\n",
"guerre de 1914-1918 et jamais reconstruits. Il faut les écarter avant toute division."
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.373837Z",
"iopub.status.busy": "2026-10-30T13:11:02.373663Z",
"iopub.status.idle": "2026-10-30T13:11:02.405876Z",
"shell.execute_reply": "2026-10-30T13:11:02.405152Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"communes sans habitant : **6**"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Commune | \n",
" DEP | \n",
" PMUN2016 | \n",
"
\n",
" \n",
" \n",
" \n",
" | 20731 | \n",
" BEAUMONT-EN-VERDUNOIS | \n",
" 55 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 20742 | \n",
" BEZONVAUX | \n",
" 55 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 20817 | \n",
" CUMIERES-LE-MORT-HOMME | \n",
" 55 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 20861 | \n",
" FLEURY-DEVANT-DOUAUMONT | \n",
" 55 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 20898 | \n",
" HAUMONT-PRES-SAMOGNEUX | \n",
" 55 | \n",
" 0.0 | \n",
"
\n",
" \n",
" | 20957 | \n",
" LOUVEMONT-COTE-DU-POIVRE | \n",
" 55 | \n",
" 0.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Commune DEP PMUN2016\n",
"20731 BEAUMONT-EN-VERDUNOIS 55 0.0\n",
"20742 BEZONVAUX 55 0.0\n",
"20817 CUMIERES-LE-MORT-HOMME 55 0.0\n",
"20861 FLEURY-DEVANT-DOUAUMONT 55 0.0\n",
"20898 HAUMONT-PRES-SAMOGNEUX 55 0.0\n",
"20957 LOUVEMONT-COTE-DU-POIVRE 55 0.0"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"display(Markdown(f\"communes sans habitant : **{(df['PMUN2016'] == 0).sum()}**\"))\n",
"display(df.loc[df['PMUN2016'] == 0, ['Commune', 'DEP', 'PMUN2016']])\n",
"\n",
"# .copy() crée un DataFrame indépendant : sans lui, l'ajout de colonne\n",
"# qui suit porterait sur une tranche du tableau d'origine\n",
"df = df[ df['PMUN2016'] > 0 ].copy()\n",
"\n",
"df['par_habitant'] = df['total'] / df['PMUN2016']"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Les cinq communes les plus émettrices au total :**"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.408340Z",
"iopub.status.busy": "2026-10-30T13:11:02.408134Z",
"iopub.status.idle": "2026-10-30T13:11:02.424321Z",
"shell.execute_reply": "2026-10-30T13:11:02.423750Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Commune | \n",
" DEP | \n",
" PMUN2016 | \n",
" total | \n",
"
\n",
" \n",
" \n",
" \n",
" | 4382 | \n",
" FOS-SUR-MER | \n",
" 13 | \n",
" 15608.0 | \n",
" 9533813.0 | \n",
"
\n",
" \n",
" | 22671 | \n",
" DUNKERQUE | \n",
" 59 | \n",
" 88108.0 | \n",
" 8643550.0 | \n",
"
\n",
" \n",
" | 4398 | \n",
" MARTIGUES | \n",
" 13 | \n",
" 48783.0 | \n",
" 4009663.0 | \n",
"
\n",
" \n",
" | 30560 | \n",
" PORT-JEROME-SUR-SEINE | \n",
" 76 | \n",
" 9903.0 | \n",
" 3668151.0 | \n",
"
\n",
" \n",
" | 31108 | \n",
" LE MESNIL-AMELOT | \n",
" 77 | \n",
" 997.0 | \n",
" 3471492.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Commune DEP PMUN2016 total\n",
"4382 FOS-SUR-MER 13 15608.0 9533813.0\n",
"22671 DUNKERQUE 59 88108.0 8643550.0\n",
"4398 MARTIGUES 13 48783.0 4009663.0\n",
"30560 PORT-JEROME-SUR-SEINE 76 9903.0 3668151.0\n",
"31108 LE MESNIL-AMELOT 77 997.0 3471492.0"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# .nlargest(5, 'total') : les 5 plus grandes valeurs de la colonne 'total'\n",
"df.nlargest(5, 'total')[['Commune', 'DEP', 'PMUN2016', 'total']].round(0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Et les cinq plus émettrices par habitant :**"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.426637Z",
"iopub.status.busy": "2026-10-30T13:11:02.426444Z",
"iopub.status.idle": "2026-10-30T13:11:02.443557Z",
"shell.execute_reply": "2026-10-30T13:11:02.442987Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" Commune | \n",
" DEP | \n",
" PMUN2016 | \n",
" par_habitant | \n",
"
\n",
" \n",
" \n",
" \n",
" | 31099 | \n",
" MAUREGARD | \n",
" 77 | \n",
" 340.0 | \n",
" 10120.0 | \n",
"
\n",
" \n",
" | 31108 | \n",
" LE MESNIL-AMELOT | \n",
" 77 | \n",
" 997.0 | \n",
" 3482.0 | \n",
"
\n",
" \n",
" | 23189 | \n",
" BAZANCOURT | \n",
" 60 | \n",
" 129.0 | \n",
" 2380.0 | \n",
"
\n",
" \n",
" | 27426 | \n",
" CHALAMPE | \n",
" 68 | \n",
" 950.0 | \n",
" 1187.0 | \n",
"
\n",
" \n",
" | 5682 | \n",
" SAINT-VALLIER | \n",
" 16 | \n",
" 137.0 | \n",
" 1127.0 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" Commune DEP PMUN2016 par_habitant\n",
"31099 MAUREGARD 77 340.0 10120.0\n",
"31108 LE MESNIL-AMELOT 77 997.0 3482.0\n",
"23189 BAZANCOURT 60 129.0 2380.0\n",
"27426 CHALAMPE 68 950.0 1187.0\n",
"5682 SAINT-VALLIER 16 137.0 1127.0"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"df.nlargest(5, 'par_habitant')[['Commune', 'DEP', 'PMUN2016', 'par_habitant']].round(0)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Deux listes sans aucun point commun. La première donne de grandes villes\n",
"industrielles, la seconde de très petites communes abritant une installation lourde :\n",
"quelques dizaines d'habitants pour une usine, et le rapport s'envole."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Agréger par département"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"**Attention au piège : la moyenne des rapports n'est pas le rapport des sommes.**\n",
"\n",
"Pour obtenir les émissions par habitant d'un département, il ne faut **pas** faire la\n",
"moyenne des `par_habitant` de ses communes : cela donnerait le même poids à un village\n",
"de 80 habitants qu'à une ville de 200 000. Il faut diviser le **total des émissions**\n",
"par le **total des habitants**.\n",
"\n",
"
"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.445865Z",
"iopub.status.busy": "2026-10-30T13:11:02.445667Z",
"iopub.status.idle": "2026-10-30T13:11:02.465099Z",
"shell.execute_reply": "2026-10-30T13:11:02.464653Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" emissions | \n",
" habitants | \n",
" communes | \n",
" par_habitant | \n",
" moyenne_des_communes | \n",
"
\n",
" \n",
" | DEP | \n",
" | \n",
" | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 13 | \n",
" 2.421180e+07 | \n",
" 1157506.0 | \n",
" 118 | \n",
" 20.9 | \n",
" 15.5 | \n",
"
\n",
" \n",
" | 23 | \n",
" 2.327727e+06 | \n",
" 119502.0 | \n",
" 256 | \n",
" 19.5 | \n",
" 29.2 | \n",
"
\n",
" \n",
" | 76 | \n",
" 2.154345e+07 | \n",
" 1255503.0 | \n",
" 707 | \n",
" 17.2 | \n",
" 14.3 | \n",
"
\n",
" \n",
" | 15 | \n",
" 2.506101e+06 | \n",
" 145969.0 | \n",
" 246 | \n",
" 17.2 | \n",
" 32.7 | \n",
"
\n",
" \n",
" | 55 | \n",
" 3.199781e+06 | \n",
" 189055.0 | \n",
" 493 | \n",
" 16.9 | \n",
" 26.8 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" emissions habitants communes par_habitant moyenne_des_communes\n",
"DEP \n",
"13 2.421180e+07 1157506.0 118 20.9 15.5\n",
"23 2.327727e+06 119502.0 256 19.5 29.2\n",
"76 2.154345e+07 1255503.0 707 17.2 14.3\n",
"15 2.506101e+06 145969.0 246 17.2 32.7\n",
"55 3.199781e+06 189055.0 493 16.9 26.8"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# .agg() applique une fonction différente à chaque colonne.\n",
"# La syntaxe nom_voulu=('colonne', 'fonction') nomme directement le résultat.\n",
"dep = df.groupby('DEP').agg(\n",
" emissions = ('total', 'sum'),\n",
" habitants = ('PMUN2016', 'sum'),\n",
" communes = ('CODGEO', 'size')\n",
")\n",
"\n",
"dep['par_habitant'] = (dep['emissions'] / dep['habitants']).round(1) # correct\n",
"dep['moyenne_des_communes'] = df.groupby('DEP')['par_habitant'].mean().round(1) # faux\n",
"\n",
"dep.sort_values('par_habitant', ascending=False).head(5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Comparons les rangs obtenus par les deux méthodes :"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.467098Z",
"iopub.status.busy": "2026-10-30T13:11:02.466847Z",
"iopub.status.idle": "2026-10-30T13:11:02.474691Z",
"shell.execute_reply": "2026-10-30T13:11:02.474187Z"
}
},
"outputs": [
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" rapport des sommes | \n",
" moyenne des communes | \n",
" écart de rang | \n",
"
\n",
" \n",
" | DEP | \n",
" | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 13 | \n",
" 1 | \n",
" 37 | \n",
" 36 | \n",
"
\n",
" \n",
" | 23 | \n",
" 2 | \n",
" 3 | \n",
" 1 | \n",
"
\n",
" \n",
" | 15 | \n",
" 3 | \n",
" 2 | \n",
" 1 | \n",
"
\n",
" \n",
" | 76 | \n",
" 3 | \n",
" 45 | \n",
" 42 | \n",
"
\n",
" \n",
" | 55 | \n",
" 5 | \n",
" 8 | \n",
" 3 | \n",
"
\n",
" \n",
" | 53 | \n",
" 6 | \n",
" 18 | \n",
" 12 | \n",
"
\n",
" \n",
" | 03 | \n",
" 7 | \n",
" 6 | \n",
" 1 | \n",
"
\n",
" \n",
" | 52 | \n",
" 7 | \n",
" 3 | \n",
" 4 | \n",
"
\n",
" \n",
" | 48 | \n",
" 9 | \n",
" 5 | \n",
" 4 | \n",
"
\n",
" \n",
" | 12 | \n",
" 10 | \n",
" 9 | \n",
" 1 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" rapport des sommes moyenne des communes écart de rang\n",
"DEP \n",
"13 1 37 36\n",
"23 2 3 1\n",
"15 3 2 1\n",
"76 3 45 42\n",
"55 5 8 3\n",
"53 6 18 12\n",
"03 7 6 1\n",
"52 7 3 4\n",
"48 9 5 4\n",
"12 10 9 1"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# .rank(ascending=False) donne le rang de chaque département, 1 pour le plus élevé\n",
"comparaison = pd.DataFrame({\n",
" 'rapport des sommes' : dep['par_habitant'].rank(ascending=False).astype(int),\n",
" 'moyenne des communes' : dep['moyenne_des_communes'].rank(ascending=False).astype(int),\n",
"})\n",
"comparaison['écart de rang'] = (comparaison['rapport des sommes']\n",
" - comparaison['moyenne des communes']).abs()\n",
"\n",
"comparaison.sort_values('rapport des sommes').head(10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Les deux méthodes ne classent pas de la même façon : le choix de l'agrégation n'est pas neutre."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Total ou par habitant ?"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.476545Z",
"iopub.status.busy": "2026-10-30T13:11:02.476394Z",
"iopub.status.idle": "2026-10-30T13:11:02.493424Z",
"shell.execute_reply": "2026-10-30T13:11:02.492921Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"**Classement par émissions totales**"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" total (kt CO₂) | \n",
" par habitant (t) | \n",
"
\n",
" \n",
" | DEP | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 59 | \n",
" 26075.0 | \n",
" 10.0 | \n",
"
\n",
" \n",
" | 13 | \n",
" 24212.0 | \n",
" 20.9 | \n",
"
\n",
" \n",
" | 76 | \n",
" 21543.0 | \n",
" 17.2 | \n",
"
\n",
" \n",
" | 77 | \n",
" 18161.0 | \n",
" 13.0 | \n",
"
\n",
" \n",
" | 62 | \n",
" 12069.0 | \n",
" 8.2 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" total (kt CO₂) par habitant (t)\n",
"DEP \n",
"59 26075.0 10.0\n",
"13 24212.0 20.9\n",
"76 21543.0 17.2\n",
"77 18161.0 13.0\n",
"62 12069.0 8.2"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/markdown": [
"**Classement par émissions par habitant**"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" total (kt CO₂) | \n",
" par habitant (t) | \n",
"
\n",
" \n",
" | DEP | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 13 | \n",
" 24212.0 | \n",
" 20.9 | \n",
"
\n",
" \n",
" | 23 | \n",
" 2328.0 | \n",
" 19.5 | \n",
"
\n",
" \n",
" | 15 | \n",
" 2506.0 | \n",
" 17.2 | \n",
"
\n",
" \n",
" | 76 | \n",
" 21543.0 | \n",
" 17.2 | \n",
"
\n",
" \n",
" | 55 | \n",
" 3200.0 | \n",
" 16.9 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" total (kt CO₂) par habitant (t)\n",
"DEP \n",
"13 24212.0 20.9\n",
"23 2328.0 19.5\n",
"15 2506.0 17.2\n",
"76 21543.0 17.2\n",
"55 3200.0 16.9"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/markdown": [
"**Les cinq départements les plus sobres par habitant**"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"\n",
"\n",
"
\n",
" \n",
" \n",
" | \n",
" total (kt CO₂) | \n",
" par habitant (t) | \n",
"
\n",
" \n",
" | DEP | \n",
" | \n",
" | \n",
"
\n",
" \n",
" \n",
" \n",
" | 75 | \n",
" 6730.0 | \n",
" 3.1 | \n",
"
\n",
" \n",
" | 92 | \n",
" 5796.0 | \n",
" 3.6 | \n",
"
\n",
" \n",
" | 94 | \n",
" 5254.0 | \n",
" 3.8 | \n",
"
\n",
" \n",
" | 93 | \n",
" 6254.0 | \n",
" 3.9 | \n",
"
\n",
" \n",
" | 83 | \n",
" 4301.0 | \n",
" 4.1 | \n",
"
\n",
" \n",
"
\n",
"
"
],
"text/plain": [
" total (kt CO₂) par habitant (t)\n",
"DEP \n",
"75 6730.0 3.1\n",
"92 5796.0 3.6\n",
"94 5254.0 3.8\n",
"93 6254.0 3.9\n",
"83 4301.0 4.1"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"cote_a_cote = pd.DataFrame({\n",
" 'total (kt CO₂)' : (dep['emissions'] / 1000).round(0),\n",
" 'par habitant (t)': dep['par_habitant'],\n",
"})\n",
"\n",
"display(Markdown(\"**Classement par émissions totales**\"))\n",
"display(cote_a_cote.nlargest(5, 'total (kt CO₂)'))\n",
"\n",
"display(Markdown(\"**Classement par émissions par habitant**\"))\n",
"display(cote_a_cote.nlargest(5, 'par habitant (t)'))\n",
"\n",
"# .nsmallest() est le symétrique de .nlargest()\n",
"display(Markdown(\"**Les cinq départements les plus sobres par habitant**\"))\n",
"display(cote_a_cote.nsmallest(5, 'par habitant (t)'))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"\n",
"**Deux questions, deux réponses.**\n",
"\n",
"En **volume**, les départements industriels et peuplés dominent : ce sont eux qui pèsent\n",
"sur le bilan national, et c'est là que des réductions en valeur absolue sont possibles.\n",
"\n",
"En **intensité**, le classement se renverse. Les départements ruraux arrivent en tête,\n",
"avec peu d'habitants mais de l'agriculture et des trajets longs, tandis que **Paris et\n",
"la petite couronne sont les plus sobres de France par habitant**, d'un facteur proche de\n",
"cinq. La densité urbaine réduit les transports et le chauffage par personne.\n",
"\n",
"Aucun des deux classements n'est « le bon ». Ils répondent à deux questions différentes,\n",
"et confondre les deux est la façon la plus commune de faire mentir des données\n",
"climatiques.\n",
"\n",
"
"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 5. Cartographier"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Un tableau de 96 lignes se lit mal ; une carte montre immédiatement la géographie\n",
"du phénomène. Nous utilisons le fond de carte `departements.geojson`, un fichier\n",
"**local** : aucune connexion n'est nécessaire.\n",
"\n",
"Chaque département y est décrit par une propriété `code` (`'01'`, `'02'`, jusqu'à `'2A'`\n",
"et `'2B'` pour la Corse), qui correspond exactement à notre colonne `DEP`."
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.495186Z",
"iopub.status.busy": "2026-10-30T13:11:02.495030Z",
"iopub.status.idle": "2026-10-30T13:11:02.513031Z",
"shell.execute_reply": "2026-10-30T13:11:02.512623Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"**96** départements dans le fond de carte"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/markdown": [
"propriétés disponibles : `{'code': '02', 'nom': 'Aisne'}`"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# un fichier GeoJSON est un fichier JSON : on le lit avec le module json\n",
"with open('departements.geojson', encoding='utf-8') as f:\n",
" france = json.load(f)\n",
"\n",
"display(Markdown(f\"**{len(france['features'])}** départements dans le fond de carte\"))\n",
"display(Markdown(\"propriétés disponibles : `\" + str(france['features'][0]['properties']) + \"`\"))"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.514793Z",
"iopub.status.busy": "2026-10-30T13:11:02.514603Z",
"iopub.status.idle": "2026-10-30T13:11:02.519876Z",
"shell.execute_reply": "2026-10-30T13:11:02.519345Z"
}
},
"outputs": [
{
"data": {
"text/markdown": [
"**96** départements cartographiables (l'outre-mer n'est pas dans ce fond de carte)"
],
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# .reset_index() ramène le code département, qui était l'index, dans une colonne :\n",
"# plotly a besoin d'une colonne pour faire la correspondance avec le fond de carte\n",
"carte = dep.reset_index()\n",
"\n",
"# on ne garde que les départements présents dans le fond de carte\n",
"codes_du_fond = {f['properties']['code'] for f in france['features']}\n",
"carte = carte[ carte['DEP'].isin(codes_du_fond) ]\n",
"\n",
"display(Markdown(f\"**{len(carte)}** départements cartographiables \"\n",
" \"(l'outre-mer n'est pas dans ce fond de carte)\"))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Les émissions totales :**"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.521681Z",
"iopub.status.busy": "2026-10-30T13:11:02.521494Z",
"iopub.status.idle": "2026-10-30T13:11:02.902430Z",
"shell.execute_reply": "2026-10-30T13:11:02.901915Z"
}
},
"outputs": [
{
"data": {
"text/html": [
" \n",
" "
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# locations indique la colonne du DataFrame qui identifie chaque zone,\n",
"# featureidkey indique où trouver l'identifiant correspondant dans le GeoJSON\n",
"fig = px.choropleth(carte,\n",
" geojson=france, locations='DEP', featureidkey='properties.code',\n",
" color='emissions', color_continuous_scale='Reds',\n",
" labels={'emissions': 't CO₂'},\n",
" title=\"Émissions totales par département (2016)\")\n",
"\n",
"# fitbounds recentre la carte sur les zones affichées, visible=False masque le globe\n",
"fig.update_geos(fitbounds=\"locations\", visible=False)\n",
"fig.update_layout(height=550, margin={\"r\":0, \"t\":40, \"l\":0, \"b\":0})\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"**Et les émissions par habitant :**"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"execution": {
"iopub.execute_input": "2026-10-30T13:11:02.915406Z",
"iopub.status.busy": "2026-10-30T13:11:02.915185Z",
"iopub.status.idle": "2026-10-30T13:11:03.175989Z",
"shell.execute_reply": "2026-10-30T13:11:03.175472Z"
}
},
"outputs": [
{
"data": {
"text/html": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig = px.choropleth(carte,\n",
" geojson=france, locations='DEP', featureidkey='properties.code',\n",
" color='par_habitant', color_continuous_scale='Reds',\n",
" labels={'par_habitant': 't CO₂ / hab'},\n",
" title=\"Émissions par habitant (2016)\")\n",
"fig.update_geos(fitbounds=\"locations\", visible=False)\n",
"fig.update_layout(height=550, margin={\"r\":0, \"t\":40, \"l\":0, \"b\":0})\n",
"fig.show()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Les deux cartes n'ont presque rien en commun.\n",
"\n",
"La première dessine la France industrielle et peuplée : vallée de la Seine, Nord,\n",
"pourtour de l'étang de Berre, grandes agglomérations.\n",
"\n",
"La seconde dessine une France rurale, et fait apparaître **l'Île-de-France en creux** :\n",
"les départements les plus denses du pays sont les plus sobres par habitant. Une carte\n",
"des émissions totales de l'Île-de-France aurait suggéré l'inverse.\n",
"\n",
"C'est la même leçon que plus haut, rendue visible d'un coup d'œil : **la carte que l'on\n",
"publie est déjà une réponse à une question que l'on a choisie.**"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Conclusion\n",
"\n",
"\n",
"\n",
"**Ce que cet exercice voulait montrer**\n",
"\n",
"- Une **valeur manquante peut être une information** : ici, `NaN` signifiait « pas\n",
" d'aéroport ». Avant de combler les trous, il faut comprendre pourquoi ils existent.\n",
"- Une **jointure se fait sur un identifiant**, jamais sur un libellé, puisque plus de\n",
" deux mille communes françaises sont homonymes.\n",
"- Les deux tables doivent porter sur **la même année**, sans quoi le rapport calculé\n",
" n'a pas de sens.\n",
"- La **moyenne des rapports n'est pas le rapport des sommes.**\n",
"- Et surtout : **total** et **par habitant** répondent à deux questions distinctes et\n",
" donnent deux classements opposés. Choisir l'un ou l'autre, c'est déjà prendre parti.\n",
"\n",
"
\n",
"\n",
"\n",
"\n",
"**Une dernière prudence.** Le producteur de ces données écrit lui-même qu'elles donnent\n",
"« des ordres de grandeur » et qu'elles n'ont pas « le niveau de finesse et de précision\n",
"des travaux d'inventaire réalisés au niveau territorial ». Nos comparaisons entre\n",
"départements sont donc légitimes ; affirmer le bilan exact d'une commune précise à\n",
"partir de ce fichier ne le serait pas.\n",
"\n",
"Savoir ce qu'une source permet de dire, et ce qu'elle ne permet pas, fait partie du\n",
"travail d'analyse.\n",
"\n",
"
"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.3"
}
},
"nbformat": 4,
"nbformat_minor": 2
}