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 forActivevs.Closedcredit 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 value365243is replaced withNaNacross allDAYS_*columns; anAPP_CREDIT_PERCratio (requested vs. approved amount) is engineered; the same split trick is applied forApprovedvs.Refusedoutcomes.POS_CASH_balanceandcredit_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) andDPD/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/3dimensionality 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
- GitHub repo (full backend + frontend + verifier):
synchrony_credit_intelligence - Original competition: Home Credit Default Risk on Kaggle