{"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":"## Inventaires des fichiers","metadata":{"_uuid":"c4f8c69b-de9a-4a20-8e64-5aa1c5e392e3","_cell_guid":"13897552-a077-4cfd-ab9c-a5ef2782d7c0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"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)","metadata":{"_uuid":"6a5c0f86-04a6-4fb6-9942-14a8e7121029","_cell_guid":"95b31b5d-8abe-49ca-8bb0-beba8b1e2e78","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2026-01-12T13:57:00.963244Z","iopub.execute_input":"2026-01-12T13:57:00.963585Z","iopub.status.idle":"2026-01-12T13:57:00.975941Z","shell.execute_reply.started":"2026-01-12T13:57:00.963561Z","shell.execute_reply":"2026-01-12T13:57:00.975194Z"},"jupyter":{"outputs_hidden":false}},"outputs":[{"name":"stdout","text":"Fichiers disponibles :\ntest.csv.7z\nstores.csv.7z\nitems.csv.7z\nholidays_events.csv.7z\ntransactions.csv.7z\ntrain.csv.7z\noil.csv.7z\nsample_submission.csv.7z\n","output_type":"stream"}],"execution_count":1},{"cell_type":"markdown","source":"## Code de décompression des fichiers\n!! n'exécuter qu'une seule fois !!","metadata":{"_uuid":"b1f0c07b-4e19-4343-9f92-a5e1caa0a883","_cell_guid":"150f945a-01e3-492f-bd4f-22cab30465fa","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"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))","metadata":{"_uuid":"e534db69-81cf-4012-896b-83fe36c8c19c","_cell_guid":"1992fdf2-3bb5-4025-82ff-5f8f9ac59c6e","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2026-01-12T13:57:00.982885Z","iopub.execute_input":"2026-01-12T13:57:00.983293Z","iopub.status.idle":"2026-01-12T13:57:46.947725Z","shell.execute_reply.started":"2026-01-12T13:57:00.983261Z","shell.execute_reply":"2026-01-12T13:57:46.947034Z"},"jupyter":{"outputs_hidden":false}},"outputs":[{"name":"stdout","text":"Décompression de test.csv.7z ...\nDécompression de stores.csv.7z ...\nDécompression de items.csv.7z ...\nDécompression de holidays_events.csv.7z ...\nDécompression de transactions.csv.7z ...\nDécompression de train.csv.7z ...\nDécompression de oil.csv.7z ...\nDécompression de sample_submission.csv.7z ...\n\nFichiers décompressés :\n['holidays_events.csv', 'oil.csv', 'transactions.csv', 'sample_submission.csv', 'test.csv', 'items.csv', 'stores.csv', 'train.csv']\n","output_type":"stream"}],"execution_count":2},{"cell_type":"markdown","source":"## Importations et configurations","metadata":{"_uuid":"568a5678-01a8-48f9-8ab1-13748ee49385","_cell_guid":"e1e6aa09-278e-49e0-9ebe-b2a638c60d99","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"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)","metadata":{"_uuid":"5877e35b-e8be-45a2-b1e1-a3d2b94fc25b","_cell_guid":"4127b3a5-f0c2-4dd4-bd30-9a8297bfcf98","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2026-01-12T13:57:46.949206Z","iopub.execute_input":"2026-01-12T13:57:46.949460Z","iopub.status.idle":"2026-01-12T13:57:47.250070Z","shell.execute_reply.started":"2026-01-12T13:57:46.949436Z","shell.execute_reply":"2026-01-12T13:57:47.249336Z"},"jupyter":{"outputs_hidden":false}},"outputs":[],"execution_count":3},{"cell_type":"markdown","source":"## Chargement des données de la base train","metadata":{"_uuid":"827e2ada-d552-48dc-a4cc-2e9d2fc25062","_cell_guid":"0d6f691f-9713-4990-8c4f-99bca418db0c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"train = 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()","metadata":{"_uuid":"629ca447-5d95-4780-9cce-b369e6917644","_cell_guid":"dc64b898-be7a-4f0b-95b2-ed7bff0f968d","trusted":true,"collapsed":false,"execution":{"iopub.status.busy":"2026-01-12T13:57:47.251096Z","iopub.execute_input":"2026-01-12T13:57:47.251509Z","iopub.status.idle":"2026-01-12T13:59:39.610429Z","shell.execute_reply.started":"2026-01-12T13:57:47.251486Z","shell.execute_reply":"2026-01-12T13:59:39.609384Z"},"jupyter":{"outputs_hidden":false}},"outputs":[{"name":"stdout","text":"Base Train\n(125497040, 5)\n","output_type":"stream"},{"execution_count":4,"output_type":"execute_result","data":{"text/plain":"        date  store_nbr  item_nbr  unit_sales onpromotion\n0 2013-01-01         25    103665        7.00         NaN\n1 2013-01-01         25    105574        1.00         NaN\n2 2013-01-01         25    105575        2.00         NaN\n3 2013-01-01         25    108079        1.00         NaN\n4 2013-01-01         25    108701        1.00         NaN","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>date</th>\n      <th>store_nbr</th>\n      <th>item_nbr</th>\n      <th>unit_sales</th>\n      <th>onpromotion</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <th>0</th>\n      <td>2013-01-01</td>\n      <td>25</td>\n      <td>103665</td>\n      <td>7.00</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>1</th>\n      <td>2013-01-01</td>\n      <td>25</td>\n      <td>105574</td>\n      <td>1.00</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>2</th>\n      <td>2013-01-01</td>\n      <td>25</td>\n      <td>105575</td>\n      <td>2.00</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>3</th>\n      <td>2013-01-01</td>\n      <td>25</td>\n      <td>108079</td>\n      <td>1.00</td>\n      <td>NaN</td>\n    </tr>\n    <tr>\n      <th>4</th>\n      <td>2013-01-01</td>\n      <td>25</td>\n      <td>108701</td>\n      <td>1.00</td>\n      <td>NaN</td>\n    </tr>\n  </tbody>\n</table>\n</div>"},"metadata":{}}],"execution_count":4},{"cell_type":"markdown","source":"\n>","metadata":{"_uuid":"ab7ed583-b45c-4862-8216-d83b87c02ea1","_cell_guid":"d8e597d2-dbf9-477d-8ab9-83ca628e0e90","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"markdown","source":"## 1. Mise en place du Pipeline de Feature Engineering  \n### Logique globale, prévention du data leakage, *feature gap* et gestion des nouvelles données\n\n---\n\n### 1.1 Pourquoi un pipeline de Feature Engineering ?\n\nDans un projet de **prévision des ventes**, le Feature Engineering ne doit pas être conçu\ncomme une suite de transformations ponctuelles appliquées une seule fois à un jeu de données.\nIl doit au contraire être pensé comme un **processus reproductible, cohérent et robuste**,\ncapable de fonctionner dans plusieurs contextes :\n\n- entraînement du modèle,\n- validation temporelle,\n- test final,\n- tests unitaires,\n- prédiction en production (nouvelles dates jamais observées).\n\nPour répondre à ces exigences, nous avons regroupé **l’ensemble des transformations**\ndans un **pipeline unique**, structuré selon la logique **Fit / Transform** :\n\n- **FIT** : apprentissage à partir de l’historique,\n- **TRANSFORM** : application des règles apprises à de nouvelles données.\n\nCette organisation garantit :\n- l’absence de fuite d’information (*data leakage*),\n- la cohérence des features entre train, validation, test et production,\n- la réutilisabilité du code dans un cadre réel.\n\n---\n\n### 1.1.1 Deux notions distinctes : *gap de split* vs *gap de features*\n\nDans notre approche, il est important de distinguer deux idées souvent confondues :\n\n1) **Gap de split temporel**  \nIl s’agit d’un intervalle (ex. 3 jours) inséré entre `train_fit` et `test` lors de l’évaluation,\npour simuler une vraie séparation temporelle.\n\n2) **Feature gap (`feature_gap_days`) : retard d’accès aux informations**  \nIl s’agit d’un paramètre du pipeline qui simule un contexte réaliste où certaines informations\n(ne serait-ce que pour des raisons de consolidation ou de disponibilité) ne sont pas connues immédiatement.\nPar exemple, avec `feature_gap_days = 3`, les features basées sur les ventes et transactions\nn’utilisent pas les 3 jours les plus récents disponibles.\n\n👉 Le *gap de split* sert l’évaluation, tandis que le *feature gap* sert la prévention du leakage\ndans la construction de certaines variables temporelles.\n\n---\n\n### 1.2 Étape FIT : apprentissage à partir de l’historique (`train_fit`)\n\nL’étape `fit()` est réalisée **une seule fois**, exclusivement à partir des données\nhistoriques d’entraînement (`train_fit`), c’est-à-dire **antérieures à toute période à prédire**.\n\n#### Principe fondamental\n\nLors du `fit()` :\n- **aucune information future n’est utilisée**,\n- aucune statistique n’est recalculée sur les périodes de validation ou de test,\n- le pipeline se contente d’**apprendre et de stocker** des objets réutilisables ensuite\n(séries indexées, dictionnaires de mapping, fréquences, etc.).\n\n---\n\n### 1.2.1 Encodages statistiques appris sur l’historique\n\nCertaines variables catégorielles sont transformées via des **encodages de fréquence** :\n\n- `store_freq` : proportion d’observations associées à chaque magasin,\n- `item_freq` : proportion d’observations associées à chaque produit.\n\nCes fréquences sont calculées **uniquement sur `train_fit`**, puis mémorisées dans le pipeline.\n\nLors du `transform()`, ces valeurs sont simplement réappliquées par `map()`.\nSi un magasin ou un produit n’a jamais été observé, la fréquence est fixée à 0.\n\n👉 Cela empêche toute fuite d’information issue des données futures.\n\n---\n\n### 1.2.2 Intégration des tables statiques (`items.csv`, `stores.csv`)\n\nLes tables `items` et `stores` contiennent des informations **structurelles**, indépendantes du temps.\n\n#### a) Table `items.csv`\n\nÀ partir de `items.csv`, le pipeline crée :\n\n- `perishable` : indicateur de périssabilité,\n- `family_freq_items` : fréquence des familles de produits,\n- `class_freq_items` : fréquence des classes,\n- `class_freq_in_family` : fréquence conditionnelle classe / famille.\n\nCes informations sont nettoyées, encodées numériquement,\npuis stockées sous forme d’une table compacte utilisée par `merge()`.\n\n---\n\n#### b) Table `stores.csv`\n\nÀ partir de `stores.csv`, le pipeline extrait :\n\n- `cluster` : cluster du magasin,\n- `type_freq`, `state_freq`, `city_freq` : fréquences par type, état et ville,\n- `city_freq_in_state` : fréquence conditionnelle ville / état.\n\n**Important :** ces fréquences (type/state/city) sont calculées de manière cohérente\npar rapport aux **observations présentes dans `train_fit`** (et non simplement sur `stores.csv`),\nce qui reflète mieux la distribution réelle des données d’entraînement.\n\nLes variables sont stockées sous forme de dictionnaires\net injectées via `map()` (plus léger qu’un `merge` complet).\n\n---\n\n### 1.2.3 Variables temporelles externes : transactions et pétrole\n\n#### a) Activité des magasins : `transactions_roll14` (avec *feature gap*)\n\nLa variable `transactions_roll14` est calculée à partir de `transactions.csv`\ncomme une moyenne mobile sur 14 jours.\n\nDans un contexte strict sans retard, on aurait :\n\n$\n\\text{transactions\\_roll14}(t)\n= \\frac{1}{14} \\sum_{j=1}^{14} \\text{transactions}_{t-j}\n$\n\nCependant, afin de simuler un contexte réaliste de disponibilité,\nnous introduisons un **retard** paramétré par `feature_gap_days = G`.\n\nLa moyenne mobile devient :\n\n$\n\\text{transactions\\_roll14}(t)\n= \\frac{1}{14} \\sum_{j=1}^{14} \\text{transactions}_{t-(G+j)}\n$\n\nAutrement dit, si \\(G = 3\\), on utilise les transactions entre \\(t-4\\) et \\(t-17\\),\net on exclut les 3 jours les plus récents.\n\nLe pipeline conserve :\n- une série indexée `(store_nbr, date) → transactions_roll14`,\n- la **dernière valeur connue par magasin**, utilisée comme fallback si la date est future.\n\n**Remarque pratique :**  \n`transactions_roll14` est une variable **au niveau magasin**.  \nDonc pour une même date \\(t\\) et un même magasin, elle est **identique pour tous les produits** du magasin.\nCette répétition est normale car la feature décrit l’activité globale du store.\n\n---\n\n#### b) Prix du pétrole : `oil_roll14` (sans gap additionnel)\n\nPour le pétrole, nous conservons une logique standard (sans *feature gap* supplémentaire) :\n\n$\n\\text{oil\\_roll14}(t)\n= \\frac{1}{14} \\sum_{j=1}^{14} \\text{prix}_{t-j}\n$\n\nLes valeurs manquantes sont propagées (`ffill`) avant calcul.\n\nPour les dates futures, le pipeline utilise une **valeur de secours** :\nla moyenne des 30 derniers jours observés.\n\n---\n\n### 1.2.4 Jours fériés et événements\n\nLes jours fériés sont traités selon une **logique hiérarchique** :\n\n- niveau **national** (tous les magasins),\n- niveau **régional** (état),\n- niveau **local** (ville).\n\nLes jours transférés (`transferred`) sont repositionnés à leur date effective.\nLe pipeline construit :\n\n- un indicateur national par date,\n- des overrides par `(store_nbr, date)` pour les niveaux régional et local.\n\n---\n\n### 1.3 Étape TRANSFORM : application aux nouvelles données\n\nL’étape `transform()` est utilisée sur :\n\n- les données d’entraînement enrichies,\n- les données de validation,\n- les données de test,\n- les observations unitaires (tests unitaires, production).\n\n#### Règle essentielle\n\nLors du `transform()` :\n- **aucune statistique n’est recalculée à partir des nouvelles données**,\n- seules les informations apprises pendant le `fit()` sont utilisées.\n\nCela garantit une parfaite cohérence entre entraînement et déploiement.\n\n---\n\n### 1.3.1 Gestion des dates futures et valeurs manquantes\n\nPour les dates jamais observées :\n\n- `transactions_roll14`  \n  - lookup si disponible via la série indexée,\n  - sinon **dernière valeur connue du magasin**,\n  - sinon 0.\n\n- `oil_roll14`  \n  - lookup si disponible,\n  - sinon **moyenne des 30 derniers jours observés**.\n\n- jours fériés  \n  - indicateurs fixés à 0 si absents.\n\nCes choix correspondent à des hypothèses réalistes de mise en production :\non n’utilise jamais d’informations futures inconnues.\n\n---\n\n### 1.4 Variables de séries temporelles : lags et moyennes glissantes (avec *feature gap*)\n\n#### 1.4.1 Motivation\n\nLes ventes présentent une forte dépendance temporelle :\neffets hebdomadaires, saisonnalité, inertie de la demande.\n\nPour capturer cette dynamique, on crée :\n\n- **lags** : ventes observées \\(k\\) jours avant,\n- **rolling means** : moyenne des ventes sur une fenêtre passée.\n\nChaque couple `(store_nbr, item_nbr)` est traité comme une série temporelle indépendante.\n\n---\n\n#### 1.4.2 Anti-leakage : règle fondamentale + retard d’accès\n\nPour prédire à la date \\(t\\), seules les informations **antérieures à \\(t\\)** sont autorisées,\net nous imposons en plus un retard de disponibilité de \\(G = \\text{feature\\_gap\\_days}\\).\n\nLa moyenne glissante sur \\(w\\) jours devient :\n\n$\n\\text{rolling}_w(t)\n= \\frac{1}{w} \\sum_{j=1}^{w} y_{t-(G+j)}\n$\n\nEt un lag \\(k\\) devient :\n\n$\n\\text{lag}_k(t) = y_{t-(G+k)}\n$\n\nAinsi, si \\(G = 3\\) :\n- `sales_roll14(t)` utilise les ventes de \\(t-4\\) à \\(t-17\\),\n- `sales_lag28(t)` utilise la vente à \\(t-31\\).\n\n---\n\n#### 1.4.3 Gestion des combinaisons absentes et choix des valeurs\n\nLes séries `(store, item)` peuvent être très **sparsées** :\ncertaines combinaisons n’ont pas de ventes observées dans l’historique récent.\n\nDans ce cas, plutôt que de remplacer par 0 (qui pourrait être interprété comme une vraie absence de vente),\nle pipeline conserve :\n- `sales_lag28` et `sales_roll14` sous forme de **NaN** lorsque la valeur est indisponible,\n- et fournit des indicateurs :\n  - `sales_lag28_avail` (disponible ou non),\n  - `sales_roll14_cnt` (nombre de jours effectivement observés sur 14),\n  - `sales_roll14_frac` (proportion de jours observés).\n\n👉 Cette stratégie est plus fidèle aux données : elle distingue “0 vente” de “historique manquant”.\n\n---\n\n### 1.5 Rôle du gap temporel (évaluation)\n\nUn **gap de split** peut être inséré entre train et test pour renforcer la séparation temporelle\net éviter que les modèles ne profitent d’une continuité trop directe entre périodes.\n\nIl s’agit d’une bonne pratique d’évaluation (simulation réaliste),\nindépendante du *feature gap* utilisé dans la construction des variables temporelles.\n\n---\n\n### 1.6 Résumé\n\n- Le Feature Engineering est entièrement **centralisé** dans un pipeline unique.\n- Le `fit()` apprend uniquement à partir de l’historique `train_fit`.\n- Le `transform()` applique les mêmes règles aux nouvelles données sans recalculer de statistiques.\n- Les variables temporelles de ventes (`lag`, `rolling`) et `transactions_roll14` intègrent un **feature gap** paramétrable.\n- `oil_roll14` reste calculé de manière standard (shift + rolling), avec fallback réaliste.\n- Les valeurs manquantes sur les séries de ventes sont conservées en **NaN**, accompagnées d’indicateurs de disponibilité.\n\n👉 Cette architecture garantit une méthodologie solide,\nreproductible et compatible avec un déploiement réel.\n","metadata":{"_uuid":"55d85d6d-b199-4b3d-b1a2-fef827a3a4f5","_cell_guid":"f726b304-89aa-4e44-a82a-c4f93592e0de","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport gc\n\n\nclass FavoritaFeaturePipeline:\n    \"\"\"\n    Pipeline Fit/Transform pour créer les features Favorita sans fuite d'information.\n\n    - feature_gap_days = retard d'accès aux données de ventes (et transactions si on le souhaite)\n      Exemple feature_gap_days=3 :\n        sales_roll14(t) utilise t-4..t-17\n        sales_lag28(t)  utilise t-31\n        transactions_roll14(t) utilise t-4..t-17 (dans ce pipeline)\n    - oil : on laisse le calcul standard (shift(1) rolling 14), sans gap additionnel.\n    - IMPORTANT : on garde NaN pour sales_lag28 et sales_roll14 quand l'historique n'existe pas.\n    \"\"\"\n\n    def __init__(self, data_dir, sales_history_days=120, feature_gap_days=0, verbose=True):\n        self.data_dir = data_dir\n        self.sales_history_days = int(sales_history_days)\n        self.feature_gap_days = int(feature_gap_days)\n        self.verbose = verbose\n\n        # learned objects\n        self.store_freq = None\n        self.item_freq  = None\n\n        self.items_fe = None\n        self.store_maps = {}\n\n        self.trans_s = None\n        self.last_roll_by_store = None\n\n        self.oil_s = None\n\n        self.holiday_nat_maps = {}\n        self.holiday_override_maps = {}\n\n        # sales lookup history\n        self.sales_s = None\n        self.sales_target_col = None\n        self.sales_history_max_date = None\n\n    # ---------------------------\n    # Utils\n    # ---------------------------\n    def _log(self, msg):\n        if self.verbose:\n            print(msg)\n\n    @staticmethod\n    def _ensure_datetime(df, col=\"date\"):\n        df[col] = pd.to_datetime(df[col]).dt.normalize()\n        return df\n\n    @staticmethod\n    def _make_key_store_date(store_series, date_series):\n        \"\"\"clé numérique compacte store-date\"\"\"\n        ymd = date_series.dt.year * 10000 + date_series.dt.month * 100 + date_series.dt.day\n        return (store_series.astype(\"int32\") * 1_000_000 + ymd.astype(\"int32\")).astype(\"int64\")\n\n    @staticmethod\n    def add_target(df, y_col=\"unit_sales\"):\n        \"\"\"\n        Optionnel : prépare la cible si elle n'existe pas.\n        - unit_sales_clean = clip des négatives à 0\n        - unit_sales_log   = log1p(unit_sales_clean)\n        \"\"\"\n        df = df.copy()\n        if \"unit_sales_clean\" not in df.columns:\n            df[\"unit_sales_clean\"] = pd.to_numeric(df[y_col], errors=\"coerce\").fillna(0).clip(lower=0).astype(\"float32\")\n        if \"unit_sales_log\" not in df.columns:\n            df[\"unit_sales_log\"] = np.log1p(df[\"unit_sales_clean\"]).astype(\"float32\")\n        return df\n\n    # ---------------------------\n    # FIT\n    # ---------------------------\n    def fit(self, history_df):\n        \"\"\"\n        history_df : dataframe historique (train_fit) contenant\n        date/store/item + cible (unit_sales_clean ou unit_sales_log)\n        \"\"\"\n        history_df = history_df.copy()\n        history_df = self._ensure_datetime(history_df, \"date\")\n\n        self._log(\"🔧 FIT...\")\n\n        # ---- détecter cible\n        if \"unit_sales_log\" in history_df.columns:\n            target_col = \"unit_sales_log\"\n        elif \"unit_sales_clean\" in history_df.columns:\n            target_col = \"unit_sales_clean\"\n        else:\n            raise ValueError(\"Il faut une colonne cible : unit_sales_clean ou unit_sales_log (tu peux appeler add_target avant).\")\n\n        self.sales_target_col = target_col\n\n        # forcer types\n        history_df[\"store_nbr\"] = history_df[\"store_nbr\"].astype(\"int16\")\n        history_df[\"item_nbr\"]  = history_df[\"item_nbr\"].astype(\"int32\")\n\n        # ---- freq enc (appris sur history_df)\n        self.store_freq = history_df[\"store_nbr\"].value_counts(normalize=True)\n        self.item_freq  = history_df[\"item_nbr\"].value_counts(normalize=True)\n\n        # ---- Sales lookup (limité sur sales_history_days)\n        max_d = history_df[\"date\"].max()\n        min_keep = max_d - pd.Timedelta(days=self.sales_history_days)\n\n        hs = history_df.loc[history_df[\"date\"] >= min_keep, [\"store_nbr\", \"item_nbr\", \"date\", target_col]].copy()\n        hs[\"store_nbr\"] = hs[\"store_nbr\"].astype(\"int16\")\n        hs[\"item_nbr\"]  = hs[\"item_nbr\"].astype(\"int32\")\n        hs[target_col]  = hs[target_col].astype(\"float32\")\n\n        hs = hs.drop_duplicates([\"store_nbr\", \"item_nbr\", \"date\"], keep=\"last\")\n        self.sales_history_max_date = hs[\"date\"].max()\n        self.sales_s = hs.set_index([\"store_nbr\", \"item_nbr\", \"date\"])[target_col]\n\n        del hs\n        gc.collect()\n\n        # ---- ITEMS\n        items = pd.read_csv(self.data_dir + \"/items.csv\")\n        items[\"item_nbr\"] = items[\"item_nbr\"].astype(\"int32\")\n        items[\"family\"] = items[\"family\"].fillna(\"UNKNOWN\").astype(str).str.strip()\n        items[\"class\"]  = items[\"class\"].fillna(-1).astype(\"int16\").astype(str)\n        items[\"perishable\"] = items[\"perishable\"].fillna(0).astype(\"int8\")\n\n        fam_freq = items[\"family\"].value_counts(normalize=True)\n        cls_freq = items[\"class\"].value_counts(normalize=True)\n\n        items[\"family_freq_items\"] = items[\"family\"].map(fam_freq).astype(\"float32\")\n        items[\"class_freq_items\"]  = items[\"class\"].map(cls_freq).astype(\"float32\")\n\n        pair_counts   = items.groupby([\"family\", \"class\"]).size()\n        family_counts = items.groupby(\"family\").size()\n\n        items[\"class_freq_in_family\"] = (\n            items.set_index([\"family\", \"class\"]).index.map(pair_counts) /\n            items[\"family\"].map(family_counts).to_numpy()\n        ).astype(\"float32\")\n\n        self.items_fe = items[[\n            \"item_nbr\", \"perishable\", \"family_freq_items\", \"class_freq_items\", \"class_freq_in_family\"\n        ]].drop_duplicates(\"item_nbr\")\n\n        del items\n        gc.collect()\n\n        # ---- STORES (freq apprises sur les OBS history_df)\n        stores = pd.read_csv(self.data_dir + \"/stores.csv\")\n        stores[\"store_nbr\"] = stores[\"store_nbr\"].astype(\"int16\")\n        stores[\"type\"]  = stores[\"type\"].fillna(\"UNKNOWN\").astype(str).str.strip()\n        stores[\"state\"] = stores[\"state\"].fillna(\"UNKNOWN\").astype(str).str.strip()\n        stores[\"city\"]  = stores[\"city\"].fillna(\"UNKNOWN\").astype(str).str.strip()\n        stores[\"cluster\"] = stores[\"cluster\"].fillna(-1).astype(\"int16\")\n\n        hist_loc = history_df[[\"store_nbr\"]].merge(\n            stores[[\"store_nbr\", \"type\", \"state\", \"city\", \"cluster\"]],\n            on=\"store_nbr\", how=\"left\"\n        )\n\n        type_freq  = hist_loc[\"type\"].value_counts(normalize=True)\n        state_freq = hist_loc[\"state\"].value_counts(normalize=True)\n        city_freq  = hist_loc[\"city\"].value_counts(normalize=True)\n\n        stores[\"type_freq\"]  = stores[\"type\"].map(type_freq).fillna(0).astype(\"float32\")\n        stores[\"state_freq\"] = stores[\"state\"].map(state_freq).fillna(0).astype(\"float32\")\n        stores[\"city_freq\"]  = stores[\"city\"].map(city_freq).fillna(0).astype(\"float32\")\n\n        pair_counts  = hist_loc.groupby([\"state\", \"city\"]).size()\n        state_counts = hist_loc.groupby(\"state\").size()\n\n        st = stores.set_index(\"store_nbr\")[[\"state\", \"city\"]]\n        num = st.index.map(lambda sn: pair_counts.get((st.loc[sn, \"state\"], st.loc[sn, \"city\"]), 0))\n        den = st[\"state\"].map(state_counts).fillna(0).to_numpy()\n\n        stores[\"city_freq_in_state\"] = np.where(den > 0, np.array(num) / den, 0.0).astype(\"float32\")\n\n        self.store_maps = {\n            \"cluster\": stores.set_index(\"store_nbr\")[\"cluster\"].to_dict(),\n            \"type_freq\": stores.set_index(\"store_nbr\")[\"type_freq\"].to_dict(),\n            \"state_freq\": stores.set_index(\"store_nbr\")[\"state_freq\"].to_dict(),\n            \"city_freq\": stores.set_index(\"store_nbr\")[\"city_freq\"].to_dict(),\n            \"city_freq_in_state\": stores.set_index(\"store_nbr\")[\"city_freq_in_state\"].to_dict(),\n        }\n\n        del stores, hist_loc\n        gc.collect()\n\n        # ---- TRANSACTIONS roll14 (avec le même gap que ventes)\n        tr = pd.read_csv(self.data_dir + \"/transactions.csv\", parse_dates=[\"date\"])\n        tr[\"date\"] = pd.to_datetime(tr[\"date\"]).dt.normalize()\n        tr[\"store_nbr\"] = tr[\"store_nbr\"].astype(\"int16\")\n        tr[\"transactions\"] = pd.to_numeric(tr[\"transactions\"], errors=\"coerce\").fillna(0)\n\n        tr = (tr.groupby([\"store_nbr\", \"date\"], as_index=False)\n                .agg(transactions=(\"transactions\", \"sum\"))\n                .sort_values([\"store_nbr\", \"date\"]))\n\n        G = self.feature_gap_days\n        tr[\"transactions_roll14\"] = (\n            tr.groupby(\"store_nbr\")[\"transactions\"]\n              .transform(lambda x: x.shift(1 + G).rolling(14, min_periods=1).mean())\n        ).astype(\"float32\")\n\n        self.trans_s = tr.set_index([\"store_nbr\", \"date\"])[\"transactions_roll14\"]\n        self.last_roll_by_store = tr.groupby(\"store_nbr\")[\"transactions_roll14\"].last()\n\n        del tr\n        gc.collect()\n\n        # ---- OIL roll14 (standard, sans gap additionnel)\n        oil = pd.read_csv(self.data_dir + \"/oil.csv\", parse_dates=[\"date\"]).sort_values(\"date\")\n        oil[\"date\"] = pd.to_datetime(oil[\"date\"]).dt.normalize()\n        oil[\"dcoilwtico\"] = pd.to_numeric(oil[\"dcoilwtico\"], errors=\"coerce\").ffill()\n        oil[\"oil_roll14\"] = oil[\"dcoilwtico\"].shift(1).rolling(14, min_periods=1).mean().astype(\"float32\")\n        self.oil_s = oil.set_index(\"date\")[\"oil_roll14\"]\n\n        del oil\n        gc.collect()\n\n        # ---- HOLIDAYS (national + override store)\n        hol = pd.read_csv(self.data_dir + \"/holidays_events.csv\")\n        hol[\"date\"] = pd.to_datetime(hol[\"date\"]).dt.normalize()\n        for col in [\"type\", \"locale\", \"locale_name\", \"description\"]:\n            hol[col] = hol[col].fillna(\"\").astype(str).str.strip()\n        hol[\"transferred\"] = hol[\"transferred\"].fillna(False).astype(int)\n\n        hol[\"event_key\"] = hol[\"description\"] + \" | \" + hol[\"locale\"] + \" | \" + hol[\"locale_name\"]\n\n        transfer_map = (\n            hol.loc[hol[\"type\"] == \"Transfer\", [\"event_key\", \"date\"]]\n               .drop_duplicates(\"event_key\")\n               .set_index(\"event_key\")[\"date\"]\n               .to_dict()\n        )\n\n        hol[\"observed_date\"] = hol[\"date\"]\n        mask_moved = (hol[\"type\"] == \"Holiday\") & (hol[\"transferred\"] == 1)\n        hol.loc[mask_moved, \"observed_date\"] = hol.loc[mask_moved, \"event_key\"].map(transfer_map)\n        hol[\"observed_date\"] = hol[\"observed_date\"].fillna(hol[\"date\"])\n\n        hol[\"f_holiday\"] = (hol[\"type\"] == \"Holiday\").astype(\"int8\")\n\n        stores_loc = pd.read_csv(self.data_dir + \"/stores.csv\")\n        stores_loc[\"store_nbr\"] = stores_loc[\"store_nbr\"].astype(\"int16\")\n        stores_loc[\"city\"]  = stores_loc[\"city\"].fillna(\"UNKNOWN\").astype(str).str.strip()\n        stores_loc[\"state\"] = stores_loc[\"state\"].fillna(\"UNKNOWN\").astype(str).str.strip()\n        store_loc = stores_loc[[\"store_nbr\", \"city\", \"state\"]].drop_duplicates(\"store_nbr\")\n\n        national = (hol[hol[\"locale\"] == \"National\"]\n                    .groupby(\"observed_date\", as_index=False)\n                    .agg(nat_h=(\"f_holiday\", lambda s: int(s.sum() > 0)))\n                    .rename(columns={\"observed_date\": \"date\"}))\n\n        self.holiday_nat_maps = {\"nat_h\": dict(zip(national[\"date\"], national[\"nat_h\"]))}\n\n        regional = hol[hol[\"locale\"] == \"Regional\"].copy()\n        regional[\"date\"] = regional[\"observed_date\"]\n        regional = regional.drop(columns=[\"observed_date\"]).merge(\n            store_loc, left_on=\"locale_name\", right_on=\"state\", how=\"inner\"\n        )\n        regional = (regional.groupby([\"date\", \"store_nbr\"], as_index=False)\n                    .agg(h=(\"f_holiday\", lambda s: int(s.sum() > 0))))\n\n        local = hol[hol[\"locale\"] == \"Local\"].copy()\n        local[\"date\"] = local[\"observed_date\"]\n        local = local.drop(columns=[\"observed_date\"]).merge(\n            store_loc, left_on=\"locale_name\", right_on=\"city\", how=\"inner\"\n        )\n        local = (local.groupby([\"date\", \"store_nbr\"], as_index=False)\n                 .agg(h=(\"f_holiday\", lambda s: int(s.sum() > 0))))\n\n        over = pd.concat([regional, local], ignore_index=True).groupby([\"date\", \"store_nbr\"], as_index=False).max()\n        over[\"key\"] = self._make_key_store_date(over[\"store_nbr\"], over[\"date\"])\n        self.holiday_override_maps = {\"h\": dict(zip(over[\"key\"], over[\"h\"]))}\n\n        del hol, stores_loc, store_loc, national, regional, local, over\n        gc.collect()\n\n        self._log(\"✅ FIT terminé.\")\n        return self\n\n    # ---------------------------\n    # TRANSFORM\n    # ---------------------------\n    def transform(self, df_new):\n        df = df_new.copy()\n        df = self._ensure_datetime(df, \"date\")\n\n        # types\n        df[\"store_nbr\"] = df[\"store_nbr\"].astype(\"int16\")\n        df[\"item_nbr\"]  = df[\"item_nbr\"].astype(\"int32\")\n\n        # ---- date feats\n        d = df[\"date\"]\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        df[\"dow\"]   = d.dt.dayofweek.astype(\"int8\")\n        df[\"is_weekend\"] = (df[\"dow\"] >= 5).astype(\"int8\")\n\n        # ---- promo (robuste)\n        if \"onpromotion\" in df.columns:\n            promo = df[\"onpromotion\"]\n            if promo.dtype == \"O\":\n                promo_bool = promo.astype(str).str.lower().isin([\"true\", \"1\", \"t\", \"yes\"])\n                promo_bool = promo_bool.where(promo.notna(), False)\n                promo = promo_bool\n            df[\"onpromo\"] = promo.fillna(False).astype(\"int8\")\n        else:\n            df[\"onpromo\"] = np.int8(0)\n\n        # ---- freq enc\n        df[\"store_freq\"] = pd.to_numeric(df[\"store_nbr\"].map(self.store_freq), errors=\"coerce\").fillna(0).astype(\"float32\")\n        df[\"item_freq\"]  = pd.to_numeric(df[\"item_nbr\"].map(self.item_freq),  errors=\"coerce\").fillna(0).astype(\"float32\")\n\n        # ---- items merge\n        df = df.merge(self.items_fe, on=\"item_nbr\", how=\"left\")\n        df[\"perishable\"] = pd.to_numeric(df[\"perishable\"], errors=\"coerce\").fillna(0).astype(\"int8\")\n        for c in [\"family_freq_items\", \"class_freq_items\", \"class_freq_in_family\"]:\n            df[c] = pd.to_numeric(df[c], errors=\"coerce\").fillna(0).astype(\"float32\")\n\n        # ---- stores maps\n        df[\"cluster\"] = pd.to_numeric(df[\"store_nbr\"].map(self.store_maps[\"cluster\"]), errors=\"coerce\").fillna(-1).astype(\"int16\")\n        for c in [\"type_freq\", \"state_freq\", \"city_freq\", \"city_freq_in_state\"]:\n            df[c] = pd.to_numeric(df[\"store_nbr\"].map(self.store_maps[c]), errors=\"coerce\").fillna(0).astype(\"float32\")\n\n        # ---- transactions_roll14\n        keys = pd.MultiIndex.from_frame(df[[\"store_nbr\", \"date\"]])\n        vals = self.trans_s.reindex(keys).to_numpy(dtype=\"float32\")\n        df[\"transactions_roll14\"] = pd.Series(vals, index=df.index)\n\n        last_vals = pd.to_numeric(df[\"store_nbr\"].map(self.last_roll_by_store), errors=\"coerce\")\n        df[\"transactions_roll14\"] = df[\"transactions_roll14\"].fillna(last_vals).fillna(0).astype(\"float32\")\n\n        # ---- oil_roll14\n        oil_vals = self.oil_s.reindex(df[\"date\"]).to_numpy(dtype=\"float32\")\n        df[\"oil_roll14\"] = pd.Series(oil_vals, index=df.index)\n        fallback_oil = float(self.oil_s.tail(30).mean()) if self.oil_s is not None and len(self.oil_s) else 0.0\n        df[\"oil_roll14\"] = df[\"oil_roll14\"].fillna(fallback_oil).astype(\"float32\")\n\n        # ---- holidays\n        df[\"is_holiday_effective\"] = df[\"date\"].map(self.holiday_nat_maps[\"nat_h\"]).fillna(0).astype(\"int8\")\n        k = self._make_key_store_date(df[\"store_nbr\"], df[\"date\"])\n        h_over = pd.Series(k).map(self.holiday_override_maps[\"h\"]).fillna(0).to_numpy()\n        df[\"is_holiday_effective\"] = np.maximum(df[\"is_holiday_effective\"].to_numpy(), h_over).astype(\"int8\")\n\n        # ============================\n        # SALES LAG28 + ROLL14 (NaN + gap)\n        # ============================\n        G = self.feature_gap_days\n\n        # --- LAG 28 (décalé)\n        L = 28\n        lag_idx = pd.MultiIndex.from_frame(pd.DataFrame({\n            \"store_nbr\": df[\"store_nbr\"].astype(\"int16\"),\n            \"item_nbr\":  df[\"item_nbr\"].astype(\"int32\"),\n            \"date\":      df[\"date\"] - pd.Timedelta(days=L + G)\n        }))\n        lag_vals = self.sales_s.reindex(lag_idx).to_numpy(dtype=\"float32\")\n\n        df[f\"sales_lag{L}\"] = pd.Series(lag_vals, index=df.index).astype(\"float32\")  # ✅ garde NaN\n        df[f\"sales_lag{L}_avail\"] = (~pd.isna(lag_vals)).astype(\"int8\")\n\n        # --- ROLL 14 (t-(G+1) .. t-(G+14))\n        W = 14\n        acc = np.zeros(len(df), dtype=\"float32\")\n        cnt = np.zeros(len(df), dtype=\"int16\")\n\n        for j in range(1, W + 1):\n            r_idx = pd.MultiIndex.from_frame(pd.DataFrame({\n                \"store_nbr\": df[\"store_nbr\"].astype(\"int16\"),\n                \"item_nbr\":  df[\"item_nbr\"].astype(\"int32\"),\n                \"date\":      df[\"date\"] - pd.Timedelta(days=G + j)\n            }))\n            vals = self.sales_s.reindex(r_idx).to_numpy(dtype=\"float32\")\n            mask = ~pd.isna(vals)\n            acc[mask] += vals[mask]\n            cnt[mask] += 1\n\n        roll = np.full(len(df), np.nan, dtype=\"float32\")\n        np.divide(acc, cnt, out=roll, where=cnt > 0)  # ✅ pas de warning\n\n        df[f\"sales_roll{W}\"] = pd.Series(roll, index=df.index).astype(\"float32\")\n        df[f\"sales_roll{W}_cnt\"] = cnt.astype(\"int16\")\n        df[f\"sales_roll{W}_frac\"] = (cnt / float(W)).astype(\"float32\")\n\n        return df\n","metadata":{"_uuid":"5be462a3-f710-44a7-97a2-0e80e0c47d7f","_cell_guid":"9321075e-3f79-433b-a923-04ccea52f3d5","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-12T13:59:39.611986Z","iopub.execute_input":"2026-01-12T13:59:39.612288Z","iopub.status.idle":"2026-01-12T13:59:39.772679Z","shell.execute_reply.started":"2026-01-12T13:59:39.612262Z","shell.execute_reply":"2026-01-12T13:59:39.771237Z"}},"outputs":[],"execution_count":5},{"cell_type":"markdown","source":"### 2.1. Application du pipeline sur les données d'entrainement (déjà split)","metadata":{"_uuid":"d353c916-dd1f-46cb-90f1-be258a52b9ca","_cell_guid":"1f1aa3ae-b696-451d-aa70-5f6eea540b56","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import pandas as pd\n\ntrain[\"date\"] = pd.to_datetime(train[\"date\"]).dt.normalize()\n\nTOTAL_DAYS = 84\nTEST_DAYS  = 14\nGAP_DAYS   = 3\n\nmax_date = train[\"date\"].max()\nstart_84 = max_date - pd.Timedelta(days=TOTAL_DAYS - 1)\n\ntrain_84 = train.loc[train[\"date\"] >= start_84].copy()\n\n# --- TEST ---\nend_test = max_date\nstart_test = end_test - pd.Timedelta(days=TEST_DAYS - 1)\n\n# --- GAP (juste avant test) ---\nend_gap = start_test - pd.Timedelta(days=1)\nstart_gap = end_gap - pd.Timedelta(days=GAP_DAYS - 1)\n\n# --- TRAIN = le reste ---\ntrain_fit = train_84.loc[train_84[\"date\"] < start_gap].copy()\ngap_df    = train_84.loc[(train_84[\"date\"] >= start_gap) & (train_84[\"date\"] <= end_gap)].copy()\ntest_df   = train_84.loc[(train_84[\"date\"] >= start_test) & (train_84[\"date\"] <= end_test)].copy()\n\nprint(\"Train:\", train_fit.shape, train_fit[\"date\"].min(), train_fit[\"date\"].max())\nprint(\"Gap  :\", gap_df.shape, gap_df[\"date\"].min(), gap_df[\"date\"].max())\nprint(\"Test :\", test_df.shape, test_df[\"date\"].min(), test_df[\"date\"].max())\n\n# FIT sur train uniquement\npipe = FavoritaFeaturePipeline(\n    \"/kaggle/working/favorita_data\",\n    sales_history_days=120,\n    feature_gap_days=3,\n    verbose=True\n)\n# 1) créer la cible sur train_fit et test_df (et gap_df aussi si on veut)\ntrain_fit = FavoritaFeaturePipeline.add_target(train_fit, y_col=\"unit_sales\")\ntest_df   = FavoritaFeaturePipeline.add_target(test_df,   y_col=\"unit_sales\")\ngap_df    = FavoritaFeaturePipeline.add_target(gap_df,    y_col=\"unit_sales\")\n\n \n# 2) fit\npipe.fit(train_fit)\n\n\n\n# TRANSFORM\nX_train = pipe.transform(train_fit)\nX_gap   = pipe.transform(gap_df)\nX_test  = pipe.transform(test_df)\nprint(\"Transform terminé\")","metadata":{"_uuid":"faea66d9-1a61-4575-af8d-56c7a4505852","_cell_guid":"57e9c919-288a-4a79-8040-eee47e36bdac","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-12T14:06:48.060356Z","iopub.execute_input":"2026-01-12T14:06:48.061109Z","iopub.status.idle":"2026-01-12T14:07:40.017620Z","shell.execute_reply.started":"2026-01-12T14:06:48.061086Z","shell.execute_reply":"2026-01-12T14:07:40.015640Z"}},"outputs":[{"name":"stdout","text":"Train: (7078551, 5) 2017-05-24 00:00:00 2017-07-29 00:00:00\nGap  : (327256, 5) 2017-07-30 00:00:00 2017-08-01 00:00:00\nTest : (1461581, 5) 2017-08-02 00:00:00 2017-08-15 00:00:00\n🔧 FIT...\n✅ FIT terminé.\n","output_type":"stream"}],"execution_count":7},{"cell_type":"code","source":"print(X_train.head())","metadata":{"_uuid":"a8d6c9ed-38b1-4ba3-8ef7-c56133e63214","_cell_guid":"60114afe-b63d-4813-96be-15d78341c55c","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-12T14:08:02.062226Z","iopub.execute_input":"2026-01-12T14:08:02.063460Z","iopub.status.idle":"2026-01-12T14:08:02.073935Z","shell.execute_reply.started":"2026-01-12T14:08:02.063399Z","shell.execute_reply":"2026-01-12T14:08:02.073083Z"}},"outputs":[{"name":"stdout","text":"        date  store_nbr  item_nbr  unit_sales onpromotion  unit_sales_clean  \\\n0 2017-05-24          1     99197        2.00       False              2.00   \n1 2017-05-24          1    103520        4.00       False              4.00   \n2 2017-05-24          1    103665       -1.00       False              0.00   \n3 2017-05-24          1    105574        5.00       False              5.00   \n4 2017-05-24          1    105575       12.00       False             12.00   \n\n   unit_sales_log  year  month  day  dow  is_weekend  onpromo  store_freq  \\\n0            1.10  2017      5   24    2           0        0        0.02   \n1            1.61  2017      5   24    2           0        0        0.02   \n2            0.00  2017      5   24    2           0        0        0.02   \n3            1.79  2017      5   24    2           0        0        0.02   \n4            2.56  2017      5   24    2           0        0        0.02   \n\n   item_freq  perishable  family_freq_items  class_freq_items  \\\n0       0.00           0               0.33              0.00   \n1       0.00           0               0.33              0.01   \n2       0.00           1               0.03              0.00   \n3       0.00           0               0.33              0.01   \n4       0.00           0               0.33              0.01   \n\n   class_freq_in_family  cluster  type_freq  state_freq  city_freq  \\\n0                  0.00       13       0.36        0.41       0.40   \n1                  0.04       13       0.36        0.41       0.40   \n2                  0.13       13       0.36        0.41       0.40   \n3                  0.02       13       0.36        0.41       0.40   \n4                  0.02       13       0.36        0.41       0.40   \n\n   city_freq_in_state  transactions_roll14  oil_roll14  is_holiday_effective  \\\n0                0.95              1534.71       48.22                     1   \n1                0.95              1534.71       48.22                     1   \n2                0.95              1534.71       48.22                     1   \n3                0.95              1534.71       48.22                     1   \n4                0.95              1534.71       48.22                     1   \n\n   sales_lag28  sales_lag28_avail  sales_roll14  sales_roll14_cnt  \\\n0          NaN                  0           NaN                 0   \n1          NaN                  0           NaN                 0   \n2          NaN                  0           NaN                 0   \n3          NaN                  0           NaN                 0   \n4          NaN                  0           NaN                 0   \n\n   sales_roll14_frac  \n0               0.00  \n1               0.00  \n2               0.00  \n3               0.00  \n4               0.00  \n","output_type":"stream"}],"execution_count":8},{"cell_type":"code","source":"print(X_test.head())","metadata":{"_uuid":"87fd7e9b-8206-4467-9b9d-cbd1c67aaa91","_cell_guid":"cecf5d78-e685-46e5-9663-1714b6a9e87d","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-12T14:08:41.860909Z","iopub.execute_input":"2026-01-12T14:08:41.861717Z","iopub.status.idle":"2026-01-12T14:08:41.872384Z","shell.execute_reply.started":"2026-01-12T14:08:41.861693Z","shell.execute_reply":"2026-01-12T14:08:41.871262Z"}},"outputs":[{"name":"stdout","text":"        date  store_nbr  item_nbr  unit_sales onpromotion  unit_sales_clean  \\\n0 2017-08-02          1     96995        1.00       False              1.00   \n1 2017-08-02          1    103520        1.00       False              1.00   \n2 2017-08-02          1    103665        2.00       False              2.00   \n3 2017-08-02          1    105574        8.00       False              8.00   \n4 2017-08-02          1    105575        8.00       False              8.00   \n\n   unit_sales_log  year  month  day  dow  is_weekend  onpromo  store_freq  \\\n0            0.69  2017      8    2    2           0        0        0.02   \n1            0.69  2017      8    2    2           0        0        0.02   \n2            1.10  2017      8    2    2           0        0        0.02   \n3            2.20  2017      8    2    2           0        0        0.02   \n4            2.20  2017      8    2    2           0        0        0.02   \n\n   item_freq  perishable  family_freq_items  class_freq_items  \\\n0       0.00           0               0.33              0.00   \n1       0.00           0               0.33              0.01   \n2       0.00           1               0.03              0.00   \n3       0.00           0               0.33              0.01   \n4       0.00           0               0.33              0.01   \n\n   class_freq_in_family  cluster  type_freq  state_freq  city_freq  \\\n0                  0.00       13       0.36        0.41       0.40   \n1                  0.04       13       0.36        0.41       0.40   \n2                  0.13       13       0.36        0.41       0.40   \n3                  0.02       13       0.36        0.41       0.40   \n4                  0.02       13       0.36        0.41       0.40   \n\n   city_freq_in_state  transactions_roll14  oil_roll14  is_holiday_effective  \\\n0                0.95              1545.86       47.53                     0   \n1                0.95              1545.86       47.53                     0   \n2                0.95              1545.86       47.53                     0   \n3                0.95              1545.86       47.53                     0   \n4                0.95              1545.86       47.53                     0   \n\n   sales_lag28  sales_lag28_avail  sales_roll14  sales_roll14_cnt  \\\n0          NaN                  0          1.24                 2   \n1          NaN                  0          1.03                11   \n2         0.69                  1          1.36                 9   \n3          NaN                  0          1.80                14   \n4         1.95                  1          2.24                14   \n\n   sales_roll14_frac  \n0               0.14  \n1               0.79  \n2               0.64  \n3               1.00  \n4               1.00  \n","output_type":"stream"}],"execution_count":9},{"cell_type":"markdown","source":"### 2.1. Tests unitaires du pipeline\n\n---\n\n#### 2.1.1 Pourquoi faire des tests unitaires dans un projet de prévision des ventes ?\n\nDans un projet de prévision **temporelle** (comme Favorita), le risque principal n’est pas seulement d’avoir des bugs,\nmais surtout d’avoir un pipeline qui :\n\n- produit des features incohérentes entre train et test,\n- introduit une fuite d’information (*data leakage*) sans qu’on s’en rende compte,\n- casse en “production” (ex. date future, item rare, store absent, promo manquante),\n- renvoie des valeurs `NaN`, `inf`, ou des features mal typées.\n\nLes **tests unitaires** permettent donc de vérifier que le pipeline fonctionne comme un **module fiable** :  \n> on donne une entrée (même très petite) → on obtient une sortie stable, cohérente et exploitable par le modèle.\n\n---\n\n#### 2.1.2 Qu’appelle-t-on “test unitaire” ici ?\n\nDans ce projet, un test unitaire correspond à une vérification automatique sur une ou plusieurs propriétés du pipeline, par exemple :\n\n- le pipeline crée bien toutes les colonnes attendues,\n- aucune feature n’est recalculée à partir des nouvelles données (validation/test/production),\n- les lags et rollings proviennent uniquement de l’historique appris au `fit()`,\n- les valeurs de secours (*fallbacks*) sont correctes (dates futures),\n- les features restent numériques (et donc compatibles avec le modèle).\n\nDans le contexte Favorita, on distingue deux grandes familles de tests unitaires :\n\n1) **Tests sur des datasets (train/test)**\n   - vérifier dimensions, colonnes, absence de valeurs impossibles,\n   - vérifier cohérence temporelle (split + gap).\n\n2) **Tests “production” sur une observation unique**\n   - vérifier que le pipeline marche quand on lui donne une seule ligne :  \n     `(date, store_nbr, item_nbr, onpromotion)`.\n\n---\n\n#### 2.1.3 Unit test “production” : une seule ligne en entrée\n\n##### Objectif du test\n\nEn situation réelle, le modèle reçoit souvent une ligne du type :\n\n- date = `2017-08-16`\n- store_nbr = `1`\n- item_nbr = `96995`\n- onpromotion = `False`\n\nL’utilisateur ne fournit **jamais** les ventes du jour (la cible) : c’est justement ce qu’on veut prédire.\n\nLe test unitaire vérifie donc que :\n\n- le pipeline ne demande pas `unit_sales`,\n- il enrichit correctement la ligne avec toutes les features,\n- il gère les dates futures sans planter,\n- il respecte la logique anti-leakage et le **feature gap**.\n\n---\n\n#### 2.1.4 Comment les features sont-elles calculées dans ce cas ?\n\nLe pipeline applique exactement la même logique que pour le test ou la validation.\n\n##### A) Features calendrier (toujours disponibles)\nCalculées directement à partir de la date :\n\n- `year`, `month`, `day`, `dow`, `is_weekend`, etc.\n\n> Aucun besoin d’historique.\n\n##### B) Promotion\nSi `onpromotion` est fourni :\n\n- `onpromo = 1` si True, sinon 0\n\nSi non fourni :\n\n- `onpromo = 0` par défaut\n\n##### C) Encodages statistiques (`store_freq`, `item_freq`)\nCes variables ne sont **pas recalculées** sur la nouvelle ligne.\n\nElles utilisent des objets appris au `fit()` :\n\n- `store_freq = fréquence du store dans train_fit`\n- `item_freq = fréquence du produit dans train_fit`\n\nSi le store/item n’existe pas dans l’historique :\n\n- le `map()` renvoie `NaN`\n- puis `fillna(0)` → on obtient `0`\n\n##### D) Variables externes : `transactions_roll14` (avec *feature gap*)\nCette feature est obtenue par lookup via :\n\n- index `(store_nbr, date)`\n\nDeux cas :\n\n1. **Date disponible dans `transactions.csv`**  \n   → on récupère directement la valeur `transactions_roll14` calculée dans `fit()`\n\n2. **Date future ou manquante**  \n   → fallback : la **dernière valeur connue** du magasin  \n   `last_roll_by_store[store]`\n\nSi même ça n’existe pas (store absent) :\n\n- fallback final = `0`\n\n**Important :** `transactions_roll14` est une variable **au niveau magasin**.  \nDonc pour un même `(store, date)`, elle est identique pour tous les items du store : c’est normal.\n\n**Feature gap :** si `feature_gap_days = G`, alors :\n\n\\[\ntransactions\\_roll14(t)\n= \\frac{1}{14} \\sum_{j=1}^{14} transactions_{t-(G+j)}\n\\]\n\nAutrement dit, si \\(G=3\\), on utilise \\(t-4..t-17\\) (et pas les 3 jours les plus récents).\n\n##### E) Variables externes : `oil_roll14` (sans gap additionnel)\nMême principe :\n\n1. si la date existe dans `oil.csv` → lookup direct  \n2. sinon → fallback : moyenne des 30 derniers jours connus (calculée au fit)\n\nLe test unitaire vérifie donc que le pipeline ne casse pas quand la date est future.\n\n##### F) Variables jours fériés : `is_holiday_effective`\nLe pipeline vérifie si la date est un jour férié :\n\n- d’abord au niveau national,\n- puis overwrite au niveau store (local/régional) si applicable.\n\nSi aucune correspondance :\n\n- `is_holiday_effective = 0`\n\n##### G) Variables de séries temporelles (cible) : lags et rollings (avec *feature gap*)\nCes variables doivent absolument respecter l’anti-leakage et le retard d’accès aux informations.\n\nDans le pipeline :\n\n- `sales_history_days = 120` (fenêtre max d’historique stockée)\n- `sales_lag28`\n- `sales_roll14`\n\nLe pipeline stocke une série historique :\n\n\\[\n(store\\_nbr, item\\_nbr, date) \\rightarrow y\n\\]\navec \\(y = unit\\_sales\\_log\\) (ou `unit_sales_clean` selon la cible utilisée).\n\n**Lag 28 avec gap \\(G\\)** :\n\\[\nsales\\_lag28(t) = y_{t-(28+G)}\n\\]\n\n- le pipeline cherche la valeur à la date `t-(28+G)`\n- si absent → la valeur reste **NaN**\n- on ajoute un indicateur :\n  - `sales_lag28_avail = 1` si trouvé, sinon 0\n\n**Rolling 14 avec gap \\(G\\)** :\n\\[\nsales\\_roll14(t) = \\frac{1}{cnt}\\sum_{j=1}^{14} y_{t-(G+j)}\n\\]\n\n- le pipeline cherche `y(t-(G+1)) ... y(t-(G+14))`\n- il additionne ce qui existe (`acc`)\n- il compte le nombre de valeurs trouvées (`cnt`)\n\nRésultat :\n\n- si `cnt > 0` : `sales_roll14 = acc / cnt`\n- sinon : `sales_roll14 = NaN`\n\nOn conserve aussi :\n- `sales_roll14_cnt`\n- `sales_roll14_frac = cnt/14`\n\n✅ **Pourquoi c’est important ?**  \nParce que la stratégie `NaN + cnt/frac` permet de distinguer :\n- “historique manquant” → NaN (avec `cnt=0`)\n- “ventes réellement proches de 0” → valeur proche de 0 (avec `cnt>0`)\n\n---\n\n\n\n#### 2.1.6 Exemple de scénario de test unitaire “clair”\n\nOn choisit une observation :\n\n- date = `2017-08-16`\n- store = `1`\n- item = `96995`\n- promo = `False`\n\nLe pipeline :\n\n1. crée les features date (`dow`, `month`, etc.)\n2. convertit `promo` en indicateur numérique\n3. récupère `store_freq` et `item_freq` appris au fit\n4. récupère `transactions_roll14` (lookup ou dernière valeur store)\n5. récupère `oil_roll14` (lookup ou moyenne 30j)\n6. détermine `is_holiday_effective`\n7. calcule `sales_lag28` et `sales_roll14` avec **feature gap**, à partir de l’historique uniquement\n8. renvoie une ligne enrichie exploitable par le modèle\n\n","metadata":{"_uuid":"47bec59b-59ed-442f-937a-aed0a3407782","_cell_guid":"d5e07731-1611-4540-9702-348f63ef26f0","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false}}},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\n# 1) Exemple d'input \"prod\"\nnew_row = pd.DataFrame({\n    \"date\": [\"2017-08-16\"],\n    \"store_nbr\": [2],\n    \"item_nbr\": [396540],\n    \"onpromotion\": [False]\n})\nnew_row[\"date\"] = pd.to_datetime(new_row[\"date\"])\n\n# 2) Transform\nnew_enriched = pipe.transform(new_row)\n\nprint(\"=== INPUT ===\")\nprint(new_row)\n\nprint(\"\\n=== OUTPUT (features) ===\")\nprint(new_enriched.T)  # transpose pour voir colonne par colonne","metadata":{"_uuid":"8b893b25-ed07-4d59-a3cd-05b05ccd2cf5","_cell_guid":"f87ef570-6e61-494a-83f1-a889c34ae255","trusted":true,"collapsed":false,"jupyter":{"outputs_hidden":false},"execution":{"iopub.status.busy":"2026-01-12T14:09:07.332874Z","iopub.execute_input":"2026-01-12T14:09:07.333192Z","iopub.status.idle":"2026-01-12T14:09:07.400121Z","shell.execute_reply.started":"2026-01-12T14:09:07.333172Z","shell.execute_reply":"2026-01-12T14:09:07.399320Z"}},"outputs":[{"name":"stdout","text":"=== INPUT ===\n        date  store_nbr  item_nbr  onpromotion\n0 2017-08-16          2    396540        False\n\n=== OUTPUT (features) ===\n                                        0\ndate                  2017-08-16 00:00:00\nstore_nbr                               2\nitem_nbr                           396540\nonpromotion                         False\nyear                                 2017\nmonth                                   8\nday                                    16\ndow                                     2\nis_weekend                              0\nonpromo                                 0\nstore_freq                           0.02\nitem_freq                            0.00\nperishable                              0\nfamily_freq_items                    0.00\nclass_freq_items                     0.00\nclass_freq_in_family                 0.00\ncluster                                13\ntype_freq                            0.36\nstate_freq                           0.41\ncity_freq                            0.40\ncity_freq_in_state                   0.95\ntransactions_roll14               1806.50\noil_roll14                          49.06\nis_holiday_effective                    0\nsales_lag28                           NaN\nsales_lag28_avail                       0\nsales_roll14                          NaN\nsales_roll14_cnt                        0\nsales_roll14_frac                    0.00\n","output_type":"stream"}],"execution_count":11}]}