Synchrony Hackathon β€” Credit Intelligence: Pre-processed Dataset + Trained Model

This repo hosts the pre-processed training data and trained scoring model for the Next-Gen Credit Intelligence project, built for the Synchrony Hackathon (Problem Statement 1 β€” expanding credit access to New-to-Credit and thin-file applicants using alternative data).

Full project source (backend, frontend, verifier, RAG layer) lives in the companion GitHub repo: synchrony_credit_intelligence.

Files in this repo

File Size Description
application_train_enriched.csv ~881 MB The fully joined, feature-engineered training table β€” output of the preprocessing pipeline described below
model_artifact.joblib ~304 MB The trained, ready-to-load DualModelScoringEngine (LightGBM + CatBoost blend) β€” drop this straight into backend/data/ to skip training entirely

Model performance

The blended LightGBM + CatBoost scoring engine achieves 0.7962 AUC on out-of-fold validation.

Source dataset β€” credit

The underlying data is Kaggle's Home Credit Default Risk competition dataset, provided by Home Credit Group. All rights to the original raw data belong to Home Credit Group and Kaggle; this repo hosts only a derived, feature-engineered artifact built from that data for hackathon purposes, plus the model trained on it. If you use this data, please also credit the original Kaggle competition.

We chose this dataset because Home Credit's own stated business problem β€” lending to clients with little or no formal credit history β€” is a direct match for the New-to-Credit / thin-file objective the hackathon problem statement asks for, and its multi-table structure (bureau history, previous applications, POS/credit-card balances, installment payments) provides genuine behavioral signal rather than a single flat application form.

How application_train_enriched.csv was built

The raw Kaggle tables (application_train.csv, bureau.csv, bureau_balance.csv, previous_application.csv, POS_CASH_balance.csv, credit_card_balance.csv, installments_payments.csv) are independently aggregated to the applicant grain (SK_ID_CURR) and joined onto the main application table via build_kaggle_features.py (included below, and in the GitHub repo at backend/scripts/build_kaggle_features.py):

  • bureau + bureau_balance β€” one-hot encoded and aggregated with min/max/mean/var/sum statistics on credit amount, overdue amount, and days-credit fields, computed once overall and again separately for Active vs. Closed credit lines (the "Active/Closed split" trick), so the model can distinguish a currently-struggling open loan from a historically-struggled but closed one.
  • previous_application β€” the Home Credit sentinel value 365243 is replaced with NaN across all DAYS_* columns; an APP_CREDIT_PERC ratio (requested vs. approved amount) is engineered; the same split trick is applied for Approved vs. Refused outcomes.
  • POS_CASH_balance and credit_card_balance β€” one-hot encoded and aggregated with days-past-due statistics (SK_DPD, SK_DPD_DEF) plus a raw record count per applicant as an exposure/tenure proxy.
  • installments_payments β€” PAYMENT_PERC/PAYMENT_DIFF (paid vs. scheduled) and DPD/DBD (days late / days early, floored at zero) are computed per installment and aggregated.
  • Memory management β€” every aggregated table is passed through reduce_mem_usage(), which downcasts integer/float columns to the smallest safe dtype.
  • EXT_SOURCE_1/2/3 dimensionality reduction β€” a median-imputed 2-component PCA projection (EXT_PCA_1, EXT_PCA_2) is added as a denoised summary signal alongside the raw, highly-correlated external scores.

This is a one-time offline job β€” the enriched CSV in this repo is the direct output of running the script below against the raw Kaggle tables.

build_kaggle_features.py (preprocessing script)
import pandas as pd
import numpy as np
import gc
import os
import warnings
from sklearn.impute import SimpleImputer
from sklearn.decomposition import PCA

warnings.simplefilter(action='ignore', category=FutureWarning)
warnings.simplefilter(action='ignore', category=RuntimeWarning)

def reduce_mem_usage(df):
    """Iterate through all columns of a dataframe and modify the data type to reduce memory usage."""
    start_mem = df.memory_usage().sum() / 1024**2
    for col in df.columns:
        col_type = df[col].dtype
        if col_type != object and not pd.api.types.is_categorical_dtype(col_type):
            c_min, c_max = df[col].min(), df[col].max()
            if str(col_type)[:3] == 'int':
                if c_min > np.iinfo(np.int8).min and c_max < np.iinfo(np.int8).max:
                    df[col] = df[col].astype(np.int8)
                elif c_min > np.iinfo(np.int16).min and c_max < np.iinfo(np.int16).max:
                    df[col] = df[col].astype(np.int16)
                elif c_min > np.iinfo(np.int32).min and c_max < np.iinfo(np.int32).max:
                    df[col] = df[col].astype(np.int32)
                elif c_min > np.iinfo(np.int64).min and c_max < np.iinfo(np.int64).max:
                    df[col] = df[col].astype(np.int64)  
            else:
                if c_min > np.finfo(np.float16).min and c_max < np.finfo(np.float16).max:
                    df[col] = df[col].astype(np.float32) # Using float32 for safety with LGBM
                elif c_min > np.finfo(np.float32).min and c_max < 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
    print(f'Memory decreased to {end_mem:.2f} MB')
    return df

def one_hot_encoder(df, nan_as_category=True):
    original_columns = list(df.columns)
    categorical_columns = [col for col in df.columns if df[col].dtype == 'object']
    df = pd.get_dummies(df, columns=categorical_columns, dummy_na=nan_as_category)
    new_columns = [c for c in df.columns if c not in original_columns]
    return df, new_columns

def process_bureau_and_balance():
    print("Processing bureau and bureau_balance...")
    bureau = pd.read_csv('data/bureau.csv')
    bb = pd.read_csv('data/bureau_balance.csv')

    bb, bb_cat = one_hot_encoder(bb)
    bureau, bureau_cat = one_hot_encoder(bureau)

    bb_aggregations = {'MONTHS_BALANCE': ['min', 'max', 'size']}
    for col in bb_cat: bb_aggregations[col] = ['mean']
    bb_agg = bb.groupby('SK_ID_BUREAU').agg(bb_aggregations)
    bb_agg.columns = pd.Index([e[0] + "_" + e[1].upper() for e in bb_agg.columns.tolist()])
    bureau = bureau.join(bb_agg, how='left', on='SK_ID_BUREAU')
    bureau.drop(['SK_ID_BUREAU'], axis=1, inplace=True)

    num_aggregations = {
        'DAYS_CREDIT': ['min', 'max', 'mean', 'var'],
        'DAYS_CREDIT_ENDDATE': ['min', 'max', 'mean'],
        'DAYS_CREDIT_UPDATE': ['mean'],
        'CREDIT_DAY_OVERDUE': ['max', 'mean'],
        'AMT_CREDIT_MAX_OVERDUE': ['mean'],
        'AMT_CREDIT_SUM': ['max', 'mean', 'sum'],
        'AMT_CREDIT_SUM_DEBT': ['max', 'mean', 'sum'],
        'AMT_CREDIT_SUM_OVERDUE': ['mean'],
        'AMT_CREDIT_SUM_LIMIT': ['mean', 'sum'],
        'AMT_ANNUITY': ['max', 'mean'],
        'CNT_CREDIT_PROLONG': ['sum'],
        'MONTHS_BALANCE_MIN': ['min'],
        'MONTHS_BALANCE_MAX': ['max'],
        'MONTHS_BALANCE_SIZE': ['mean', 'sum']
    }
    cat_aggregations = {}
    for cat in bureau_cat: cat_aggregations[cat] = ['mean']
    for cat in bb_cat: cat_aggregations[cat + "_MEAN"] = ['mean']

    bureau_agg = bureau.groupby('SK_ID_CURR').agg({**num_aggregations, **cat_aggregations})
    bureau_agg.columns = pd.Index(['BURO_' + e[0] + "_" + e[1].upper() for e in bureau_agg.columns.tolist()])

    # TOP KAGGLE TRICK: Active vs Closed Split
    active = bureau[bureau['CREDIT_ACTIVE_Active'] == 1]
    active_agg = active.groupby('SK_ID_CURR').agg(num_aggregations)
    active_agg.columns = pd.Index(['ACTIVE_' + e[0] + "_" + e[1].upper() for e in active_agg.columns.tolist()])
    bureau_agg = bureau_agg.join(active_agg, how='left')

    closed = bureau[bureau['CREDIT_ACTIVE_Closed'] == 1]
    closed_agg = closed.groupby('SK_ID_CURR').agg(num_aggregations)
    closed_agg.columns = pd.Index(['CLOSED_' + e[0] + "_" + e[1].upper() for e in closed_agg.columns.tolist()])
    bureau_agg = bureau_agg.join(closed_agg, how='left')

    return reduce_mem_usage(bureau_agg)

def process_previous_applications():
    print("Processing previous_application...")
    prev = pd.read_csv('data/previous_application.csv')
    prev, cat_cols = one_hot_encoder(prev, nan_as_category=True)
    
    for col in ['DAYS_FIRST_DRAWING', 'DAYS_FIRST_DUE', 'DAYS_LAST_DUE_1ST_VERSION', 'DAYS_LAST_DUE', 'DAYS_TERMINATION']:
        prev[col].replace(365243, np.nan, inplace=True)
    
    prev['APP_CREDIT_PERC'] = prev['AMT_APPLICATION'] / prev['AMT_CREDIT']

    num_aggregations = {
        'AMT_ANNUITY': ['min', 'max', 'mean'],
        'AMT_APPLICATION': ['min', 'max', 'mean'],
        'AMT_CREDIT': ['min', 'max', 'mean'],
        'APP_CREDIT_PERC': ['min', 'max', 'mean', 'var'],
        'AMT_DOWN_PAYMENT': ['min', 'max', 'mean'],
        'AMT_GOODS_PRICE': ['min', 'max', 'mean'],
        'HOUR_APPR_PROCESS_START': ['min', 'max', 'mean'],
        'RATE_DOWN_PAYMENT': ['min', 'max', 'mean'],
        'DAYS_DECISION': ['min', 'max', 'mean'],
        'CNT_PAYMENT': ['mean', 'sum'],
    }
    cat_aggregations = {}
    for cat in cat_cols: cat_aggregations[cat] = ['mean']

    prev_agg = prev.groupby('SK_ID_CURR').agg({**num_aggregations, **cat_aggregations})
    prev_agg.columns = pd.Index(['PREV_' + e[0] + "_" + e[1].upper() for e in prev_agg.columns.tolist()])

    # TOP KAGGLE TRICK: Approved vs Refused Split
    approved = prev[prev['NAME_CONTRACT_STATUS_Approved'] == 1]
    approved_agg = approved.groupby('SK_ID_CURR').agg(num_aggregations)
    approved_agg.columns = pd.Index(['APPROVED_' + e[0] + "_" + e[1].upper() for e in approved_agg.columns.tolist()])
    prev_agg = prev_agg.join(approved_agg, how='left')

    refused = prev[prev['NAME_CONTRACT_STATUS_Refused'] == 1]
    refused_agg = refused.groupby('SK_ID_CURR').agg(num_aggregations)
    refused_agg.columns = pd.Index(['REFUSED_' + e[0] + "_" + e[1].upper() for e in refused_agg.columns.tolist()])
    prev_agg = prev_agg.join(refused_agg, how='left')

    return reduce_mem_usage(prev_agg)

def process_pos_cash():
    print("Processing POS_CASH_balance...")
    pos = pd.read_csv('data/POS_CASH_balance.csv')
    pos, cat_cols = one_hot_encoder(pos, nan_as_category=True)
    
    aggregations = {
        'MONTHS_BALANCE': ['max', 'mean', 'size'],
        'SK_DPD': ['max', 'mean'],
        'SK_DPD_DEF': ['max', 'mean']
    }
    for cat in cat_cols: aggregations[cat] = ['mean']
    
    pos_agg = pos.groupby('SK_ID_CURR').agg(aggregations)
    pos_agg.columns = pd.Index(['POS_' + e[0] + "_" + e[1].upper() for e in pos_agg.columns.tolist()])
    pos_agg['POS_COUNT'] = pos.groupby('SK_ID_CURR').size()
    return reduce_mem_usage(pos_agg)

def process_credit_card():
    print("Processing credit_card_balance...")
    cc = pd.read_csv('data/credit_card_balance.csv')
    cc, cat_cols = one_hot_encoder(cc, nan_as_category=True)
    
    cc.drop(['SK_ID_PREV'], axis=1, inplace=True)
    cc_agg = cc.groupby('SK_ID_CURR').agg(['min', 'max', 'mean', 'sum', 'var'])
    cc_agg.columns = pd.Index(['CC_' + e[0] + "_" + e[1].upper() for e in cc_agg.columns.tolist()])
    cc_agg['CC_COUNT'] = cc.groupby('SK_ID_CURR').size()
    return reduce_mem_usage(cc_agg)

def process_main_and_pca(df):
    """Applies Dimensionality Reduction to the heavily correlated EXT_SOURCE variables"""
    print("Applying PCA to EXT_SOURCES...")
    ext_cols = ['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3']
    
    # PCA cannot handle NaNs, so we impute with median just for the projection
    imputer = SimpleImputer(strategy='median')
    ext_imputed = imputer.fit_transform(df[ext_cols])
    
    pca = PCA(n_components=2, random_state=42)
    ext_pca = pca.fit_transform(ext_imputed)
    
    df['EXT_PCA_1'] = ext_pca[:, 0]
    df['EXT_PCA_2'] = ext_pca[:, 1]
    return df

def process_installments_payments():
    print("Processing installments_payments...")
    ins = pd.read_csv('data/installments_payments.csv')
    ins, cat_cols = one_hot_encoder(ins, nan_as_category=True)
    
    # Percentage and difference features
    ins['PAYMENT_PERC'] = ins['AMT_PAYMENT'] / ins['AMT_INSTALMENT']
    ins['PAYMENT_DIFF'] = ins['AMT_INSTALMENT'] - ins['AMT_PAYMENT']
    
    # Days past due and days before due (no negative values)
    ins['DPD'] = ins['DAYS_ENTRY_PAYMENT'] - ins['DAYS_INSTALMENT']
    ins['DBD'] = ins['DAYS_INSTALMENT'] - ins['DAYS_ENTRY_PAYMENT']
    ins['DPD'] = ins['DPD'].apply(lambda x: x if x > 0 else 0)
    ins['DBD'] = ins['DBD'].apply(lambda x: x if x > 0 else 0)
    
    aggregations = {
        'NUM_INSTALMENT_VERSION': ['nunique'],
        'DPD': ['max', 'mean', 'sum'],
        'DBD': ['max', 'mean', 'sum'],
        'PAYMENT_PERC': ['max', 'mean', 'sum', 'var'],
        'PAYMENT_DIFF': ['max', 'mean', 'sum', 'var'],
        'AMT_INSTALMENT': ['max', 'mean', 'sum'],
        'AMT_PAYMENT': ['min', 'max', 'mean', 'sum'],
        'DAYS_ENTRY_PAYMENT': ['max', 'mean', 'sum']
    }
    for cat in cat_cols:
        aggregations[cat] = ['mean']
        
    ins_agg = ins.groupby('SK_ID_CURR').agg(aggregations)
    ins_agg.columns = pd.Index(['INSTAL_' + e[0] + "_" + e[1].upper() for e in ins_agg.columns.tolist()])
    
    # Count installment accounts
    ins_agg['INSTAL_COUNT'] = ins.groupby('SK_ID_CURR').size()
    return reduce_mem_usage(ins_agg)

if __name__ == '__main__':
    print("Loading application_train.csv...")
    df = pd.read_csv('data/application_train.csv')
    df = reduce_mem_usage(df)
    
    if os.path.exists('data/bureau.csv'):
        bureau_agg = process_bureau_and_balance()
        df = df.join(bureau_agg, how='left', on='SK_ID_CURR')
        del bureau_agg; gc.collect()
        
    if os.path.exists('data/previous_application.csv'):
        prev_agg = process_previous_applications()
        df = df.join(prev_agg, how='left', on='SK_ID_CURR')
        del prev_agg; gc.collect()
        
    if os.path.exists('data/POS_CASH_balance.csv'):
        pos_agg = process_pos_cash()
        df = df.join(pos_agg, how='left', on='SK_ID_CURR')
        del pos_agg; gc.collect()
        
    if os.path.exists('data/credit_card_balance.csv'):
        cc_agg = process_credit_card()
        df = df.join(cc_agg, how='left', on='SK_ID_CURR')
        del cc_agg; gc.collect()

    if os.path.exists('data/installments_payments.csv'):
        ins_agg = process_installments_payments()
        df = df.join(ins_agg, how='left', on='SK_ID_CURR')
        del ins_agg; gc.collect()
        
    df = process_main_and_pca(df)
    
    output_path = 'data/application_train_enriched.csv'
    df.to_csv(output_path, index=False)
    print(f"Success! Enriched dataset saved to {output_path} with {df.shape[1]} features.")

Model architecture (brief)

model_artifact.joblib is a DualModelScoringEngine: a blended LightGBM + CatBoost ensemble trained on the identical numeric design matrix, with raw log-odds margins averaged 50/50 across folds so that exact TreeSHAP attributions sum precisely to the blended output (required for downstream verification in the full pipeline). High-cardinality categoricals (ORGANIZATION_TYPE, OCCUPATION_TYPE) are handled with leak-safe, 10-fold out-of-fold target encoding rather than one-hot expansion.

Usage

import joblib
import pandas as pd

model = joblib.load("model_artifact.joblib")
df = pd.read_csv("application_train_enriched.csv")

# see the companion GitHub repo (backend/app/scoring.py) for the exact
# DualModelScoringEngine interface used to score a single applicant

Related

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support