Spaces:
Sleeping
Sleeping
| # 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 | |
| # ============================================================================= | |
| 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}") |