Files
ENI-ml-mlops/tp_module1_preparation_donnees.ipynb

1378 lines
78 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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
}