# Ce fichier constitue le pipeline complet de prétraitement des données pour # un modèle de scoring de crédit (type Home Credit Default Risk). # # L'objectif est de fusionner plusieurs sources de données (demandes de prêt, # historique bureau de crédit, remboursements, cartes de crédit, etc.) en un # seul DataFrame prêt à l'entraînement d'un modèle de machine learning. # # Flux général : # application_train.csv → données principales du demandeur # bureau.csv + bureau_balance.csv → historique crédits externes (Credit Bureau) # previous_application.csv → demandes de prêt passées chez Home Credit # POS_CASH_balance.csv → soldes mensuels des prêts/POS précédents # installments_payments.csv → historique des paiements d'échéances # credit_card_balance.csv → soldes mensuels des cartes de crédit # # Toutes ces tables sont agrégées par client (SK_ID_CURR) puis fusionnées. # ============================================================================= import os import gc import time import re import numpy as np import pandas as pd from contextlib import contextmanager import multiprocessing as mp from functools import partial from scipy.stats import kurtosis, iqr, skew from sklearn.linear_model import LinearRegression import warnings warnings.simplefilter(action='ignore', category=FutureWarning) warnings.filterwarnings('ignore', category=pd.errors.PerformanceWarning) # ============================================================================= # FONCTION PRINCIPALE : load_dataset # ============================================================================= def load_dataset(debug=False): """ Charge et prétraite toutes les sources de données, puis retourne le DataFrame final fusionné (données d'entraînement uniquement). Paramètre : debug (bool) : Si True, charge seulement 30 000 lignes par fichier pour accélérer les tests. Par défaut False (toutes les données). Retourne : df (pd.DataFrame) : DataFrame final avec toutes les features engineered, prêt pour l'entraînement d'un modèle ML. """ # En mode debug, on limite à 30 000 lignes pour aller plus vite num_rows = 30000 if debug else None # Chargement des données principales des demandeurs de prêt with timer("application_train"): df = get_train(DATA_DIRECTORY, num_rows=num_rows) print("Application dataframe shape: ", df.shape) # Fusion avec les données du bureau de crédit externe # Le bureau de crédit contient l'historique des crédits ouverts ailleurs with timer("Bureau and bureau_balance data"): bureau_df = get_bureau(DATA_DIRECTORY, num_rows=num_rows) df = pd.merge(df, bureau_df, on='SK_ID_CURR', how='left') print("Bureau dataframe shape: ", bureau_df.shape) del bureau_df; gc.collect() # Libère la mémoire immédiatement après fusion # Fusion avec les demandes de prêt précédentes chez Home Credit with timer("previous_application"): prev_df = get_previous_applications(DATA_DIRECTORY, num_rows) df = pd.merge(df, prev_df, on='SK_ID_CURR', how='left') print("Previous dataframe shape: ", prev_df.shape) del prev_df; gc.collect() # Fusion avec les balances mensuelles des prêts/POS, remboursements et cartes with timer("previous applications balances"): # Soldes mensuels des prêts à la consommation / POS (Point Of Sale) pos = get_pos_cash(DATA_DIRECTORY, num_rows) df = pd.merge(df, pos, on='SK_ID_CURR', how='left') print("Pos-cash dataframe shape: ", pos.shape) del pos; gc.collect() # Historique détaillé des paiements d'échéances ins = get_installment_payments(DATA_DIRECTORY, num_rows) df = pd.merge(df, ins, on='SK_ID_CURR', how='left') print("Installments dataframe shape: ", ins.shape) del ins; gc.collect() # Soldes mensuels des cartes de crédit cc = get_credit_card(DATA_DIRECTORY, num_rows) df = pd.merge(df, cc, on='SK_ID_CURR', how='left') print("Credit card dataframe shape: ", cc.shape) del cc; gc.collect() # Post-traitement final df = add_ratios_features(df) # Ajout de ratios croisant plusieurs sources df = replace_infinite_with_nan(df) # Remplace les +/-inf par NaN (divisions par zéro) df = reduce_memory(df) # Réduit l'empreinte mémoire en castant les types numériques df = sanitize_feature_names(df) # Nettoie les noms de colonnes (caractères spéciaux, doublons) # Suppression colonnes > 40 % NaN nan_pct = df.isna().mean() * 100 df = df.drop(columns=nan_pct[nan_pct > 40].index.tolist()) # Suppression quasi-constantes (>= 99 % même valeur) PROTECTED_COLS = ["SK_ID_CURR", "TARGET"] dominant_ratio = df.apply(lambda s: s.value_counts(normalize=True, dropna=False).iloc[0]) quasi_cst = dominant_ratio[(dominant_ratio >= 0.99) & (~dominant_ratio.index.isin(PROTECTED_COLS))].index df = df.drop(columns=quasi_cst) # Feature engineering df["ANNUITY_CREDIT_RATIO"] = df["GROUP_ANNUITY_MEAN"] / df["GROUP_CREDIT_MEAN"] df["DEF_ESCALATION"] = df["DEF_60_CNT_SOCIAL_CIRCLE"] - df["DEF_30_CNT_SOCIAL_CIRCLE"] # Suppressions validées (mode full) cols_to_drop = [ "AMT_CREDIT", "EXT_SOURCES_MEAN", "DAYS_EMPLOYED", "EXT_SOURCES_MIN", "INCOME_TO_EMPLOYED_RATIO", "AMT_ANNUITY", "AMT_GOODS_PRICE", "EXT_SOURCES_NANMEDIAN", "EMPLOYED_TO_BIRTH_RATIO", "AGE_RANGE", "REGION_RATING_CLIENT_W_CITY", "DAYS_LAST_PHONE_CHANGE", "INCOME_TO_BIRTH_RATIO", "DEF_60_CNT_SOCIAL_CIRCLE", "ID_TO_BIRTH_RATIO", "NEW_DOC_KURT", "GROUP_ANNUITY_MEAN", ] df = df.drop(columns=[c for c in cols_to_drop if c in df.columns]) return df # ============================================================================= # FEATURES CROISÉES : add_ratios_features # ============================================================================= def add_ratios_features(df): """ Crée des features de ratio croisant les données de la demande principale avec les informations agrégées du bureau et des demandes précédentes. Ces ratios permettent au modèle de comparer le comportement actuel du client à son historique de crédit. """ # Ratio crédit du bureau / revenu total # Mesure la charge de crédit externe par rapport aux revenus df['BUREAU_INCOME_CREDIT_RATIO'] = df['BUREAU_AMT_CREDIT_SUM_MEAN'] / df['AMT_INCOME_TOTAL'] df['BUREAU_ACTIVE_CREDIT_TO_INCOME_RATIO'] = df['BUREAU_ACTIVE_AMT_CREDIT_SUM_SUM'] / df['AMT_INCOME_TOTAL'] # Ratio crédit actuel / crédit des demandes précédentes approuvées # Permet de voir si le client emprunte plus ou moins qu'habituellement df['CURRENT_TO_APPROVED_CREDIT_MIN_RATIO'] = df['APPROVED_AMT_CREDIT_MIN'] / df['AMT_CREDIT'] df['CURRENT_TO_APPROVED_CREDIT_MAX_RATIO'] = df['APPROVED_AMT_CREDIT_MAX'] / df['AMT_CREDIT'] df['CURRENT_TO_APPROVED_CREDIT_MEAN_RATIO'] = df['APPROVED_AMT_CREDIT_MEAN'] / df['AMT_CREDIT'] # Ratio annuité actuelle / annuités des demandes précédentes # Mesure si la mensualité demandée est cohérente avec le passé df['CURRENT_TO_APPROVED_ANNUITY_MAX_RATIO'] = df['APPROVED_AMT_ANNUITY_MAX'] / df['AMT_ANNUITY'] df['CURRENT_TO_APPROVED_ANNUITY_MEAN_RATIO'] = df['APPROVED_AMT_ANNUITY_MEAN'] / df['AMT_ANNUITY'] # Ratios entre les paiements effectifs (installments) et l'annuité actuelle df['PAYMENT_MIN_TO_ANNUITY_RATIO'] = df['INS_AMT_PAYMENT_MIN'] / df['AMT_ANNUITY'] df['PAYMENT_MAX_TO_ANNUITY_RATIO'] = df['INS_AMT_PAYMENT_MAX'] / df['AMT_ANNUITY'] df['PAYMENT_MEAN_TO_ANNUITY_RATIO'] = df['INS_AMT_PAYMENT_MEAN'] / df['AMT_ANNUITY'] # Ratio crédit/annuité des demandes précédentes vs actuelle # Compare la durée implicite du prêt (crédit ÷ annuité) avec le passé df['CTA_CREDIT_TO_ANNUITY_MAX_RATIO'] = df['APPROVED_CREDIT_TO_ANNUITY_RATIO_MAX'] / df[ 'CREDIT_TO_ANNUITY_RATIO'] df['CTA_CREDIT_TO_ANNUITY_MEAN_RATIO'] = df['APPROVED_CREDIT_TO_ANNUITY_RATIO_MEAN'] / df[ 'CREDIT_TO_ANNUITY_RATIO'] # Ratios temporels # Normalise les durées (en jours) par rapport à l'âge et à l'ancienneté du client df['DAYS_DECISION_MEAN_TO_BIRTH'] = df['APPROVED_DAYS_DECISION_MEAN'] / df['DAYS_BIRTH'] df['DAYS_CREDIT_MEAN_TO_BIRTH'] = df['BUREAU_DAYS_CREDIT_MEAN'] / df['DAYS_BIRTH'] df['DAYS_DECISION_MEAN_TO_EMPLOYED'] = df['APPROVED_DAYS_DECISION_MEAN'] / df['DAYS_EMPLOYED'] df['DAYS_CREDIT_MEAN_TO_EMPLOYED'] = df['BUREAU_DAYS_CREDIT_MEAN'] / df['DAYS_EMPLOYED'] # Les ratios peuvent produire +/-inf si le dénominateur vaut 0 → on les remplace par NaN df = replace_infinite_with_nan(df) return df # ============================================================================= # PIPELINE DEMANDE PRINCIPALE : get_train # ============================================================================= def get_train(path, num_rows=None): """ Lit et prétraite le fichier application_train.csv. Ce fichier contient une ligne par demande de prêt en cours, avec les informations socio-démographiques du demandeur, les montants demandés, et la variable cible TARGET (1 = défaut de paiement, 0 = remboursé). Paramètre : path (str) : Répertoire contenant les fichiers CSV. num_rows (int) : Nombre de lignes à lire (None = toutes). Retourne : df (pd.DataFrame) : DataFrame prétraité avec features engineered. """ df = pd.read_csv(os.path.join(path, 'application_train.csv'), nrows=num_rows) # Nettoyage des données aberrantes df = df[df['CODE_GENDER'] != 'XNA'] # Supprime 4 individus avec genre inconnu ('XNA') df = df[df['AMT_INCOME_TOTAL'] < 20000000] # Supprime un outlier extrême (117M€ dans le train, max 4M dans le test) df['DAYS_EMPLOYED'] = df['DAYS_EMPLOYED'].replace(365243, np.nan) # 365243 = valeur sentinelle "non employé" df['DAYS_LAST_PHONE_CHANGE'] = df['DAYS_LAST_PHONE_CHANGE'].replace(0, np.nan) # 0 = pas de changement connu # Features sur les documents fournis # FLAG_DOC_X indique si le client a fourni le document X (0/1) docs = [f for f in df.columns if 'FLAG_DOC' in f] df['DOCUMENT_COUNT'] = df[docs].sum(axis=1) # Nombre total de documents fournis df['NEW_DOC_KURT'] = df[docs].kurtosis(axis=1) # Kurtosis = mesure de la distribution des documents fournis # Tranche d'âge # Basée sur l'analyse de la corrélation TARGET=1 / âge df['AGE_RANGE'] = df['DAYS_BIRTH'].apply(lambda x: get_age_label(x)) # Features dérivées des scores externes (EXT_SOURCE_1/2/3) # Ces scores sont fournis par des bureaux de crédit externes et sont très prédictifs df['EXT_SOURCES_PROD'] = df['EXT_SOURCE_1'] * df['EXT_SOURCE_2'] * df['EXT_SOURCE_3'] # Somme pondérée (SOURCE_3 compte double car plus prédictif) df['EXT_SOURCES_WEIGHTED'] = df.EXT_SOURCE_1 * 2 + df.EXT_SOURCE_2 * 1 + df.EXT_SOURCE_3 * 3 # Agrégations statistiques des 3 scores externes sur chaque ligne warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered') for function_name in ['min', 'max', 'mean', 'nanmedian', 'var']: feature_name = 'EXT_SOURCES_{}'.format(function_name.upper()) df[feature_name] = eval('np.{}'.format(function_name))( df[['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3']], axis=1) # Ratios financiers internes à la demande # Rapport entre le montant emprunté et la mensualité → durée implicite du prêt df['CREDIT_TO_ANNUITY_RATIO'] = df['AMT_CREDIT'] / df['AMT_ANNUITY'] # Rapport entre le crédit accordé et la valeur du bien financé df['CREDIT_TO_GOODS_RATIO'] = df['AMT_CREDIT'] / df['AMT_GOODS_PRICE'] # Ratios de charge financière par rapport aux revenus df['ANNUITY_TO_INCOME_RATIO'] = df['AMT_ANNUITY'] / df['AMT_INCOME_TOTAL'] df['CREDIT_TO_INCOME_RATIO'] = df['AMT_CREDIT'] / df['AMT_INCOME_TOTAL'] df['INCOME_TO_EMPLOYED_RATIO'] = df['AMT_INCOME_TOTAL'] / df['DAYS_EMPLOYED'] df['INCOME_TO_BIRTH_RATIO'] = df['AMT_INCOME_TOTAL'] / df['DAYS_BIRTH'] # Ratios temporels : ancienneté professionnelle, véhicule, etc. par rapport à l'âge df['EMPLOYED_TO_BIRTH_RATIO'] = df['DAYS_EMPLOYED'] / df['DAYS_BIRTH'] df['ID_TO_BIRTH_RATIO'] = df['DAYS_ID_PUBLISH'] / df['DAYS_BIRTH'] df['CAR_TO_BIRTH_RATIO'] = df['OWN_CAR_AGE'] / df['DAYS_BIRTH'] df['CAR_TO_EMPLOYED_RATIO'] = df['OWN_CAR_AGE'] / df['DAYS_EMPLOYED'] df['PHONE_TO_BIRTH_RATIO'] = df['DAYS_LAST_PHONE_CHANGE'] / df['DAYS_BIRTH'] # Statistiques de groupe # Pour chaque combinaison (type d'organisation, niveau d'éducation, profession, # tranche d'âge, genre), calcule des statistiques de référence # → permet de comparer un individu à ses "pairs" group_cols = ['ORGANIZATION_TYPE', 'NAME_EDUCATION_TYPE', 'OCCUPATION_TYPE', 'AGE_RANGE', 'CODE_GENDER'] df = do_median(df, group_cols, 'EXT_SOURCES_MEAN', 'GROUP_EXT_SOURCES_MEDIAN') df = do_std(df, group_cols, 'EXT_SOURCES_MEAN', 'GROUP_EXT_SOURCES_STD') df = do_mean(df, group_cols, 'AMT_INCOME_TOTAL', 'GROUP_INCOME_MEAN') df = do_std(df, group_cols, 'AMT_INCOME_TOTAL', 'GROUP_INCOME_STD') df = do_mean(df, group_cols, 'CREDIT_TO_ANNUITY_RATIO', 'GROUP_CREDIT_TO_ANNUITY_MEAN') df = do_std(df, group_cols, 'CREDIT_TO_ANNUITY_RATIO', 'GROUP_CREDIT_TO_ANNUITY_STD') df = do_mean(df, group_cols, 'AMT_CREDIT', 'GROUP_CREDIT_MEAN') df = do_mean(df, group_cols, 'AMT_ANNUITY', 'GROUP_ANNUITY_MEAN') df = do_std(df, group_cols, 'AMT_ANNUITY', 'GROUP_ANNUITY_STD') # Encodage des variables catégorielles # Les colonnes de type texte sont converties en entiers avec pd.factorize df, le_encoded_cols = label_encoder(df, None) # Suppression des colonnes peu informatives df = drop_application_columns(df) return df def drop_application_columns(df): """ Supprime les colonnes identifiées comme peu importantes par analyse de permutation feature importance. Ces colonnes ajoutent du bruit sans améliorer les performances du modèle. """ drop_list = [ 'CNT_CHILDREN', 'CNT_FAM_MEMBERS', 'HOUR_APPR_PROCESS_START', 'FLAG_EMP_PHONE', 'FLAG_MOBIL', 'FLAG_CONT_MOBILE', 'FLAG_EMAIL', 'FLAG_PHONE', 'FLAG_OWN_REALTY', 'REG_REGION_NOT_LIVE_REGION', 'REG_REGION_NOT_WORK_REGION', 'REG_CITY_NOT_WORK_CITY', 'OBS_30_CNT_SOCIAL_CIRCLE', 'OBS_60_CNT_SOCIAL_CIRCLE', 'AMT_REQ_CREDIT_BUREAU_DAY', 'AMT_REQ_CREDIT_BUREAU_MON', 'AMT_REQ_CREDIT_BUREAU_YEAR', 'COMMONAREA_MODE', 'NONLIVINGAREA_MODE', 'ELEVATORS_MODE', 'NONLIVINGAREA_AVG', 'FLOORSMIN_MEDI', 'LANDAREA_MODE', 'NONLIVINGAREA_MEDI', 'LIVINGAPARTMENTS_MODE', 'FLOORSMIN_AVG', 'LANDAREA_AVG', 'FLOORSMIN_MODE', 'LANDAREA_MEDI', 'COMMONAREA_MEDI', 'YEARS_BUILD_AVG', 'COMMONAREA_AVG', 'BASEMENTAREA_AVG', 'BASEMENTAREA_MODE', 'NONLIVINGAPARTMENTS_MEDI', 'BASEMENTAREA_MEDI', 'LIVINGAPARTMENTS_AVG', 'ELEVATORS_AVG', 'YEARS_BUILD_MEDI', 'ENTRANCES_MODE', 'NONLIVINGAPARTMENTS_MODE', 'LIVINGAREA_MODE', 'LIVINGAPARTMENTS_MEDI', 'YEARS_BUILD_MODE', 'YEARS_BEGINEXPLUATATION_AVG', 'ELEVATORS_MEDI', 'LIVINGAREA_MEDI', 'YEARS_BEGINEXPLUATATION_MODE', 'NONLIVINGAPARTMENTS_AVG', 'HOUSETYPE_MODE', 'FONDKAPREMONT_MODE', 'EMERGENCYSTATE_MODE' ] # Supprime également la plupart des indicateurs de documents (peu discriminants) for doc_num in [2,4,5,6,7,9,10,11,12,13,14,15,16,17,19,20,21]: drop_list.append('FLAG_DOCUMENT_{}'.format(doc_num)) df.drop(drop_list, axis=1, inplace=True) return df def get_age_label(days_birth): """ Convertit un nombre de jours depuis la naissance (négatif dans le dataset) en un label de tranche d'âge (entier de 1 à 5). Tranches : 1 : moins de 27 ans 2 : 27 à 39 ans 3 : 40 à 49 ans 4 : 50 à 64 ans 5 : 65 à 98 ans 0 : valeur invalide (≥ 99 ans) """ age_years = -days_birth / 365 # DAYS_BIRTH est négatif dans le dataset (jours avant aujourd'hui) if age_years < 27: return 1 elif age_years < 40: return 2 elif age_years < 50: return 3 elif age_years < 65: return 4 elif age_years < 99: return 5 else: return 0 # ============================================================================= # PIPELINE BUREAU : get_bureau / get_bureau_balance # ============================================================================= def get_bureau(path, num_rows=None): """ Lit et agrège les données du Credit Bureau externe. Le Credit Bureau contient l'historique de tous les crédits du client auprès d'autres institutions financières (pas seulement Home Credit). Sources : bureau.csv : Un ligne par crédit externe par client bureau_balance.csv : Historique mensuel de chaque crédit externe Retourne : agg_bureau (pd.DataFrame) : Une ligne par client (SK_ID_CURR) avec des features agrégées. """ bureau = pd.read_csv(os.path.join(path, 'bureau.csv'), nrows=num_rows) # Features engineered sur chaque crédit # Durée totale du crédit (du début à la date de fin prévue) bureau['CREDIT_DURATION'] = -bureau['DAYS_CREDIT'] + bureau['DAYS_CREDIT_ENDDATE'] # Différence entre la date de fin prévue et la date de clôture réelle bureau['ENDDATE_DIF'] = bureau['DAYS_CREDIT_ENDDATE'] - bureau['DAYS_ENDDATE_FACT'] # Taux d'endettement : dette restante / montant total du crédit bureau['DEBT_PERCENTAGE'] = bureau['AMT_CREDIT_SUM'] / bureau['AMT_CREDIT_SUM_DEBT'] # Différence absolue entre le crédit accordé et la dette restante bureau['DEBT_CREDIT_DIFF'] = bureau['AMT_CREDIT_SUM'] - bureau['AMT_CREDIT_SUM_DEBT'] # Ratio crédit / annuité (durée implicite du remboursement) bureau['CREDIT_TO_ANNUITY_RATIO'] = bureau['AMT_CREDIT_SUM'] / bureau['AMT_ANNUITY'] # Encodage one-hot des variables catégorielles du bureau # Ex: CREDIT_ACTIVE → CREDIT_ACTIVE_Active, CREDIT_ACTIVE_Closed, etc. bureau, categorical_cols = one_hot_encoder(bureau, nan_as_category=False) # Fusion avec les données mensuelles du bureau (bureau_balance) bureau = bureau.merge(get_bureau_balance(path, num_rows), how='left', on='SK_ID_BUREAU') # Certaines colonnes STATUS peuvent être absentes après encodage one-hot si # aucun crédit n'avait ce statut dans le sous-ensemble chargé → on les initialise à 0 for col in ['STATUS_0', 'STATUS_1', 'STATUS_2', 'STATUS_3', 'STATUS_4', 'STATUS_5', 'STATUS_C', 'STATUS_X']: if col not in bureau.columns: bureau[col] = 0 # Indicateur de retard de paiement # STATUS_1 à STATUS_5 indiquent des retards de 1 à 5 mois → on les cumule bureau['STATUS_12345'] = 0 for i in range(1,6): bureau['STATUS_12345'] += bureau['STATUS_{}'.format(i)] # Agrégation par durée de crédit (MONTHS_BALANCE_SIZE) # Calcule les moyennes des features clés pour chaque "durée de crédit" # puis fusionne ces statistiques sur chaque ligne individuelle features = ['AMT_CREDIT_MAX_OVERDUE', 'AMT_CREDIT_SUM_OVERDUE', 'AMT_CREDIT_SUM', 'AMT_CREDIT_SUM_DEBT', 'DEBT_PERCENTAGE', 'DEBT_CREDIT_DIFF', 'STATUS_0', 'STATUS_12345'] agg_length = bureau.groupby('MONTHS_BALANCE_SIZE')[features].mean().reset_index() agg_length.rename({feat: 'LL_' + feat for feat in features}, axis=1, inplace=True) bureau = bureau.merge(agg_length, how='left', on='MONTHS_BALANCE_SIZE') del agg_length; gc.collect() # Agrégation générale de tous les crédits par client agg_bureau = group(bureau, 'BUREAU_', BUREAU_AGG) # Agrégation séparée pour les crédits actifs et fermés # Les crédits actifs et fermés ont des comportements très différents active = bureau[bureau['CREDIT_ACTIVE_Active'] == 1] agg_bureau = group_and_merge(active, agg_bureau, 'BUREAU_ACTIVE_', BUREAU_ACTIVE_AGG) closed = bureau[bureau['CREDIT_ACTIVE_Closed'] == 1] agg_bureau = group_and_merge(closed, agg_bureau, 'BUREAU_CLOSED_', BUREAU_CLOSED_AGG) del active, closed; gc.collect() # Agrégation par type de crédit # Crédit consommation, carte de crédit, hypothèque, prêt auto, microcrédit for credit_type in ['Consumer credit', 'Credit card', 'Mortgage', 'Car loan', 'Microloan']: type_df = bureau[bureau['CREDIT_TYPE_' + credit_type] == 1] prefix = 'BUREAU_' + credit_type.split(' ')[0].upper() + '_' agg_bureau = group_and_merge(type_df, agg_bureau, prefix, BUREAU_LOAN_TYPE_AGG) del type_df; gc.collect() # Agrégation par fenêtre temporelle (6 et 12 derniers mois) for time_frame in [6, 12]: prefix = "BUREAU_LAST{}M_".format(time_frame) time_frame_df = bureau[bureau['DAYS_CREDIT'] >= -30*time_frame] agg_bureau = group_and_merge(time_frame_df, agg_bureau, prefix, BUREAU_TIME_AGG) del time_frame_df; gc.collect() # Impayé maximal du dernier crédit contracté sort_bureau = bureau.sort_values(by=['DAYS_CREDIT']) gr = sort_bureau.groupby('SK_ID_CURR')['AMT_CREDIT_MAX_OVERDUE'].last().reset_index() gr.rename({'AMT_CREDIT_MAX_OVERDUE': 'BUREAU_LAST_LOAN_MAX_OVERDUE'}, inplace=True) agg_bureau = agg_bureau.merge(gr, on='SK_ID_CURR', how='left') # Ratios dette / crédit total # BUREAU_DEBT_OVER_CREDIT : taux global d'endettement sur tous les crédits agg_bureau['BUREAU_DEBT_OVER_CREDIT'] = \ agg_bureau['BUREAU_AMT_CREDIT_SUM_DEBT_SUM'] / agg_bureau['BUREAU_AMT_CREDIT_SUM_SUM'] # Version pour les crédits actifs uniquement agg_bureau['BUREAU_ACTIVE_DEBT_OVER_CREDIT'] = \ agg_bureau['BUREAU_ACTIVE_AMT_CREDIT_SUM_DEBT_SUM'] / agg_bureau['BUREAU_ACTIVE_AMT_CREDIT_SUM_SUM'] return agg_bureau def get_bureau_balance(path, num_rows=None): """ Lit et agrège bureau_balance.csv. Ce fichier contient l'historique mensuel de chaque crédit externe (un enregistrement par mois et par crédit), incluant le statut de paiement. Retourne : bb_processed (pd.DataFrame) : Agrégé par SK_ID_BUREAU (identifiant du crédit externe). """ bb = pd.read_csv(os.path.join(path, 'bureau_balance.csv'), nrows=num_rows) # One-hot encode le statut mensuel (STATUS : 0, 1, 2, ..., C, X) bb, categorical_cols = one_hot_encoder(bb, nan_as_category=False) # Calcule le taux moyen de chaque statut par crédit bb_processed = bb.groupby('SK_ID_BUREAU')[categorical_cols].mean().reset_index() # Ajoute les statistiques sur la durée de l'historique (en mois) agg = {'MONTHS_BALANCE': ['min', 'max', 'mean', 'size']} bb_processed = group_and_merge(bb, bb_processed, '', agg, 'SK_ID_BUREAU') del bb; gc.collect() return bb_processed # ============================================================================= # PIPELINE DEMANDES PRÉCÉDENTES : get_previous_applications # ============================================================================= def get_previous_applications(path, num_rows=None): """ Lit et agrège previous_application.csv. Ce fichier contient toutes les demandes de crédit passées du client chez Home Credit (une ligne par demande), qu'elles aient été approuvées ou refusées. Retourne : agg_prev (pd.DataFrame) : Une ligne par client avec features agrégées. """ prev = pd.read_csv(os.path.join(path, 'previous_application.csv'), nrows=num_rows) # Chargement du fichier des paiements pour calculer l'état des prêts actifs pay = pd.read_csv(os.path.join(path, 'installments_payments.csv'), nrows=num_rows) # Encodage one-hot des colonnes catégorielles les plus importantes ohe_columns = [ 'NAME_CONTRACT_STATUS', 'NAME_CONTRACT_TYPE', 'CHANNEL_TYPE', 'NAME_TYPE_SUITE', 'NAME_YIELD_GROUP', 'PRODUCT_COMBINATION', 'NAME_PRODUCT_TYPE', 'NAME_CLIENT_TYPE'] prev, categorical_cols = one_hot_encoder(prev, ohe_columns, nan_as_category=False) # Features engineered # Différence entre le montant demandé et le montant accordé prev['APPLICATION_CREDIT_DIFF'] = prev['AMT_APPLICATION'] - prev['AMT_CREDIT'] # Ratio demandé / accordé (>1 = le client a demandé plus qu'accordé) prev['APPLICATION_CREDIT_RATIO'] = prev['AMT_APPLICATION'] / prev['AMT_CREDIT'] # Durée implicite du remboursement (en mois) prev['CREDIT_TO_ANNUITY_RATIO'] = prev['AMT_CREDIT'] / prev['AMT_ANNUITY'] # Part de l'apport personnel par rapport au crédit total prev['DOWN_PAYMENT_TO_CREDIT'] = prev['AMT_DOWN_PAYMENT'] / prev['AMT_CREDIT'] # Taux d'intérêt simplifié : (total payé / montant emprunté - 1) / nb mensualités total_payment = prev['AMT_ANNUITY'] * prev['CNT_PAYMENT'] prev['SIMPLE_INTERESTS'] = (total_payment / prev['AMT_CREDIT'] - 1) / prev['CNT_PAYMENT'] # Analyse des prêts actifs (approuvés mais pas encore terminés) # DAYS_LAST_DUE = 365243 est la valeur sentinelle pour "prêt toujours en cours" approved = prev[prev['NAME_CONTRACT_STATUS_Approved'] == 1] active_df = approved[approved['DAYS_LAST_DUE'] == 365243] # Calcule combien a déjà été remboursé sur ces prêts actifs active_pay = pay[pay['SK_ID_PREV'].isin(active_df['SK_ID_PREV'])] active_pay_agg = active_pay.groupby('SK_ID_PREV')[['AMT_INSTALMENT', 'AMT_PAYMENT']].sum() active_pay_agg.reset_index(inplace=True) # Différence entre ce qui était dû et ce qui a été payé (retard cumulé) active_pay_agg['INSTALMENT_PAYMENT_DIFF'] = active_pay_agg['AMT_INSTALMENT'] - active_pay_agg['AMT_PAYMENT'] # Fusionne avec les prêts actifs pour obtenir la dette restante et le taux de remboursement active_df = active_df.merge(active_pay_agg, on='SK_ID_PREV', how='left') active_df['REMAINING_DEBT'] = active_df['AMT_CREDIT'] - active_df['AMT_PAYMENT'] active_df['REPAYMENT_RATIO'] = active_df['AMT_PAYMENT'] / active_df['AMT_CREDIT'] # Agrégation des prêts actifs par client active_agg_df = group(active_df, 'PREV_ACTIVE_', PREVIOUS_ACTIVE_AGG) # Ratio global de remboursement des prêts actifs active_agg_df['TOTAL_REPAYMENT_RATIO'] = active_agg_df['PREV_ACTIVE_AMT_PAYMENT_SUM'] / \ active_agg_df['PREV_ACTIVE_AMT_CREDIT_SUM'] del active_pay, active_pay_agg, active_df; gc.collect() # Nettoyage : remplacement des valeurs sentinelles par NaN # 365243 indique une date non renseignée dans ce contexte prev['DAYS_FIRST_DRAWING'] = prev['DAYS_FIRST_DRAWING'].replace(365243, np.nan) prev['DAYS_FIRST_DUE'] = prev['DAYS_FIRST_DUE'].replace(365243, np.nan) prev['DAYS_LAST_DUE_1ST_VERSION'] = prev['DAYS_LAST_DUE_1ST_VERSION'].replace(365243, np.nan) prev['DAYS_LAST_DUE'] = prev['DAYS_LAST_DUE'].replace(365243, np.nan) prev['DAYS_TERMINATION'] = prev['DAYS_TERMINATION'].replace(365243, np.nan) # Différence entre la date de fin prévue initialement et celle révisée prev['DAYS_LAST_DUE_DIFF'] = prev['DAYS_LAST_DUE_1ST_VERSION'] - prev['DAYS_LAST_DUE'] approved['DAYS_LAST_DUE_DIFF'] = approved['DAYS_LAST_DUE_1ST_VERSION'] - approved['DAYS_LAST_DUE'] # Agrégation générale de toutes les demandes précédentes categorical_agg = {key: ['mean'] for key in categorical_cols} # Moyenne des colonnes one-hot agg_prev = group(prev, 'PREV_', {**PREVIOUS_AGG, **categorical_agg}) # Fusionne avec les agrégations des prêts actifs agg_prev = agg_prev.merge(active_agg_df, how='left', on='SK_ID_CURR') del active_agg_df; gc.collect() # Agrégations séparées par statut (approuvé / refusé) agg_prev = group_and_merge(approved, agg_prev, 'APPROVED_', PREVIOUS_APPROVED_AGG) refused = prev[prev['NAME_CONTRACT_STATUS_Refused'] == 1] agg_prev = group_and_merge(refused, agg_prev, 'REFUSED_', PREVIOUS_REFUSED_AGG) del approved, refused; gc.collect() # Agrégations par type de prêt (consommation / cash) for loan_type in ['Consumer loans', 'Cash loans']: type_df = prev[prev['NAME_CONTRACT_TYPE_{}'.format(loan_type)] == 1] prefix = 'PREV_' + loan_type.split(" ")[0] + '_' agg_prev = group_and_merge(type_df, agg_prev, prefix, PREVIOUS_LOAN_TYPE_AGG) del type_df; gc.collect() # Analyse des prêts avec retards de paiement # LATE_PAYMENT = 1 si le paiement a été effectué après la date d'échéance pay['LATE_PAYMENT'] = pay['DAYS_ENTRY_PAYMENT'] - pay['DAYS_INSTALMENT'] pay['LATE_PAYMENT'] = pay['LATE_PAYMENT'].apply(lambda x: 1 if x > 0 else 0) dpd_id = pay[pay['LATE_PAYMENT'] > 0]['SK_ID_PREV'].unique() # Agrégation des caractéristiques des prêts ayant connu des retards agg_dpd = group_and_merge(prev[prev['SK_ID_PREV'].isin(dpd_id)], agg_prev, 'PREV_LATE_', PREVIOUS_LATE_PAYMENTS_AGG) del agg_dpd, dpd_id; gc.collect() # Agrégation par fenêtre temporelle récente (12 et 24 derniers mois) for time_frame in [12, 24]: time_frame_df = prev[prev['DAYS_DECISION'] >= -30*time_frame] prefix = 'PREV_LAST{}M_'.format(time_frame) agg_prev = group_and_merge(time_frame_df, agg_prev, prefix, PREVIOUS_TIME_AGG) del time_frame_df; gc.collect() del prev; gc.collect() return agg_prev # ============================================================================= # PIPELINE POS-CASH : get_pos_cash # ============================================================================= def get_pos_cash(path, num_rows=None): """ Lit et agrège POS_CASH_balance.csv. Ce fichier contient le statut mensuel des prêts à la consommation et des contrats POS (Point Of Sale = achats à crédit en magasin) précédents. Retourne : pos_agg (pd.DataFrame) : Agrégé par client (SK_ID_CURR). """ pos = pd.read_csv(os.path.join(path, 'POS_CASH_balance.csv'), nrows=num_rows) pos, categorical_cols = one_hot_encoder(pos, nan_as_category=False) # Indicateur binaire : retard de paiement ce mois-ci (SK_DPD > 0 = jours de retard) pos['LATE_PAYMENT'] = pos['SK_DPD'].apply(lambda x: 1 if x > 0 else 0) # Agrégation globale par client categorical_agg = {key: ['mean'] for key in categorical_cols} pos_agg = group(pos, 'POS_', {**POS_CASH_AGG, **categorical_agg}) # Features sur l'état d'avancement des prêts # Trie par prêt et par mois pour accéder au premier/dernier enregistrement sort_pos = pos.sort_values(by=['SK_ID_PREV', 'MONTHS_BALANCE']) gp = sort_pos.groupby('SK_ID_PREV') df = pd.DataFrame() df['SK_ID_CURR'] = gp['SK_ID_CURR'].first() df['MONTHS_BALANCE_MAX'] = gp['MONTHS_BALANCE'].max() # Taux moyen de prêts arrivés à complétion df['POS_LOAN_COMPLETED_MEAN'] = gp['NAME_CONTRACT_STATUS_Completed'].mean() # Indicateur : prêt remboursé avant l'échéance initiale # (CNT_INSTALMENT initial > CNT_INSTALMENT final = le nb d'échéances a diminué) df['POS_COMPLETED_BEFORE_MEAN'] = gp['CNT_INSTALMENT'].first() - gp['CNT_INSTALMENT'].last() df['POS_COMPLETED_BEFORE_MEAN'] = df.apply(lambda x: 1 if x['POS_COMPLETED_BEFORE_MEAN'] > 0 and x['POS_LOAN_COMPLETED_MEAN'] > 0 else 0, axis=1) # Nombre et ratio d'échéances restantes (futures) df['POS_REMAINING_INSTALMENTS'] = gp['CNT_INSTALMENT_FUTURE'].last() df['POS_REMAINING_INSTALMENTS_RATIO'] = gp['CNT_INSTALMENT_FUTURE'].last() / gp['CNT_INSTALMENT'].last() # Agrège par client et fusionne df_gp = df.groupby('SK_ID_CURR').sum().reset_index() df_gp.drop(['MONTHS_BALANCE_MAX'], axis=1, inplace=True) pos_agg = pd.merge(pos_agg, df_gp, on='SK_ID_CURR', how='left') del df, gp, df_gp, sort_pos; gc.collect() # Taux de retard sur les 3 demandes les plus récentes pos = do_sum(pos, ['SK_ID_PREV'], 'LATE_PAYMENT', 'LATE_PAYMENT_SUM') # Identifie le dernier mois de chaque prêt last_month_df = pos.groupby('SK_ID_PREV')['MONTHS_BALANCE'].idxmax() # Prend les 3 prêts les plus récents par client sort_pos = pos.sort_values(by=['SK_ID_PREV', 'MONTHS_BALANCE']) gp = sort_pos.iloc[last_month_df].groupby('SK_ID_CURR').tail(3) gp_mean = gp.groupby('SK_ID_CURR').mean().reset_index() pos_agg = pd.merge(pos_agg, gp_mean[['SK_ID_CURR','LATE_PAYMENT_SUM']], on='SK_ID_CURR', how='left') # Suppression des colonnes catégorielles peu utiles drop_features = [ 'POS_NAME_CONTRACT_STATUS_Canceled_MEAN', 'POS_NAME_CONTRACT_STATUS_Amortized debt_MEAN', 'POS_NAME_CONTRACT_STATUS_XNA_MEAN'] pos_agg.drop([f for f in drop_features if f in pos_agg.columns], axis=1, inplace=True) return pos_agg # ============================================================================= # PIPELINE PAIEMENTS D'ÉCHÉANCES : get_installment_payments # ============================================================================= def get_installment_payments(path, num_rows=None): """ Lit et agrège installments_payments.csv. Ce fichier contient l'historique détaillé de chaque paiement d'échéance, pour chaque prêt précédent ou en cours du client. Retourne : pay_agg (pd.DataFrame) : Agrégé par client (SK_ID_CURR). """ pay = pd.read_csv(os.path.join(path, 'installments_payments.csv'), nrows=num_rows) # Regroupement des paiements par numéro d'échéance # Un client peut payer en plusieurs fois pour une même échéance → on les somme pay = do_sum(pay, ['SK_ID_PREV', 'NUM_INSTALMENT_NUMBER'], 'AMT_PAYMENT', 'AMT_PAYMENT_GROUPED') pay['PAYMENT_DIFFERENCE'] = pay['AMT_INSTALMENT'] - pay['AMT_PAYMENT_GROUPED'] # Solde non payé pay['PAYMENT_RATIO'] = pay['AMT_INSTALMENT'] / pay['AMT_PAYMENT_GROUPED'] # Taux de couverture pay['PAID_OVER_AMOUNT'] = pay['AMT_PAYMENT'] - pay['AMT_INSTALMENT'] # Surpayement éventuel pay['PAID_OVER'] = (pay['PAID_OVER_AMOUNT'] > 0).astype(int) # Indicateur surpayement # Indicateurs de ponctualité de paiement # DPD : Days Past Due = nombre de jours de retard (0 si payé à temps ou en avance) pay['DPD'] = pay['DAYS_ENTRY_PAYMENT'] - pay['DAYS_INSTALMENT'] pay['DPD'] = pay['DPD'].apply(lambda x: 0 if x <= 0 else x) # DBD : Days Before Due = nombre de jours d'avance (0 si en retard ou à temps) pay['DBD'] = pay['DAYS_INSTALMENT'] - pay['DAYS_ENTRY_PAYMENT'] pay['DBD'] = pay['DBD'].apply(lambda x: 0 if x <= 0 else x) # Indicateur binaire : paiement effectué avant l'échéance pay['LATE_PAYMENT'] = pay['DBD'].apply(lambda x: 1 if x > 0 else 0) # Ratios de paiement pay['INSTALMENT_PAYMENT_RATIO'] = pay['AMT_PAYMENT'] / pay['AMT_INSTALMENT'] # Ratio de paiement uniquement pour les paiements tardifs (0 sinon) pay['LATE_PAYMENT_RATIO'] = pay.apply(lambda x: x['INSTALMENT_PAYMENT_RATIO'] if x['LATE_PAYMENT'] == 1 else 0, axis=1) # Indicateur : retard significatif (ratio > 5% de l'échéance) pay['SIGNIFICANT_LATE_PAYMENT'] = pay['LATE_PAYMENT_RATIO'].apply(lambda x: 1 if x > 0.05 else 0) # Indicateurs de retard par seuil de jours (7 jours et 15 jours) pay['DPD_7'] = pay['DPD'].apply(lambda x: 1 if x >= 7 else 0) pay['DPD_15'] = pay['DPD'].apply(lambda x: 1 if x >= 15 else 0) # Agrégation globale par client pay_agg = group(pay, 'INS_', INSTALLMENTS_AGG) # Agrégation sur les paiements récents (36 et 60 derniers mois) for months in [36, 60]: recent_prev_id = pay[pay['DAYS_INSTALMENT'] >= -30*months]['SK_ID_PREV'].unique() pay_recent = pay[pay['SK_ID_PREV'].isin(recent_prev_id)] prefix = 'INS_{}M_'.format(months) pay_agg = group_and_merge(pay_recent, pay_agg, prefix, INSTALLMENTS_TIME_AGG) # Features de tendance sur les K dernières échéances # Utilise une régression linéaire pour détecter si les retards augmentent ou diminuent group_features = ['SK_ID_CURR', 'SK_ID_PREV', 'DPD', 'LATE_PAYMENT', 'PAID_OVER_AMOUNT', 'PAID_OVER', 'DAYS_INSTALMENT'] gp = pay[group_features].groupby('SK_ID_CURR') func = partial(trend_in_last_k_instalment_features, periods=INSTALLMENTS_LAST_K_TREND_PERIODS) g = parallel_apply(gp, func, index_name='SK_ID_CURR', chunk_size=10000).reset_index() pay_agg = pay_agg.merge(g, on='SK_ID_CURR', how='left') # Features sur le dernier prêt uniquement g = parallel_apply(gp, installments_last_loan_features, index_name='SK_ID_CURR', chunk_size=10000).reset_index() pay_agg = pay_agg.merge(g, on='SK_ID_CURR', how='left') return pay_agg def trend_in_last_k_instalment_features(gr, periods): """ Calcule la pente (tendance) des retards et surpaiements sur les K dernières échéances de chaque client. Une pente positive de DPD indique que les retards s'aggravent → signal de risque. Une pente négative indique une amélioration du comportement de paiement. Paramètres : gr : Groupe de paiements d'un client (DataFrame) periods : Liste de périodes K (ex: [12, 24, 60, 120]) Retourne : features (dict) : Pentes calculées pour chaque période et chaque feature. """ gr_ = gr.copy() # Trie par date d'échéance décroissante → les plus récentes en premier gr_.sort_values(['DAYS_INSTALMENT'], ascending=False, inplace=True) features = {} for period in periods: gr_period = gr_.iloc[:period] # Garde seulement les K dernières échéances features = add_trend_feature(features, gr_period, 'DPD', '{}_TREND_'.format(period)) features = add_trend_feature(features, gr_period, 'PAID_OVER_AMOUNT', '{}_TREND_'.format(period)) return features def installments_last_loan_features(gr): """ Calcule des statistiques sur les paiements du dernier prêt actif du client. Se concentre sur le prêt le plus récent (SK_ID_PREV le plus récent) pour capturer le comportement actuel, pas seulement l'historique global. Retourne : features (dict) : Statistiques (sum, mean, max, std, etc.) sur DPD, LATE_PAYMENT et PAID_OVER_AMOUNT pour le dernier prêt. """ gr_ = gr.copy() gr_.sort_values(['DAYS_INSTALMENT'], ascending=False, inplace=True) # Identifie l'ID du prêt le plus récent (première ligne après tri décroissant) last_installment_id = gr_['SK_ID_PREV'].iloc[0] gr_ = gr_[gr_['SK_ID_PREV'] == last_installment_id] # Filtre sur ce prêt uniquement features = {} features = add_features_in_group(features, gr_, 'DPD', ['sum', 'mean', 'max', 'std'], 'LAST_LOAN_') features = add_features_in_group(features, gr_, 'LATE_PAYMENT', ['count', 'mean'], 'LAST_LOAN_') features = add_features_in_group(features, gr_, 'PAID_OVER_AMOUNT', ['sum', 'mean', 'max', 'min', 'std'], 'LAST_LOAN_') features = add_features_in_group(features, gr_, 'PAID_OVER', ['count', 'mean'], 'LAST_LOAN_') return features # ============================================================================= # PIPELINE CARTES DE CRÉDIT : get_credit_card # ============================================================================= def get_credit_card(path, num_rows=None): """ Lit et agrège credit_card_balance.csv. Ce fichier contient les soldes mensuels de chaque carte de crédit précédente ou en cours du client. Retourne : cc_agg (pd.DataFrame) : Agrégé par client (SK_ID_CURR). """ cc = pd.read_csv(os.path.join(path, 'credit_card_balance.csv'), nrows=num_rows) cc, cat_cols = one_hot_encoder(cc, nan_as_category=False) # Correction d'une faute de frappe dans le nom de colonne du CSV source cc.rename(columns={'AMT_RECIVABLE': 'AMT_RECEIVABLE'}, inplace=True) # Features engineered # Taux d'utilisation du plafond de crédit (balance / limite) cc['LIMIT_USE'] = cc['AMT_BALANCE'] / cc['AMT_CREDIT_LIMIT_ACTUAL'] # Ratio entre le paiement effectué et le minimum dû cc['PAYMENT_DIV_MIN'] = cc['AMT_PAYMENT_CURRENT'] / cc['AMT_INST_MIN_REGULARITY'] # Indicateur de retard (SK_DPD = jours de retard) cc['LATE_PAYMENT'] = cc['SK_DPD'].apply(lambda x: 1 if x > 0 else 0) # Ratio retraits ATM / limite de crédit (mesure le type d'utilisation) cc['DRAWING_LIMIT_RATIO'] = cc['AMT_DRAWINGS_ATM_CURRENT'] / cc['AMT_CREDIT_LIMIT_ACTUAL'] # Agrégation globale par client cc_card_agg = {k: v for k, v in CREDIT_CARD_AGG.items() if k in cc.columns} cc_agg = cc.groupby('SK_ID_CURR').agg(cc_card_agg) # Renomme les colonnes : CC_NOMCOLONNE_AGREGATION (ex: CC_AMT_BALANCE_MAX) cc_agg.columns = pd.Index(['CC_' + e[0] + "_" + e[1].upper() for e in cc_agg.columns.tolist()]) cc_agg.reset_index(inplace=True) # Features sur le dernier mois de chaque carte # Prend la ligne correspondant au mois le plus récent de chaque carte last_ids = cc.groupby('SK_ID_PREV')['MONTHS_BALANCE'].idxmax() last_months_df = cc[cc.index.isin(last_ids)] cc_agg = group_and_merge(last_months_df, cc_agg, 'CC_LAST_', {'AMT_BALANCE': ['mean', 'max']}) # Agrégation sur les périodes récentes (12, 24, 48 derniers mois) for months in [12, 24, 48]: cc_prev_id = cc[cc['MONTHS_BALANCE'] >= -months]['SK_ID_PREV'].unique() cc_recent = cc[cc['SK_ID_PREV'].isin(cc_prev_id)] prefix = 'INS_{}M_'.format(months) cc_agg = group_and_merge(cc_recent, cc_agg, prefix, CREDIT_CARD_TIME_AGG) return cc_agg # ============================================================================= # FONCTIONS UTILITAIRES # ============================================================================= @contextmanager def timer(name): """ Gestionnaire de contexte pour mesurer le temps d'exécution d'un bloc. Utilisation : with timer("nom de l'étape"): # code à mesurer Affiche : "nom de l'étape - done in Xs" """ t0 = time.time() yield print("{} - done in {:.0f}s".format(name, time.time() - t0)) def group(df_to_agg, prefix, aggregations, aggregate_by='SK_ID_CURR'): """ Agrège un DataFrame par une colonne clé et renomme les colonnes résultantes. Paramètres : df_to_agg : DataFrame source à agréger prefix : Préfixe à ajouter aux noms de colonnes (ex: 'BUREAU_') aggregations : Dictionnaire {colonne: [liste d'agrégations]} ex: {'AMT_CREDIT': ['mean', 'max']} aggregate_by : Colonne de regroupement (défaut: 'SK_ID_CURR') Retourne : DataFrame agrégé avec colonnes nommées PREFIX_COLONNE_AGREGATION ex: BUREAU_AMT_CREDIT_MEAN, BUREAU_AMT_CREDIT_MAX """ # Filtre les colonnes demandées qui n'existent pas dans le DataFrame aggregations = {k: v for k, v in aggregations.items() if k in df_to_agg.columns} agg_df = df_to_agg.groupby(aggregate_by).agg(aggregations) agg_df.columns = pd.Index(['{}{}_{}'.format(prefix, e[0], e[1].upper()) for e in agg_df.columns.tolist()]) return agg_df.reset_index() def group_and_merge(df_to_agg, df_to_merge, prefix, aggregations, aggregate_by='SK_ID_CURR'): """ Agrège df_to_agg puis fusionne (left join) le résultat dans df_to_merge. Raccourci combinant group() + merge() en une seule opération. """ agg_df = group(df_to_agg, prefix, aggregations, aggregate_by=aggregate_by) return df_to_merge.merge(agg_df, how='left', on=aggregate_by) def do_mean(df, group_cols, counted, agg_name): """ Calcule la moyenne de `counted` groupée par `group_cols` et l'ajoute au DataFrame. Utile pour créer des features de référence de groupe (ex : revenu moyen du groupe socio-démographique du client). """ gp = df[group_cols + [counted]].groupby(group_cols)[counted].mean().reset_index().rename( columns={counted: agg_name}) df = df.merge(gp, on=group_cols, how='left') del gp gc.collect() return df def do_median(df, group_cols, counted, agg_name): """ Calcule la médiane de `counted` groupée par `group_cols` et l'ajoute au DataFrame. La médiane est plus robuste aux outliers que la moyenne. """ gp = df[group_cols + [counted]].groupby(group_cols)[counted].median().reset_index().rename( columns={counted: agg_name}) df = df.merge(gp, on=group_cols, how='left') del gp gc.collect() return df def do_std(df, group_cols, counted, agg_name): """ Calcule l'écart-type de `counted` groupé par `group_cols` et l'ajoute au DataFrame. Mesure la dispersion au sein du groupe → un client avec une valeur très éloignée de la moyenne de son groupe peut être un signal de risque. """ gp = df[group_cols + [counted]].groupby(group_cols)[counted].std().reset_index().rename( columns={counted: agg_name}) df = df.merge(gp, on=group_cols, how='left') del gp gc.collect() return df def do_sum(df, group_cols, counted, agg_name): """ Calcule la somme de `counted` groupée par `group_cols` et l'ajoute au DataFrame. Utile pour agréger des indicateurs binaires (ex : nombre total de retards). """ gp = df[group_cols + [counted]].groupby(group_cols)[counted].sum().reset_index().rename( columns={counted: agg_name}) df = df.merge(gp, on=group_cols, how='left') del gp gc.collect() return df def one_hot_encoder(df, categorical_columns=None, nan_as_category=True): """ Applique un encodage one-hot aux colonnes catégorielles. Chaque valeur unique d'une colonne catégorielle devient une nouvelle colonne binaire. Ex: CREDIT_ACTIVE = ['Active', 'Closed'] → CREDIT_ACTIVE_Active + CREDIT_ACTIVE_Closed Paramètres : df : DataFrame source categorical_columns : Liste de colonnes à encoder (None = toutes les colonnes texte) nan_as_category : Si True, crée une colonne séparée pour les valeurs NaN Retourne : df : DataFrame avec les nouvelles colonnes one-hot categorical_cols : Liste des noms des nouvelles colonnes créées """ original_columns = list(df.columns) if not categorical_columns: categorical_columns = [col for col in df.columns if df[col].dtype == 'object' or df[col].dtype.name == 'str'] df = pd.get_dummies(df, columns=categorical_columns, dummy_na=nan_as_category) categorical_columns = [c for c in df.columns if c not in original_columns] return df, categorical_columns def label_encoder(df, categorical_columns=None): """ Encode les variables catégorielles en entiers avec pd.factorize. Contrairement à l'encodage one-hot, cette méthode ne crée pas de nouvelles colonnes : elle remplace les valeurs texte par des entiers (0, 1, 2, ...). Utilisé ici pour les colonnes binaires ou ordinales. Retourne : df : DataFrame avec colonnes encodées categorical_cols : Liste des colonnes qui ont été encodées """ if not categorical_columns: categorical_columns = [col for col in df.columns if df[col].dtype == 'object' or df[col].dtype.name == 'str'] for col in categorical_columns: df[col], uniques = pd.factorize(df[col]) return df, categorical_columns def add_features(feature_name, aggs, features, feature_names, groupby): """ Calcule et fusionne des agrégations statistiques avancées dans un DataFrame features. Supporte les agrégations standard (mean, max, etc.) ainsi que : 'kurt' : kurtosis (mesure des queues de distribution) 'iqr' : intervalle interquartile (robustesse aux outliers) Paramètres : feature_name : Nom de la feature à agréger aggs : Liste des agrégations à appliquer features : DataFrame dans lequel fusionner les résultats feature_names : Liste des noms de features (étendue en place) groupby : Objet GroupBy pandas Retourne : (features, feature_names) : DataFrame mis à jour et noms de colonnes ajoutés """ feature_names.extend(['{}_{}'.format(feature_name, agg) for agg in aggs]) for agg in aggs: if agg == 'kurt': agg_func = kurtosis elif agg == 'iqr': agg_func = iqr else: agg_func = agg # Nom de méthode pandas standard (ex: 'mean', 'max') g = groupby[feature_name].agg(agg_func).reset_index().rename(index=str, columns={feature_name: '{}_{}'.format(feature_name, agg)}) features = features.merge(g, on='SK_ID_CURR', how='left') return features, feature_names def add_features_in_group(features, gr_, feature_name, aggs, prefix): """ Calcule des statistiques sur un groupe (sous-DataFrame) et les stocke dans un dict. Utilisé dans les fonctions de traitement par groupe (ex: installments_last_loan_features). Paramètres : features : Dictionnaire de résultats à compléter gr_ : Sous-DataFrame du groupe courant feature_name : Nom de la colonne à agréger aggs : Liste des agrégations : 'sum', 'mean', 'max', 'min', 'std', 'count', 'skew', 'kurt', 'iqr', 'median' prefix : Préfixe des clés dans le dictionnaire (ex: 'LAST_LOAN_') Retourne : features (dict) : Dictionnaire enrichi des statistiques calculées """ for agg in aggs: if agg == 'sum': features['{}{}_sum'.format(prefix, feature_name)] = gr_[feature_name].sum() elif agg == 'mean': features['{}{}_mean'.format(prefix, feature_name)] = gr_[feature_name].mean() elif agg == 'max': features['{}{}_max'.format(prefix, feature_name)] = gr_[feature_name].max() elif agg == 'min': features['{}{}_min'.format(prefix, feature_name)] = gr_[feature_name].min() elif agg == 'std': features['{}{}_std'.format(prefix, feature_name)] = gr_[feature_name].std() elif agg == 'count': features['{}{}_count'.format(prefix, feature_name)] = gr_[feature_name].count() elif agg == 'skew': features['{}{}_skew'.format(prefix, feature_name)] = skew(gr_[feature_name]) elif agg == 'kurt': features['{}{}_kurt'.format(prefix, feature_name)] = kurtosis(gr_[feature_name]) elif agg == 'iqr': features['{}{}_iqr'.format(prefix, feature_name)] = iqr(gr_[feature_name]) elif agg == 'median': features['{}{}_median'.format(prefix, feature_name)] = gr_[feature_name].median() return features def add_trend_feature(features, gr, feature_name, prefix): """ Calcule la tendance (pente) d'une feature sur une série temporelle. Utilise une régression linéaire simple : la pente (coef_[0]) indique si la feature augmente ou diminue au fil du temps. Ex : une pente positive de DPD indique que les retards s'aggravent. Paramètres : features : Dictionnaire des features à enrichir gr : Groupe (DataFrame trié par temps) feature_name : Colonne sur laquelle calculer la tendance prefix : Préfixe du nom de la feature (ex: '12_TREND_') Retourne : features (dict) : Dictionnaire avec la pente ajoutée (NaN si calcul impossible) """ y = gr[feature_name].values try: x = np.arange(0, len(y)).reshape(-1, 1) # Variable temporelle (indice de position) lr = LinearRegression() lr.fit(x, y) trend = lr.coef_[0] # Pente de la droite de régression except: trend = np.nan # Retourne NaN si le calcul échoue (ex: groupe vide ou uniforme) features['{}{}'.format(prefix, feature_name)] = trend return features def parallel_apply(groups, func, index_name='Index', num_workers=0, chunk_size=100000): """ Applique une fonction à chaque groupe d'un GroupBy en parallèle. Divise les groupes en chunks et utilise un Pool de processus pour accélérer les calculs sur les gros datasets. Paramètres : groups : Objet GroupBy pandas func : Fonction à appliquer à chaque groupe (retourne un dict) index_name : Nom de l'index dans le DataFrame résultant num_workers: Nombre de processus parallèles (0 = utilise NUM_THREADS) chunk_size : Nombre de groupes traités par batch Retourne : features (pd.DataFrame) : DataFrame avec une ligne par groupe """ if num_workers <= 0: num_workers = NUM_THREADS indeces, features = [], [] for index_chunk, groups_chunk in chunk_groups(groups, chunk_size): with mp.pool.Pool(num_workers) as executor: features_chunk = executor.map(func, groups_chunk) # Traitement parallèle features.extend(features_chunk) indeces.extend(index_chunk) features = pd.DataFrame(features) features.index = indeces features.index.name = index_name return features def chunk_groups(groupby_object, chunk_size): """ Générateur qui divise un objet GroupBy en chunks de taille fixe. Permet de traiter de très grands GroupBy en mémoire contrôlée. Paramètres : groupby_object : GroupBy pandas à découper chunk_size : Nombre de groupes par chunk Yields : (index_chunk, group_chunk) : Tuple (liste d'index, liste de DataFrames) """ n_groups = groupby_object.ngroups group_chunk, index_chunk = [], [] for i, (index, df) in enumerate(groupby_object): group_chunk.append(df) index_chunk.append(index) # Yield dès que le chunk est plein, ou à la fin des groupes if (i + 1) % chunk_size == 0 or i + 1 == n_groups: group_chunk_, index_chunk_ = group_chunk.copy(), index_chunk.copy() group_chunk, index_chunk = [], [] yield index_chunk_, group_chunk_ def reduce_memory(df): """ Réduit l'empreinte mémoire du DataFrame en utilisant les types numériques les plus petits possibles. Pour chaque colonne numérique, vérifie les valeurs min/max et choisit le type le plus compact : - int64 → int8/int16/int32 si les valeurs le permettent - float64 → float16/float32 si les valeurs le permettent Affiche la réduction de mémoire obtenue. Retourne : df (pd.DataFrame) : Même DataFrame avec des types optimisés """ start_mem = df.memory_usage().sum() / 1024 ** 2 print('Initial df memory usage is {:.2f} MB for {} columns' .format(start_mem, len(df.columns))) for col in df.columns: col_type = df[col].dtypes if col_type != object: # Ne touche pas les colonnes texte cmin = df[col].min() cmax = df[col].max() if str(col_type)[:3] == 'int': # Choisit le plus petit type entier compatible if cmin > np.iinfo(np.int8).min and cmax < np.iinfo(np.int8).max: df[col] = df[col].astype(np.int8) elif cmin > np.iinfo(np.int16).min and cmax < np.iinfo(np.int16).max: df[col] = df[col].astype(np.int16) elif cmin > np.iinfo(np.int32).min and cmax < np.iinfo(np.int32).max: df[col] = df[col].astype(np.int32) elif cmin > np.iinfo(np.int64).min and cmax < np.iinfo(np.int64).max: df[col] = df[col].astype(np.int64) else: # Choisit le plus petit type flottant compatible if cmin > np.finfo(np.float16).min and cmax < np.finfo(np.float16).max: df[col] = df[col].astype(np.float16) elif cmin > np.finfo(np.float32).min and cmax < np.finfo(np.float32).max: df[col] = df[col].astype(np.float32) else: df[col] = df[col].astype(np.float64) end_mem = df.memory_usage().sum() / 1024 ** 2 memory_reduction = 100 * (start_mem - end_mem) / start_mem print('Final memory usage is: {:.2f} MB - decreased by {:.1f}%'.format(end_mem, memory_reduction)) return df def replace_infinite_with_nan(df): """ Remplace toutes les valeurs +inf et -inf par NaN dans les colonnes numériques. Ces valeurs infinies apparaissent lors des divisions par zéro dans les calculs de ratios. Les modèles ML ne peuvent pas traiter les infinis, donc on les convertit en NaN (valeurs manquantes gérées par imputation). """ numeric_cols = df.select_dtypes(include=[np.number]).columns if len(numeric_cols) == 0: return df inf_mask = np.isinf(df[numeric_cols].to_numpy()) inf_count = int(inf_mask.sum()) if inf_count > 0: print(f"Replacing {inf_count} infinite values with NaN") df[numeric_cols] = df[numeric_cols].replace([np.inf, -np.inf], np.nan) return df def sanitize_feature_names(df): """ Normalise les noms de colonnes pour qu'ils ne contiennent que des caractères alphanumériques et underscores (ASCII-safe). Certains encodages one-hot créent des noms avec des espaces ou caractères spéciaux (ex: 'CREDIT_TYPE_Consumer credit') qui sont incompatibles avec certains frameworks ML (notamment LightGBM). Traitement : 1. Remplace tous les caractères non alphanumériques par '_' 2. Supprime les underscores multiples consécutifs 3. Résout les doublons de noms en ajoutant un suffixe numérique Affiche le nombre de colonnes renommées ou dédoublonnées. """ original_cols = [str(col) for col in df.columns] cleaned_cols = [] changed_count = 0 for col in original_cols: # Remplace tout caractère non alphanumérique/underscore par '_' clean = re.sub(r'[^0-9A-Za-z_]+', '_', col) # Réduit les séquences d'underscores multiples à un seul clean = re.sub(r'__+', '_', clean).strip('_') if not clean: clean = 'feature' # Nom de repli si la colonne devient vide après nettoyage if clean != col: changed_count += 1 cleaned_cols.append(clean) # Résolution des doublons (deux colonnes différentes pourraient avoir le même nom nettoyé) unique_cols = [] seen = {} duplicate_resolved = 0 for col in cleaned_cols: count = seen.get(col, 0) if count == 0: unique_cols.append(col) else: unique_cols.append(f"{col}_{count}") # Ajoute un suffixe numérique pour différencier duplicate_resolved += 1 seen[col] = count + 1 df.columns = unique_cols if changed_count > 0 or duplicate_resolved > 0: print( f"Sanitized feature names: {changed_count} renamed, " f"{duplicate_resolved} duplicates disambiguated" ) return df # ============================================================================= # CONFIGURATIONS GLOBALES # ============================================================================= # Nombre de threads pour le traitement parallèle NUM_THREADS = 4 # Répertoire contenant tous les fichiers CSV source DATA_DIRECTORY = "data/raw/" # Périodes (en nombre d'échéances) pour les features de tendance des paiements # Ex: les 12, 24, 60 et 120 dernières échéances INSTALLMENTS_LAST_K_TREND_PERIODS = [12, 24, 60, 120] # ============================================================================= # DICTIONNAIRES D'AGRÉGATIONS # Ces dictionnaires définissent quelles statistiques calculer sur quelles colonnes # lors des agrégations par client. Format : {nom_colonne: [liste_d_agrégations]} # ============================================================================= # Agrégations générales sur tous les crédits du bureau BUREAU_AGG = { 'SK_ID_BUREAU': ['nunique'], # Nombre de crédits externes distincts 'DAYS_CREDIT': ['min', 'max', 'mean'], # Ancienneté des crédits (en jours) 'DAYS_CREDIT_ENDDATE': ['min', 'max'], # Dates de fin prévues 'AMT_CREDIT_MAX_OVERDUE': ['max', 'mean'], # Impayé maximal historique 'AMT_CREDIT_SUM': ['max', 'mean', 'sum'], # Montants des crédits 'AMT_CREDIT_SUM_DEBT': ['max', 'mean', 'sum'], # Dettes restantes 'AMT_CREDIT_SUM_OVERDUE': ['max', 'mean', 'sum'], # Montants en retard 'AMT_ANNUITY': ['mean'], # Mensualité moyenne 'DEBT_CREDIT_DIFF': ['mean', 'sum'], # Différence crédit - dette 'MONTHS_BALANCE_MEAN': ['mean', 'var'], # Moyenne des balances mensuelles 'MONTHS_BALANCE_SIZE': ['mean', 'sum'], # Durée de l'historique mensuel 'STATUS_0': ['mean'], # % de mois sans retard 'STATUS_1': ['mean'], # % de mois avec 1 mois de retard 'STATUS_12345': ['mean'], # % de mois avec au moins 1 mois de retard 'STATUS_C': ['mean'], # % de mois où le crédit était clôturé 'STATUS_X': ['mean'], # % de mois avec statut inconnu 'CREDIT_ACTIVE_Active': ['mean'], # Proportion de crédits actifs 'CREDIT_ACTIVE_Closed': ['mean'], # Proportion de crédits fermés 'CREDIT_ACTIVE_Sold': ['mean'], # Proportion de crédits vendus 'CREDIT_TYPE_Consumer credit': ['mean'], # Proportion par type de crédit 'CREDIT_TYPE_Credit card': ['mean'], 'CREDIT_TYPE_Car loan': ['mean'], 'CREDIT_TYPE_Mortgage': ['mean'], 'CREDIT_TYPE_Microloan': ['mean'], 'LL_AMT_CREDIT_SUM_OVERDUE': ['mean'], # Features basées sur la durée du crédit (LL = loan length) 'LL_DEBT_CREDIT_DIFF': ['mean'], 'LL_STATUS_12345': ['mean'], } # Agrégations sur les crédits actifs uniquement BUREAU_ACTIVE_AGG = { 'DAYS_CREDIT': ['max', 'mean'], 'DAYS_CREDIT_ENDDATE': ['min', 'max'], 'AMT_CREDIT_MAX_OVERDUE': ['max', 'mean'], 'AMT_CREDIT_SUM': ['max', 'sum'], 'AMT_CREDIT_SUM_DEBT': ['mean', 'sum'], 'AMT_CREDIT_SUM_OVERDUE': ['max', 'mean'], 'DAYS_CREDIT_UPDATE': ['min', 'mean'], # Date de dernière mise à jour du crédit 'DEBT_PERCENTAGE': ['mean'], 'DEBT_CREDIT_DIFF': ['mean'], 'CREDIT_TO_ANNUITY_RATIO': ['mean'], 'MONTHS_BALANCE_MEAN': ['mean', 'var'], 'MONTHS_BALANCE_SIZE': ['mean', 'sum'], } # Agrégations sur les crédits fermés uniquement BUREAU_CLOSED_AGG = { 'DAYS_CREDIT': ['max', 'var'], 'DAYS_CREDIT_ENDDATE': ['max'], 'AMT_CREDIT_MAX_OVERDUE': ['max', 'mean'], 'AMT_CREDIT_SUM_OVERDUE': ['mean'], 'AMT_CREDIT_SUM': ['max', 'mean', 'sum'], 'AMT_CREDIT_SUM_DEBT': ['max', 'sum'], 'DAYS_CREDIT_UPDATE': ['max'], 'ENDDATE_DIF': ['mean'], # Différence entre date de fin prévue et réelle 'STATUS_12345': ['mean'], } # Agrégations par type de prêt (consommation, carte, hypothèque, auto, micro) BUREAU_LOAN_TYPE_AGG = { 'DAYS_CREDIT': ['mean', 'max'], 'AMT_CREDIT_MAX_OVERDUE': ['mean', 'max'], 'AMT_CREDIT_SUM': ['mean', 'max'], 'AMT_CREDIT_SUM_DEBT': ['mean', 'max'], 'DEBT_PERCENTAGE': ['mean'], 'DEBT_CREDIT_DIFF': ['mean'], 'DAYS_CREDIT_ENDDATE': ['max'], } # Agrégations sur les crédits récents (6 ou 12 derniers mois) BUREAU_TIME_AGG = { 'AMT_CREDIT_MAX_OVERDUE': ['max', 'mean'], 'AMT_CREDIT_SUM_OVERDUE': ['mean'], 'AMT_CREDIT_SUM': ['max', 'sum'], 'AMT_CREDIT_SUM_DEBT': ['mean', 'sum'], 'DEBT_PERCENTAGE': ['mean'], 'DEBT_CREDIT_DIFF': ['mean'], 'STATUS_0': ['mean'], 'STATUS_12345': ['mean'], } # Agrégations sur toutes les demandes précédentes PREVIOUS_AGG = { 'SK_ID_PREV': ['nunique'], # Nombre de demandes précédentes 'AMT_ANNUITY': ['min', 'max', 'mean'], 'AMT_DOWN_PAYMENT': ['max', 'mean'], 'HOUR_APPR_PROCESS_START': ['min', 'max', 'mean'], # Heure de la demande 'RATE_DOWN_PAYMENT': ['max', 'mean'], 'DAYS_DECISION': ['min', 'max', 'mean'], # Jours depuis la décision d'octroi 'CNT_PAYMENT': ['max', 'mean'], # Nombre d'échéances 'DAYS_TERMINATION': ['max'], # Date de fin du contrat 'CREDIT_TO_ANNUITY_RATIO': ['mean', 'max'], 'APPLICATION_CREDIT_DIFF': ['min', 'max', 'mean'], 'APPLICATION_CREDIT_RATIO': ['min', 'max', 'mean', 'var'], 'DOWN_PAYMENT_TO_CREDIT': ['mean'], } # Agrégations sur les prêts actifs en cours PREVIOUS_ACTIVE_AGG = { 'SK_ID_PREV': ['nunique'], 'SIMPLE_INTERESTS': ['mean'], # Taux d'intérêt simplifié 'AMT_ANNUITY': ['max', 'sum'], 'AMT_APPLICATION': ['max', 'mean'], 'AMT_CREDIT': ['sum'], 'AMT_DOWN_PAYMENT': ['max', 'mean'], 'DAYS_DECISION': ['min', 'mean'], 'CNT_PAYMENT': ['mean', 'sum'], 'DAYS_LAST_DUE_1ST_VERSION': ['min', 'max', 'mean'], 'AMT_PAYMENT': ['sum'], # Total déjà remboursé 'INSTALMENT_PAYMENT_DIFF': ['mean', 'max'], # Retard cumulé 'REMAINING_DEBT': ['max', 'mean', 'sum'], # Dette restante 'REPAYMENT_RATIO': ['mean'], # Taux de remboursement } # Agrégations sur les demandes approuvées PREVIOUS_APPROVED_AGG = { 'SK_ID_PREV': ['nunique'], 'AMT_ANNUITY': ['min', 'max', 'mean'], 'AMT_CREDIT': ['min', 'max', 'mean'], 'AMT_DOWN_PAYMENT': ['max'], 'AMT_GOODS_PRICE': ['max'], 'HOUR_APPR_PROCESS_START': ['min', 'max'], 'DAYS_DECISION': ['min', 'mean'], 'CNT_PAYMENT': ['max', 'mean'], 'DAYS_TERMINATION': ['mean'], 'CREDIT_TO_ANNUITY_RATIO': ['mean', 'max'], 'APPLICATION_CREDIT_DIFF': ['max'], 'APPLICATION_CREDIT_RATIO': ['min', 'max', 'mean'], 'DAYS_FIRST_DRAWING': ['max', 'mean'], # Date du premier versement 'DAYS_FIRST_DUE': ['min', 'mean'], # Date de la première échéance 'DAYS_LAST_DUE_1ST_VERSION': ['min', 'max', 'mean'], 'DAYS_LAST_DUE': ['max', 'mean'], 'DAYS_LAST_DUE_DIFF': ['min', 'max', 'mean'], # Écart entre date de fin prévue et réelle 'SIMPLE_INTERESTS': ['min', 'max', 'mean'], } # Agrégations sur les demandes refusées PREVIOUS_REFUSED_AGG = { 'AMT_APPLICATION': ['max', 'mean'], 'AMT_CREDIT': ['min', 'max'], 'DAYS_DECISION': ['min', 'max', 'mean'], 'CNT_PAYMENT': ['max', 'mean'], 'APPLICATION_CREDIT_DIFF': ['min', 'max', 'mean', 'var'], 'APPLICATION_CREDIT_RATIO': ['min', 'mean'], 'NAME_CONTRACT_TYPE_Consumer loans': ['mean'], 'NAME_CONTRACT_TYPE_Cash loans': ['mean'], 'NAME_CONTRACT_TYPE_Revolving loans': ['mean'], } # Agrégations sur les prêts avec retards de paiement PREVIOUS_LATE_PAYMENTS_AGG = { 'DAYS_DECISION': ['min', 'max', 'mean'], 'DAYS_LAST_DUE_1ST_VERSION': ['min', 'max', 'mean'], 'APPLICATION_CREDIT_DIFF': ['min'], 'NAME_CONTRACT_TYPE_Consumer loans': ['mean'], 'NAME_CONTRACT_TYPE_Cash loans': ['mean'], 'NAME_CONTRACT_TYPE_Revolving loans': ['mean'], } # Agrégations par type de prêt précédent (consommation / cash) PREVIOUS_LOAN_TYPE_AGG = { 'AMT_CREDIT': ['sum'], 'AMT_ANNUITY': ['mean', 'max'], 'SIMPLE_INTERESTS': ['min', 'mean', 'max', 'var'], 'APPLICATION_CREDIT_DIFF': ['min', 'var'], 'APPLICATION_CREDIT_RATIO': ['min', 'max', 'mean'], 'DAYS_DECISION': ['max'], 'DAYS_LAST_DUE_1ST_VERSION': ['max', 'mean'], 'CNT_PAYMENT': ['mean'], } # Agrégations sur les demandes récentes (12 ou 24 derniers mois) PREVIOUS_TIME_AGG = { 'AMT_CREDIT': ['sum'], 'AMT_ANNUITY': ['mean', 'max'], 'SIMPLE_INTERESTS': ['mean', 'max'], 'DAYS_DECISION': ['min', 'mean'], 'DAYS_LAST_DUE_1ST_VERSION': ['min', 'max', 'mean'], 'APPLICATION_CREDIT_DIFF': ['min'], 'APPLICATION_CREDIT_RATIO': ['min', 'max', 'mean'], 'NAME_CONTRACT_TYPE_Consumer loans': ['mean'], 'NAME_CONTRACT_TYPE_Cash loans': ['mean'], 'NAME_CONTRACT_TYPE_Revolving loans': ['mean'], } # Agrégations sur les soldes POS/Cash POS_CASH_AGG = { 'SK_ID_PREV': ['nunique'], # Nombre de prêts POS distincts 'MONTHS_BALANCE': ['min', 'max', 'size'], # Durée de l'historique 'SK_DPD': ['max', 'mean', 'sum', 'var'], # Jours de retard (Days Past Due) 'SK_DPD_DEF': ['max', 'mean', 'sum'], # Jours de retard définis (seuil institution) 'LATE_PAYMENT': ['mean'] # Taux de mois avec retard } # Agrégations sur les paiements d'échéances INSTALLMENTS_AGG = { 'SK_ID_PREV': ['size', 'nunique'], # Nombre total de lignes et prêts distincts 'DAYS_ENTRY_PAYMENT': ['min', 'max', 'mean'], # Dates de paiement 'AMT_INSTALMENT': ['min', 'max', 'mean', 'sum'], # Montants des échéances dues 'AMT_PAYMENT': ['min', 'max', 'mean', 'sum'], # Montants effectivement payés 'DPD': ['max', 'mean', 'var'], # Retard en jours (Days Past Due) 'DBD': ['max', 'mean', 'var'], # Avance en jours (Days Before Due) 'PAYMENT_DIFFERENCE': ['mean'], # Solde non payé par échéance 'PAYMENT_RATIO': ['mean'], # Taux de couverture de l'échéance 'LATE_PAYMENT': ['mean', 'sum'], # Taux et nombre de paiements tardifs 'SIGNIFICANT_LATE_PAYMENT': ['mean', 'sum'], # Retards significatifs (>5%) 'LATE_PAYMENT_RATIO': ['mean'], # Ratio moyen des paiements tardifs 'DPD_7': ['mean'], # Taux de retards >= 7 jours 'DPD_15': ['mean'], # Taux de retards >= 15 jours 'PAID_OVER': ['mean'] # Taux de surpaiements } # Agrégations temporelles des paiements (périodes récentes) INSTALLMENTS_TIME_AGG = { 'SK_ID_PREV': ['size'], 'DAYS_ENTRY_PAYMENT': ['min', 'max', 'mean'], 'AMT_INSTALMENT': ['min', 'max', 'mean', 'sum'], 'AMT_PAYMENT': ['min', 'max', 'mean', 'sum'], 'DPD': ['max', 'mean', 'var'], 'DBD': ['max', 'mean', 'var'], 'PAYMENT_DIFFERENCE': ['mean'], 'PAYMENT_RATIO': ['mean'], 'LATE_PAYMENT': ['mean'], 'SIGNIFICANT_LATE_PAYMENT': ['mean'], 'LATE_PAYMENT_RATIO': ['mean'], 'DPD_7': ['mean'], 'DPD_15': ['mean'], } # Agrégations sur les soldes de cartes de crédit CREDIT_CARD_AGG = { 'MONTHS_BALANCE': ['min'], # Ancienneté de l'historique 'AMT_BALANCE': ['max'], # Solde maximal 'AMT_CREDIT_LIMIT_ACTUAL': ['max'], # Plafond de crédit maximal 'AMT_DRAWINGS_ATM_CURRENT': ['max', 'sum'], # Retraits ATM 'AMT_DRAWINGS_CURRENT': ['max', 'sum'], # Total des retraits 'AMT_DRAWINGS_POS_CURRENT': ['max', 'sum'], # Retraits en point de vente 'AMT_INST_MIN_REGULARITY': ['max', 'mean'], # Paiement minimum régulier 'AMT_PAYMENT_TOTAL_CURRENT': ['max', 'mean', 'sum', 'var'], # Paiements effectués 'AMT_TOTAL_RECEIVABLE': ['max', 'mean'], # Total des créances 'CNT_DRAWINGS_ATM_CURRENT': ['max', 'mean', 'sum'], # Nombre de retraits ATM 'CNT_DRAWINGS_CURRENT': ['max', 'mean', 'sum'], # Nombre total de retraits 'CNT_DRAWINGS_POS_CURRENT': ['mean'], # Retraits en point de vente 'SK_DPD': ['mean', 'max', 'sum'], # Jours de retard 'SK_DPD_DEF': ['max', 'sum'], # Jours de retard définis 'LIMIT_USE': ['max', 'mean'], # Taux d'utilisation du plafond 'PAYMENT_DIV_MIN': ['min', 'mean'], # Ratio paiement / minimum dû 'LATE_PAYMENT': ['max', 'sum'], # Retards de paiement } # Agrégations temporelles des cartes de crédit (12, 24, 48 derniers mois) CREDIT_CARD_TIME_AGG = { 'CNT_DRAWINGS_ATM_CURRENT': ['mean'], # Fréquence des retraits ATM récents 'SK_DPD': ['max', 'sum'], # Retards récents 'AMT_BALANCE': ['mean', 'max'], # Solde récent 'LIMIT_USE': ['max', 'mean'] # Utilisation récente du plafond } if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Prétraitement des données Home Credit") parser.add_argument("--debug", action="store_true", help="Mode debug : charge 30 000 lignes seulement") parser.add_argument("--output", type=str, default="data/preprocessed.csv", help="Chemin du CSV de sortie") args = parser.parse_args() with timer("Prétraitement complet"): df = load_dataset(debug=args.debug) output = args.output if os.path.isdir(output) or output.endswith("/") or output.endswith("\\"): output = os.path.join(output, "preprocessed.csv") os.makedirs(os.path.dirname(output) or ".", exist_ok=True) print(f"Sauvegarde du DataFrame ({df.shape[0]} lignes, {df.shape[1]} colonnes) dans '{output}'...") df.to_csv(output, index=False) print(f"Fichier sauvegardé : {output}")