{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":7391,"databundleVersionId":44328,"sourceType":"competition"}],"dockerImageVersionId":31234,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"Bibliothèques nécessaires","metadata":{}},{"cell_type":"markdown","source":"## Inventaires des fichiers","metadata":{}},{"cell_type":"code","source":"import os\n\nDATA_DIR = \"/kaggle/input/favorita-grocery-sales-forecasting\"\n\nprint(\"Fichiers disponibles :\")\nfor f in os.listdir(DATA_DIR):\n    print(f)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:42:00.627469Z","iopub.execute_input":"2026-01-08T11:42:00.627867Z","iopub.status.idle":"2026-01-08T11:42:00.652006Z","shell.execute_reply.started":"2026-01-08T11:42:00.627835Z","shell.execute_reply":"2026-01-08T11:42:00.650982Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Code de décompression des fichiers\n!! n'exécuter qu'une seule fois !!","metadata":{}},{"cell_type":"code","source":"import os\nimport subprocess\n\nEXTRACT_DIR = \"/kaggle/working/favorita_data\"\nos.makedirs(EXTRACT_DIR, exist_ok=True)\n\nfor file in os.listdir(DATA_DIR):\n    if file.endswith(\".7z\"):\n        archive_path = os.path.join(DATA_DIR, file)\n        print(f\"Décompression de {file} ...\")\n        subprocess.run(\n            [\"7z\", \"x\", archive_path, f\"-o{EXTRACT_DIR}\", \"-y\"],\n            stdout=subprocess.DEVNULL\n        )\n\nprint(\"\\nFichiers décompressés :\")\nprint(os.listdir(EXTRACT_DIR))\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:42:07.268923Z","iopub.execute_input":"2026-01-08T11:42:07.269303Z","iopub.status.idle":"2026-01-08T11:43:14.530743Z","shell.execute_reply.started":"2026-01-08T11:42:07.269269Z","shell.execute_reply":"2026-01-08T11:43:14.529529Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Importations et configurations","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\n\npd.set_option(\"display.max_columns\", 50)\npd.set_option(\"display.float_format\", \"{:.2f}\".format)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:43:19.713333Z","iopub.execute_input":"2026-01-08T11:43:19.714115Z","iopub.status.idle":"2026-01-08T11:43:21.182379Z","shell.execute_reply.started":"2026-01-08T11:43:19.714076Z","shell.execute_reply":"2026-01-08T11:43:21.180977Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Chargement des données de la base train et de la base test","metadata":{}},{"cell_type":"code","source":"\ntrain = pd.read_csv(\n    f\"{EXTRACT_DIR}/train.csv\",\n    usecols=[\"date\", \"store_nbr\", \"item_nbr\", \"unit_sales\", \"onpromotion\"],\n    parse_dates=[\"date\"],\n    low_memory=False\n)\nprint(\"Base Train\")\nprint(train.shape)\ntrain.head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:43:24.967791Z","iopub.execute_input":"2026-01-08T11:43:24.968362Z","iopub.status.idle":"2026-01-08T11:45:04.859314Z","shell.execute_reply.started":"2026-01-08T11:43:24.968322Z","shell.execute_reply":"2026-01-08T11:45:04.858319Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test = pd.read_csv(\n    f\"{EXTRACT_DIR}/test.csv\",\n    usecols=[\"date\", \"store_nbr\", \"item_nbr\", \"onpromotion\"],\n    parse_dates=[\"date\"],\n    low_memory=False\n)\nprint(\"Base Test\")\nprint(test.shape)\ntest.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:47:55.277599Z","iopub.execute_input":"2026-01-08T11:47:55.277966Z","iopub.status.idle":"2026-01-08T11:47:57.644506Z","shell.execute_reply.started":"2026-01-08T11:47:55.277934Z","shell.execute_reply":"2026-01-08T11:47:57.643548Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Quelques vérifications de base","metadata":{}},{"cell_type":"code","source":"train.info()\ntrain.isna().mean() * 100","metadata":{"trusted":true,"execution":{"execution_failed":"2026-01-08T03:32:22.174Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"","metadata":{}},{"cell_type":"markdown","source":"## 1. Traitement de la variable cible (*unit_sales*)\n\nLa variable **`unit_sales`** représente le nombre d’unités vendues par produit et par jour.  \nElle constitue la **variable cible** de notre analyse et nécessite un traitement spécifique en raison de ses caractéristiques statistiques.","metadata":{}},{"cell_type":"markdown","source":"### 1.1 Analyse descriptive initiale\n\nUne exploration préliminaire révèle que la distribution de `unit_sales` est :\n- sans valeurs manquantes\n- comportant des **valeurs nulles** (jours sans vente).\n- marquée par la présence de **valeurs extrêmes** ;\n- **fortement asymétrique à droite** ;","metadata":{}},{"cell_type":"markdown","source":"### 1.2 Traitement des valeurs aberrantes\n\n\n> La variable **`unit_sales`** représente le nombre d’unités effectivement vendues par produit et par jour.  Lors de l’exploration des données, un nombre limité d’observations négatives a été identifié. Ces valeurs correspondent à des **retours de produits** et non à des ventes au sens strict.L’objectif principal de cette étude étant la **prédiction des ventes**, et non la modélisation des retours.Par ailleurs, la proportion de valeurs négatives est très faible au regard de la taille totale du jeu de données, ce qui limite leur impact statistique global.Dans ce contexte, les valeurs négatives de `unit_sales` sont assimilées à des **jours sans vente effective** et sont donc remplacées par zéro. Ce choix nous permet de conserver l’intégralité des observations temporelles et d’éviter l’introduction d’un signal bruité lié à des événements rares.\n\n\n\n","metadata":{}},{"cell_type":"code","source":"# ------------------------------------------------------------\n# Target variable preprocessing: handling negative unit_sales\n# ------------------------------------------------------------\n\nimport numpy as np\n\n# The variable `unit_sales` may contain negative values corresponding\n# to product returns. Since the objective is to predict actual sales,\n# these values are treated as days with zero effective sales.\n\n# Step 1: Replace negative values with zero\ntrain[\"unit_sales_clean\"] = train[\"unit_sales\"].clip(lower=0)\n\n# Quick sanity checks\nprint(\"Number of negative values after cleaning:\",\n      (train[\"unit_sales_clean\"] < 0).sum())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:48:09.869497Z","iopub.execute_input":"2026-01-08T11:48:09.869904Z","iopub.status.idle":"2026-01-08T11:48:11.189102Z","shell.execute_reply.started":"2026-01-08T11:48:09.869868Z","shell.execute_reply":"2026-01-08T11:48:11.188094Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n> Les valeurs élevées de `unit_sales` correspondent à des périodes de forte demande (pics de ventes) et ne sont pas considérées comme des erreurs de mesure.Elles sont donc conservées afin de ne pas biaiser la distribution et de préserver l’information utile pour la modélisation.\n","metadata":{}},{"cell_type":"code","source":"# Statistiques simples sur unit_sales\ntrain[\"unit_sales_clean\"].describe(percentiles=[0.95, 0.99])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:48:18.77367Z","iopub.execute_input":"2026-01-08T11:48:18.774079Z","iopub.status.idle":"2026-01-08T11:48:22.920757Z","shell.execute_reply.started":"2026-01-08T11:48:18.774049Z","shell.execute_reply":"2026-01-08T11:48:22.919829Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 1.3 Normalisation de la variable\n\nDans ce jeu de données, la variable cible `unit_sales` contient de nombreuses valeurs nulles (jours sans vente).\nOr, la transformation logarithmique classique pose un problème immédiat :\n\n- $\\log(0)$ n’est pas défini (cela produit $-\\infty$), ce qui empêche la préparation correcte des données\n  et l’entraînement du modèle.\n\nPour éviter cela, on utilise la fonction `log1p`, qui correspond à :\n\n$$\n\\text{log1p}(x) = \\log(1 + x)\n$$\n\nAinsi :\n- si $x = 0$, alors $\\log(1 + 0) = \\log(1) = 0$ \n- on peut donc transformer la variable cible **sans perdre** les observations à zéro.\n\nEn plus de gérer les zéros, `log1p` est une transformation standard en prévision des ventes car elle :\n- réduit l’asymétrie (forte queue à droite),\n- atténue l’influence des valeurs extrêmes,\n- stabilise la variance,\n- facilite l’apprentissage des modèles.\n\nEnfin, `log1p` est préférable à $\\log(1 + x)$ pour des raisons numériques :\nelle est implémentée de façon plus stable lorsque $x$ est petit.\n\n> **En résumé :** `log1p` permet d’appliquer une transformation logarithmique en présence de zéros,\n> tout en rendant la distribution de la variable cible plus adaptée aux modèles de prédiction.\n\n*(Inverse utile : si le modèle prédit $y = \\log(1 + x)$, alors $x = e^{y} - 1$,\nce qui correspond à `expm1` en Python.)*\n","metadata":{}},{"cell_type":"code","source":"import numpy as np\nimport matplotlib.pyplot as plt\n\n# --- 1) Sample (si pas déjà fait) ---\ntrain_sample = train.sample(2_000_000, random_state=42)\n\n# --- 2) Transformation (log transform) ---\ntrain_sample[\"unit_sales_log\"] = np.log1p(train_sample[\"unit_sales_clean\"])\n\n# --- 3) Cap à 200 pour mieux voir la distribution brute ---\n# (le log n'a pas besoin de cap, mais on peut capper aussi si tu veux comparer proprement)\ntrain_sample[\"unit_sales_clean_cap200\"] = train_sample[\"unit_sales_clean\"].clip(0, 200)\n\n# Option : log sur la version cappée à 200 (comparaison très visuelle)\ntrain_sample[\"unit_sales_log_cap200\"] = np.log1p(train_sample[\"unit_sales_clean_cap200\"])\n\n# --- 4) Deux histogrammes sur une seule figure ---\nfig, axes = plt.subplots(1, 2, figsize=(14, 5))\n\n# Avant : unit_sales cappée à 200\naxes[0].hist(train_sample[\"unit_sales_clean_cap200\"], bins=100)\naxes[0].set_title(\"Avant : unit_sales (négatives→0, cappée à 200)\")\naxes[0].set_xlabel(\"unit_sales_clean (cap 200)\")\naxes[0].set_ylabel(\"Fréquence\")\n\n# Après : log1p(unit_sales) (sur version cappée pour une comparaison cohérente)\naxes[1].hist(train_sample[\"unit_sales_log_cap200\"], bins=100)\naxes[1].set_title(\"Après : log(unit_sales) (cap 200)\")\naxes[1].set_xlabel(\"log1p(unit_sales_clean) (cap 200)\")\naxes[1].set_ylabel(\"Fréquence\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:48:38.090215Z","iopub.execute_input":"2026-01-08T11:48:38.090579Z","iopub.status.idle":"2026-01-08T11:48:46.077606Z","shell.execute_reply.started":"2026-01-08T11:48:38.090551Z","shell.execute_reply":"2026-01-08T11:48:46.076515Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n> Malgré la transformation logarithmique, la distribution de la variable cible ne suit pas une loi normale. Cela est attendu, car `unit_sales` est une variable de comptagefortement asymétrique et caractérisée par un grand nombre de zéros.L’objectif de la transformation logarithmique n’est donc pas d’imposer une normalité,mais de réduire l’asymétrie et l’influence des valeurs extrêmes afin de faciliter l’apprentissage des modèles de prédiction.\n","metadata":{}},{"cell_type":"markdown","source":"## 2. Traitement des variables explicatives (bases `train` et `test`)\n\nL’objectif est de préparer des variables explicatives cohérentes, informatives et utilisables par des modèles de régression, tout en garantissant :\n- une **cohérence parfaite entre train et test** (mêmes transformations, mêmes colonnes),\n- l’absence de **fuite d’information** (*data leakage*),\n- une préparation robuste face aux **valeurs manquantes** et aux **types de données**.\n\n#### Variables présentes dans `train` / `test`\n\nLes bases `train` et `test` partagent les variables explicatives principales :\n- `date` : variable temporelle (utilisée ensuite pour créer des features calendaires, lags, rolling, etc.)\n- `store_nbr`, `item_nbr` : identifiants (catégoriels) du magasin et du produit\n- `onpromotion` : indicateur de promotion (variable explicative importante)\n\nLa base `train` contient en plus la cible `unit_sales`, qui n’existe pas dans `test`.\n\n> Les transformations doivent être appliquées de manière cohérente sur les jeux d’entraînement et de test.  \nEn particulier, les encodages basés sur des statistiques (ex. `store_freq`, `item_freq`) sont calculés sur le **train** puis “mappés” sur le test afin d’éviter toute fuite d’information (*data leakage*)","metadata":{}},{"cell_type":"markdown","source":" ###  2.1. Traitement de `date` (variables temporelles)\n\nLa colonne `date` est convertie en format *datetime* afin d’extraire des variables calendaires utiles à la prévision des ventes.  \nDans la grande distribution, la demande est fortement influencée par des effets de **saisonnalité** (mois), de **cycle hebdomadaire** (jour de semaine/week-end) et de **périodicité** (semaine de l’année).\n\nNous créons donc des variables telles que `year`, `month`, `day`, `dow` (jour de semaine), `week` (semaine ISO), `dayofyear`, ainsi que des indicateurs `is_month_start` et `is_month_end` qui peuvent capturer certains comportements liés aux débuts/fins de mois (paie, inventaires, habitudes de consommation).\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# Sécurité : s'assurer que c'est bien une date\ntrain[\"date\"] = pd.to_datetime(train[\"date\"])\ntest[\"date\"] = pd.to_datetime(train[\"date\"])\n\ndef add_date_features(df, date_col=\"date\"):\n    \"\"\"Ajoute des variables temporelles à partir d'une colonne date.\"\"\"\n    d = df[date_col]\n\n    # Composantes calendaires\n    df[\"year\"]  = d.dt.year.astype(\"int16\")\n    df[\"month\"] = d.dt.month.astype(\"int8\")\n    df[\"day\"]   = d.dt.day.astype(\"int8\")\n\n    # Jour de la semaine (0=lundi, 6=dimanche)\n    df[\"dow\"] = d.dt.dayofweek.astype(\"int8\")\n\n    # Week-end\n    df[\"is_weekend\"] = (df[\"dow\"] >= 5).astype(\"int8\")\n\n    # Numéro de semaine ISO (1..53)\n    iso = d.dt.isocalendar()\n    df[\"week\"] = iso.week.astype(\"int8\")\n\n    # Jour de l'année (1..366)\n    df[\"dayofyear\"] = d.dt.dayofyear.astype(\"int16\")\n\n    # Début/fin de mois (effet paie / inventaire)\n    df[\"is_month_start\"] = d.dt.is_month_start.astype(\"int8\")\n    df[\"is_month_end\"]   = d.dt.is_month_end.astype(\"int8\")\n\n    return df\n\ntrain = add_date_features(train)\ntest = add_date_features(test)\n\n# Check rapide\ntrain[[\"date\",\"year\",\"month\",\"dow\",\"week\",\"is_weekend\", \"item_nbr\"]].head()\ntest[[\"date\",\"year\",\"month\",\"dow\",\"week\",\"is_weekend\", \"item_nbr\"]].head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:48:59.522002Z","iopub.execute_input":"2026-01-08T11:48:59.522491Z","iopub.status.idle":"2026-01-08T11:49:39.559462Z","shell.execute_reply.started":"2026-01-08T11:48:59.522452Z","shell.execute_reply":"2026-01-08T11:49:39.55847Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":" ###  2.2. Traitement de `onpromotion`\n\nLa variable `onpromotion` indique si un produit était en promotion au moment de la vente.  \nLes promotions ont généralement un impact direct sur la demande, c’est donc une variable explicative importante.\n\nLes valeurs manquantes sont interprétées comme *absence de promotion* (hypothèse standard en pratique), puis la variable est convertie en indicateur numérique (`onpromo` ∈ {0,1}) afin d’être directement exploitable par les modèles.  \nEnfin, une variable `promo_weekend` est ajoutée pour capturer l’effet potentiellement différent des promotions durant le week-end.\n\n","metadata":{}},{"cell_type":"code","source":"# Nettoyage: les NA = pas en promo (standard Kaggle Favorita)\ntrain[\"onpromotion\"] = train[\"onpromotion\"].fillna(False)\ntest[\"onpromotion\"] = test[\"onpromotion\"].fillna(False)\n\n# Convertir en int (0/1) pour les modèles\ntrain[\"onpromo\"] = train[\"onpromotion\"].astype(\"int8\")\ntest[\"onpromo\"] = test[\"onpromotion\"].astype(\"int8\")\n\n# Option : feature simple (souvent utile)\n# promo le week-end vs semaine (interaction légère)\ntrain[\"promo_weekend\"] = (train[\"onpromo\"] * train[\"is_weekend\"]).astype(\"int8\")\ntest[\"promo_weekend\"] = (test[\"onpromo\"] * test[\"is_weekend\"]).astype(\"int8\")\n\ntrain[[\"onpromotion\",\"onpromo\",\"promo_weekend\"]].head()\ntest[[\"onpromotion\",\"onpromo\",\"promo_weekend\"]].head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:50:07.529009Z","iopub.execute_input":"2026-01-08T11:50:07.529294Z","iopub.status.idle":"2026-01-08T11:50:08.916828Z","shell.execute_reply.started":"2026-01-08T11:50:07.529261Z","shell.execute_reply":"2026-01-08T11:50:08.915792Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":" ###  2.3. Traitement de `store_nbr`\n\n`store_nbr` est un identifiant de magasin. Même s’il est codé numériquement, il ne représente pas une quantité : un magasin “10” n’est pas “deux fois” un magasin “5”.  \nCette variable est donc traitée comme **catégorielle**.\n\nEn complément, nous construisons une variable de type *frequency encoding* (`store_freq`) correspondant au nombre d’observations associées à chaque magasin sur le total. \nCette information peut servir de proxy de l’activité ou de la présence du magasin dans l’historique.\n\n","metadata":{}},{"cell_type":"code","source":"# Cast en category (ok pour mémoire)\ntrain[\"store_nbr\"] = train[\"store_nbr\"].astype(\"int16\").astype(\"category\")\ntest[\"store_nbr\"]  = test[\"store_nbr\"].astype(\"int16\").astype(\"category\")\n\n# Frequency encoding calculé SUR TRAIN uniquement (pas de fuite)\nstore_freq = train[\"store_nbr\"].value_counts(normalize=True)\n\n# Application sur train et sur test\ntrain[\"store_freq\"] = train[\"store_nbr\"].map(store_freq).astype(\"float32\")\ntest[\"store_freq\"]  = test[\"store_nbr\"].map(store_freq).astype(\"float32\")\n\n# Affichage à 4 décimales (sans modifier les valeurs)\ntrain[[\"store_nbr\", \"store_freq\"]].head().style.format({\"store_freq\": \"{:.4f}\"})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:55:28.976146Z","iopub.execute_input":"2026-01-08T11:55:28.976518Z","iopub.status.idle":"2026-01-08T11:55:33.169359Z","shell.execute_reply.started":"2026-01-08T11:55:28.976488Z","shell.execute_reply":"2026-01-08T11:55:33.168077Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":" ###  2.4. Traitement de `item_nbr`\n\n`item_nbr` est un identifiant produit. Il est donc considéré comme une variable **catégorielle** plutôt qu’une variable quantitative.\n\nComme pour les magasins, nous ajoutons un *frequency encoding* (`item_freq`) qui mesure la fréquence d’apparition de chaque produit dans l’historique.  \nCela peut capturer indirectement des effets de popularité (produits souvent vendus vs produits rares) et aider certains modèles à mieux différencier les items.\n\n","metadata":{}},{"cell_type":"code","source":"# Cast en category (mémoire) — optionnel, mais ok\ntrain[\"item_nbr\"] = train[\"item_nbr\"].astype(\"int32\").astype(\"category\")\ntest[\"item_nbr\"]  = test[\"item_nbr\"].astype(\"int32\").astype(\"category\")\n\n# Frequency encoding calculé SUR TRAIN uniquement\nitem_freq = train[\"item_nbr\"].value_counts(normalize=True)\n\n# Application sur train et test (attention : mapper sur la colonne du bon dataframe)\ntrain[\"item_freq\"] = train[\"item_nbr\"].map(item_freq).astype(\"float32\")\ntest[\"item_freq\"]  = test[\"item_nbr\"].map(item_freq).fillna(0).astype(\"float32\")\n\n# Affichage à 4 décimales (sans modifier les valeurs)\ntrain[[\"item_nbr\", \"item_freq\"]].head().style.format({\"item_freq\": \"{:.4f}\"})","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:55:37.064465Z","iopub.execute_input":"2026-01-08T11:55:37.064872Z","iopub.status.idle":"2026-01-08T11:55:42.117321Z","shell.execute_reply.started":"2026-01-08T11:55:37.064838Z","shell.execute_reply":"2026-01-08T11:55:42.116445Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 3. Traitement des variables explicatives issues des autres bases\n\nLe jeu de données *Favorita* est fourni sous forme de plusieurs tables complémentaires (items, stores, oil, transactions, holidays).  \nCes sources externes apportent des facteurs explicatifs importants des ventes : caractéristiques produit, contexte magasin, chocs de prix (pétrole), activité magasin (transactions), événements calendaires (jours fériés).\n\nLes étapes ci-dessous détaillent le traitement des variables par table.\n","metadata":{}},{"cell_type":"markdown","source":"### 3.1.  Base `items` (informations produit)\n\nLa base `items` fournit des métadonnées produit qui expliquent une grande partie de la variabilité des ventes :\n- `family` : famille de produit (33 modalités),\n- `class` : classe produit (337 modalités),\n- `perishable` : indicateur binaire (produit périssable ou non).\n\nComme ces variables sont structurelles (elles ne dépendent pas du temps), elles sont intégrées au jeu principal via une jointure sur `item_nbr`.\n\n#### Traitements appliqués\n\n2. **Nettoyage / typage**\n   - `perishable` est conservée en numérique (0/1).\n   - Les valeurs manquantes éventuelles sont remplacées par des valeurs neutres (`UNKNOWN` pour les catégories, 0 pour `perishable`).\n\n3. **Encodage des variables catégorielles**\n   - `family` (33 modalités) et `class` (337 modalités) sont des variables catégorielles : même si `class` est codée par des entiers, il s’agit d’un identifiant de groupe et non d’une grandeur numérique.\n   - Pour rendre ces variables compatibles avec des modèles linéaires et la plupart des algorithmes, on les transforme en variables indicatrices via un **one-hot encoding**.\n   - La cardinalité de `class` (337) reste raisonnable : l’encodage produit un nombre de colonnes maîtrisé et exploitable, notamment grâce à l’utilisation de représentations creuses (*sparse*) dans les étapes suivantes.\n\nÀ l’issue de cette étape, toutes les variables issues de `items` sont prêtes à être utilisées dans la suite du pipeline (sans hypothèse d’ordre artificiel entre catégories).\n\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# ------------------------------------------------------------\n# 1) Importation Copie de travail (on évite de modifier items original)\n# ------------------------------------------------------------\nitems = pd.read_csv(EXTRACT_DIR + \"/items.csv\")\n\nitems_fe = items.copy()\n\n# ------------------------------------------------------------\n# 2) Nettoyage / valeurs manquantes / typage\n# ------------------------------------------------------------\nitems_fe[\"family\"] = items_fe[\"family\"].fillna(\"UNKNOWN\")\n\n# class: identifiant catégoriel (même si numérique) -> string\nitems_fe[\"class\"] = items_fe[\"class\"].fillna(-1).astype(\"int16\").astype(str)\n\n# perishable: binaire (0/1)\nitems_fe[\"perishable\"] = items_fe[\"perishable\"].fillna(0).astype(\"int8\")\n\n# ------------------------------------------------------------\n# 3) One-hot encoding sur family et class (sur items uniquement)\n# ------------------------------------------------------------\nitems_fe = pd.get_dummies(items_fe, columns=[\"family\", \"class\"], drop_first=True)\n\n# ------------------------------------------------------------\n# 4) Check : features créées depuis items\n# ------------------------------------------------------------\nitems_features = [c for c in items_fe.columns\n                 if c.startswith(\"family_\") or c.startswith(\"class_\") or c == \"perishable\"]\n\nprint(\"Nb de features créées depuis items :\", len(items_features))\nprint(\"Exemples de colonnes :\", items_features[:10])\n\n# (Option) vérifier qu'on a toujours item_nbr pour la future fusion\nprint(\"item_nbr dans items_fe ?\", \"item_nbr\" in items_fe.columns)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:56:29.057749Z","iopub.execute_input":"2026-01-08T11:56:29.05965Z","iopub.status.idle":"2026-01-08T11:56:29.097473Z","shell.execute_reply.started":"2026-01-08T11:56:29.059566Z","shell.execute_reply":"2026-01-08T11:56:29.096133Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.2. Base `stores`\n\nLa base `stores` fournit des informations structurelles sur chaque magasin :\n- `city` et `state` : contexte géographique,\n- `type` : typologie du magasin (format/segment),\n- `cluster` : regroupement interne de magasins (profil similaire).\n\nCes variables expliquent des différences de demande entre magasins (taille, zone, clientèle, etc.).\nComme elles sont principalement catégorielles, on les prépare via un encodage numérique.\n\n#### Traitements appliqués\n1. **Nettoyage**\n   - Remplacement des valeurs manquantes par `UNKNOWN` pour les variables qualitatives.\n2. **Typage**\n   - `city`, `state`, `type` : variables catégorielles.\n   - `cluster` : identifiant de groupe, traité comme catégoriel (même si numérique).\n3. **Encodage**\n   - Application d’un **one-hot encoding** sur `city`, `state`, `type` et `cluster`\n     afin de rendre ces informations exploitables par des modèles (notamment linéaires),\n     sans imposer d’ordre artificiel entre catégories.\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# ------------------------------------------------------------\n# 1) Importation et Copie de travail\n# ------------------------------------------------------------\nstores = pd.read_csv(EXTRACT_DIR + \"/stores.csv\")\nstores_fe = stores.copy()\n\n# ------------------------------------------------------------\n# 2) Nettoyage / valeurs manquantes\n# ------------------------------------------------------------\n# Colonnes qualitatives\nfor col in [\"city\", \"state\", \"type\"]:\n    if col in stores_fe.columns:\n        stores_fe[col] = stores_fe[col].fillna(\"UNKNOWN\").astype(str)\n\n# cluster : identifiant (souvent numérique) -> string pour le traiter comme catégorie\nif \"cluster\" in stores_fe.columns:\n    stores_fe[\"cluster\"] = stores_fe[\"cluster\"].fillna(-1).astype(\"int16\").astype(str)\n\n# ------------------------------------------------------------\n# 3) One-hot encoding (sur stores seulement)\n# ------------------------------------------------------------\ncat_cols = [c for c in [\"city\", \"state\", \"type\", \"cluster\"] if c in stores_fe.columns]\nstores_fe = pd.get_dummies(stores_fe, columns=cat_cols, drop_first=True)\n\n# ------------------------------------------------------------\n# 4) Check rapide : features créées depuis stores\n# ------------------------------------------------------------\nstores_features = [c for c in stores_fe.columns\n                  if c.startswith(\"city_\") or c.startswith(\"state_\") or c.startswith(\"type_\") or c.startswith(\"cluster_\")]\n\nprint(\"Nb de features créées depuis stores :\", len(stores_features))\nprint(\"Exemples de colonnes :\", stores_features[:10])\nprint(\"store_nbr dans stores_fe ?\", \"store_nbr\" in stores_fe.columns)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:56:47.69775Z","iopub.execute_input":"2026-01-08T11:56:47.698338Z","iopub.status.idle":"2026-01-08T11:56:47.72688Z","shell.execute_reply.started":"2026-01-08T11:56:47.698306Z","shell.execute_reply":"2026-01-08T11:56:47.72552Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.3 Base `transactions` : feature engineering + fusion (train/test)\n\nLa base `transactions` fournit le nombre total de transactions **par magasin et par jour** via la clé (`store_nbr`, `date`).  \nCette information sert de **proxy d’activité/affluence** du magasin et apporte un signal complémentaire aux ventes par produit.\n\n#### 3.3.1 Traitements appliqués (préparation de `trans_feat`)\n\n**a) Nettoyage & typage**\n- Conversion de `date` au format datetime.\n- Conversion de `store_nbr` en entier.\n- Remplacement des valeurs manquantes de `transactions` par 0.\n\n**b) Unicité de la clé**\n- La clé attendue est unique : (`store_nbr`, `date`).\n- En cas de doublons, agrégation par somme pour obtenir **une seule ligne par magasin et par jour**.\n\n**c) Feature temporelle : moyenne mobile 14 jours**\nPour capter la dynamique récente sans fuite d’information, on calcule :\n\n- `transactions_roll14` : moyenne des transactions des **14 jours précédents**, en excluant le jour courant.\n\nTechniquement, l’exclusion du jour courant est assurée par `shift(1)` avant le `rolling(14)`.  \nEn début de série, on accepte une moyenne calculée sur les jours disponibles (`min_periods = 1`).\n\nLe résultat de cette étape est une table `trans_feat` au niveau (`store_nbr`, `date`) contenant la variable `transactions_roll14`.\n\n#### 3.3.2 Fusion dans `train` et `test`\n\nLa fusion est réalisée par jointure gauche (`left join`) sur les clés :\n\n- **clés de jointure** : (`store_nbr`, `date`)\n- **objectif** : ajouter `transactions_roll14` à chaque observation (store-item-date) du jeu `train` et du jeu `test`, sans modifier le nombre de lignes.\n\n#### 3.3.3 Spécificité du jeu Favorita : continuité sur la période test\n\nLe fichier `transactions.csv` s’arrête au **15/08/2017**, tandis que `test` commence le **16/08/2017**.  \nAinsi, sur la période de test, la variable `transactions` (et donc `transactions_roll14`) n’est **pas observée**.\n\nPour conserver une continuité temporelle cohérente sans inventer de valeurs futures, on applique la règle suivante :\n\n- après fusion, si `transactions_roll14` est manquant dans `test`, on le remplace par la **dernière valeur disponible par magasin** (carry-forward).\n\nCe choix est logique car pour le **premier jour du test**, la meilleure information disponible sur l’activité récente d’un magasin est celle estimée à partir de l’historique immédiatement précédent (jusqu’au 15/08/2017).  \nEn dernier recours (magasin sans historique), on remplit par 0.\n\n> **En résumé :** `transactions_roll14` est une feature calculée sur l’historique disponible, fusionnée sur (`store_nbr`, `date`), puis prolongée sur `test` par la dernière valeur connue afin d’assurer la continuité.\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\n# --- dates train/test (juste pour repères) ---\ntrain[\"date\"] = pd.to_datetime(train[\"date\"])\ntest[\"date\"]  = pd.to_datetime(test[\"date\"])\n\ntrain_max_date = train[\"date\"].max()\ntest_min_date  = test[\"date\"].min()\n\nprint(\"train max:\", train_max_date)\nprint(\"test  min:\", test_min_date)\nprint(\"gap days :\", (test_min_date - train_max_date).days)\n\n# ------------------------------------------------------------\n# 1) Calcul des features transactions sur transactions.csv\n#    (qui s'arrête au 2017-08-15)\n# ------------------------------------------------------------\ntransactions = pd.read_csv(EXTRACT_DIR + \"/transactions.csv\")\n\ntransactions_fe = transactions.copy()\ntransactions_fe[\"date\"] = pd.to_datetime(transactions_fe[\"date\"])\ntransactions_fe[\"store_nbr\"] = transactions_fe[\"store_nbr\"].astype(\"int16\")\ntransactions_fe[\"transactions\"] = transactions_fe[\"transactions\"].fillna(0)\n\n# Unicité\ntransactions_fe = (transactions_fe\n    .groupby([\"store_nbr\",\"date\"], as_index=False)\n    .agg(transactions=(\"transactions\",\"sum\"))\n    .sort_values([\"store_nbr\",\"date\"])\n)\n\n# Rolling passé (anti-leak)\ntransactions_fe[\"transactions_roll14\"] = (\n    transactions_fe.groupby(\"store_nbr\")[\"transactions\"]\n    .transform(lambda x: x.shift(1).rolling(14, min_periods=1).mean())\n).astype(\"float32\")\n\ntrans_feat = transactions_fe[[\"store_nbr\",\"date\",\"transactions_roll14\"]].copy()\n\n# ------------------------------------------------------------\n# 2) Merge dans train/test\n# ------------------------------------------------------------\ntrain = train.merge(trans_feat, on=[\"store_nbr\",\"date\"], how=\"left\")\ntest  = test.merge(trans_feat,  on=[\"store_nbr\",\"date\"], how=\"left\")\n\n# ------------------------------------------------------------\n# 3) Continuité sur test : carry-forward de la dernière valeur train (par store)\n# ------------------------------------------------------------\n# dernière valeur disponible dans transactions_fe (par store)\nlast_roll_by_store = (transactions_fe\n    .sort_values([\"store_nbr\",\"date\"])\n    .groupby(\"store_nbr\")[\"transactions_roll14\"]\n    .last()\n)\n\n# pour test : si NA (normal, car pas de transactions), on met la dernière valeur connue\ntest[\"transactions_roll14\"] = (\n    test[\"transactions_roll14\"]\n    .fillna(test[\"store_nbr\"].map(last_roll_by_store))\n    .fillna(0)\n    .astype(\"float32\")\n)\n\n# pour train : si NA (tout début de série), on met 0\ntrain[\"transactions_roll14\"] = train[\"transactions_roll14\"].fillna(0).astype(\"float32\")\n\ntrain[[\"date\",\"store_nbr\",\"transactions_roll14\"]].head(), test[[\"date\",\"store_nbr\",\"transactions_roll14\"]].head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:58:11.478249Z","iopub.execute_input":"2026-01-08T11:58:11.479577Z","iopub.status.idle":"2026-01-08T11:58:11.594751Z","shell.execute_reply.started":"2026-01-08T11:58:11.479477Z","shell.execute_reply":"2026-01-08T11:58:11.593504Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n> Gestion de `transactions` sur la période de test :\n> Le fichier `transactions.csv` s’arrête au **15/08/2017**, tandis que la période de test débute au **16/08/2017**.  Par conséquent, les transactions réelles ne sont pas observées sur l’horizon de prévision.Pour éviter toute fuite d’information et conserver la cohérence temporelle, les features de type moyenne mobile (`transactions_roll14`) sont calculées uniquement sur l’historique disponible (jusqu’au 15/08).  Ensuite, pour chaque magasin, la valeur de `transactions_roll14` utilisée sur la période de test est fixée à la **dernière valeur disponible** (carry-forward).Ce choix assure une continuité naturelle : la première date de test utilise exactement l’information passée la plus récente, sans supposer des transactions futures inconnues.\n\n","metadata":{}},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"\n> \n\n","metadata":{}},{"cell_type":"markdown","source":"### 3.4. Base `oil`\n\nLa base `oil` contient une série temporelle du prix du pétrole (`dcoilwtico`) indexée par la date.\nCette variable permet de capturer des effets macroéconomiques globaux (coûts de production, inflation,\ncontexte économique général) susceptibles d’influencer indirectement les ventes.\n\nUne particularité importante de cette série est la présence fréquente de valeurs manquantes\n(week-ends, jours non cotés ou dates absentes). Plutôt que de supprimer ces dates, on reconstruit\nune série quotidienne continue afin de faciliter les fusions temporelles et la création de variables\ntemporelles.\n\n#### Traitements appliqués\n\n**1. Typage et organisation temporelle**\n- Conversion de la variable `date` au format datetime.\n- Tri chronologique de la série.\n\n**2. Reconstruction du calendrier**\n- Création d’un index journalier complet entre la date minimale et maximale observées.\n- Cette étape permet d’obtenir une série temporelle régulière, sans rupture dans le temps.\n\n**3. Gestion des valeurs manquantes**\n- Les valeurs manquantes du prix du pétrole sont imputées par interpolation temporelle,\n  suivie d’une propagation de la dernière valeur connue (`forward fill`).\n- Ce choix permet d’obtenir une série continue, exploitable pour la modélisation.\n\n\n**4. Création de variables temporelles**\n- Création de moyennes mobiles du prix du pétrole sur les 14 derniers jours observés,\n  calculées exclusivement à partir du passé afin d’éviter toute fuite d’information.\n- Ces variables capturent la tendance récente du marché pétrolier sous une forme lissée\n  et interprétable.\n\n","metadata":{}},{"cell_type":"code","source":"test[\"date\"].max()","metadata":{"trusted":true,"execution":{"execution_failed":"2026-01-08T20:37:02.081Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\n# ------------------------------------------------------------\n# Feature engineering — base oil\n# ------------------------------------------------------------\noil = pd.read_csv(EXTRACT_DIR + \"/oil.csv\")\noil_fe = oil.copy()\n\n# Typage + tri\noil_fe[\"date\"] = pd.to_datetime(oil_fe[\"date\"])\noil_fe = oil_fe.sort_values(\"date\")\n\n# Valeurs manquantes : ffill (série macro -> on propage la dernière valeur connue)\noil_fe[\"dcoilwtico\"] = oil_fe[\"dcoilwtico\"].ffill()\n\n# Feature rolling (14 jours précédents) + anti-leak (exclut le jour courant)\noil_fe[\"oil_roll14\"] = (\n    oil_fe[\"dcoilwtico\"]\n    .shift(1)\n    .rolling(window=14, min_periods=1)\n    .mean()\n).astype(\"float32\")\n\n# Table finale à merger\noil_feat = oil_fe[[\"date\", \"oil_roll14\"]].copy()\n\n# ------------------------------------------------------------\n# Fusion dans train/test\n# ------------------------------------------------------------\ntrain[\"date\"] = pd.to_datetime(train[\"date\"])\ntest[\"date\"]  = pd.to_datetime(test[\"date\"])\n\ntrain = train.merge(oil_feat, on=\"date\", how=\"left\")\ntest  = test.merge(oil_feat,  on=\"date\", how=\"left\")\n\n# ------------------------------------------------------------\n# Continuité si jamais oil.csv ne couvre pas la période test\n# (fallback : dernière valeur connue avant le début du test)\n# ------------------------------------------------------------\nlast_oil_roll14 = (oil_feat\n    .loc[oil_feat[\"date\"] <= train[\"date\"].max(), \"oil_roll14\"]\n    .dropna()\n    .iloc[-1]\n)\n\ntrain[\"oil_roll14\"] = train[\"oil_roll14\"].fillna(0).astype(\"float32\")\ntest[\"oil_roll14\"]  = test[\"oil_roll14\"].fillna(last_oil_roll14).astype(\"float32\")\n\n# Check\nprint(\"oil_feat max date:\", oil_feat[\"date\"].max())\nprint(\"train max date   :\", train[\"date\"].max())\nprint(\"test  min date   :\", test[\"date\"].min())\ntrain[[\"date\",\"oil_roll14\"]].head(), test[[\"date\",\"oil_roll14\"]].head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T11:59:16.751599Z","iopub.execute_input":"2026-01-08T11:59:16.751989Z","iopub.status.idle":"2026-01-08T11:59:16.787686Z","shell.execute_reply.started":"2026-01-08T11:59:16.751953Z","shell.execute_reply":"2026-01-08T11:59:16.786553Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 3.5 Traitement des jours fériés et événements (holidays_events)\n\nLes jours fériés et événements calendaires constituent un déterminant important de la demande, mais leur impact dépend fortement de leur **date réelle de célébration** et de leur **portée géographique**. Afin de modéliser cet effet de manière réaliste, un traitement spécifique de la base `holidays_events` a été mis en place.\n\n#### Correction des jours fériés transférés\n\nCertains jours fériés sont officiellement déplacés par les autorités vers une autre date (par exemple pour créer un long week-end). Dans ce cas, le jour initialement prévu se comporte comme un jour normal, tandis que l’impact réel sur la demande se manifeste à la date de célébration effective.\n\nPour tenir compte de ce phénomène, les jours fériés marqués comme *transferred* ont été repositionnés sur leur **date réelle de célébration**. Cette étape garantit que l’effet calendaire est associé au bon jour dans le jeu de données, évitant ainsi toute mauvaise attribution de l’impact sur les ventes.\n\n#### Prise en compte de la portée géographique\n\nL’effet d’un jour férié dépend également de son **niveau géographique** :\n\n- les jours fériés **nationaux** affectent l’ensemble des magasins ;\n- les jours fériés **régionaux** n’impactent que les magasins situés dans la région concernée ;\n- les jours fériés **locaux** n’impactent que les magasins situés dans la ville concernée.\n\nLa base `stores` a donc été utilisée pour associer à chaque magasin son **État** et sa **ville**, permettant d’appliquer correctement la portée de chaque événement.\n\n#### Construction des variables explicatives\n\nPour chaque couple *(magasin, date)*, plusieurs indicateurs binaires ont été créés :\n\n- `is_holiday_national` : présence d’un jour férié national ;\n- `is_holiday_regional` : présence d’un jour férié régional affectant le magasin ;\n- `is_holiday_local` : présence d’un jour férié local affectant le magasin ;\n- `is_additional` : jour férié additionnel autour d’un événement principal ;\n- `is_bridge` : jour de pont ;\n- `is_work_day` : jour travaillé exceptionnel (rattrapage).\n\nUne variable synthétique `is_holiday_effective` a également été construite. Elle indique la présence d’au moins un jour férié effectif (national, régional ou local) pour le magasin considéré.\n\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\n\n# ============================================================\n# 1) Préparation de la base holidays_events\n# ============================================================\nholidays = pd.read_csv(EXTRACT_DIR + \"/holidays_events.csv\")\nhol = holidays.copy()\nhol[\"date\"] = pd.to_datetime(hol[\"date\"])\n\n# Nettoyage des champs texte\nfor col in [\"type\", \"locale\", \"locale_name\", \"description\"]:\n    hol[col] = hol[col].astype(str).str.strip()\n\n# transferred -> 0/1\nhol[\"transferred\"] = hol[\"transferred\"].fillna(False).astype(int)\n\n# Clé événement (permet de relier Holiday ↔ Transfer)\nhol[\"event_key\"] = (\n    hol[\"description\"] + \" | \" +\n    hol[\"locale\"] + \" | \" +\n    hol[\"locale_name\"]\n)\n\n# ============================================================\n# 2) Correction des dates transférées\n# ============================================================\n\n# Dictionnaire : event_key → date réelle de célébration\ntransfer_map = (\n    hol.loc[hol[\"type\"] == \"Transfer\", [\"event_key\", \"date\"]]\n    .drop_duplicates(subset=[\"event_key\"])\n    .set_index(\"event_key\")[\"date\"]\n    .to_dict()\n)\n\n# Date réellement observée\nhol[\"observed_date\"] = hol[\"date\"]\n\nmask_transferred = (hol[\"type\"] == \"Holiday\") & (hol[\"transferred\"] == 1)\nhol.loc[mask_transferred, \"observed_date\"] = (\n    hol.loc[mask_transferred, \"event_key\"].map(transfer_map)\n)\n\n# Sécurité : fallback sur la date initiale\nhol[\"observed_date\"] = hol[\"observed_date\"].fillna(hol[\"date\"])\n\n# ============================================================\n# 3) Jointure avec les magasins (contexte géographique)\n# ============================================================\n\n# stores contient : store_nbr | state | city\nhol = hol.merge(\n    stores[[\"store_nbr\", \"state\", \"city\"]],\n    how=\"cross\"   # produit cartésien volontaire\n)\n\n# ============================================================\n# 4) Création des indicateurs par portée\n# ============================================================\n\nhol[\"is_holiday_national\"] = (\n    (hol[\"type\"] == \"Holiday\") &\n    (hol[\"locale\"] == \"National\")\n).astype(int)\n\nhol[\"is_holiday_regional\"] = (\n    (hol[\"type\"] == \"Holiday\") &\n    (hol[\"locale\"] == \"Regional\") &\n    (hol[\"locale_name\"] == hol[\"state\"])\n).astype(int)\n\nhol[\"is_holiday_local\"] = (\n    (hol[\"type\"] == \"Holiday\") &\n    (hol[\"locale\"] == \"Local\") &\n    (hol[\"locale_name\"] == hol[\"city\"])\n).astype(int)\n\nhol[\"is_additional\"] = (hol[\"type\"] == \"Additional\").astype(int)\nhol[\"is_bridge\"]     = (hol[\"type\"] == \"Bridge\").astype(int)\nhol[\"is_work_day\"]   = (hol[\"type\"] == \"Work Day\").astype(int)\n\n# ============================================================\n# 5) Agrégation finale par magasin et par date\n# ============================================================\n\nholidays_store_date = (\n    hol.groupby([\"store_nbr\", \"observed_date\"], as_index=False)\n    .agg(\n        is_holiday_national=(\"is_holiday_national\", \"max\"),\n        is_holiday_regional=(\"is_holiday_regional\", \"max\"),\n        is_holiday_local=(\"is_holiday_local\", \"max\"),\n        is_additional=(\"is_additional\", \"max\"),\n        is_bridge=(\"is_bridge\", \"max\"),\n        is_work_day=(\"is_work_day\", \"max\"),\n    )\n    .rename(columns={\"observed_date\": \"date\"})\n    .sort_values([\"store_nbr\", \"date\"])\n    .reset_index(drop=True)\n)\n\n# ============================================================\n# 6) Variable synthèse (au moins un jour férié effectif)\n# ============================================================\n\nholidays_store_date[\"is_holiday_effective\"] = (\n    holidays_store_date[\n        [\"is_holiday_national\", \"is_holiday_regional\", \"is_holiday_local\"]\n    ].max(axis=1)\n)\n\n# ============================================================\n# 7) Sanity checks\n# ============================================================\n\nprint(\"Shape holidays_store_date :\", holidays_store_date.shape)\nprint(holidays_store_date.head())\n\nprint(\"\\nJours avec férié effectif :\", holidays_store_date[\"is_holiday_effective\"].sum())\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-01-08T20:37:02.08Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 4. Création de variables de séries temporelles : *lags* et statistiques glissantes\n\nDans un problème de prévision des ventes, la variable cible présente généralement une **dépendance temporelle** : les ventes d’aujourd’hui sont souvent liées à celles des jours précédents (inertie) et suivent des **cycles** (notamment hebdomadaires).  \nPour capturer cette dynamique, on crée des variables dites de **série temporelle**, en particulier :\n\n- **Lags (retards)** : valeurs observées *k jours avant* (ex. `lag_1` = ventes d’hier, `lag_7` = ventes du même jour la semaine précédente).\n- **Moyennes glissantes (rolling means)** : moyenne des ventes sur une fenêtre de temps passée (ex. moyenne des 4 dernières semaines).\n\n### 4.1 Principe fondamental : éviter la fuite d’information (*data leakage*)\n\nLorsqu’on prédit les ventes au jour \\(t\\), il est interdit d’utiliser des informations du jour \\(t\\) lui-même (ou du futur).  \nAinsi, une moyenne glissante “naïve” calculée sur \\([t-6, \\dots, t]\\) inclurait la valeur de \\(t\\), ce qui reviendrait à tricher.\n\n**Solution standard :** on calcule les statistiques glissantes sur la série **décalée d’un jour** :\n\n$$\n\\text{rolling\\_mean}_w(t) = \\frac{1}{w}\\sum_{j=1}^{w} y_{t-j}\n$$\n\nEn pratique : `shift(1)` puis `rolling(w)`.\n\n### 4.2 Panel temporel : calcul par (magasin, produit)\n\nLe jeu de données n’est pas une seule série temporelle mais un **panel** : chaque couple \\((store\\_nbr, item\\_nbr)\\) possède sa propre série.  \nLes lags et rolling doivent donc être calculés **à l’intérieur de chaque couple (store, item)**, en triant par date.\n","metadata":{}},{"cell_type":"markdown","source":"### 4.3 Création des features lags et rolling sur tout le dataset\n\nPour prédire les ventes, nous créons des variables temporelles qui capturent la **dépendance passée** des ventes.  \nChaque couple `(store_nbr, item_nbr)` est considéré comme une série temporelle indépendante.\n\n#### Les types de features créées :\n\n1. **Lags (retards)** : valeurs passées de la variable cible.\n   - `lag_1` : ventes du jour précédent\n   - `lag_7` : ventes du même jour la semaine précédente\n   - `lag_14`, `lag_28` : ventes des deux et quatre semaines précédentes\n\n   Ces lags permettent au modèle de capturer :\n   - l’**inertie** à court terme (`lag_1`)\n   - la **saisonnalité hebdomadaire** (`lag_7`)\n   - des **tendances à moyen terme** (`lag_14`, `lag_28`)\n\n2. **Rolling means (moyennes glissantes)** : moyenne des ventes sur une fenêtre glissante dans le passé.\n   - Fenêtres : 7, 14, 28 jours\n   - Calculées sur les valeurs **décalées d’un jour** (`shift(1)`) pour éviter de “regarder dans le futur”\n   - Capturent la **tendance récente** des ventes et lissent le bruit.\n\n\n> **Règle importante** : toutes ces features sont calculées **par série (store × item)**. On ne mélange jamais les ventes de différents produits ou magasins pour éviter de créer des informations incohérentes.\n\n","metadata":{}},{"cell_type":"code","source":"import numpy as np\n\n# Toujours trier AVANT de créer des variables temporelles\ntrain = train.sort_values([\"store_nbr\", \"item_nbr\", \"date\"]).copy()\n\ng = train.groupby([\"store_nbr\", \"item_nbr\"], sort=False)\n\n# --- 1) Lags (retards) ---\nLAGS = [7, 84]\nfor k in LAGS:\n    train[f\"sales_lag{k}\"] = g[\"unit_sales\"].shift(k)\n\n# --- 2) Rolling means sur le passé (anti-leakage : shift(1) puis rolling) ---\nWINDOWS = [28, 84 ]\nfor w in WINDOWS:\n    train[f\"sales_roll{w}_mean\"] = (\n        g[\"unit_sales\"]\n        .shift(1)                 # exclut le jour t\n        .rolling(w)\n        .mean()\n        .reset_index(level=[0, 1], drop=True)\n    )\n\n# Check rapide\ncols_created = [c for c in train.columns if c.startswith(\"sales_lag\") or c.startswith(\"sales_roll\")]\nprint(\"Nb de variables créées :\", len(cols_created))\nprint(cols_created[:10])\ntrain[[\"date\", \"store_nbr\", \"item_nbr\", \"unit_sales\"] + cols_created[:6]].head()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-01-08T04:17:19.143883Z","iopub.execute_input":"2026-01-08T04:17:19.144195Z","execution_failed":"2026-01-08T04:17:54.651Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 5. Fusion des bases et construction de la table finale\n\nL’objectif est de construire une table finale au niveau **(date, store_nbr, item_nbr)**, qui correspond exactement à la granularité des ventes à prédire.  \nToutes les bases externes sont donc ramenées à une granularité compatible *avant* la fusion (traitements déjà réalisés), afin d’éviter les erreurs classiques :\n- explosion du nombre de lignes (jointure sur clés non uniques),\n- duplication silencieuse d’observations,\n- incohérence des features temporelles.\n\n### 5.1 Tables fusionnées et clés de jointure\n\nLes fusions sont effectuées selon les clés suivantes :\n\n- `items_clean` (métadonnées produits) : jointure sur `item_nbr`  \n  → variables structurelles du produit (famille, classe, périssable)\n\n- `stores_clean` (métadonnées magasins) : jointure sur `store_nbr`  \n  → variables géographiques et typologie du magasin (ville, état, type, cluster)\n\n- `holidays_store_date` (jours fériés contextualisés) : jointure sur (`date`, `store_nbr`)  \n  → indicateurs binaires tenant compte de la portée *national / régional / local*\n\n- `trans_feat` (transactions) : jointure sur (`date`, `store_nbr`)  \n  → variables d’activité magasin, incluant des features de type lag/rolling\n\n- `oil_feat` (prix du pétrole) : jointure sur `date`  \n  → features macro temporelles (interpolation et lags/rolling)\n\n### 5.2 Ordre de fusion\n\nL’ordre est volontairement structuré :\n1. ajout des variables **statiques** (produit puis magasin) ;\n2. ajout des variables **temporelles au niveau magasin-date** (holidays, transactions) ;\n3. ajout des variables **temporelles au niveau date** (oil).\n\nCet ordre minimise les risques d’incohérence et facilite les contrôles de qualité.\n\n### 5.3 Contrôle strict de stabilité (anti-explosion)\n\nAprès chaque fusion, un contrôle automatique vérifie que :\n- le **nombre de lignes ne change pas** (sinon cela révèle des doublons sur les clés de jointure dans la table fusionnée),\n- les clés sont uniques dans les tables de droite (par ex. `item_nbr` unique dans `items_clean`).\n\nCette règle garantit une table finale cohérente et réutilisable pour :\n- la création des features temporelles sur la cible (lags/rolling de `unit_sales`),\n- la modélisation et la prédiction sur le test,\n- et l’industrialisation (tests unitaires faciles sur les fonctions de merge).\n","metadata":{}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\n# ------------------------------------------------------------\n# 0) Sécurisation typage minimal (si pas déjà fait)\n# ------------------------------------------------------------\nfor df in [train, test]:\n    df[\"date\"] = pd.to_datetime(df[\"date\"])\n    df[\"store_nbr\"] = df[\"store_nbr\"].astype(\"int16\")\n    df[\"item_nbr\"]  = df[\"item_nbr\"].astype(\"int32\")\n\nitems_clean[\"item_nbr\"] = items_clean[\"item_nbr\"].astype(\"int32\")\nstores_clean[\"store_nbr\"] = stores_clean[\"store_nbr\"].astype(\"int16\")\n\nholidays_store_date[\"date\"] = pd.to_datetime(holidays_store_date[\"date\"])\nholidays_store_date[\"store_nbr\"] = holidays_store_date[\"store_nbr\"].astype(\"int16\")\n\ntrans_feat[\"date\"] = pd.to_datetime(trans_feat[\"date\"])\ntrans_feat[\"store_nbr\"] = trans_feat[\"store_nbr\"].astype(\"int16\")\n\noil_feat[\"date\"] = pd.to_datetime(oil_feat[\"date\"])\n\n\n# ------------------------------------------------------------\n# 1) Fonction de merge \"safe\" (empêche explosion de lignes)\n# ------------------------------------------------------------\ndef safe_merge(left, right, on, how=\"left\", name=\"\"):\n    before = left.shape[0]\n    out = left.merge(right, on=on, how=how)\n    after = out.shape[0]\n    if after != before:\n        raise ValueError(\n            f\"[MERGE ERROR] {name}: nb lignes a changé ({before} -> {after}). \"\n            f\"Probable doublons sur clés {on} dans la table de droite.\"\n        )\n    return out\n\n\n# ------------------------------------------------------------\n# 2) Contrôles clés uniques sur les tables de droite\n# ------------------------------------------------------------\nassert items_clean[\"item_nbr\"].is_unique, \"items_clean: item_nbr non unique\"\nassert stores_clean[\"store_nbr\"].is_unique, \"stores_clean: store_nbr non unique\"\n\n# Pour les tables temporelles : vérifier unicité sur clés\nassert not holidays_store_date.duplicated([\"date\", \"store_nbr\"]).any(), \"holidays_store_date: doublons date-store\"\nassert not trans_feat.duplicated([\"date\", \"store_nbr\"]).any(), \"trans_feat: doublons date-store\"\nassert oil_feat[\"date\"].is_unique, \"oil_feat: date non unique\"\n\n\n# ------------------------------------------------------------\n# 3) Fusion dans l'ordre recommandé\n# ------------------------------------------------------------\ndef build_final(df_base, is_train=True):\n    df = df_base.copy()\n\n    # 1) Statiques\n    df = safe_merge(df, items_clean, on=\"item_nbr\", name=\"items_clean\")\n    df = safe_merge(df, stores_clean, on=\"store_nbr\", name=\"stores_clean\")\n\n    # 2) Holidays (date-store OK pour train & test)\n    df = safe_merge(df, holidays_store_date, on=[\"date\", \"store_nbr\"], name=\"holidays_store_date\")\n\n    # 3) Transactions (LOGIQUE DIFFÉRENTE)\n    if is_train:\n        df = safe_merge(df, trans_feat_train, on=[\"date\", \"store_nbr\"], name=\"trans_feat_train\")\n    else:\n        df = safe_merge(df, trans_feat_test, on=[\"store_nbr\"], name=\"trans_feat_test\")\n\n    # 4) Oil (date uniquement)\n    df = safe_merge(df, oil_feat, on=\"date\", name=\"oil_feat\")\n\n    # --------------------------------------------------------\n    # NA handling\n    # --------------------------------------------------------\n    holiday_flag_cols = [c for c in df.columns if c.startswith(\"is_\")]\n    for c in holiday_flag_cols:\n        df[c] = df[c].fillna(0).astype(\"int8\")\n\n    temp_cols = [c for c in df.columns if c.startswith(\"trans_\") or c.startswith(\"oil_\")]\n    for c in temp_cols:\n        df[c] = df[c].fillna(0)\n\n    return df\n\ntrain_merged = build_final(train, is_train=True)\ntest_merged  = build_final(test,  is_train=False)\n\nprint(\"train_merged :\", train_merged.shape)\nprint(\"test_merged  :\", test_merged.shape)\n\n\n# ------------------------------------------------------------\n# 4) Harmonisation colonnes train/test (important pour modèle)\n# ------------------------------------------------------------\n# (on garde la cible uniquement dans train)\ntarget_col = \"unit_sales\"\n\ncommon_cols = sorted(set(train_merged.columns) - {target_col})\n# test doit avoir exactement les mêmes colonnes que train (hors cible)\ntest_merged = test_merged.reindex(columns=common_cols + [\"id\"] if \"id\" in test_merged.columns else common_cols)\n\n# Option : réordonner train dans le même ordre\ntrain_merged = train_merged.reindex(columns=common_cols + [target_col])\n\nprint(\"Colonnes alignées ✔️\")\nprint(\"Nb colonnes train:\", train_merged.shape[1], \" | test:\", test_merged.shape[1])\n\n\n# ------------------------------------------------------------\n# 5) Sanity checks finaux\n# ------------------------------------------------------------\n# Vérifier date max/min\nprint(\"Train date min/max:\", train_merged[\"date\"].min(), \"->\", train_merged[\"date\"].max())\n\n# Vérifier taux de NA résiduel (devrait être très faible)\nna_top = (train_merged.isna().mean().sort_values(ascending=False) * 100).head(15)\nprint(\"\\nTop NA% (train):\")\nprint(na_top)\n","metadata":{"trusted":true,"execution":{"execution_failed":"2026-01-08T03:32:22.175Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 6. Feature selection","metadata":{}}]}