1378 lines
78 KiB
Plaintext
1378 lines
78 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "4e4a7a6c",
|
||
"metadata": {},
|
||
"source": [
|
||
"# TP Module 1 — Découverte du pipeline de préparation des données\n",
|
||
"\n",
|
||
"## Contexte\n",
|
||
"\n",
|
||
"Tout au long de cette semaine de formation MLOps, nous allons travailler sur un seul et même projet, le **fil rouge** :\n",
|
||
"prédire la consommation électrique de clients portugais à partir de leur historique de consommation.\n",
|
||
"\n",
|
||
"Les données brutes ne sont **jamais directement exploitables**. Avant de pouvoir entraîner un modèle, il faut :\n",
|
||
"\n",
|
||
"1. comprendre la structure du fichier brut ;\n",
|
||
"2. le transformer dans un format exploitable ;\n",
|
||
"3. identifier et traiter les valeurs atypiques (*outliers*).\n",
|
||
"\n",
|
||
"C'est exactement ce que vous allez faire dans ce notebook — les mêmes étapes que celles utilisées dans le vrai pipeline\n",
|
||
"de préparation des données du projet.\n",
|
||
"\n",
|
||
"## Objectifs pédagogiques\n",
|
||
"\n",
|
||
"À la fin de ce TP, vous serez capable de :\n",
|
||
"\n",
|
||
"- expliquer pourquoi un format de données \"large\" est difficile à analyser ;\n",
|
||
"- transformer un tableau du format large vers le format long (`melt`) ;\n",
|
||
"- appliquer une conversion d'unité simple sur une colonne ;\n",
|
||
"- agréger des données avec `groupby` ;\n",
|
||
"- identifier des valeurs atypiques à partir de statistiques simples et d'un histogramme.\n",
|
||
"\n",
|
||
"## Comment utiliser ce notebook\n",
|
||
"\n",
|
||
"- Exécutez les cellules **dans l'ordre**, de haut en bas (`Shift + Entrée`).\n",
|
||
"- Certaines cellules contiennent `...` (trois points) à la place d'un morceau de code : c'est à vous de le remplacer.\n",
|
||
"- Après chaque exercice, une cellule de **validation** vous dit si votre réponse est correcte (✅) ou non (❌).\n",
|
||
"- Vous pouvez relancer une cellule de validation autant de fois que nécessaire après avoir corrigé votre code.\n",
|
||
"\n",
|
||
"Aucune de ces étapes ne nécessite de notion mathématique au-delà d'une moyenne, d'une somme ou d'une division.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"id": "bf3d4438",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:17:34.045887Z",
|
||
"iopub.status.busy": "2026-07-20T18:17:34.045707Z",
|
||
"iopub.status.idle": "2026-07-20T18:17:37.180194Z",
|
||
"shell.execute_reply": "2026-07-20T18:17:37.179129Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Environnement prêt.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Cellule d'installation / import — à exécuter en premier\n",
|
||
"import pandas as pd\n",
|
||
"import matplotlib.pyplot as plt\n",
|
||
"from pathlib import Path\n",
|
||
"import numpy as np\n",
|
||
"\n",
|
||
"pd.set_option(\"display.max_columns\", 8)\n",
|
||
"plt.rcParams[\"figure.figsize\"] = (9, 4.5)\n",
|
||
"\n",
|
||
"def check(condition: bool, success_msg: str, fail_msg: str):\n",
|
||
" '''Affiche un message de validation clair après un exercice.'''\n",
|
||
" if condition:\n",
|
||
" print(f\"✅ {success_msg}\")\n",
|
||
" else:\n",
|
||
" print(f\"❌ {fail_msg}\")\n",
|
||
" print(\" → Relis la consigne ci-dessus et corrige la cellule précédente, puis relance-la avant de revalider.\")\n",
|
||
"\n",
|
||
"print(\"Environnement prêt.\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "ccb662f3",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 1 — Comprendre les données brutes\n",
|
||
"\n",
|
||
"### Le problème concret\n",
|
||
"\n",
|
||
"Le fichier brut contient la consommation électrique de plusieurs centaines de clients, mesurée toutes les 15 minutes,\n",
|
||
"sur plusieurs années. Il est structuré ainsi :\n",
|
||
"\n",
|
||
"```\n",
|
||
"timestamp | client_1 | client_2 | client_3 | ...\n",
|
||
"2011-01-01 00:00:00 | 0.0 | 45.2 | 12.1 | ...\n",
|
||
"2011-01-01 00:15:00 | 0.0 | 44.8 | 11.9 | ...\n",
|
||
"```\n",
|
||
"\n",
|
||
"Chaque **colonne** est un client, chaque **ligne** est un instant. On appelle ça un format **large** (*wide*).\n",
|
||
"\n",
|
||
"### Pourquoi c'est un problème\n",
|
||
"\n",
|
||
"Essayez de répondre avant d'exécuter la cellule suivante : comment feriez-vous, avec ce format, pour calculer la\n",
|
||
"consommation moyenne d'**un seul client** sur **un mois** ? Et pour comparer 370 clients entre eux ?\n",
|
||
"\n",
|
||
"C'est possible, mais peu pratique. La plupart des outils d'analyse (agrégation, visualisation, machine learning)\n",
|
||
"attendent un format où chaque ligne est une observation unique.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "5b44358f",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:17:37.182589Z",
|
||
"iopub.status.busy": "2026-07-20T18:17:37.182274Z",
|
||
"iopub.status.idle": "2026-07-20T18:17:49.540711Z",
|
||
"shell.execute_reply": "2026-07-20T18:17:49.539717Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Dimensions du tableau : 140256 lignes x 370 colonnes (clients)\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>MT_001</th>\n",
|
||
" <th>MT_002</th>\n",
|
||
" <th>MT_003</th>\n",
|
||
" <th>MT_004</th>\n",
|
||
" <th>...</th>\n",
|
||
" <th>MT_367</th>\n",
|
||
" <th>MT_368</th>\n",
|
||
" <th>MT_369</th>\n",
|
||
" <th>MT_370</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>timestamp</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>2011-01-01 00:15:00</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2011-01-01 00:30:00</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2011-01-01 00:45:00</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2011-01-01 01:00:00</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2011-01-01 01:15:00</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>5 rows × 370 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" MT_001 MT_002 MT_003 MT_004 ... MT_367 MT_368 \\\n",
|
||
"timestamp ... \n",
|
||
"2011-01-01 00:15:00 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n",
|
||
"2011-01-01 00:30:00 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n",
|
||
"2011-01-01 00:45:00 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n",
|
||
"2011-01-01 01:00:00 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n",
|
||
"2011-01-01 01:15:00 0.0 0.0 0.0 0.0 ... 0.0 0.0 \n",
|
||
"\n",
|
||
" MT_369 MT_370 \n",
|
||
"timestamp \n",
|
||
"2011-01-01 00:15:00 0.0 0.0 \n",
|
||
"2011-01-01 00:30:00 0.0 0.0 \n",
|
||
"2011-01-01 00:45:00 0.0 0.0 \n",
|
||
"2011-01-01 01:00:00 0.0 0.0 \n",
|
||
"2011-01-01 01:15:00 0.0 0.0 \n",
|
||
"\n",
|
||
"[5 rows x 370 columns]"
|
||
]
|
||
},
|
||
"execution_count": 2,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Chargement des données brutes\n",
|
||
"#\n",
|
||
"# Sur la VM du cours, le fichier se trouve normalement à l'emplacement ci-dessous.\n",
|
||
"# Si vous suivez ce notebook en dehors de la VM (test, révision...), un petit jeu de données\n",
|
||
"# de démonstration est généré automatiquement pour que le notebook reste utilisable.\n",
|
||
"\n",
|
||
"RAW_DATA_PATH = Path(\"data/raw.txt\")\n",
|
||
"\n",
|
||
"\n",
|
||
"def load_raw_data(path: Path) -> pd.DataFrame:\n",
|
||
" if path.exists():\n",
|
||
" df = pd.read_csv(path, delimiter=\";\", index_col=0, decimal=\",\")\n",
|
||
" df.index.name = \"timestamp\"\n",
|
||
" df.index = pd.to_datetime(df.index)\n",
|
||
" return df\n",
|
||
"\n",
|
||
" print(\"⚠️ Fichier réel introuvable — génération d'un jeu de données de démonstration (mode démo).\")\n",
|
||
" print(f\" Emplacement attendu : {path.resolve()}\")\n",
|
||
" rng = np.random.default_rng(42)\n",
|
||
" timestamps = pd.date_range(\"2011-01-01\", periods=96 * 60, freq=\"15min\")\n",
|
||
" n_clients = 8\n",
|
||
" data = {}\n",
|
||
" for i in range(1, n_clients + 1):\n",
|
||
" base_level = rng.uniform(5, 60)\n",
|
||
" noise = rng.normal(0, base_level * 0.1, size=len(timestamps))\n",
|
||
" data[f\"client_{i}\"] = np.clip(base_level + noise, 0, None)\n",
|
||
" # un client \"extrême\" pour illustrer les outliers\n",
|
||
" data[\"client_extreme\"] = rng.uniform(3000, 4000, size=len(timestamps))\n",
|
||
" df = pd.DataFrame(data, index=timestamps)\n",
|
||
" df.index.name = \"timestamp\"\n",
|
||
" return df\n",
|
||
"\n",
|
||
"\n",
|
||
"df_raw = load_raw_data(RAW_DATA_PATH)\n",
|
||
"print(f\"Dimensions du tableau : {df_raw.shape[0]} lignes x {df_raw.shape[1]} colonnes (clients)\")\n",
|
||
"df_raw.head()\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "990a3a87",
|
||
"metadata": {},
|
||
"source": [
|
||
"**À observer :** chaque colonne est bien un client différent, et l'index est un horodatage. C'est le format\n",
|
||
"large décrit plus haut.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e7a15c06",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 2 — Passer au format long\n",
|
||
"\n",
|
||
"### Explication simple\n",
|
||
"\n",
|
||
"`melt` est une fonction pandas qui \"empile\" les colonnes les unes sous les autres. Sur un petit exemple :\n",
|
||
"\n",
|
||
"| timestamp | client_1 | client_2 |\n",
|
||
"|---|---|---|\n",
|
||
"| 08h00 | 2.1 | 0.8 |\n",
|
||
"\n",
|
||
"devient :\n",
|
||
"\n",
|
||
"| timestamp | individual | consumption |\n",
|
||
"|---|---|---|\n",
|
||
"| 08h00 | client_1 | 2.1 |\n",
|
||
"| 08h00 | client_2 | 0.8 |\n",
|
||
"\n",
|
||
"### À vous de jouer\n",
|
||
"\n",
|
||
"Complétez la cellule ci-dessous : remplacez les `...` par le bon nom de colonne.\n",
|
||
"- `var_name` : le nom de la nouvelle colonne qui contiendra les **identifiants de clients** (aujourd'hui en en-têtes de colonnes).\n",
|
||
"- `value_name` : le nom de la nouvelle colonne qui contiendra les **valeurs de consommation**.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "241f3ffb",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:17:49.543799Z",
|
||
"iopub.status.busy": "2026-07-20T18:17:49.543667Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:03.439887Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:03.439096Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>timestamp</th>\n",
|
||
" <th>individual</th>\n",
|
||
" <th>consumption</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>2011-01-01 00:15:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>2011-01-01 00:30:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>2011-01-01 00:45:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>2011-01-01 01:00:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>2011-01-01 01:15:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" timestamp individual consumption\n",
|
||
"0 2011-01-01 00:15:00 MT_001 0.0\n",
|
||
"1 2011-01-01 00:30:00 MT_001 0.0\n",
|
||
"2 2011-01-01 00:45:00 MT_001 0.0\n",
|
||
"3 2011-01-01 01:00:00 MT_001 0.0\n",
|
||
"4 2011-01-01 01:15:00 MT_001 0.0"
|
||
]
|
||
},
|
||
"execution_count": 3,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# TODO : remplace les ... ci-dessous\n",
|
||
"df_long = df_raw.reset_index().melt(\n",
|
||
" id_vars=\"timestamp\",\n",
|
||
" var_name=\"individual\", # TODO : comment nommer la colonne des identifiants clients ?\n",
|
||
" value_name=\"consumption\", # TODO : comment nommer la colonne des valeurs de consommation ?\n",
|
||
")\n",
|
||
"\n",
|
||
"df_long.head()\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "1ab6ce40",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:03.441790Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:03.441677Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:03.448238Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:03.447663Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"✅ Les colonnes 'individual' et 'consumption' sont bien présentes.\n",
|
||
"✅ Le tableau contient bien 51894720 lignes (une ligne par mesure et par client).\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Validation — exécute cette cellule pour vérifier ton résultat\n",
|
||
"expected_rows = df_raw.shape[0] * df_raw.shape[1]\n",
|
||
"\n",
|
||
"check(\n",
|
||
" \"individual\" in df_long.columns and \"consumption\" in df_long.columns,\n",
|
||
" \"Les colonnes 'individual' et 'consumption' sont bien présentes.\",\n",
|
||
" \"Il manque une colonne nommée 'individual' ou 'consumption' — relis les noms attendus dans la consigne.\",\n",
|
||
")\n",
|
||
"check(\n",
|
||
" len(df_long) == expected_rows,\n",
|
||
" f\"Le tableau contient bien {expected_rows} lignes (une ligne par mesure et par client).\",\n",
|
||
" f\"Le tableau devrait contenir {expected_rows} lignes, il en contient {len(df_long)}.\",\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "f7773903",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 3 — Convertir la puissance (kW) en énergie (kWh)\n",
|
||
"\n",
|
||
"### Explication simple\n",
|
||
"\n",
|
||
"Le fichier brut donne une **puissance** instantanée en kW, mesurée toutes les 15 minutes. Pour obtenir de\n",
|
||
"l'**énergie** consommée (ce qui compte réellement sur une facture d'électricité), il faut multiplier la puissance\n",
|
||
"par la durée de l'intervalle, exprimée en heures :\n",
|
||
"\n",
|
||
"```\n",
|
||
"énergie (kWh) = puissance (kW) x durée (h)\n",
|
||
"```\n",
|
||
"\n",
|
||
"Un intervalle de 15 minutes correspond à 0,25 heure (un quart d'heure), donc :\n",
|
||
"\n",
|
||
"```\n",
|
||
"kWh = kW x 0.25 (ce qui revient à diviser par 4)\n",
|
||
"```\n",
|
||
"\n",
|
||
"### À vous de jouer\n",
|
||
"\n",
|
||
"Créez une nouvelle colonne `consumption_kwh` dans `df_long`, égale à la colonne `consumption` divisée par 4.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 5,
|
||
"id": "dbc0dc2a",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:03.450257Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:03.450153Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:03.641665Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:03.640902Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>timestamp</th>\n",
|
||
" <th>individual</th>\n",
|
||
" <th>consumption</th>\n",
|
||
" <th>consumption_kwh</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>2011-01-01 00:15:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>2011-01-01 00:30:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>2011-01-01 00:45:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>2011-01-01 01:00:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>2011-01-01 01:15:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" timestamp individual consumption consumption_kwh\n",
|
||
"0 2011-01-01 00:15:00 MT_001 0.0 0.0\n",
|
||
"1 2011-01-01 00:30:00 MT_001 0.0 0.0\n",
|
||
"2 2011-01-01 00:45:00 MT_001 0.0 0.0\n",
|
||
"3 2011-01-01 01:00:00 MT_001 0.0 0.0\n",
|
||
"4 2011-01-01 01:15:00 MT_001 0.0 0.0"
|
||
]
|
||
},
|
||
"execution_count": 5,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# TODO : complète la ligne ci-dessous\n",
|
||
"df_long[\"consumption_kwh\"] = df_long[\"consumption\"] / 4\n",
|
||
"\n",
|
||
"df_long.head()\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"id": "f0bd6526",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:03.643849Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:03.643731Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:03.647142Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:03.646499Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"✅ La conversion kWh est correcte.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Validation\n",
|
||
"sample_ok = np.isclose(df_long[\"consumption_kwh\"].iloc[0], df_long[\"consumption\"].iloc[0] / 4)\n",
|
||
"\n",
|
||
"check(\n",
|
||
" \"consumption_kwh\" in df_long.columns and sample_ok,\n",
|
||
" \"La conversion kWh est correcte.\",\n",
|
||
" \"La colonne 'consumption_kwh' est absente ou incorrecte — vérifie que tu as bien divisé par 4.\",\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "45c1f5ee",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 4 — Agréger la consommation par mois\n",
|
||
"\n",
|
||
"### Explication simple\n",
|
||
"\n",
|
||
"On souhaite maintenant obtenir, pour chaque client, sa consommation totale **par mois**. Deux étapes :\n",
|
||
"\n",
|
||
"1. extraire le mois (année + mois) de chaque timestamp ;\n",
|
||
"2. sommer la consommation en kWh, groupée par client et par mois.\n",
|
||
"\n",
|
||
"pandas fournit `.dt.to_period(\"M\")` pour extraire facilement une période mensuelle à partir d'une date.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 7,
|
||
"id": "c41e1f71",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:03.649582Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:03.649486Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:23.666995Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:23.663881Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>timestamp</th>\n",
|
||
" <th>individual</th>\n",
|
||
" <th>consumption</th>\n",
|
||
" <th>consumption_kwh</th>\n",
|
||
" <th>year_month</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>2011-01-01 00:15:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>2011-01</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>2011-01-01 00:30:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>2011-01</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>2011-01-01 00:45:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>2011-01</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>2011-01-01 01:00:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>2011-01</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>2011-01-01 01:15:00</td>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>2011-01</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" timestamp individual consumption consumption_kwh year_month\n",
|
||
"0 2011-01-01 00:15:00 MT_001 0.0 0.0 2011-01\n",
|
||
"1 2011-01-01 00:30:00 MT_001 0.0 0.0 2011-01\n",
|
||
"2 2011-01-01 00:45:00 MT_001 0.0 0.0 2011-01\n",
|
||
"3 2011-01-01 01:00:00 MT_001 0.0 0.0 2011-01\n",
|
||
"4 2011-01-01 01:15:00 MT_001 0.0 0.0 2011-01"
|
||
]
|
||
},
|
||
"execution_count": 7,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Cette partie est fournie à titre de démonstration — lisez-la et exécutez-la.\n",
|
||
"df_long[\"timestamp\"] = pd.to_datetime(df_long[\"timestamp\"])\n",
|
||
"df_long[\"year_month\"] = df_long[\"timestamp\"].dt.to_period(\"M\").astype(str)\n",
|
||
"\n",
|
||
"df_long.head()\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "61a34d4b",
|
||
"metadata": {},
|
||
"source": [
|
||
"### À vous de jouer\n",
|
||
"\n",
|
||
"Utilisez `groupby` pour sommer la colonne `consumption_kwh`, groupée par `individual` et `year_month`.\n",
|
||
"\n",
|
||
"Indice : `df.groupby([...])[\"colonne\"].sum()`\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 8,
|
||
"id": "00faeb41",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:23.672420Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:23.672168Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:31.548778Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:31.547803Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th></th>\n",
|
||
" <th>individual</th>\n",
|
||
" <th>year_month</th>\n",
|
||
" <th>consumption_kwh</th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>0</th>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>2011-01</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>1</th>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>2011-02</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>2</th>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>2011-03</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>3</th>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>2011-04</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>4</th>\n",
|
||
" <td>MT_001</td>\n",
|
||
" <td>2011-05</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
" individual year_month consumption_kwh\n",
|
||
"0 MT_001 2011-01 0.0\n",
|
||
"1 MT_001 2011-02 0.0\n",
|
||
"2 MT_001 2011-03 0.0\n",
|
||
"3 MT_001 2011-04 0.0\n",
|
||
"4 MT_001 2011-05 0.0"
|
||
]
|
||
},
|
||
"execution_count": 8,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# TODO : complète la ligne ci-dessous\n",
|
||
"df_monthly = df_long.groupby([\"individual\", \"year_month\"])[\"consumption_kwh\"].sum().reset_index()\n",
|
||
"\n",
|
||
"df_monthly.head()\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 9,
|
||
"id": "3635bc6d",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:31.559844Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:31.559617Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:34.822669Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:34.821270Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"✅ Les colonnes attendues sont présentes.\n",
|
||
"✅ Le tableau agrégé contient bien 18130 lignes (370 clients x 49 mois).\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Validation\n",
|
||
"n_individuals = df_long[\"individual\"].nunique()\n",
|
||
"n_months = df_long[\"year_month\"].nunique()\n",
|
||
"expected_rows_monthly = n_individuals * n_months\n",
|
||
"\n",
|
||
"check(\n",
|
||
" {\"individual\", \"year_month\", \"consumption_kwh\"}.issubset(df_monthly.columns),\n",
|
||
" \"Les colonnes attendues sont présentes.\",\n",
|
||
" \"Il manque une colonne — vérifie les noms utilisés dans le groupby.\",\n",
|
||
")\n",
|
||
"check(\n",
|
||
" len(df_monthly) == expected_rows_monthly,\n",
|
||
" f\"Le tableau agrégé contient bien {expected_rows_monthly} lignes ({n_individuals} clients x {n_months} mois).\",\n",
|
||
" f\"Le tableau agrégé devrait contenir {expected_rows_monthly} lignes, il en contient {len(df_monthly)}.\",\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d36596d5",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 5 — Construire la matrice client x mois\n",
|
||
"\n",
|
||
"Pour comparer facilement les clients entre eux, on transforme le tableau agrégé en une **matrice** : une ligne par\n",
|
||
"client, une colonne par mois. C'est l'opération inverse de `melt`, elle s'appelle `pivot`.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 10,
|
||
"id": "78457d1a",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:34.826544Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:34.826379Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:34.933494Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:34.932604Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"<div>\n",
|
||
"<style scoped>\n",
|
||
" .dataframe tbody tr th:only-of-type {\n",
|
||
" vertical-align: middle;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe tbody tr th {\n",
|
||
" vertical-align: top;\n",
|
||
" }\n",
|
||
"\n",
|
||
" .dataframe thead th {\n",
|
||
" text-align: right;\n",
|
||
" }\n",
|
||
"</style>\n",
|
||
"<table border=\"1\" class=\"dataframe\">\n",
|
||
" <thead>\n",
|
||
" <tr style=\"text-align: right;\">\n",
|
||
" <th>year_month</th>\n",
|
||
" <th>2011-01</th>\n",
|
||
" <th>2011-02</th>\n",
|
||
" <th>2011-03</th>\n",
|
||
" <th>2011-04</th>\n",
|
||
" <th>...</th>\n",
|
||
" <th>2014-10</th>\n",
|
||
" <th>2014-11</th>\n",
|
||
" <th>2014-12</th>\n",
|
||
" <th>2015-01</th>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>individual</th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" <th></th>\n",
|
||
" </tr>\n",
|
||
" </thead>\n",
|
||
" <tbody>\n",
|
||
" <tr>\n",
|
||
" <th>MT_001</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>1210.659898</td>\n",
|
||
" <td>1426.395939</td>\n",
|
||
" <td>1574.555838</td>\n",
|
||
" <td>0.634518</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>MT_002</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>19199.679943</td>\n",
|
||
" <td>16753.911807</td>\n",
|
||
" <td>16997.688478</td>\n",
|
||
" <td>4.978663</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>MT_003</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>1274.326672</td>\n",
|
||
" <td>1232.406603</td>\n",
|
||
" <td>1231.972198</td>\n",
|
||
" <td>0.434405</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>MT_004</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>80881.097561</td>\n",
|
||
" <td>90533.028455</td>\n",
|
||
" <td>105789.634146</td>\n",
|
||
" <td>44.715447</td>\n",
|
||
" </tr>\n",
|
||
" <tr>\n",
|
||
" <th>MT_005</th>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>0.0</td>\n",
|
||
" <td>...</td>\n",
|
||
" <td>32704.268293</td>\n",
|
||
" <td>41645.731707</td>\n",
|
||
" <td>52302.439024</td>\n",
|
||
" <td>21.036585</td>\n",
|
||
" </tr>\n",
|
||
" </tbody>\n",
|
||
"</table>\n",
|
||
"<p>5 rows × 49 columns</p>\n",
|
||
"</div>"
|
||
],
|
||
"text/plain": [
|
||
"year_month 2011-01 2011-02 2011-03 2011-04 ... 2014-10 \\\n",
|
||
"individual ... \n",
|
||
"MT_001 0.0 0.0 0.0 0.0 ... 1210.659898 \n",
|
||
"MT_002 0.0 0.0 0.0 0.0 ... 19199.679943 \n",
|
||
"MT_003 0.0 0.0 0.0 0.0 ... 1274.326672 \n",
|
||
"MT_004 0.0 0.0 0.0 0.0 ... 80881.097561 \n",
|
||
"MT_005 0.0 0.0 0.0 0.0 ... 32704.268293 \n",
|
||
"\n",
|
||
"year_month 2014-11 2014-12 2015-01 \n",
|
||
"individual \n",
|
||
"MT_001 1426.395939 1574.555838 0.634518 \n",
|
||
"MT_002 16753.911807 16997.688478 4.978663 \n",
|
||
"MT_003 1232.406603 1231.972198 0.434405 \n",
|
||
"MT_004 90533.028455 105789.634146 44.715447 \n",
|
||
"MT_005 41645.731707 52302.439024 21.036585 \n",
|
||
"\n",
|
||
"[5 rows x 49 columns]"
|
||
]
|
||
},
|
||
"execution_count": 10,
|
||
"metadata": {},
|
||
"output_type": "execute_result"
|
||
}
|
||
],
|
||
"source": [
|
||
"# TODO : complète les paramètres ci-dessous\n",
|
||
"df_matrix = df_monthly.pivot(\n",
|
||
" index=\"individual\", # TODO : quelle colonne doit devenir une ligne par client ?\n",
|
||
" columns=\"year_month\", # TODO : quelle colonne doit devenir une colonne par mois ?\n",
|
||
" values=\"consumption_kwh\",\n",
|
||
")\n",
|
||
"\n",
|
||
"df_matrix.head()\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 11,
|
||
"id": "553ddb73",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:34.939575Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:34.939351Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:34.945581Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:34.944525Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"✅ La matrice contient bien 370 lignes, une par client.\n",
|
||
"✅ La matrice contient bien 49 colonnes, une par mois.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Validation\n",
|
||
"check(\n",
|
||
" df_matrix.shape[0] == n_individuals,\n",
|
||
" f\"La matrice contient bien {n_individuals} lignes, une par client.\",\n",
|
||
" f\"La matrice devrait contenir {n_individuals} lignes, elle en contient {df_matrix.shape[0]}.\",\n",
|
||
")\n",
|
||
"check(\n",
|
||
" df_matrix.shape[1] == n_months,\n",
|
||
" f\"La matrice contient bien {n_months} colonnes, une par mois.\",\n",
|
||
" f\"La matrice devrait contenir {n_months} colonnes, elle en contient {df_matrix.shape[1]}.\",\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "7bc5ea84",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 6 — Visualiser la distribution de consommation\n",
|
||
"\n",
|
||
"Cette partie est une démonstration : contentez-vous d'exécuter les cellules et d'observer le résultat.\n",
|
||
"\n",
|
||
"On calcule la consommation mensuelle **moyenne** de chaque client, puis on regarde sa distribution sur l'ensemble\n",
|
||
"des clients.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 12,
|
||
"id": "5c8f39a0",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:34.949345Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:34.949162Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:35.450188Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:35.449473Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAwUAAAGwCAYAAAD4/cmWAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAAX+VJREFUeJzt3Xd0VNXax/HfJJBOAgHSIIQSWuhGwNAhkVBEUbiKciEoRaUooKhYqCqKUkSaFURFRCkqKi00BUSKSK+igBAiIgQCBEj2+4cr52VIAhnuJAHm+1lr1srZZ5+9n1NnnpxmM8YYAQAAAHBZbgUdAAAAAICCRVIAAAAAuDiSAgAAAMDFkRQAAAAALo6kAAAAAHBxJAUAAACAiyMpAAAAAFwcSQEAAADg4kgKAAAAABdHUoAbyrBhw2Sz2fKlr2bNmqlZs2bW8IoVK2Sz2fTll1/mS//dunVT2bJl86Wv3Mic/xUrVjilvd9//102m03Tp093Snu4MUyfPl02m02///57QYfisrJbB1cezwBHXbkNcQx3PSQFyDOZX1yZHy8vL4WFhSk+Pl4TJkzQ6dOnndLPkSNHNGzYMG3evNkp7TnTjRwbcDWvvvqq5s+fX9BhALjFTZ48mcTjBkFSgDw3YsQIffzxx5oyZYr69esnSerfv79q1KihLVu22NV98cUXde7cOYfaP3LkiIYPH+7wD+/Fixdr8eLFDk3jqKvF9t5772n37t152j9wvXJKCrp06aJz584pIiIi/4MCkG8iIiJ07tw5denSJU/7ISm4cRQq6ABw62vdurVuv/12a3jw4MFatmyZ7rrrLt19993auXOnvL29JUmFChVSoUJ5u1mePXtWPj4+8vDwyNN+rqVw4cIF2j9wPdzd3eXu7l7QYQC4zKVLl5SRkeHU77XMM/xwHZwpQIFo0aKFXnrpJf3xxx/65JNPrPLs7ilYsmSJGjVqpKJFi8rPz0+VK1fW888/L+nf6+Dr1q0rSXr44YetS5Uy/+vQrFkzVa9eXRs3blSTJk3k4+NjTZvTNbjp6el6/vnnFRISIl9fX9199906dOiQXZ2yZcuqW7duWaa9vM1rxZbdPQWpqal66qmnFB4eLk9PT1WuXFlvvvmmjDF29Ww2m/r27av58+erevXq8vT0VLVq1bRw4cLsF/gVDh8+rPbt28vX11dBQUEaMGCA0tLSsq27bt06tWrVSgEBAfLx8VHTpk21evXqXPVzpS1btqhbt24qX768vLy8FBISokceeUR///13rqY/f/68hg0bpkqVKsnLy0uhoaG67777tH//fquOs5fh6dOn1b9/f5UtW1aenp4KCgrSnXfeqU2bNtnV++KLLxQdHS1vb2+VKFFC//3vf/Xnn3/a1enWrZv8/Px08OBB3XXXXfLz81OpUqU0adIkSdLWrVvVokUL+fr6KiIiQjNnzrSbPvOSvB9//FFPPPGESpYsqaJFi+rRRx/VhQsXdPLkSXXt2lXFihVTsWLF9Mwzz2SZ7zfffFMNGjRQ8eLF5e3trejo6Cz30dhsNqWmpuqjjz6yttvM7T2newomT56satWqydPTU2FhYerTp49OnjxpVydzf9yxY4eaN28uHx8flSpVSqNHj1ZuZK6zL774QlFRUfL29lZMTIy2bt0qSXrnnXcUGRkpLy8vNWvWLNv7HnKzPWceh/bt26du3bqpaNGiCggI0MMPP6yzZ8/a1b3a8elqyyune3icub+lpaVp6NChioyMlKenp8LDw/XMM8/kuK9fLnNdbdmyRU2bNpWPj48iIyOtbWXlypWqX7++vL29VblyZS1dujRLG7/88otat24tf39/+fn5KTY2Vj/99JM1/rfffpPNZtO4ceOyTLtmzRrZbDZ99tlnVtmff/6pRx55RMHBwdb++uGHH9pNl7lcZ8+erVdeeUWlS5eWl5eXYmNjtW/fvmznMTfbozOW5caNG9WgQQN5e3urXLlymjp1ql29CxcuaMiQIYqOjlZAQIB8fX3VuHFjLV++3K5e5rX+b775psaPH68KFSrI09NTO3bsuGocn3zyierVqycfHx8VK1ZMTZo0uerZ8pzuKdi1a5c6duyowMBAeXl56fbbb9fXX39tVydzu1+9erUGDhyokiVLytfXV/fee6/++usvq17ZsmW1fft2rVy50jrWcG9MATJAHpk2bZqRZNavX5/t+EOHDhlJpmPHjlbZ0KFDzeWb5bZt24yHh4e5/fbbzVtvvWWmTp1qnn76adOkSRNjjDFJSUlmxIgRRpLp1auX+fjjj83HH39s9u/fb4wxpmnTpiYkJMSULFnS9OvXz7zzzjtm/vz51rimTZtafS1fvtxIMjVq1DA1a9Y0Y8eONc8995zx8vIylSpVMmfPnrXqRkREmISEhCzzdHmb14otISHBREREWNNmZGSYFi1aGJvNZnr06GEmTpxo2rVrZySZ/v372/UjydSqVcuEhoaakSNHmvHjx5vy5csbHx8fc/z48auul7Nnz5pKlSoZLy8v88wzz5jx48eb6OhoU7NmTSPJLF++3KqbmJhoPDw8TExMjBkzZowZN26cqVmzpvHw8DDr1q27aj8HDhwwksy0adOssjfffNM0btzYjBgxwrz77rvmySefNN7e3qZevXomIyPjqu1dunTJxMbGGkmmU6dOZuLEiWbUqFGmRYsW1jrNi2X40EMPGQ8PDzNw4EDz/vvvm9dff920a9fOfPLJJ1adzG29bt26Zty4cea5554z3t7epmzZsuaff/6x6iUkJBgvLy8TFRVlHnvsMTNp0iTToEEDazmFhYWZQYMGmbfffttUq1bNuLu7m99++y1LP7Vr1zatWrUykyZNMl26dDGSzDPPPGMaNWpkHnroITN58mRz1113GUnmo48+spvv0qVLm969e5uJEyeasWPHmnr16hlJZsGCBVadjz/+2Hh6eprGjRtb2+2aNWvsYjhw4IBVP3O/jYuLM2+//bbp27evcXd3N3Xr1jUXLlyw6jVt2tSEhYWZ8PBw8+STT5rJkyebFi1aGEnmu+++u+r6z1xnNWvWNOHh4ea1114zr732mgkICDBlypQxEydONFFRUWbMmDHmxRdfNB4eHqZ58+Z20+d2e86cnzp16pj77rvPTJ482fTo0cNazpmudXzKaXkZ8//Hm+vZ37Jr88rjWXp6umnZsqXx8fEx/fv3N++8847p27evKVSokLnnnnuuuawvX1eZ22RUVJRxd3c3s2bNMiEhIWbYsGFm/PjxplSpUiYgIMCkpKTYLRtfX19r/3rttddMuXLljKenp/npp5+seg0bNjTR0dFZ+u/du7cpUqSISU1NNcb8ezwtXbq0CQ8PNyNGjDBTpkwxd999t5Fkxo0bl2W51qlTx0RHR5tx48aZYcOGGR8fH1OvXr0c5/Fq26OzlmVQUJDp27evmTBhgmnUqJGRZD744AOr3l9//WVCQ0PNwIEDzZQpU8zo0aNN5cqVTeHChc0vv/xi1cs8tkZFRZny5cub1157zYwbN8788ccfOcYwbNgwI8k0aNDAvPHGG+att94yDz30kHn22Wft4rx8G8ruGL5t2zYTEBBgoqKizOuvv24mTpxomjRpYmw2m5k7d65VL3MbrVOnjmnRooV5++23zVNPPWXc3d3N/fffb9WbN2+eKV26tKlSpYp1rFm8ePE1lynyBkkB8sy1kgJjjAkICDB16tSxhq9MCsaNG2ckmb/++ivHNtavX5/lwJWpadOmRpKZOnVqtuOySwpKlSpl9+U2e/ZsI8m89dZbVllukoJrxXZlUjB//nwjybz88st29Tp27GhsNpvZt2+fVSbJeHh42JX9+uuvRpJ5++23s/R1ufHjxxtJZvbs2VZZamqqiYyMtPuRkpGRYSpWrGji4+PtfrCfPXvWlCtXztx5551X7Se7L5TLE6tMn332mZFkVq1addX2PvzwQyPJjB07Nsu4zPjyYhkGBASYPn365BjXhQsXTFBQkKlevbo5d+6cVb5gwQIjyQwZMsQqS0hIMJLMq6++apX9888/xtvb29hsNjNr1iyrfNeuXUaSGTp0qFWWuU9duU5iYmKMzWYzjz32mFV26dIlU7p0abvt0Zis6+DChQumevXqpkWLFnblvr6+2W7jV/4gTU5ONh4eHqZly5YmPT3dqjdx4kQjyXz44YdWWeb+OGPGDKssLS3NhISEmA4dOmTp60qSjKenp92P4XfeecdIMiEhIXb77eDBg+3idGR7zjwOPfLII3b933vvvaZ48eLWcG6OT7lNChyJLzdJwccff2zc3NzMDz/8YNfv1KlTjSSzevXqHGPObE+SmTlzplWWuU26ubnZ/bBftGhRln29ffv2xsPDw/oniDHGHDlyxBQpUsQuacpcfzt37rTKLly4YEqUKGG3/XXv3t2EhoZm+adHp06dTEBAgLVdZy7XqlWrmrS0NKveW2+9ZSSZrVu3ZpnHa22PzlqWY8aMseundu3aJigoyEqcL126ZBezMf8eH4KDg+22xcxjq7+/v0lOTr5q38YYs3fvXuPm5mbuvfdeu33UGGO3reUmKYiNjTU1atQw58+ft2ujQYMGpmLFilZZ5jYaFxdn18eAAQOMu7u7OXnypFVWrVq1LMcpFAwuH0KB8vPzu+pTiIoWLSpJ+uqrr5SRkXFdfXh6eurhhx/Odf2uXbuqSJEi1nDHjh0VGhqq77777rr6z63vvvtO7u7ueuKJJ+zKn3rqKRlj9P3339uVx8XFqUKFCtZwzZo15e/vr99+++2a/YSGhqpjx45WmY+Pj3r16mVXb/Pmzdq7d68eeugh/f333zp+/LiOHz+u1NRUxcbGatWqVQ6vk8x7R6R/LwU6fvy47rjjDknKcjnOlebMmaMSJUpYN6tfLvOSs7xYhkWLFtW6det05MiRbOPasGGDkpOT1bt3b7vrb9u2basqVaro22+/zTJNjx497NqvXLmyfH19df/991vllStXVtGiRbNdn927d7e7zK5+/foyxqh79+5Wmbu7u26//fYs01++Dv755x+dOnVKjRs3vubyz8nSpUt14cIF9e/fX25u//+V0rNnT/n7+2eZfz8/P/33v/+1hj08PFSvXr1rbreZYmNj7S67q1+/viSpQ4cOdvttZnlmu9ezPT/22GN2w40bN9bff/+tlJQUSc45PmVy9v72xRdfqGrVqqpSpYrV1vHjx9WiRQtJynJJSnb8/PzUqVMnazhzm6xataq1fKWsyzo9PV2LFy9W+/btVb58eateaGioHnroIf3444/WMrz//vvl5eWlTz/91Kq3aNEiHT9+3NpOjDGaM2eO2rVrJ2OM3fzEx8fr1KlTWbbfhx9+2O76+saNG9vFePk8Xmt7dMayLFSokB599FG7fh599FElJydr48aNkv7dZzNjzsjI0IkTJ3Tp0iXdfvvt2e6fHTp0UMmSJa/Z9/z585WRkaEhQ4bY7aOSslyuezUnTpzQsmXLdP/99+v06dPWcvj7778VHx+vvXv3ZrlkslevXnZ9NG7cWOnp6frjjz9y3S/yDzcao0CdOXNGQUFBOY5/4IEH9P7776tHjx567rnnFBsbq/vuu08dO3bMcnDLSalSpRy6+apixYp2wzabTZGRkXn+XPY//vhDYWFhdj9sJKlq1arW+MuVKVMmSxvFihXTP//8c81+IiMjs3wZVK5c2W547969kqSEhIQc2zp16pSKFSt21f4ud+LECQ0fPlyzZs1ScnJylrauZv/+/apcufJVb0TPi2U4evRoJSQkKDw8XNHR0WrTpo26du1q/djJbPPK5SdJVapU0Y8//mhX5uXlleWLPCAgQKVLl86yTgICArJdn1fGHRAQIEkKDw+/5vQLFizQyy+/rM2bN9tdD3297wfJaf49PDxUvnz5LMs8u/ksVqxYlieR5cSReZdkzf/1bM9X9pU57p9//pG/v79Tjk+ZnL2/7d27Vzt37szxR+OV+192ctomr7Ws//rrL509ezbbfaJq1arKyMjQoUOHVK1aNRUtWlTt2rXTzJkzNXLkSEnSp59+qlKlSlk/uv/66y+dPHlS7777rt59991czc/V1t215vHK7dEZyzIsLEy+vr52ZZUqVZL077X7mf8c+eijjzRmzBjt2rVLFy9etOqWK1cuS5vZlWVn//79cnNzU1RUVK7q52Tfvn0yxuill17SSy+9lG2d5ORklSpVyhrO7XrAjYGkAAXm8OHDOnXqlCIjI3Os4+3trVWrVmn58uX69ttvtXDhQn3++edq0aKFFi9enKunoFz+n1FnyekHVHp6er49mSWnfswVN5Zer8z/Sr7xxhuqXbt2tnX8/PwcavP+++/XmjVrNGjQINWuXVt+fn7KyMhQq1at/uf/tF6P3CzD+++/X40bN9a8efO0ePFivfHGG3r99dc1d+5ctW7d2ml9OrI+HWnj8ul/+OEH3X333WrSpIkmT56s0NBQFS5cWNOmTctyU3Ne+V+32+tdftezPV+rzdwcn652rLics/e3jIwM1ahRQ2PHjs12/JU/7LPjjG01N7p27aovvvhCa9asUY0aNfT111+rd+/eVmKVuWz++9//5pg01axZ87pizE09ZyzL3Pjkk0/UrVs3tW/fXoMGDVJQUJDc3d01atQou4cpZMqL77aryVwPTz/9tOLj47Otc+X3eV5/T8G5SApQYD7++GNJyvHgksnNzU2xsbGKjY3V2LFj9eqrr+qFF17Q8uXLFRcX5/Q3IGf+xy6TMUb79u2z+9IpVqxYlierSP/+1/Ty0+WOxBYREaGlS5fq9OnTdv/p3rVrlzXeGSIiIrRt2zYZY+ziu/KdCZmX1fj7+ysuLu5/7veff/5RYmKihg8friFDhljlVy7vnFSoUEHr1q3TxYsXc3yca14tw9DQUPXu3Vu9e/dWcnKybrvtNr3yyitq3bq11ebu3but/2xm2r179w31PP85c+bIy8tLixYtkqenp1U+bdq0LHVzu+1ePv+Xb/sXLlzQgQMHnLLtOIOzt+dM1zo+Zf5n9MrjxZVnUJwdX4UKFfTrr78qNjY2394Sn6lkyZLy8fHJ9j0su3btkpubm90P6VatWqlkyZL69NNPVb9+fZ09e9bu2fglS5ZUkSJFlJ6eXiDbkzOW5ZEjR5Sammp3tmDPnj2SZF0O9+WXX6p8+fKaO3euXT9Dhw69/uD1b/wZGRnasWNHjglnbmTu34ULF3bqesjv7RM5454CFIhly5Zp5MiRKleunDp37pxjvRMnTmQpyzyoZV76kHmQze5H+vWYMWOG3X0OX375pY4ePWr3X+EKFSrop59+0oULF6yyBQsWZHl0qSOxtWnTRunp6Zo4caJd+bhx42Sz2a7rv9I59XPkyBG7x1CePXs2y2n56OhoVahQQW+++abOnDmTpZ3LHyuXG5n/MbryP0Tjx4/P1fQdOnTQ8ePHsyyfy9t09jJMT0/PcllTUFCQwsLCrO3v9ttvV1BQkKZOnWp3Oc7333+vnTt3qm3btg71mZcy/3N9+X+pf//992xfUubr65ur7TYuLk4eHh6aMGGC3br94IMPdOrUqRtm/p29PUu5Oz5l/thftWqVVSc9PT3P97f7779ff/75p957770s486dO6fU1FSH2nOEu7u7WrZsqa+++srusstjx45p5syZatSokfz9/a3yQoUK6cEHH9Ts2bM1ffp01ahRw+6fMO7u7urQoYPmzJmjbdu2ZenvetadI5yxLC9duqR33nnHGr5w4YLeeecdlSxZUtHR0ZKyP0auW7dOa9eu/Z/ib9++vdzc3DRixIgsZ2Qd+Y99UFCQmjVrpnfeeUdHjx7NMv5610NujzXIe5wpQJ77/vvvtWvXLl26dEnHjh3TsmXLtGTJEkVEROjrr7++6stRRowYoVWrVqlt27aKiIhQcnKyJk+erNKlS6tRo0aS/v3SLVq0qKZOnaoiRYrI19dX9evXz/X1llcKDAxUo0aN9PDDD+vYsWMaP368IiMj1bNnT6tOjx499OWXX6pVq1a6//77tX//fn3yySd2N606Glu7du3UvHlzvfDCC/r9999Vq1YtLV68WF999ZX69++fpe3r1bNnT02cOFFdu3bVxo0bFRoaqo8//lg+Pj529dzc3PT++++rdevWqlatmh5++GGVKlVKf/75p5YvXy5/f3998803ue7X399fTZo00ejRo3Xx4kWVKlVKixcv1oEDB3I1fdeuXTVjxgwNHDhQP//8sxo3bqzU1FQtXbpUvXv31j333OP0ZXj69GmVLl1aHTt2VK1ateTn56elS5dq/fr1GjNmjKR//2v2+uuv6+GHH1bTpk314IMP6tixY3rrrbdUtmxZDRgwwKE+81Lbtm01duxYtWrVSg899JCSk5M1adIk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9XrrYZ06zhij06dPKyws7Jp1b/qkwMPDQ9HR0UpMTFT79u0l/fsjPzExUX379s12Gk9PT3l6etqVFS1aNI8jvTp/f3828FsQ6/XWxHq9NbFeb02s11sP69Qx1zpDkOmmTwokaeDAgUpISNDtt9+uevXqafz48UpNTbWeRgQAAAAgZ7dEUvDAAw/or7/+0pAhQ5SUlKTatWtr4cKFWW4+BgAAAJDVLZEUSFLfvn1zvFzoRubp6amhQ4dmuZwJNzfW662J9XprYr3emlivtx7Wad6ymdw8owgAAADALYuHLwMAAAAujqQAAAAAcHEkBQAAAICLIykAAAAAXBxJQT6YNGmSypYtKy8vL9WvX18///zzVet/8cUXqlKliry8vFSjRg199913+RQpHOHIep0+fbpsNpvdx8vLKx+jRW6sWrVK7dq1U1hYmGw2m+bPn3/NaVasWKHbbrtNnp6eioyM1PTp0/M8TuSeo+t0xYoVWfZVm82mpKSk/AkYuTJq1CjVrVtXRYoUUVBQkNq3b6/du3dfczq+X29c17NO+W51LpKCPPb5559r4MCBGjp0qDZt2qRatWopPj5eycnJ2dZfs2aNHnzwQXXv3l2//PKL2rdvr/bt22vbtm35HDmuxtH1Kv37BsajR49anz/++CMfI0ZupKamqlatWpo0aVKu6h84cEBt27ZV8+bNtXnzZvXv3189evTQokWL8jhS5Jaj6zTT7t277fbXoKCgPIoQ12PlypXq06ePfvrpJy1ZskQXL15Uy5YtlZqamuM0fL/e2K5nnUp8tzqVQZ6qV6+e6dOnjzWcnp5uwsLCzKhRo7Ktf//995u2bdvaldWvX988+uijeRonHOPoep02bZoJCAjIp+jgDJLMvHnzrlrnmWeeMdWqVbMre+CBB0x8fHweRobrlZt1unz5ciPJ/PPPP/kSE5wjOTnZSDIrV67MsQ7frzeX3KxTvludizMFeejChQvauHGj4uLirDI3NzfFxcVp7dq12U6zdu1au/qSFB8fn2N95L/rWa+SdObMGUVERCg8PFz33HOPtm/fnh/hIg+xv966ateurdDQUN15551avXp1QYeDazh16pQkKTAwMMc67K83l9ysU4nvVmciKchDx48fV3p6uoKDg+3Kg4ODc7w+NSkpyaH6yH/Xs14rV66sDz/8UF999ZU++eQTZWRkqEGDBjp8+HB+hIw8ktP+mpKSonPnzhVQVPhfhIaGaurUqZozZ47mzJmj8PBwNWvWTJs2bSro0JCDjIwM9e/fXw0bNlT16tVzrMf3680jt+uU71bnKlTQAQCuICYmRjExMdZwgwYNVLVqVb3zzjsaOXJkAUYG4HKVK1dW5cqVreEGDRpo//79GjdunD7++OMCjAw56dOnj7Zt26Yff/yxoEOBk+R2nfLd6lycKchDJUqUkLu7u44dO2ZXfuzYMYWEhGQ7TUhIiEP1kf+uZ71eqXDhwqpTp4727duXFyEin+S0v/r7+8vb27uAooKz1atXj331BtW3b18tWLBAy5cvV+nSpa9al+/Xm4Mj6/RKfLf+b0gK8pCHh4eio6OVmJholWVkZCgxMdEus71cTEyMXX1JWrJkSY71kf+uZ71eKT09XVu3blVoaGhehYl8wP7qGjZv3sy+eoMxxqhv376aN2+eli1bpnLlyl1zGvbXG9v1rNMr8d36PyroO51vdbNmzTKenp5m+vTpZseOHaZXr16maNGiJikpyRhjTJcuXcxzzz1n1V+9erUpVKiQefPNN83OnTvN0KFDTeHChc3WrVsLahaQDUfX6/Dhw82iRYvM/v37zcaNG02nTp2Ml5eX2b59e0HNArJx+vRp88svv5hffvnFSDJjx441v/zyi/njjz+MMcY899xzpkuXLlb93377zfj4+JhBgwaZnTt3mkmTJhl3d3ezcOHCgpoFXMHRdTpu3Dgzf/58s3fvXrN161bz5JNPGjc3N7N06dKCmgVk4/HHHzcBAQFmxYoV5ujRo9bn7NmzVh2+X28u17NO+W51LpKCfPD222+bMmXKGA8PD1OvXj3z008/WeOaNm1qEhIS7OrPnj3bVKpUyXh4eJhq1aqZb7/9Np8jRm44sl779+9v1Q0ODjZt2rQxmzZtKoCocTWZj6O88pO5LhMSEkzTpk2zTFO7dm3j4eFhypcvb6ZNm5bvcSNnjq7T119/3VSoUMF4eXmZwMBA06xZM7Ns2bKCCR45ym6dSrLb//h+vblczzrlu9W5bMYYk3/nJQAAAADcaLinAAAAAHBxJAUAAACAiyMpAAAAAFwcSQEAAADg4kgKAAAAABdHUgAAAAC4OJICAAAAwMWRFABAAbHZbJo/f35Bh3HTmj59uooWLWoNDxs2TLVr1y6weG5lFy5cUGRkpNasWSNJ+v3332Wz2bR582an9rNixQrZbDadPHkyxzpTp05Vu3btnNovUJBWrVqldu3aKSws7Lq+F4YNGyabzZbl4+vr61A7JAUAHJKUlKR+/fqpfPny8vT0VHh4uNq1a6fExMSCDu2GldOP1aNHj6p169b5HxDgoKlTp6pcuXJq0KBBrup36tRJrVq1sitbuHChbDabhg0bZlc+bNgwlSlTJtexPPLII9q0aZN++OGHXE8D3MhSU1NVq1YtTZo06bqmf/rpp3X06FG7T1RUlP7zn/841A5JAYBc+/333xUdHa1ly5bpjTfe0NatW7Vw4UI1b95cffr0KejwbjohISHy9PQs6DCAqzLGaOLEierevXuup2nevLlWr16tS5cuWWXLly9XeHi4VqxYYVd3+fLlat68ea7b9vDw0EMPPaQJEybkehrgRta6dWu9/PLLuvfee7Mdn5aWpqefflqlSpWSr6+v6tevb7cf+fn5KSQkxPocO3ZMO3bscGiflUgKADigd+/estls+vnnn9WhQwdVqlRJ1apV08CBA/XTTz9Z9Q4ePKh77rlHfn5+8vf31/33369jx45Z4zP/c/7xxx+rbNmyCggIUKdOnXT69GmrzpdffqkaNWrI29tbxYsXV1xcnFJTUyVJGRkZGjFihEqXLi1PT0/Vrl1bCxcutKbNvLRh9uzZaty4sby9vVW3bl3t2bNH69ev1+233y4/Pz+1bt1af/31lzVdt27d1L59e7366qsKDg5W0aJFNWLECF26dEmDBg1SYGCgSpcurWnTptktl2effVaVKlWSj4+Pypcvr5deekkXL16U9O8lLsOHD9evv/5qndKdPn26pKyXD23dulUtWrSw5rlXr146c+ZMlvjefPNNhYaGqnjx4urTp4/VV3Yyl/WHH36oMmXKyM/PT71791Z6erpGjx6tkJAQBQUF6ZVXXrGb7uTJk+rRo4dKliwpf39/tWjRQr/++qvT1mGzZs3Uv39/uz7bt2+vbt26WcPX+iLMjffff19Vq1aVl5eXqlSposmTJ1+1frNmzdSvXz/1799fxYoVU3BwsN577z2lpqbq4YcfVpEiRRQZGanvv//ebrqVK1eqXr168vT0VGhoqJ577jnrB/GMGTNUvHhxpaWlZZnfLl26WMNfffWVbrvtNnl5eal8+fIaPny43Y9qm82m999/X/fe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PXko0/79+3X+/HnVqVPH4WmBG82ZM2e0b98+a/jAgQPavHmzAgMDValSJXXu3Fldu3bVmDFjVKdOHf31119KTExUzZo17R4A8OGHHyo0NPS6H3XN04cA5NqkSZOUnp6uevXqac6cOdq7d6927typCRMmKCYmRpIUFxenGjVqqHPnztq0aZN+/vlnde3aVU2bNtXtt9+eq37WrVunV199VRs2bNDBgwc1d+5c/fXXX6pataokadCgQXr99df1+eefa/fu3Xruuee0efNmPfnkk3k27zmpWLGiDh48qFmzZmn//v2aMGGC5s2bZ1enbNmy1kH++PHjWZ5CI0mdO3eWl5eXEhIStG3bNi1fvlz9+vVTly5drus08P8iLi5OMTExat++vRYvXqzff/9da9as0QsvvGD3NJmrudY6bNGihb799lt9++232rVrlx5//HG7F1Zd/kU4d+5cHThwQD///LNGjRpl9x/2qxk+fLhGjRqlCRMmaM+ePdq6daumTZumsWPHOrxMrqZ37946dOiQ+vXrp127dumrr77S0KFDNXDgQLtLtB566CEdPnxY7733nh555BG7NoYMGaIZM2Zo+PDh2r59u3bu3KlZs2bpxRdfdGqs19NP5pOEtm/fnu34fv366eWXX9Zdd92lH3/8Mcu0kydPVmRkpN123LRpU7399tuqVKmSwsLCHJ6PH374QeXLl1eFChUcnha40WzYsEF16tSxktyBAweqTp06GjJkiCRp2rRp6tq1q5566ilVrlxZ7du31/r16+0up8vIyLAuS7zW2dCckBQAyLXy5ctr06ZNat68uZ566ilVr15dd955pxITEzVlyhRJ//53+KuvvlKxYsXUpEkTxcXFqXz58vr8889z3Y+/v79WrVqlNm3aqFKlSnrxxRc1ZswY678fTzzxhAYOHKinnnpKNWrU0MKFC/X111+rYsWKeTLfV3P33XdrwIAB6tu3r2rXrq01a9bopZdesqvToUMHtWrVSs2bN1fJkiX12WefZWnHx8dHixYt0okTJ1S3bl117NhRsbGxmjhxYn7NisVms+m7775TkyZN9PDDD6tSpUrq1KmT/vjjj1wnKNdah4888ogSEhKshLF8+fJZ/mOcmy/Cq+nRo4fef/99TZs2TTVq1FDTpk01ffp0lStXzrEFcg2lSpXSd999p59//lm1atXSY489pu7du2f5oR0QEKAOHTrIz89P7du3txsXHx+vBQsWaPHixapbt67uuOMOjRs3ThEREU6N9Xr6KV68uO699159+umnOdbp37+/hg8frjZt2lhvPZb+TQpOnz5t3U+QqWnTpjp9+vR1nSWQpM8++8zuXhHgZtasWTOZfx/+Y/fJfHx14cKFNXz4cB04cEAXLlzQkSNHNHfuXLt7ndzc3HTo0KEsj5d2hM3k9u5BAADwP4mNjVW1atVuuhdvbdmyRXfeeaf279+fq3s68tL27dvVokUL7dmz55o3jQPIPZICAADy2D///KMVK1aoY8eO2rFjR77fJ+IM06dPV3R0tN1/JwvC0qVLlZ6ervj4+AKNA7jVkBQAAJDHypYtq3/++UcvvfSSnn766YIOBwCyICkAAAAAXBw3GgMAAAAujqQAAAAAcHEkBQAAAICLIykAAAAAXBxJAQAAAODiSAoAAAAAF0dSAAAAALg4kgIAAADAxZEUAAAAAC7u/wCViBkIONMpLQAAAABJRU5ErkJggg==",
|
||
"text/plain": [
|
||
"<Figure size 900x450 with 1 Axes>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
},
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"count 3.700000e+02\n",
|
||
"mean 3.782134e+05\n",
|
||
"std 1.728977e+06\n",
|
||
"min 5.853957e+02\n",
|
||
"25% 2.997984e+04\n",
|
||
"50% 7.634961e+04\n",
|
||
"75% 2.160856e+05\n",
|
||
"max 2.691197e+07\n",
|
||
"dtype: float64\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"average_monthly_consumption = df_matrix.mean(axis=1)\n",
|
||
"\n",
|
||
"fig, ax = plt.subplots()\n",
|
||
"average_monthly_consumption.plot(kind=\"hist\", bins=30, ax=ax, color=\"#0F6E56\", edgecolor=\"white\")\n",
|
||
"ax.set_xlabel(\"Consommation mensuelle moyenne (kWh)\")\n",
|
||
"ax.set_ylabel(\"Nombre de clients\")\n",
|
||
"ax.set_title(\"Distribution de la consommation mensuelle moyenne par client\")\n",
|
||
"plt.show()\n",
|
||
"\n",
|
||
"print(average_monthly_consumption.describe())\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "3761226f",
|
||
"metadata": {},
|
||
"source": [
|
||
"**À observer :** sur le jeu de données réel du projet (370 clients), cette distribution est très étalée vers\n",
|
||
"la droite — quelques clients consomment énormément plus que la médiane. Sur les vraies données, le client le plus\n",
|
||
"gros consommateur affiche une consommation mensuelle moyenne environ **350 fois supérieure** à celle du client\n",
|
||
"médian.\n",
|
||
"\n",
|
||
"C'est exactement ce type de client \"extrême\" que l'on doit repérer avant d'entraîner un modèle : sans traitement,\n",
|
||
"il fausserait toutes les analyses statistiques.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "78771409",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Partie 7 — Détecter les clients atypiques\n",
|
||
"\n",
|
||
"### Le problème concret\n",
|
||
"\n",
|
||
"Certains clients ont un profil de consommation très différent du reste de la population : soit parce qu'ils sont\n",
|
||
"inactifs une partie de l'année (consommation nulle certains mois), soit parce que leur consommation est\n",
|
||
"extrêmement élevée par rapport aux autres. Ces clients doivent être identifiés avant d'aller plus loin dans le\n",
|
||
"projet.\n",
|
||
"\n",
|
||
"### Les deux règles\n",
|
||
"\n",
|
||
"1. **Client inactif** : on exclut tout client ayant eu **au moins un mois à consommation nulle**.\n",
|
||
"2. **Client extrême** : on exclut tout client dont la consommation mensuelle **moyenne** dépasse\n",
|
||
" **1 000 000 kWh/mois**.\n",
|
||
"\n",
|
||
"### À vous de jouer\n",
|
||
"\n",
|
||
"Complétez les trois lignes ci-dessous :\n",
|
||
"- `nbr_zero_consumption` : pour chaque client (chaque ligne de `df_matrix`), comptez le nombre de mois à 0.\n",
|
||
"- `average_monthly_consumption` : déjà calculé plus haut, réutilisez-le.\n",
|
||
"- `is_kept` : `True` si le client respecte les deux règles, `False` sinon.\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 13,
|
||
"id": "fd9ec023",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:35.453401Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:35.453282Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:35.461414Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:35.460613Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"Clients avant filtrage : 370\n",
|
||
"Clients après filtrage : 143\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# TODO : complète les lignes marquées ...\n",
|
||
"nbr_zero_consumption = (df_matrix == 0).sum(axis=1) # nombre de mois à 0, par client — fourni\n",
|
||
"average_monthly_consumption = df_matrix.mean(axis=1) # déjà calculé plus haut\n",
|
||
"\n",
|
||
"rule_1_ok = nbr_zero_consumption == 0 # TODO : condition \"aucun mois à 0\" (indice : opérateur de comparaison)\n",
|
||
"rule_2_ok = average_monthly_consumption <= 1_000_000 # TODO : condition \"moyenne <= 1 000 000\"\n",
|
||
"\n",
|
||
"is_kept = rule_1_ok & rule_2_ok\n",
|
||
"df_filtered = df_matrix[is_kept]\n",
|
||
"\n",
|
||
"print(f\"Clients avant filtrage : {len(df_matrix)}\")\n",
|
||
"print(f\"Clients après filtrage : {len(df_filtered)}\")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 14,
|
||
"id": "9ff3cb01",
|
||
"metadata": {
|
||
"execution": {
|
||
"iopub.execute_input": "2026-07-20T18:18:35.464519Z",
|
||
"iopub.status.busy": "2026-07-20T18:18:35.464406Z",
|
||
"iopub.status.idle": "2026-07-20T18:18:35.468159Z",
|
||
"shell.execute_reply": "2026-07-20T18:18:35.467194Z"
|
||
}
|
||
},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"✅ Les règles produisent bien des valeurs booléennes (Vrai / Faux).\n",
|
||
"✅ Le filtrage a bien réduit ou conservé le nombre de clients.\n",
|
||
"✅ Il reste des clients après filtrage — le résultat est exploitable.\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"# Validation\n",
|
||
"check(\n",
|
||
" is_kept.dtype == bool,\n",
|
||
" \"Les règles produisent bien des valeurs booléennes (Vrai / Faux).\",\n",
|
||
" \"rule_1_ok et rule_2_ok doivent être des comparaisons (ex: nbr_zero_consumption <= 0).\",\n",
|
||
")\n",
|
||
"check(\n",
|
||
" len(df_filtered) <= len(df_matrix),\n",
|
||
" \"Le filtrage a bien réduit ou conservé le nombre de clients.\",\n",
|
||
" \"Le nombre de clients après filtrage ne devrait jamais augmenter — vérifie tes conditions.\",\n",
|
||
")\n",
|
||
"check(\n",
|
||
" df_filtered.shape[0] > 0,\n",
|
||
" \"Il reste des clients après filtrage — le résultat est exploitable.\",\n",
|
||
" \"Aucun client ne passe le filtrage : vérifie que tes conditions ne sont pas inversées.\",\n",
|
||
")\n"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "4dcd9c00",
|
||
"metadata": {},
|
||
"source": [
|
||
"---\n",
|
||
"## Bilan\n",
|
||
"\n",
|
||
"Vous venez de reproduire, à la main, les deux premières étapes du pipeline de préparation des données du projet\n",
|
||
"fil rouge :\n",
|
||
"\n",
|
||
"| Étape que vous avez faite | Équivalent dans le vrai pipeline |\n",
|
||
"|---|---|\n",
|
||
"| Format large → format long, conversion kWh, agrégation mensuelle | module `dataset_preparation` |\n",
|
||
"| Détection et filtrage des clients atypiques | module `outlier_detection` |\n",
|
||
"\n",
|
||
"### Ce qu'il faut retenir\n",
|
||
"\n",
|
||
"- Les données brutes ne sont presque jamais directement utilisables : il faut toujours une étape de préparation.\n",
|
||
"- Une conversion d'unité mal comprise (ici kW → kWh) peut fausser silencieusement toute une analyse.\n",
|
||
"- Quelques valeurs extrêmes peuvent dominer une analyse statistique si elles ne sont pas traitées.\n",
|
||
"\n",
|
||
"### Et ensuite ?\n",
|
||
"\n",
|
||
"Vous avez fait tourner ce traitement **une seule fois, à la main, dans ce notebook**. Mais si les données changent\n",
|
||
"demain, ou si un autre membre de l'équipe doit reproduire exactement le même résultat : comment être sûr d'obtenir\n",
|
||
"le même résultat, avec la même version du code et des données ?\n",
|
||
"\n",
|
||
"C'est précisément le sujet du **Module 2** : versionner le code (Git), versionner les données (DVC), et suivre les\n",
|
||
"expériences (MLflow) pour rendre tout ce travail reproductible.\n",
|
||
"\n",
|
||
"## Critères de réussite de ce TP\n",
|
||
"\n",
|
||
"- [ ] Toutes les cellules de validation affichent ✅\n",
|
||
"- [ ] Je peux expliquer, avec mes propres mots, la différence entre format large et format long\n",
|
||
"- [ ] Je peux expliquer pourquoi on divise par 4 pour convertir en kWh\n",
|
||
"- [ ] Je peux citer les deux règles utilisées pour détecter un client atypique\n"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": "Python 3 (ipykernel)",
|
||
"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.14.5"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|