{ "cells": [ { "cell_type": "markdown", "id": "6083b4ff", "metadata": {}, "source": [ "# 02 - Préparation des features (Feature Engineering)\n", "\n", "Ce notebook implémente le processus complet de préparation des données pour le projet Home Credit Default Risk.\n", "\n", "**Objectifs principaux :**\n", "- Charger et fusionner toutes les tables de données\n", "- Créer des features (caractéristiques) pertinentes par agrégation\n", "- Encoder les variables catégorielles\n", "- Préparer le jeu de données final pour la modélisation\n", "\n", "**Approche utilisée :**\n", "- Fonction modulaire pour chaque table de données\n", "- Agrégations statistiques (min, max, mean, sum, var) sur les données groupées\n", "- Création de ratios et pourcentages entre variables importantes\n", "- Features spécifiques pour les crédits actifs/fermés et les demandes approuvées/refusées" ] }, { "cell_type": "code", "execution_count": 1, "id": "ec6ca912", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ Bibliothèques importées avec succès\n" ] } ], "source": [ "# Import des bibliothèques nécessaires\n", "import numpy as np\n", "import pandas as pd\n", "import gc # Garbage collector pour libérer la mémoire\n", "import time\n", "from contextlib import contextmanager\n", "import warnings\n", "warnings.simplefilter(action='ignore', category=FutureWarning)\n", "\n", "print(\"✓ Bibliothèques importées avec succès\")" ] }, { "cell_type": "markdown", "id": "2eafa35c", "metadata": {}, "source": [ "## 1. Fonctions utilitaires\n", "\n", "Nous commençons par définir des fonctions helper qui seront utilisées tout au long du notebook." ] }, { "cell_type": "code", "execution_count": 2, "id": "790000b4", "metadata": {}, "outputs": [], "source": [ "# Fonction pour mesurer le temps d'exécution\n", "@contextmanager\n", "def timer(title):\n", " \"\"\"\n", " Context manager pour mesurer le temps d'exécution d'un bloc de code.\n", " Usage: with timer(\"Mon processus\"):\n", " # code à mesurer\n", " \"\"\"\n", " t0 = time.time()\n", " yield\n", " print(\"{} - terminé en {:.0f}s\".format(title, time.time() - t0))" ] }, { "cell_type": "markdown", "id": "f36b5c19", "metadata": {}, "source": [ "### Encodage One-Hot des variables catégorielles\n", "\n", "Le One-Hot encoding transforme les variables catégorielles en colonnes binaires (0 ou 1).\n", "Par exemple, si une colonne \"Couleur\" contient [\"Rouge\", \"Bleu\"], elle sera transformée en deux colonnes : \"Couleur_Rouge\" et \"Couleur_Bleu\"." ] }, { "cell_type": "code", "execution_count": 3, "id": "b02ee9c3", "metadata": {}, "outputs": [], "source": [ "def one_hot_encoder(df, nan_as_category=True):\n", " \"\"\"\n", " Applique le One-Hot encoding aux colonnes catégorielles.\n", " \n", " Paramètres:\n", " -----------\n", " df : DataFrame\n", " Le DataFrame à encoder\n", " nan_as_category : bool\n", " Si True, les valeurs manquantes (NaN) sont traitées comme une catégorie à part\n", " \n", " Retourne:\n", " ---------\n", " df : DataFrame encodé\n", " new_columns : liste des nouvelles colonnes créées\n", " \"\"\"\n", " original_columns = list(df.columns)\n", " # Identifier les colonnes avec type 'object' (chaînes de caractères = catégorielles)\n", " categorical_columns = [col for col in df.columns if df[col].dtype == 'object']\n", " # Appliquer pd.get_dummies pour créer les colonnes binaires\n", " df = pd.get_dummies(df, columns=categorical_columns, dummy_na=nan_as_category)\n", " # Retourner aussi la liste des nouvelles colonnes créées\n", " new_columns = [c for c in df.columns if c not in original_columns]\n", " return df, new_columns" ] }, { "cell_type": "markdown", "id": "a9acd8b9", "metadata": {}, "source": [ "## 2. Traitement de application_train.csv et application_test.csv\n", "\n", "Ces fichiers contiennent les informations principales sur chaque demande de crédit (données du client, montants, etc.)." ] }, { "cell_type": "code", "execution_count": 4, "id": "3945bb46", "metadata": {}, "outputs": [], "source": [ "def application_train_test(num_rows=None, nan_as_category=False):\n", " \"\"\"\n", " Charge et prétraite les données d'application (train + test).\n", " \n", " Étapes :\n", " 1. Charge les fichiers train et test\n", " 2. Fusionne les deux datasets\n", " 3. Nettoie les données (suppression de valeurs aberrantes)\n", " 4. Encode les variables catégorielles\n", " 5. Crée de nouvelles features (ratios, pourcentages)\n", " \"\"\"\n", " # Chargement des données\n", " df = pd.read_csv('../data/raw/application_train.csv', nrows=num_rows)\n", " test_df = pd.read_csv('../data/raw/application_test.csv', nrows=num_rows)\n", " print(\"Échantillons train: {}, test: {}\".format(len(df), len(test_df)))\n", " \n", " # Fusionner train et test pour appliquer les mêmes transformations\n", " # Note: Utiliser pd.concat() au lieu de .append() (deprecated dans pandas 2.0+)\n", " df = pd.concat([df, test_df], ignore_index=True)\n", " \n", " # Nettoyage : Supprimer les 4 applications avec CODE_GENDER = 'XNA' (valeur aberrante)\n", " df = df[df['CODE_GENDER'] != 'XNA']\n", " \n", " # Encodage binaire (0 ou 1) pour les features avec seulement 2 catégories\n", " for bin_feature in ['CODE_GENDER', 'FLAG_OWN_CAR', 'FLAG_OWN_REALTY']:\n", " df[bin_feature], uniques = pd.factorize(df[bin_feature])\n", " \n", " # One-Hot encoding pour les autres features catégorielles\n", " df, cat_cols = one_hot_encoder(df, nan_as_category)\n", " \n", " # Nettoyage : La valeur 365243 pour DAYS_EMPLOYED est une valeur sentinel (code pour \"inconnu\")\n", " # On la remplace par NaN\n", " df['DAYS_EMPLOYED'].replace(365243, np.nan, inplace=True)\n", " \n", " # Création de nouvelles features (ratios et pourcentages)\n", " # Ces ratios sont souvent plus informatifs que les valeurs absolues\n", " \n", " # Pourcentage d'emploi par rapport à l'âge\n", " df['DAYS_EMPLOYED_PERC'] = df['DAYS_EMPLOYED'] / df['DAYS_BIRTH']\n", " \n", " # Pourcentage du crédit par rapport au revenu\n", " df['INCOME_CREDIT_PERC'] = df['AMT_INCOME_TOTAL'] / df['AMT_CREDIT']\n", " \n", " # Revenu par personne dans le foyer\n", " df['INCOME_PER_PERSON'] = df['AMT_INCOME_TOTAL'] / df['CNT_FAM_MEMBERS']\n", " \n", " # Pourcentage de l'annuité par rapport au revenu (capacité de remboursement)\n", " df['ANNUITY_INCOME_PERC'] = df['AMT_ANNUITY'] / df['AMT_INCOME_TOTAL']\n", " \n", " # Taux de paiement : annuité / montant du crédit\n", " df['PAYMENT_RATE'] = df['AMT_ANNUITY'] / df['AMT_CREDIT']\n", " \n", " # Libération de la mémoire\n", " del test_df\n", " gc.collect()\n", " \n", " return df" ] }, { "cell_type": "markdown", "id": "c64f7dff", "metadata": {}, "source": [ "## 3. Traitement de bureau.csv et bureau_balance.csv\n", "\n", "**bureau.csv** : Historique des crédits antérieurs du client auprès d'autres institutions financières \n", "**bureau_balance.csv** : Historique mensuel des soldes pour ces crédits bureau\n", "\n", "**Stratégie :**\n", "- Agréger bureau_balance au niveau bureau (une ligne par crédit)\n", "- Créer des features distinctes pour les crédits ACTIFS vs FERMÉS\n", "- Agréger au niveau client (SK_ID_CURR)" ] }, { "cell_type": "code", "execution_count": 5, "id": "0f2323dc", "metadata": {}, "outputs": [], "source": [ "def bureau_and_balance(num_rows=None, nan_as_category=True):\n", " \"\"\"\n", " Traite les données bureau (crédits externes du client).\n", " \n", " Étapes :\n", " 1. Charge bureau et bureau_balance\n", " 2. Agrège bureau_balance par crédit (SK_ID_BUREAU)\n", " 3. Fusionne avec bureau\n", " 4. Crée des agrégations générales par client\n", " 5. Crée des features spécifiques pour crédits actifs\n", " 6. Crée des features spécifiques pour crédits fermés\n", " \"\"\"\n", " # Chargement des données\n", " bureau = pd.read_csv('../data/raw/bureau.csv', nrows=num_rows)\n", " bb = pd.read_csv('../data/raw/bureau_balance.csv', nrows=num_rows)\n", " \n", " # Encodage des variables catégorielles\n", " bb, bb_cat = one_hot_encoder(bb, nan_as_category)\n", " bureau, bureau_cat = one_hot_encoder(bureau, nan_as_category)\n", " \n", " # === BUREAU BALANCE : Agrégation au niveau crédit ===\n", " # Pour chaque crédit (SK_ID_BUREAU), on calcule des statistiques sur les mois\n", " bb_aggregations = {'MONTHS_BALANCE': ['min', 'max', 'size']}\n", " # Pour chaque colonne catégorielle encodée, on calcule la moyenne\n", " for col in bb_cat:\n", " bb_aggregations[col] = ['mean']\n", " \n", " bb_agg = bb.groupby('SK_ID_BUREAU').agg(bb_aggregations)\n", " # Renommer les colonnes pour indiquer la provenance\n", " bb_agg.columns = pd.Index([e[0] + \"_\" + e[1].upper() for e in bb_agg.columns.tolist()])\n", " \n", " # Joindre les agrégations de bureau_balance à bureau\n", " bureau = bureau.join(bb_agg, how='left', on='SK_ID_BUREAU')\n", " bureau.drop(['SK_ID_BUREAU'], axis=1, inplace=True)\n", " \n", " del bb, bb_agg\n", " gc.collect()\n", " \n", " # === BUREAU : Agrégations numériques ===\n", " # Définir les agrégations à calculer pour chaque feature numérique\n", " num_aggregations = {\n", " 'DAYS_CREDIT': ['min', 'max', 'mean', 'var'],\n", " 'DAYS_CREDIT_ENDDATE': ['min', 'max', 'mean'],\n", " 'DAYS_CREDIT_UPDATE': ['mean'],\n", " 'CREDIT_DAY_OVERDUE': ['max', 'mean'],\n", " 'AMT_CREDIT_MAX_OVERDUE': ['mean'],\n", " 'AMT_CREDIT_SUM': ['max', 'mean', 'sum'],\n", " 'AMT_CREDIT_SUM_DEBT': ['max', 'mean', 'sum'],\n", " 'AMT_CREDIT_SUM_OVERDUE': ['mean'],\n", " 'AMT_CREDIT_SUM_LIMIT': ['mean', 'sum'],\n", " 'AMT_ANNUITY': ['max', 'mean'],\n", " 'CNT_CREDIT_PROLONG': ['sum'],\n", " 'MONTHS_BALANCE_MIN': ['min'],\n", " 'MONTHS_BALANCE_MAX': ['max'],\n", " 'MONTHS_BALANCE_SIZE': ['mean', 'sum']\n", " }\n", " \n", " # === BUREAU : Agrégations catégorielles ===\n", " cat_aggregations = {}\n", " for cat in bureau_cat:\n", " cat_aggregations[cat] = ['mean']\n", " for cat in bb_cat:\n", " cat_aggregations[cat + \"_MEAN\"] = ['mean']\n", " \n", " # Agrégation générale par client (SK_ID_CURR)\n", " bureau_agg = bureau.groupby('SK_ID_CURR').agg({**num_aggregations, **cat_aggregations})\n", " bureau_agg.columns = pd.Index(['BURO_' + e[0] + \"_\" + e[1].upper() for e in bureau_agg.columns.tolist()])\n", " \n", " # === CRÉDITS ACTIFS : Features spécifiques ===\n", " # Filtrer uniquement les crédits actifs et créer des agrégations spécifiques\n", " active = bureau[bureau['CREDIT_ACTIVE_Active'] == 1]\n", " active_agg = active.groupby('SK_ID_CURR').agg(num_aggregations)\n", " active_agg.columns = pd.Index(['ACTIVE_' + e[0] + \"_\" + e[1].upper() for e in active_agg.columns.tolist()])\n", " bureau_agg = bureau_agg.join(active_agg, how='left', on='SK_ID_CURR')\n", " \n", " del active, active_agg\n", " gc.collect()\n", " \n", " # === CRÉDITS FERMÉS : Features spécifiques ===\n", " # Même logique pour les crédits fermés\n", " closed = bureau[bureau['CREDIT_ACTIVE_Closed'] == 1]\n", " closed_agg = closed.groupby('SK_ID_CURR').agg(num_aggregations)\n", " closed_agg.columns = pd.Index(['CLOSED_' + e[0] + \"_\" + e[1].upper() for e in closed_agg.columns.tolist()])\n", " bureau_agg = bureau_agg.join(closed_agg, how='left', on='SK_ID_CURR')\n", " \n", " del closed, closed_agg, bureau\n", " gc.collect()\n", " \n", " return bureau_agg" ] }, { "cell_type": "markdown", "id": "614f1115", "metadata": {}, "source": [ "## 4. Traitement de previous_application.csv\n", "\n", "Ce fichier contient toutes les demandes de crédit précédentes du client chez Home Credit.\n", "\n", "**Stratégie :**\n", "- Créer des agrégations générales\n", "- Features spécifiques pour demandes APPROUVÉES\n", "- Features spécifiques pour demandes REFUSÉES" ] }, { "cell_type": "code", "execution_count": 6, "id": "26379308", "metadata": {}, "outputs": [], "source": [ "def previous_applications(num_rows=None, nan_as_category=True):\n", " \"\"\"\n", " Traite les demandes de crédit précédentes.\n", " \n", " Étapes :\n", " 1. Charge previous_application\n", " 2. Nettoie les valeurs sentinelles (365243 = inconnu)\n", " 3. Crée de nouvelles features (ratios)\n", " 4. Agrégations générales par client\n", " 5. Features spécifiques pour demandes approuvées\n", " 6. Features spécifiques pour demandes refusées\n", " \"\"\"\n", " # Chargement des données\n", " prev = pd.read_csv('../data/raw/previous_application.csv', nrows=num_rows)\n", " prev, cat_cols = one_hot_encoder(prev, nan_as_category=True)\n", " \n", " # Nettoyage : Remplacer les valeurs sentinel 365243 par NaN\n", " prev['DAYS_FIRST_DRAWING'].replace(365243, np.nan, inplace=True)\n", " prev['DAYS_FIRST_DUE'].replace(365243, np.nan, inplace=True)\n", " prev['DAYS_LAST_DUE_1ST_VERSION'].replace(365243, np.nan, inplace=True)\n", " prev['DAYS_LAST_DUE'].replace(365243, np.nan, inplace=True)\n", " prev['DAYS_TERMINATION'].replace(365243, np.nan, inplace=True)\n", " \n", " # Nouvelle feature : Pourcentage entre montant demandé et montant reçu\n", " # Indique si le client a obtenu ce qu'il demandait\n", " prev['APP_CREDIT_PERC'] = prev['AMT_APPLICATION'] / prev['AMT_CREDIT']\n", " \n", " # === Agrégations numériques ===\n", " num_aggregations = {\n", " 'AMT_ANNUITY': ['min', 'max', 'mean'],\n", " 'AMT_APPLICATION': ['min', 'max', 'mean'],\n", " 'AMT_CREDIT': ['min', 'max', 'mean'],\n", " 'APP_CREDIT_PERC': ['min', 'max', 'mean', 'var'],\n", " 'AMT_DOWN_PAYMENT': ['min', 'max', 'mean'],\n", " 'AMT_GOODS_PRICE': ['min', 'max', 'mean'],\n", " 'HOUR_APPR_PROCESS_START': ['min', 'max', 'mean'],\n", " 'RATE_DOWN_PAYMENT': ['min', 'max', 'mean'],\n", " 'DAYS_DECISION': ['min', 'max', 'mean'],\n", " 'CNT_PAYMENT': ['mean', 'sum'],\n", " }\n", " \n", " # === Agrégations catégorielles ===\n", " cat_aggregations = {}\n", " for cat in cat_cols:\n", " cat_aggregations[cat] = ['mean']\n", " \n", " # Agrégation générale par client\n", " prev_agg = prev.groupby('SK_ID_CURR').agg({**num_aggregations, **cat_aggregations})\n", " prev_agg.columns = pd.Index(['PREV_' + e[0] + \"_\" + e[1].upper() for e in prev_agg.columns.tolist()])\n", " \n", " # === DEMANDES APPROUVÉES : Features spécifiques ===\n", " approved = prev[prev['NAME_CONTRACT_STATUS_Approved'] == 1]\n", " approved_agg = approved.groupby('SK_ID_CURR').agg(num_aggregations)\n", " approved_agg.columns = pd.Index(['APPROVED_' + e[0] + \"_\" + e[1].upper() for e in approved_agg.columns.tolist()])\n", " prev_agg = prev_agg.join(approved_agg, how='left', on='SK_ID_CURR')\n", " \n", " # === DEMANDES REFUSÉES : Features spécifiques ===\n", " refused = prev[prev['NAME_CONTRACT_STATUS_Refused'] == 1]\n", " refused_agg = refused.groupby('SK_ID_CURR').agg(num_aggregations)\n", " refused_agg.columns = pd.Index(['REFUSED_' + e[0] + \"_\" + e[1].upper() for e in refused_agg.columns.tolist()])\n", " prev_agg = prev_agg.join(refused_agg, how='left', on='SK_ID_CURR')\n", " \n", " del refused, refused_agg, approved, approved_agg, prev\n", " gc.collect()\n", " \n", " return prev_agg" ] }, { "cell_type": "markdown", "id": "2b440c44", "metadata": {}, "source": [ "## 5. Traitement de POS_CASH_balance.csv\n", "\n", "Ce fichier contient les historiques mensuels des soldes pour les crédits POS (Point of Sale) et CASH." ] }, { "cell_type": "code", "execution_count": 7, "id": "d6cbe990", "metadata": {}, "outputs": [], "source": [ "def pos_cash(num_rows=None, nan_as_category=True):\n", " \"\"\"\n", " Traite les données de soldes POS et CASH.\n", " \n", " Agrège les informations mensuelles au niveau client :\n", " - Nombre de mois d'historique\n", " - Retards de paiement (DPD = Days Past Due)\n", " - Distribution des statuts de paiement\n", " \"\"\"\n", " # Chargement des données\n", " pos = pd.read_csv('../data/raw/POS_CASH_balance.csv', nrows=num_rows)\n", " pos, cat_cols = one_hot_encoder(pos, nan_as_category=True)\n", " \n", " # === Agrégations ===\n", " aggregations = {\n", " 'MONTHS_BALANCE': ['max', 'mean', 'size'], # size = nombre de mois\n", " 'SK_DPD': ['max', 'mean'], # Jours de retard\n", " 'SK_DPD_DEF': ['max', 'mean'] # Jours de retard (définition alternative)\n", " }\n", " \n", " # Agrégations pour les colonnes catégorielles\n", " for cat in cat_cols:\n", " aggregations[cat] = ['mean']\n", " \n", " pos_agg = pos.groupby('SK_ID_CURR').agg(aggregations)\n", " pos_agg.columns = pd.Index(['POS_' + e[0] + \"_\" + e[1].upper() for e in pos_agg.columns.tolist()])\n", " \n", " # Compter le nombre de comptes POS CASH pour chaque client\n", " pos_agg['POS_COUNT'] = pos.groupby('SK_ID_CURR').size()\n", " \n", " del pos\n", " gc.collect()\n", " \n", " return pos_agg" ] }, { "cell_type": "markdown", "id": "aa798b95", "metadata": {}, "source": [ "## 6. Traitement de installments_payments.csv\n", "\n", "Ce fichier contient l'historique de remboursement des versements précédents (installments). \n", "**Idée clé :** Comparer ce qui devait être payé (AMT_INSTALMENT) avec ce qui a réellement été payé (AMT_PAYMENT)." ] }, { "cell_type": "code", "execution_count": 8, "id": "afce104d", "metadata": {}, "outputs": [], "source": [ "def installments_payments(num_rows=None, nan_as_category=True):\n", " \"\"\"\n", " Traite l'historique des paiements par versements.\n", " \n", " Crée des features pour mesurer le comportement de paiement :\n", " - DPD : Days Past Due (jours de retard)\n", " - DBD : Days Before Due (jours d'avance)\n", " - PAYMENT_PERC : Pourcentage payé vs attendu\n", " - PAYMENT_DIFF : Différence entre attendu et payé\n", " \"\"\"\n", " # Chargement des données\n", " ins = pd.read_csv('../data/raw/installments_payments.csv', nrows=num_rows)\n", " ins, cat_cols = one_hot_encoder(ins, nan_as_category=True)\n", " \n", " # === Nouvelles features de comportement de paiement ===\n", " \n", " # Pourcentage payé par rapport au montant prévu\n", " ins['PAYMENT_PERC'] = ins['AMT_PAYMENT'] / ins['AMT_INSTALMENT']\n", " \n", " # Différence entre montant prévu et montant payé (positif = sous-paiement)\n", " ins['PAYMENT_DIFF'] = ins['AMT_INSTALMENT'] - ins['AMT_PAYMENT']\n", " \n", " # DPD : Days Past Due = nombre de jours de retard (seulement valeurs positives)\n", " ins['DPD'] = ins['DAYS_ENTRY_PAYMENT'] - ins['DAYS_INSTALMENT']\n", " ins['DPD'] = ins['DPD'].apply(lambda x: x if x > 0 else 0)\n", " \n", " # DBD : Days Before Due = nombre de jours d'avance (seulement valeurs positives)\n", " ins['DBD'] = ins['DAYS_INSTALMENT'] - ins['DAYS_ENTRY_PAYMENT']\n", " ins['DBD'] = ins['DBD'].apply(lambda x: x if x > 0 else 0)\n", " \n", " # === Agrégations ===\n", " aggregations = {\n", " 'NUM_INSTALMENT_VERSION': ['nunique'], # Nombre de versions différentes\n", " 'DPD': ['max', 'mean', 'sum'], # Statistiques sur les retards\n", " 'DBD': ['max', 'mean', 'sum'], # Statistiques sur les avances\n", " 'PAYMENT_PERC': ['max', 'mean', 'sum', 'var'], # Comportement de paiement\n", " 'PAYMENT_DIFF': ['max', 'mean', 'sum', 'var'],\n", " 'AMT_INSTALMENT': ['max', 'mean', 'sum'],\n", " 'AMT_PAYMENT': ['min', 'max', 'mean', 'sum'],\n", " 'DAYS_ENTRY_PAYMENT': ['max', 'mean', 'sum']\n", " }\n", " \n", " for cat in cat_cols:\n", " aggregations[cat] = ['mean']\n", " \n", " ins_agg = ins.groupby('SK_ID_CURR').agg(aggregations)\n", " ins_agg.columns = pd.Index(['INSTAL_' + e[0] + \"_\" + e[1].upper() for e in ins_agg.columns.tolist()])\n", " \n", " # Compter le nombre de versements pour chaque client\n", " ins_agg['INSTAL_COUNT'] = ins.groupby('SK_ID_CURR').size()\n", " \n", " del ins\n", " gc.collect()\n", " \n", " return ins_agg" ] }, { "cell_type": "markdown", "id": "cb6d85ea", "metadata": {}, "source": [ "## 7. Traitement de credit_card_balance.csv\n", "\n", "Ce fichier contient les historiques mensuels des soldes de cartes de crédit." ] }, { "cell_type": "code", "execution_count": 9, "id": "9461c88a", "metadata": {}, "outputs": [], "source": [ "def credit_card_balance(num_rows=None, nan_as_category=True):\n", " \"\"\"\n", " Traite les données de soldes de cartes de crédit.\n", " \n", " Stratégie :\n", " - Agrégations numériques classiques sur les colonnes numériques\n", " - Agrégations catégorielles adaptées (proportions par statut)\n", " \"\"\"\n", " # Chargement des données\n", " cc = pd.read_csv('../data/raw/credit_card_balance.csv', nrows=num_rows)\n", " \n", " # On n'a pas besoin de SK_ID_PREV pour les agrégations finales\n", " cc.drop(['SK_ID_PREV'], axis=1, inplace=True)\n", " \n", " # === Agrégations numériques ===\n", " numeric_cols = cc.select_dtypes(exclude=['object']).columns.tolist()\n", " numeric_cols = [c for c in numeric_cols if c != 'SK_ID_CURR']\n", " num_agg = {col: ['min', 'max', 'mean', 'sum', 'var'] for col in numeric_cols}\n", " cc_num_agg = cc.groupby('SK_ID_CURR').agg(num_agg)\n", " cc_num_agg.columns = pd.Index(['CC_' + e[0] + \"_\" + e[1].upper() for e in cc_num_agg.columns.tolist()])\n", " \n", " # === Agrégations catégorielles ===\n", " if 'NAME_CONTRACT_STATUS' in cc.columns:\n", " if nan_as_category:\n", " cc['NAME_CONTRACT_STATUS'] = cc['NAME_CONTRACT_STATUS'].fillna('Unknown')\n", " \n", " # Créer un crosstab avec proportions\n", " cc_cat_agg = pd.crosstab(\n", " cc['SK_ID_CURR'], \n", " cc['NAME_CONTRACT_STATUS'], \n", " normalize='index'\n", " ).fillna(0)\n", " cc_cat_agg.columns = ['CC_STATUS_' + str(col) for col in cc_cat_agg.columns]\n", " cc_agg = cc_num_agg.join(cc_cat_agg, how='left')\n", " else:\n", " cc_agg = cc_num_agg\n", " \n", " # Compter le nombre de lignes (mois) de carte de crédit par client\n", " cc_agg['CC_COUNT'] = cc.groupby('SK_ID_CURR').size()\n", " \n", " del cc\n", " gc.collect()\n", " \n", " return cc_agg" ] }, { "cell_type": "markdown", "id": "440e0782", "metadata": {}, "source": [ "## 8. Fonction principale : Fusion de toutes les données\n", "\n", "Cette fonction orchestre tout le processus :\n", "1. Charge et traite les données principales (application)\n", "2. Charge et fusionne chaque table secondaire\n", "3. Retourne le DataFrame final prêt pour la modélisation" ] }, { "cell_type": "code", "execution_count": 10, "id": "1b341561", "metadata": {}, "outputs": [], "source": [ "def prepare_full_dataset(debug=False):\n", " \"\"\"\n", " Fonction principale qui orchestre toute la préparation des données.\n", " \n", " Paramètres:\n", " -----------\n", " debug : bool\n", " Si True, charge seulement 10000 lignes de chaque fichier (pour tests rapides)\n", " \n", " Retourne:\n", " ---------\n", " df : DataFrame complet avec toutes les features\n", " \"\"\"\n", " # En mode debug, on limite le nombre de lignes pour aller plus vite\n", " num_rows = 10000 if debug else None\n", " \n", " # === 1. Charger les données principales ===\n", " print(\"\\n\" + \"=\"*80)\n", " print(\"ÉTAPE 1 : Chargement des données application (train + test)\")\n", " print(\"=\"*80)\n", " df = application_train_test(num_rows)\n", " print(f\"✓ Shape après application : {df.shape}\")\n", " \n", " # === 2. Bureau et bureau_balance ===\n", " print(\"\\n\" + \"=\"*80)\n", " print(\"ÉTAPE 2 : Traitement des données Bureau (crédits externes)\")\n", " print(\"=\"*80)\n", " with timer(\"Traitement bureau et bureau_balance\"):\n", " bureau = bureau_and_balance(num_rows)\n", " print(f\" Bureau shape: {bureau.shape}\")\n", " df = df.join(bureau, how='left', on='SK_ID_CURR')\n", " del bureau\n", " gc.collect()\n", " print(f\"✓ Shape après fusion bureau : {df.shape}\")\n", " \n", " # === 3. Previous applications ===\n", " print(\"\\n\" + \"=\"*80)\n", " print(\"ÉTAPE 3 : Traitement des demandes précédentes\")\n", " print(\"=\"*80)\n", " with timer(\"Traitement previous_applications\"):\n", " prev = previous_applications(num_rows)\n", " print(f\" Previous applications shape: {prev.shape}\")\n", " df = df.join(prev, how='left', on='SK_ID_CURR')\n", " del prev\n", " gc.collect()\n", " print(f\"✓ Shape après fusion previous : {df.shape}\")\n", " \n", " # === 4. POS-CASH balance ===\n", " print(\"\\n\" + \"=\"*80)\n", " print(\"ÉTAPE 4 : Traitement des soldes POS-CASH\")\n", " print(\"=\"*80)\n", " with timer(\"Traitement POS-CASH balance\"):\n", " pos = pos_cash(num_rows)\n", " print(f\" Pos-cash balance shape: {pos.shape}\")\n", " df = df.join(pos, how='left', on='SK_ID_CURR')\n", " del pos\n", " gc.collect()\n", " print(f\"✓ Shape après fusion POS : {df.shape}\")\n", " \n", " # === 5. Installments payments ===\n", " print(\"\\n\" + \"=\"*80)\n", " print(\"ÉTAPE 5 : Traitement des paiements par versements\")\n", " print(\"=\"*80)\n", " with timer(\"Traitement installments payments\"):\n", " ins = installments_payments(num_rows)\n", " print(f\" Installments payments shape: {ins.shape}\")\n", " df = df.join(ins, how='left', on='SK_ID_CURR')\n", " del ins\n", " gc.collect()\n", " print(f\"✓ Shape après fusion installments : {df.shape}\")\n", " \n", " # === 6. Credit card balance ===\n", " print(\"\\n\" + \"=\"*80)\n", " print(\"ÉTAPE 6 : Traitement des soldes de cartes de crédit\")\n", " print(\"=\"*80)\n", " with timer(\"Traitement credit card balance\"):\n", " cc = credit_card_balance(num_rows)\n", " print(f\" Credit card balance shape: {cc.shape}\")\n", " df = df.join(cc, how='left', on='SK_ID_CURR')\n", " del cc\n", " gc.collect()\n", " print(f\"✓ Shape après fusion credit card : {df.shape}\")\n", " \n", " print(\"\\n\" + \"=\"*80)\n", " print(\"PRÉPARATION TERMINÉE !\")\n", " print(\"=\"*80)\n", " print(f\"Dataset final : {df.shape[0]} lignes, {df.shape[1]} colonnes\")\n", " \n", " return df" ] }, { "cell_type": "markdown", "id": "7862dde1", "metadata": {}, "source": [ "## 9. Exécution du pipeline de préparation\n", "\n", "Maintenant, exécutons le pipeline complet pour préparer nos données." ] }, { "cell_type": "code", "execution_count": 11, "id": "c3fef44a", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🚀 Début de la préparation des données...\n", "Mode: DEBUG (10000 lignes)\n", "\n", "\n", "================================================================================\n", "ÉTAPE 1 : Chargement des données application (train + test)\n", "================================================================================\n", "Échantillons train: 10000, test: 10000\n", "✓ Shape après application : (20000, 245)\n", "\n", "================================================================================\n", "ÉTAPE 2 : Traitement des données Bureau (crédits externes)\n", "================================================================================\n", " Bureau shape: (2011, 108)\n", "Traitement bureau et bureau_balance - terminé en 0s\n", "✓ Shape après fusion bureau : (20000, 353)\n", "\n", "================================================================================\n", "ÉTAPE 3 : Traitement des demandes précédentes\n", "================================================================================\n", " Previous applications shape: (9734, 242)\n", "Traitement previous_applications - terminé en 0s\n", "✓ Shape après fusion previous : (20000, 595)\n", "\n", "================================================================================\n", "ÉTAPE 4 : Traitement des soldes POS-CASH\n", "================================================================================\n", " Pos-cash balance shape: (9494, 15)\n", "Traitement POS-CASH balance - terminé en 0s\n", "✓ Shape après fusion POS : (20000, 610)\n", "\n", "================================================================================\n", "ÉTAPE 5 : Traitement des paiements par versements\n", "================================================================================\n", " Installments payments shape: (8893, 26)\n", "Traitement installments payments - terminé en 0s\n", "✓ Shape après fusion installments : (20000, 636)\n", "\n", "================================================================================\n", "ÉTAPE 6 : Traitement des soldes de cartes de crédit\n", "================================================================================\n", " Credit card balance shape: (9520, 106)\n", "Traitement credit card balance - terminé en 0s\n", "✓ Shape après fusion credit card : (20000, 742)\n", "\n", "================================================================================\n", "PRÉPARATION TERMINÉE !\n", "================================================================================\n", "Dataset final : 20000 lignes, 742 colonnes\n", "Pipeline complet de préparation - terminé en 1s\n" ] } ], "source": [ "# Exécuter en mode DEBUG (10000 lignes) pour un test rapide\n", "# Pour la version complète, mettre debug=False\n", "DEBUG_MODE = True\n", "\n", "print(\"🚀 Début de la préparation des données...\")\n", "print(f\"Mode: {'DEBUG (10000 lignes)' if DEBUG_MODE else 'COMPLET'}\\n\")\n", "\n", "with timer(\"Pipeline complet de préparation\"):\n", " df_final = prepare_full_dataset(debug=DEBUG_MODE)" ] }, { "cell_type": "markdown", "id": "4bae04f2", "metadata": {}, "source": [ "## 10. Exploration du dataset final\n", "\n", "Examinons le résultat de notre préparation." ] }, { "cell_type": "code", "execution_count": 12, "id": "97cf2f45", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📊 APERÇU DU DATASET FINAL\n", "================================================================================\n", "Nombre de lignes : 20,000\n", "Nombre de colonnes (features) : 742\n", "\n", "Mémoire utilisée : 95.73 MB\n", "\n", "Premières colonnes :\n", "['SK_ID_CURR', 'TARGET', 'CODE_GENDER', 'FLAG_OWN_CAR', 'FLAG_OWN_REALTY', 'CNT_CHILDREN', 'AMT_INCOME_TOTAL', 'AMT_CREDIT', 'AMT_ANNUITY', 'AMT_GOODS_PRICE', 'REGION_POPULATION_RELATIVE', 'DAYS_BIRTH', 'DAYS_EMPLOYED', 'DAYS_REGISTRATION', 'DAYS_ID_PUBLISH', 'OWN_CAR_AGE', 'FLAG_MOBIL', 'FLAG_EMP_PHONE', 'FLAG_WORK_PHONE', 'FLAG_CONT_MOBILE']\n" ] } ], "source": [ "# Aperçu général du dataset\n", "print(\"📊 APERÇU DU DATASET FINAL\")\n", "print(\"=\"*80)\n", "print(f\"Nombre de lignes : {df_final.shape[0]:,}\")\n", "print(f\"Nombre de colonnes (features) : {df_final.shape[1]:,}\")\n", "print(f\"\\nMémoire utilisée : {df_final.memory_usage(deep=True).sum() / 1024**2:.2f} MB\")\n", "print(\"\\nPremières colonnes :\")\n", "print(df_final.columns.tolist()[:20])" ] }, { "cell_type": "markdown", "id": "a6f4abb4", "metadata": {}, "source": [ "### Séparation train/test" ] }, { "cell_type": "code", "execution_count": 13, "id": "baddbf20", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📊 SÉPARATION TRAIN / TEST\n", "================================================================================\n", "Train shape : (10000, 742)\n", "Test shape : (10000, 742)\n", "\n", "Distribution de la variable cible (TARGET) dans train :\n", "TARGET\n", "0.0 9225\n", "1.0 775\n", "Name: count, dtype: int64\n", "\n", "Pourcentage de défaut : 7.75%\n" ] } ], "source": [ "# Séparer les données train (avec TARGET) et test (sans TARGET)\n", "train_df = df_final[df_final['TARGET'].notnull()].copy()\n", "test_df = df_final[df_final['TARGET'].isnull()].copy()\n", "\n", "print(\"📊 SÉPARATION TRAIN / TEST\")\n", "print(\"=\"*80)\n", "print(f\"Train shape : {train_df.shape}\")\n", "print(f\"Test shape : {test_df.shape}\")\n", "print(f\"\\nDistribution de la variable cible (TARGET) dans train :\")\n", "print(train_df['TARGET'].value_counts())\n", "print(f\"\\nPourcentage de défaut : {train_df['TARGET'].mean()*100:.2f}%\")" ] }, { "cell_type": "markdown", "id": "44a3c899", "metadata": {}, "source": [ "### Analyse des valeurs manquantes" ] }, { "cell_type": "code", "execution_count": 14, "id": "8d195eec", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📊 COLONNES AVEC VALEURS MANQUANTES (>1%)\n", "================================================================================\n", "Nombre de colonnes concernées : 551\n", "\n", "Top 10 colonnes avec le plus de valeurs manquantes :\n", " Colonne Valeurs_manquantes Pourcentage\n", " BURO_MONTHS_BALANCE_MIN_MIN 10000 100.0\n", " BURO_STATUS_C_MEAN_MEAN 10000 100.0\n", " BURO_STATUS_1_MEAN_MEAN 10000 100.0\n", " BURO_STATUS_nan_MEAN_MEAN 10000 100.0\n", " BURO_STATUS_0_MEAN_MEAN 10000 100.0\n", " BURO_STATUS_2_MEAN_MEAN 10000 100.0\n", " BURO_STATUS_X_MEAN_MEAN 10000 100.0\n", " BURO_MONTHS_BALANCE_MAX_MAX 10000 100.0\n", "BURO_MONTHS_BALANCE_SIZE_MEAN 10000 100.0\n", " BURO_STATUS_3_MEAN_MEAN 10000 100.0\n" ] } ], "source": [ "# Calculer le pourcentage de valeurs manquantes par colonne\n", "missing_values = train_df.isnull().sum()\n", "missing_percent = (missing_values / len(train_df)) * 100\n", "missing_df = pd.DataFrame({\n", " 'Colonne': missing_values.index,\n", " 'Valeurs_manquantes': missing_values.values,\n", " 'Pourcentage': missing_percent.values\n", "})\n", "\n", "# Filtrer les colonnes avec au moins 1% de valeurs manquantes\n", "missing_df = missing_df[missing_df['Pourcentage'] > 1].sort_values('Pourcentage', ascending=False)\n", "\n", "print(\"📊 COLONNES AVEC VALEURS MANQUANTES (>1%)\")\n", "print(\"=\"*80)\n", "print(f\"Nombre de colonnes concernées : {len(missing_df)}\")\n", "print(\"\\nTop 10 colonnes avec le plus de valeurs manquantes :\")\n", "print(missing_df.head(10).to_string(index=False))" ] }, { "cell_type": "markdown", "id": "64a9febd", "metadata": {}, "source": [ "### Aperçu des features créées par catégorie" ] }, { "cell_type": "code", "execution_count": 15, "id": "810668eb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📊 RÉPARTITION DES FEATURES PAR ORIGINE\n", "================================================================================\n", "Application : 245 features\n", "Bureau (BURO) : 54 features\n", "Bureau Active : 27 features\n", "Bureau Closed : 27 features\n", "Previous (PREV) : 182 features\n", "Approved : 30 features\n", "Refused : 30 features\n", "POS Cash : 15 features\n", "Installments : 26 features\n", "Credit Card : 106 features\n", "\n", "TOTAL : 742 features\n" ] } ], "source": [ "# Compter les features par préfixe (provenance)\n", "prefixes = {\n", " 'Application': [col for col in df_final.columns if not any(col.startswith(p) for p in ['BURO_', 'ACTIVE_', 'CLOSED_', 'PREV_', 'APPROVED_', 'REFUSED_', 'POS_', 'INSTAL_', 'CC_'])],\n", " 'Bureau (BURO)': [col for col in df_final.columns if col.startswith('BURO_')],\n", " 'Bureau Active': [col for col in df_final.columns if col.startswith('ACTIVE_')],\n", " 'Bureau Closed': [col for col in df_final.columns if col.startswith('CLOSED_')],\n", " 'Previous (PREV)': [col for col in df_final.columns if col.startswith('PREV_')],\n", " 'Approved': [col for col in df_final.columns if col.startswith('APPROVED_')],\n", " 'Refused': [col for col in df_final.columns if col.startswith('REFUSED_')],\n", " 'POS Cash': [col for col in df_final.columns if col.startswith('POS_')],\n", " 'Installments': [col for col in df_final.columns if col.startswith('INSTAL_')],\n", " 'Credit Card': [col for col in df_final.columns if col.startswith('CC_')]\n", "}\n", "\n", "print(\"📊 RÉPARTITION DES FEATURES PAR ORIGINE\")\n", "print(\"=\"*80)\n", "for name, cols in prefixes.items():\n", " print(f\"{name:20s} : {len(cols):4d} features\")\n", " \n", "print(f\"\\n{'TOTAL':20s} : {df_final.shape[1]:4d} features\")" ] }, { "cell_type": "markdown", "id": "6c484431", "metadata": {}, "source": [ "## 11. Sauvegarde des données préparées\n", "\n", "Sauvegardons nos datasets préparés pour une utilisation ultérieure dans la modélisation." ] }, { "cell_type": "code", "execution_count": 16, "id": "458e51ff", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✓ Dataset complet sauvegardé : ../data/processed/features_full.csv\n", " Taille du fichier : 34.23 MB\n", "\n", "✓ Train sauvegardé : ../data/processed/features_train.csv\n", " Taille du fichier : 17.04 MB\n", "\n", "✓ Test sauvegardé : ../data/processed/features_test.csv\n", " Taille du fichier : 17.21 MB\n" ] } ], "source": [ "# Créer le répertoire de sortie s'il n'existe pas\n", "import os\n", "os.makedirs('../data/processed', exist_ok=True)\n", "\n", "# Sauvegarder le dataset complet\n", "output_path_full = '../data/processed/features_full.csv'\n", "df_final.to_csv(output_path_full, index=False)\n", "print(f\"✓ Dataset complet sauvegardé : {output_path_full}\")\n", "print(f\" Taille du fichier : {os.path.getsize(output_path_full) / 1024**2:.2f} MB\")\n", "\n", "# Sauvegarder séparément train et test\n", "output_path_train = '../data/processed/features_train.csv'\n", "output_path_test = '../data/processed/features_test.csv'\n", "\n", "train_df.to_csv(output_path_train, index=False)\n", "test_df.to_csv(output_path_test, index=False)\n", "\n", "print(f\"\\n✓ Train sauvegardé : {output_path_train}\")\n", "print(f\" Taille du fichier : {os.path.getsize(output_path_train) / 1024**2:.2f} MB\")\n", "print(f\"\\n✓ Test sauvegardé : {output_path_test}\")\n", "print(f\" Taille du fichier : {os.path.getsize(output_path_test) / 1024**2:.2f} MB\")" ] }, { "cell_type": "markdown", "id": "886c3450", "metadata": {}, "source": [ "## 12. Résumé et prochaines étapes\n", "\n", "### ✅ Ce qui a été fait dans ce notebook :\n", "\n", "1. **Chargement et fusion** de 7 tables de données différentes\n", "2. **Nettoyage** des valeurs aberrantes et sentinelles (365243 → NaN)\n", "3. **Encodage** des variables catégorielles (One-Hot encoding)\n", "4. **Création de features** par agrégation (min, max, mean, sum, var)\n", "5. **Features spécifiques** :\n", " - Ratios et pourcentages (ex: INCOME_CREDIT_PERC, PAYMENT_RATE)\n", " - Comportement de paiement (DPD, DBD, PAYMENT_PERC)\n", " - Distinction crédits actifs/fermés\n", " - Distinction demandes approuvées/refusées\n", "6. **Séparation** train/test\n", "7. **Sauvegarde** des données préparées\n", "\n", "### 📊 Résultat :\n", "\n", "- **Dataset final** : ~{df_final.shape[1]} features créées\n", "- **Prêt pour la modélisation** avec LightGBM ou autre algorithme\n", "\n", "### 🔜 Prochaines étapes :\n", "\n", "1. **Feature Selection** : Identifier les features les plus importantes\n", "2. **Modélisation** : Entraîner un modèle LightGBM avec validation croisée\n", "3. **Optimisation** : Tuning des hyperparamètres\n", "4. **Évaluation** : Analyser les performances (ROC-AUC)\n", "5. **Prédictions** : Générer les prédictions pour le test set\n", "\n", "---\n", "\n", "**Note importante** : Ce notebook utilise l'approche du kernel Kaggle \"LightGBM with Simple Features\" de jsaguiar, qui a obtenu d'excellents résultats sur cette compétition. L'approche privilégie la création de nombreuses features par agrégation, ce qui peut entraîner de l'overfitting. Une sélection de features sera donc importante dans les étapes suivantes." ] } ], "metadata": { "kernelspec": { "display_name": "OC_P6", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 5 }