Skip to content
Sangam Gupta
Credit Risk Predictor

The analysis notebook

The exploration, modelling and evaluation behind Credit Risk Predictor, as it runs. Code cells, printed results and charts are shown as saved in the notebook on GitHub.

In [30]
import pandas as pd
import numpy as np

df = pd.read_csv("../data/application_train.csv")

df.head()
SK_ID_CURR TARGET NAME_CONTRACT_TYPE CODE_GENDER FLAG_OWN_CAR FLAG_OWN_REALTY CNT_CHILDREN AMT_INCOME_TOTAL AMT_CREDIT AMT_ANNUITY ... FLAG_DOCUMENT_18 FLAG_DOCUMENT_19 FLAG_DOCUMENT_20 FLAG_DOCUMENT_21 AMT_REQ_CREDIT_BUREAU_HOUR AMT_REQ_CREDIT_BUREAU_DAY AMT_REQ_CREDIT_BUREAU_WEEK AMT_REQ_CREDIT_BUREAU_MON AMT_REQ_CREDIT_BUREAU_QRT AMT_REQ_CREDIT_BUREAU_YEAR
0 100002 1 Cash loans M N Y 0 202500.0 406597.5 24700.5 ... 0 0 0 0 0.0 0.0 0.0 0.0 0.0 1.0
1 100003 0 Cash loans F N N 0 270000.0 1293502.5 35698.5 ... 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0
2 100004 0 Revolving loans M Y Y 0 67500.0 135000.0 6750.0 ... 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0
3 100006 0 Cash loans F N Y 0 135000.0 312682.5 29686.5 ... 0 0 0 0 NaN NaN NaN NaN NaN NaN
4 100007 0 Cash loans M N Y 0 121500.0 513000.0 21865.5 ... 0 0 0 0 0.0 0.0 0.0 0.0 0.0 0.0
5 rows × 122 columns
In [31]
df.shape
(307511, 122)
In [32]
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 307511 entries, 0 to 307510
Columns: 122 entries, SK_ID_CURR to AMT_REQ_CREDIT_BUREAU_YEAR
dtypes: float64(65), int64(41), object(16)
memory usage: 286.2+ MB
In [33]
df["TARGET"].value_counts()
TARGET
0    282686
1     24825
Name: count, dtype: int64
In [34]
df["TARGET"].value_counts(normalize=True) * 100
TARGET
0    91.927118
1     8.072882
Name: proportion, dtype: float64
In [35]
df.isnull().sum().sort_values(ascending=False).head(20)
COMMONAREA_AVG              214865
COMMONAREA_MODE             214865
COMMONAREA_MEDI             214865
NONLIVINGAPARTMENTS_MEDI    213514
NONLIVINGAPARTMENTS_MODE    213514
NONLIVINGAPARTMENTS_AVG     213514
FONDKAPREMONT_MODE          210295
LIVINGAPARTMENTS_AVG        210199
LIVINGAPARTMENTS_MEDI       210199
LIVINGAPARTMENTS_MODE       210199
FLOORSMIN_MODE              208642
FLOORSMIN_AVG               208642
FLOORSMIN_MEDI              208642
YEARS_BUILD_AVG             204488
YEARS_BUILD_MODE            204488
YEARS_BUILD_MEDI            204488
OWN_CAR_AGE                 202929
LANDAREA_MEDI               182590
LANDAREA_AVG                182590
LANDAREA_MODE               182590
dtype: int64
In [36]
missing = df.isnull().sum()

missing_percentage = (missing / len(df)) * 100

missing_data = pd.DataFrame({
    "Missing_Count": missing,
    "Missing_Percentage": missing_percentage
})

missing_data.sort_values(
    "Missing_Percentage",
    ascending=False
).head(20)
Missing_Count Missing_Percentage
COMMONAREA_AVG 214865 69.872297
COMMONAREA_MODE 214865 69.872297
COMMONAREA_MEDI 214865 69.872297
NONLIVINGAPARTMENTS_MEDI 213514 69.432963
NONLIVINGAPARTMENTS_MODE 213514 69.432963
NONLIVINGAPARTMENTS_AVG 213514 69.432963
FONDKAPREMONT_MODE 210295 68.386172
LIVINGAPARTMENTS_AVG 210199 68.354953
LIVINGAPARTMENTS_MEDI 210199 68.354953
LIVINGAPARTMENTS_MODE 210199 68.354953
FLOORSMIN_MODE 208642 67.848630
FLOORSMIN_AVG 208642 67.848630
FLOORSMIN_MEDI 208642 67.848630
YEARS_BUILD_AVG 204488 66.497784
YEARS_BUILD_MODE 204488 66.497784
YEARS_BUILD_MEDI 204488 66.497784
OWN_CAR_AGE 202929 65.990810
LANDAREA_MEDI 182590 59.376738
LANDAREA_AVG 182590 59.376738
LANDAREA_MODE 182590 59.376738
In [37]
missing_data["Missing_Percentage"].describe()
count    122.000000
mean      24.395942
std       28.446507
min        0.000000
25%        0.000000
50%        0.332021
75%       50.818020
max       69.872297
Name: Missing_Percentage, dtype: float64
In [38]
print("Columns with > 50% missing:")
print(missing_data[missing_data["Missing_Percentage"] > 50])
Columns with > 50% missing:
                          Missing_Count  Missing_Percentage
OWN_CAR_AGE                      202929           65.990810
EXT_SOURCE_1                     173378           56.381073
APARTMENTS_AVG                   156061           50.749729
BASEMENTAREA_AVG                 179943           58.515956
YEARS_BUILD_AVG                  204488           66.497784
COMMONAREA_AVG                   214865           69.872297
ELEVATORS_AVG                    163891           53.295980
ENTRANCES_AVG                    154828           50.348768
FLOORSMIN_AVG                    208642           67.848630
LANDAREA_AVG                     182590           59.376738
LIVINGAPARTMENTS_AVG             210199           68.354953
LIVINGAREA_AVG                   154350           50.193326
NONLIVINGAPARTMENTS_AVG          213514           69.432963
NONLIVINGAREA_AVG                169682           55.179164
APARTMENTS_MODE                  156061           50.749729
BASEMENTAREA_MODE                179943           58.515956
YEARS_BUILD_MODE                 204488           66.497784
COMMONAREA_MODE                  214865           69.872297
ELEVATORS_MODE                   163891           53.295980
ENTRANCES_MODE                   154828           50.348768
FLOORSMIN_MODE                   208642           67.848630
LANDAREA_MODE                    182590           59.376738
LIVINGAPARTMENTS_MODE            210199           68.354953
LIVINGAREA_MODE                  154350           50.193326
NONLIVINGAPARTMENTS_MODE         213514           69.432963
NONLIVINGAREA_MODE               169682           55.179164
APARTMENTS_MEDI                  156061           50.749729
BASEMENTAREA_MEDI                179943           58.515956
… 13 more lines
In [39]
corr = df.corr(numeric_only=True)["TARGET"].sort_values(ascending=False)

print(corr.head(15))
TARGET                         1.000000
DAYS_BIRTH                     0.078239
REGION_RATING_CLIENT_W_CITY    0.060893
REGION_RATING_CLIENT           0.058899
DAYS_LAST_PHONE_CHANGE         0.055218
DAYS_ID_PUBLISH                0.051457
REG_CITY_NOT_WORK_CITY         0.050994
FLAG_EMP_PHONE                 0.045982
REG_CITY_NOT_LIVE_CITY         0.044395
FLAG_DOCUMENT_3                0.044346
DAYS_REGISTRATION              0.041975
OWN_CAR_AGE                    0.037612
LIVE_CITY_NOT_WORK_CITY        0.032518
DEF_30_CNT_SOCIAL_CIRCLE       0.032248
DEF_60_CNT_SOCIAL_CIRCLE       0.031276
Name: TARGET, dtype: float64
In [40]
print(corr.tail(15))
LIVINGAREA_MEDI              -0.032739
LIVINGAREA_AVG               -0.032997
FLOORSMIN_MEDI               -0.033394
FLOORSMIN_AVG                -0.033614
ELEVATORS_MEDI               -0.033863
ELEVATORS_AVG                -0.034199
REGION_POPULATION_RELATIVE   -0.037227
AMT_GOODS_PRICE              -0.039645
FLOORSMAX_MODE               -0.043226
FLOORSMAX_MEDI               -0.043768
FLOORSMAX_AVG                -0.044003
DAYS_EMPLOYED                -0.044932
EXT_SOURCE_1                 -0.155317
EXT_SOURCE_2                 -0.160472
EXT_SOURCE_3                 -0.178919
Name: TARGET, dtype: float64
In [41]
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 307511 entries, 0 to 307510
Columns: 122 entries, SK_ID_CURR to AMT_REQ_CREDIT_BUREAU_YEAR
dtypes: float64(65), int64(41), object(16)
memory usage: 286.2+ MB
In [42]
#Yeh pata karne ke liye ki kaunse column categorical data wale hai
df.select_dtypes(include=["object", "string"]).columns
Index(['NAME_CONTRACT_TYPE', 'CODE_GENDER', 'FLAG_OWN_CAR', 'FLAG_OWN_REALTY',
       'NAME_TYPE_SUITE', 'NAME_INCOME_TYPE', 'NAME_EDUCATION_TYPE',
       'NAME_FAMILY_STATUS', 'NAME_HOUSING_TYPE', 'OCCUPATION_TYPE',
       'WEEKDAY_APPR_PROCESS_START', 'ORGANIZATION_TYPE', 'FONDKAPREMONT_MODE',
       'HOUSETYPE_MODE', 'WALLSMATERIAL_MODE', 'EMERGENCYSTATE_MODE'],
      dtype='object')
In [43]
""""Isse humein pata chalega:

Column
   ↓
Kitni categories?
   ↓
Missing values?
   ↓
Kaunsi category kitni baar?"""


categorical_columns = df.select_dtypes(
    include=["object", "string"]
).columns

for column in categorical_columns:
    print(f"\n{column}")
    print(df[column].value_counts(dropna=False).head(10))
NAME_CONTRACT_TYPE
NAME_CONTRACT_TYPE
Cash loans         278232
Revolving loans     29279
Name: count, dtype: int64

CODE_GENDER
CODE_GENDER
F      202448
M      105059
XNA         4
Name: count, dtype: int64

FLAG_OWN_CAR
FLAG_OWN_CAR
N    202924
Y    104587
Name: count, dtype: int64

FLAG_OWN_REALTY
FLAG_OWN_REALTY
Y    213312
N     94199
Name: count, dtype: int64

NAME_TYPE_SUITE
NAME_TYPE_SUITE
Unaccompanied      248526
Family              40149
Spouse, partner     11370
… 122 more lines

DATA CLEANING

In [44]
#SK_ID_CURR sirf applicant ki ID hai. Iska credit-risk prediction mein meaningful feature nahi hai.
df = df.drop(columns=["SK_ID_CURR"])
In [45]
#60% se zyada missing → column drop
missing_percentage = df.isnull().mean() * 100

columns_to_drop = missing_percentage[missing_percentage > 60].index

print("Columns to drop:", len(columns_to_drop))
print(columns_to_drop)
Columns to drop: 17
Index(['OWN_CAR_AGE', 'YEARS_BUILD_AVG', 'COMMONAREA_AVG', 'FLOORSMIN_AVG',
       'LIVINGAPARTMENTS_AVG', 'NONLIVINGAPARTMENTS_AVG', 'YEARS_BUILD_MODE',
       'COMMONAREA_MODE', 'FLOORSMIN_MODE', 'LIVINGAPARTMENTS_MODE',
       'NONLIVINGAPARTMENTS_MODE', 'YEARS_BUILD_MEDI', 'COMMONAREA_MEDI',
       'FLOORSMIN_MEDI', 'LIVINGAPARTMENTS_MEDI', 'NONLIVINGAPARTMENTS_MEDI',
       'FONDKAPREMONT_MODE'],
      dtype='object')
In [46]
df = df.drop(columns=columns_to_drop)
In [47]
categorical_columns = df.select_dtypes(
    include=["object", "string"]
).columns

numerical_columns = df.select_dtypes(
    include=["int64", "float64"]
).columns

print("Categorical columns:", len(categorical_columns))
print("Numerical columns:", len(numerical_columns))
Categorical columns: 15
Numerical columns: 89
In [48]
df[numerical_columns] = df[numerical_columns].fillna(
    df[numerical_columns].median()
)
In [49]
df[categorical_columns] = df[categorical_columns].fillna("Missing")
In [50]
#Yeha pe humne target variable ko alag kar diya hai aur baaki features ko X mein store kar diya hai
#Step 5 — TARGET ko X aur y mein separate karo
X = df.drop(columns=["TARGET"])
y = df["TARGET"]
In [51]
#Train test split karne ke liye sklearn ka train_test_split function use karenge
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y
)
In [52]
#Step 6 — Categorical encoding
from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(
    handle_unknown="ignore",
    sparse_output=True
)
In [53]
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X,
    y,
    test_size=0.2,
    random_state=42,
    stratify=y
)

print("X_train:", X_train.shape)
print("X_test :", X_test.shape)
print("y_train:", y_train.shape)
print("y_test :", y_test.shape)
X_train: (246008, 103)
X_test : (61503, 103)
y_train: (246008,)
y_test : (61503,)
In [54]
""""Numerical columns   = 89
Categorical columns = 15"""

#Model categorization ke liye, humne categorical columns ko one-hot encoding mein convert kar diya hai. Isse model ko categorical data samajhne mein madad milegi.
'"Numerical columns   = 89\nCategorical columns = 15'
In [55]
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder

categorical_features = X_train.select_dtypes(
    include=["object", "string"]
).columns

numerical_features = X_train.select_dtypes(
    include=["int64", "float64"]
).columns

preprocessor = ColumnTransformer(
    transformers=[
        (
            "num",
            "passthrough",
            numerical_features
        ),
        (
            "cat",
            OneHotEncoder(
                handle_unknown="ignore",
                sparse_output=False
            ),
            categorical_features
        )
    ]
)
In [56]
print("Numerical features:", len(numerical_features))
print("Categorical features:", len(categorical_features))
Numerical features: 88
Categorical features: 15
In [57]
X_train_processed = preprocessor.fit_transform(X_train)

X_test_processed = preprocessor.transform(X_test)

print("X_train processed:", X_train_processed.shape)
print("X_test processed :", X_test_processed.shape)
X_train processed: (246008, 229)
X_test processed : (61503, 229)
In [58]
df[numerical_columns] = df[numerical_columns].fillna(
    df[numerical_columns].median()
)

df[categorical_columns] = df[categorical_columns].fillna("Missing")

Model Training

In [59]
from sklearn.linear_model import LogisticRegression

model = LogisticRegression(
    class_weight="balanced",
    max_iter=1000,
    random_state=42
)

model.fit(X_train_processed, y_train)
LogisticRegression(class_weight='balanced', max_iter=1000, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
In [60]
y_pred = model.predict(X_test_processed)

y_prob = model.predict_proba(X_test_processed)[:, 1]
In [61]
from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)

print("=== LOGISTIC REGRESSION ===")

print("Accuracy  :", accuracy_score(y_test, y_pred))
print("Precision :", precision_score(y_test, y_pred))
print("Recall    :", recall_score(y_test, y_pred))
print("F1 Score  :", f1_score(y_test, y_pred))
print("ROC-AUC   :", roc_auc_score(y_test, y_prob))
=== LOGISTIC REGRESSION ===
Accuracy  : 0.585337300619482
Precision : 0.11336596385542169
Recall    : 0.6064451158106747
F1 Score  : 0.19102299762093577
ROC-AUC   : 0.6292258195496816
In [62]
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
import matplotlib.pyplot as plt

cm = confusion_matrix(y_test, y_pred)

print(cm)

disp = ConfusionMatrixDisplay(
    confusion_matrix=cm,
    display_labels=["No Default", "Default"]
)

disp.plot()
plt.title("Logistic Regression - Confusion Matrix")
plt.show()
[[32989 23549]
 [ 1954  3011]]
Chart 1 from the Credit Risk Predictor notebook
In [63]
from sklearn.metrics import classification_report

print(classification_report(
    y_test,
    y_pred,
    target_names=["No Default", "Default"]
))
precision    recall  f1-score   support

  No Default       0.94      0.58      0.72     56538
     Default       0.11      0.61      0.19      4965

    accuracy                           0.59     61503
   macro avg       0.53      0.59      0.46     61503
weighted avg       0.88      0.59      0.68     61503
In [64]
from sklearn.ensemble import RandomForestClassifier

rf_model = RandomForestClassifier(
    n_estimators=200,
    max_depth=12,
    min_samples_leaf=10,
    class_weight="balanced",
    random_state=42,
    n_jobs=-1
)

rf_model.fit(X_train_processed, y_train)
RandomForestClassifier(class_weight='balanced', max_depth=12, min_samples_leaf=10, n_estimators=200, n_jobs=-1, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
In [65]
rf_pred = rf_model.predict(X_test_processed)
rf_prob = rf_model.predict_proba(X_test_processed)[:, 1]
In [66]
from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)

print("=== RANDOM FOREST ===")

print("Accuracy :", accuracy_score(y_test, rf_pred))
print("Precision:", precision_score(y_test, rf_pred))
print("Recall   :", recall_score(y_test, rf_pred))
print("F1 Score :", f1_score(y_test, rf_pred))
print("ROC-AUC  :", roc_auc_score(y_test, rf_prob))
=== RANDOM FOREST ===
Accuracy : 0.7227127132009821
Precision: 0.16772030124786982
Recall   : 0.6145015105740181
F1 Score : 0.26351701502850233
ROC-AUC  : 0.7394137789386863
In [67]
results = pd.DataFrame({
    "Model": ["Logistic Regression","Random Forest"],
    "Accuracy": [accuracy_score(y_test, y_pred), accuracy_score(y_test, rf_pred)],
    "Precision": [precision_score(y_test, y_pred), precision_score(y_test, rf_pred)],
    "Recall": [recall_score(y_test, y_pred), recall_score(y_test, rf_pred)],
    "F1": [f1_score(y_test, y_pred), f1_score(y_test, rf_pred)],
    "ROC-AUC": [roc_auc_score(y_test, y_prob), roc_auc_score(y_test, rf_prob)]
})

results
Model Accuracy Precision Recall F1 ROC-AUC
0 Logistic Regression 0.585337 0.113366 0.606445 0.191023 0.629226
1 Random Forest 0.722713 0.167720 0.614502 0.263517 0.739414
In [68]
from sklearn.ensemble import HistGradientBoostingClassifier

gb_model = HistGradientBoostingClassifier(
    max_iter=200,
    learning_rate=0.05,
    max_leaf_nodes=31,
    l2_regularization=1.0,
    random_state=42
)

gb_model.fit(X_train_processed, y_train)
HistGradientBoostingClassifier(l2_regularization=1.0, learning_rate=0.05, max_iter=200, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
In [69]
gb_pred = gb_model.predict(X_test_processed)

gb_prob = gb_model.predict_proba(X_test_processed)[:, 1]
In [70]
print("=== HISTOGRAM GRADIENT BOOSTING ===")

print("Accuracy :", accuracy_score(y_test, gb_pred))
print("Precision:", precision_score(y_test, gb_pred))
print("Recall   :", recall_score(y_test, gb_pred))
print("F1 Score :", f1_score(y_test, gb_pred))
print("ROC-AUC  :", roc_auc_score(y_test, gb_prob))
=== HISTOGRAM GRADIENT BOOSTING ===
Accuracy : 0.9198087898151309
Precision: 0.6037735849056604
Recall   : 0.01933534743202417
F1 Score : 0.03747072599531616
ROC-AUC  : 0.759978307952619
In [71]
results = pd.DataFrame({
    "Model": [
        "Logistic Regression",
        "Random Forest",
        "HistGradientBoosting"
    ],
    
    "Accuracy": [
        accuracy_score(y_test, y_pred),
        accuracy_score(y_test, rf_pred),
        accuracy_score(y_test, gb_pred)
    ],
    
    "Precision": [
        precision_score(y_test, y_pred),
        precision_score(y_test, rf_pred),
        precision_score(y_test, gb_pred)
    ],
    
    "Recall": [
        recall_score(y_test, y_pred),
        recall_score(y_test, rf_pred),
        recall_score(y_test, gb_pred)
    ],
    
    "F1": [
        f1_score(y_test, y_pred),
        f1_score(y_test, rf_pred),
        f1_score(y_test, gb_pred)
    ],
    
    "ROC-AUC": [
        roc_auc_score(y_test, y_prob),
        roc_auc_score(y_test, rf_prob),
        roc_auc_score(y_test, gb_prob)
    ]
})

results
Model Accuracy Precision Recall F1 ROC-AUC
0 Logistic Regression 0.585337 0.113366 0.606445 0.191023 0.629226
1 Random Forest 0.722713 0.167720 0.614502 0.263517 0.739414
2 HistGradientBoosting 0.919809 0.603774 0.019335 0.037471 0.759978
In [72]
import numpy as np
from sklearn.metrics import precision_score, recall_score, f1_score

threshold_results = []

for threshold in np.arange(0.10, 0.51, 0.05):

    pred = (gb_prob >= threshold).astype(int)

    threshold_results.append({
        "Threshold": threshold,
        "Precision": precision_score(y_test, pred, zero_division=0),
        "Recall": recall_score(y_test, pred),
        "F1": f1_score(y_test, pred)
    })

threshold_df = pd.DataFrame(threshold_results)

threshold_df
Threshold Precision Recall F1
0 0.10 0.190751 0.592346 0.288574
1 0.15 0.249789 0.417925 0.312688
2 0.20 0.298715 0.290232 0.294412
3 0.25 0.346531 0.199194 0.252974
4 0.30 0.381503 0.132931 0.197162
5 0.35 0.428991 0.088218 0.146341
6 0.40 0.492754 0.054783 0.098604
7 0.45 0.530822 0.031219 0.058969
8 0.50 0.603774 0.019335 0.037471
In [73]
threshold_df.loc[
    threshold_df["F1"].idxmax()
]
Threshold    0.150000
Precision    0.249789
Recall       0.417925
F1           0.312688
Name: 1, dtype: float64
In [74]
final_threshold = 0.15

final_pred = (gb_prob >= final_threshold).astype(int)

print("=== FINAL HISTGRADIENTBOOSTING ===")

print("Threshold :", final_threshold)
print("Precision :", precision_score(y_test, final_pred))
print("Recall    :", recall_score(y_test, final_pred))
print("F1 Score  :", f1_score(y_test, final_pred))
print("ROC-AUC   :", roc_auc_score(y_test, gb_prob))
=== FINAL HISTGRADIENTBOOSTING ===
Threshold : 0.15
Precision : 0.24978933429637654
Recall    : 0.4179254783484391
F1 Score  : 0.31268836648583487
ROC-AUC   : 0.759978307952619
In [75]
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay

cm = confusion_matrix(y_test, final_pred)

print(cm)

disp = ConfusionMatrixDisplay(
    confusion_matrix=cm,
    display_labels=["No Default", "Default"]
)

disp.plot()
plt.title("Final Model - Confusion Matrix (Threshold = 0.15)")
plt.show()
[[50306  6232]
 [ 2890  2075]]
Chart 2 from the Credit Risk Predictor notebook
In [76]
print(
    classification_report(
        y_test,
        final_pred,
        target_names=["No Default", "Default"]
    )
)
precision    recall  f1-score   support

  No Default       0.95      0.89      0.92     56538
     Default       0.25      0.42      0.31      4965

    accuracy                           0.85     61503
   macro avg       0.60      0.65      0.61     61503
weighted avg       0.89      0.85      0.87     61503
In [77]
from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

fpr, tpr, thresholds = roc_curve(y_test, gb_prob)

auc = roc_auc_score(y_test, gb_prob)

plt.figure(figsize=(8, 6))

plt.plot(fpr, tpr, label=f"HistGradientBoosting (AUC = {auc:.3f})")
plt.plot([0, 1], [0, 1], linestyle="--")

plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("ROC Curve - HistGradientBoosting")
plt.legend()
plt.show()
Chart 3 from the Credit Risk Predictor notebook
In [78]
from sklearn.metrics import precision_recall_curve

precision, recall, thresholds = precision_recall_curve(
    y_test,
    gb_prob
)

plt.figure(figsize=(8, 6))

plt.plot(recall, precision)

plt.xlabel("Recall")
plt.ylabel("Precision")
plt.title("Precision-Recall Curve - HistGradientBoosting")

plt.grid()
plt.show()
Chart 4 from the Credit Risk Predictor notebook
In [79]
plt.figure(figsize=(8, 6))

plt.plot(
    threshold_df["Threshold"],
    threshold_df["Precision"],
    marker="o",
    label="Precision"
)

plt.plot(
    threshold_df["Threshold"],
    threshold_df["Recall"],
    marker="o",
    label="Recall"
)

plt.plot(
    threshold_df["Threshold"],
    threshold_df["F1"],
    marker="o",
    label="F1 Score"
)

plt.axvline(
    x=0.15,
    linestyle="--",
    label="Best Threshold = 0.15"
)

plt.xlabel("Threshold")
plt.ylabel("Score")
plt.title("Threshold Analysis")
plt.legend()
plt.grid()
plt.show()
Chart 5 from the Credit Risk Predictor notebook
In [80]
from sklearn.inspection import permutation_importance

perm_importance = permutation_importance(
    gb_model,
    X_test_processed,
    y_test,
    n_repeats=5,
    random_state=42,
    scoring="roc_auc",
    n_jobs=-1
)

feature_names = preprocessor.get_feature_names_out()

importance_df = pd.DataFrame({
    "Feature": feature_names,
    "Importance": perm_importance.importances_mean
})

importance_df = importance_df.sort_values(
    "Importance",
    ascending=False
)

importance_df.head(20)
Feature Importance
28 num__EXT_SOURCE_3 0.062478
27 num__EXT_SOURCE_2 0.048455
4 num__AMT_GOODS_PRICE 0.019804
26 num__EXT_SOURCE_1 0.019637
2 num__AMT_CREDIT 0.012610
6 num__DAYS_BIRTH 0.006826
3 num__AMT_ANNUITY 0.004238
7 num__DAYS_EMPLOYED 0.003826
90 cat__CODE_GENDER_F 0.002707
114 cat__NAME_EDUCATION_TYPE_Higher education 0.002321
93 cat__FLAG_OWN_CAR_N 0.002181
9 num__DAYS_ID_PUBLISH 0.001773
119 cat__NAME_FAMILY_STATUS_Married 0.001443
88 cat__NAME_CONTRACT_TYPE_Cash loans 0.001372
61 num__DAYS_LAST_PHONE_CHANGE 0.001181
86 num__AMT_REQ_CREDIT_BUREAU_QRT 0.000878
91 cat__CODE_GENDER_M 0.000840
63 num__FLAG_DOCUMENT_3 0.000743
112 cat__NAME_INCOME_TYPE_Working 0.000685
8 num__DAYS_REGISTRATION 0.000610
In [81]
top_features = importance_df.head(20).sort_values(
    "Importance"
)

plt.figure(figsize=(10, 8))

plt.barh(
    top_features["Feature"],
    top_features["Importance"]
)

plt.xlabel("Permutation Importance")
plt.ylabel("Feature")
plt.title("Top 20 Feature Importance - HistGradientBoosting")

plt.tight_layout()
plt.show()
Chart 6 from the Credit Risk Predictor notebook
In [82]
from sklearn.pipeline import Pipeline

final_pipeline = Pipeline([
    ("preprocessor", preprocessor),
    ("model", gb_model)
])

final_pipeline
Pipeline(steps=[('preprocessor', ColumnTransformer(transformers=[('num', 'passthrough', Index(['CNT_CHILDREN', 'AMT_INCOME_TOTAL', 'AMT_CREDIT', 'AMT_ANNUITY', 'AMT_GOODS_PRICE', 'REGION_POPULATION_RELATIVE', 'DAYS_BIRTH', 'DAYS_EMPLOYED', 'DAYS_REGISTRATION', 'DAYS_ID_PUBLISH', 'FLAG_MOBIL', 'FLAG_EMP_PHONE', 'FLAG_WORK_PHONE', 'FLAG_CONT_MOBILE', 'FLAG_PHONE', 'FL... 'NAME_TYPE_SUITE', 'NAME_INCOME_TYPE', 'NAME_EDUCATION_TYPE', 'NAME_FAMILY_STATUS', 'NAME_HOUSING_TYPE', 'OCCUPATION_TYPE', 'WEEKDAY_APPR_PROCESS_START', 'ORGANIZATION_TYPE', 'HOUSETYPE_MODE', 'WALLSMATERIAL_MODE', 'EMERGENCYSTATE_MODE'], dtype='object'))])), ('model', HistGradientBoostingClassifier(l2_regularization=1.0, learning_rate=0.05, max_iter=200, random_state=42))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
In [83]
final_pipeline.fit(X_train, y_train)
Pipeline(steps=[('preprocessor', ColumnTransformer(transformers=[('num', 'passthrough', Index(['CNT_CHILDREN', 'AMT_INCOME_TOTAL', 'AMT_CREDIT', 'AMT_ANNUITY', 'AMT_GOODS_PRICE', 'REGION_POPULATION_RELATIVE', 'DAYS_BIRTH', 'DAYS_EMPLOYED', 'DAYS_REGISTRATION', 'DAYS_ID_PUBLISH', 'FLAG_MOBIL', 'FLAG_EMP_PHONE', 'FLAG_WORK_PHONE', 'FLAG_CONT_MOBILE', 'FLAG_PHONE', 'FL... 'NAME_TYPE_SUITE', 'NAME_INCOME_TYPE', 'NAME_EDUCATION_TYPE', 'NAME_FAMILY_STATUS', 'NAME_HOUSING_TYPE', 'OCCUPATION_TYPE', 'WEEKDAY_APPR_PROCESS_START', 'ORGANIZATION_TYPE', 'HOUSETYPE_MODE', 'WALLSMATERIAL_MODE', 'EMERGENCYSTATE_MODE'], dtype='object'))])), ('model', HistGradientBoostingClassifier(l2_regularization=1.0, learning_rate=0.05, max_iter=200, random_state=42))])In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
In [84]
final_prob = final_pipeline.predict_proba(X_test)[:, 1]

final_pred = (final_prob >= 0.15).astype(int)
In [85]
from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    roc_auc_score
)

print("=== FINAL MODEL ===")

print("Accuracy :", accuracy_score(y_test, final_pred))
print("Precision:", precision_score(y_test, final_pred))
print("Recall   :", recall_score(y_test, final_pred))
print("F1 Score :", f1_score(y_test, final_pred))
print("ROC-AUC  :", roc_auc_score(y_test, final_prob))
=== FINAL MODEL ===
Accuracy : 0.8516820317708079
Precision: 0.24978933429637654
Recall   : 0.4179254783484391
F1 Score : 0.31268836648583487
ROC-AUC  : 0.759978307952619
In [86]
import joblib

joblib.dump(
    {
        "pipeline": final_pipeline,
        "threshold": 0.15
    },
    "../models/credit_risk_model.joblib"
)
['../models/credit_risk_model.joblib']
In [87]
import os

print(os.path.exists("../models/credit_risk_model.joblib"))
print(
    os.path.getsize("../models/credit_risk_model.joblib") / (1024 * 1024),
    "MB"
)
True
0.7658452987670898 MB
In [88]
import joblib

saved_model = joblib.load("../models/credit_risk_model.joblib")

print(type(saved_model))
print(saved_model.keys())
print("Threshold:", saved_model["threshold"])
<class 'dict'>
dict_keys(['pipeline', 'threshold'])
Threshold: 0.15
In [89]
loaded_pipeline = saved_model["pipeline"]
threshold = saved_model["threshold"]

sample = X_test.iloc[:5]

sample_prob = loaded_pipeline.predict_proba(sample)[:, 1]

sample_pred = (sample_prob >= threshold).astype(int)

print("Probabilities:", sample_prob)
print("Predictions:", sample_pred)
Probabilities: [0.04600577 0.04040459 0.23743445 0.06401579 0.08211444]
Predictions: [0 0 1 0 0]
In
#Gemini Code
import os
import joblib

# Target paths relative to notebook directory
model_save_path = "../models/credit_risk_model.joblib"
defaults_save_path = "../models/app_defaults.joblib"

# 1. Create app_defaults if missing (using training features sample/median)
app_defaults = X_train.iloc[0].to_dict()

# 2. Save Model Dictionary
joblib.dump(
    {
        "pipeline": final_pipeline,
        "threshold": 0.15
    },
    model_save_path
)

# 3. Save App Defaults
joblib.dump(app_defaults, defaults_save_path)

print("Models and defaults successfully re-saved!")
Models and defaults successfully re-saved!
In [94]
app_defaults = X_train.iloc[0].to_dict()
joblib.dump(app_defaults, "../models/app_defaults.joblib")
['../models/app_defaults.joblib']