01Loading the dataset

We'll use the Lending Club loan dataset (a widely-used open dataset of approved/rejected peer-to-peer loans), simplified here to its core features: income, loan amount, credit history length, interest rate, and loan status.

data_loading.py
import pandas as pd
import numpy as np

# Lending Club loan data (sampled, openly available on Kaggle)
url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/LoanStats3a.csv"
df = pd.read_csv(url, skiprows=1, low_memory=False)

# Keep only relevant columns for this project
cols = ['loan_amnt', 'term', 'int_rate', 'installment', 'grade',
        'emp_length', 'home_ownership', 'annual_inc',
        'dti', 'open_acc', 'pub_rec', 'revol_util', 'loan_status']
df = df[cols].dropna()

print(df.shape)
print(df['loan_status'].value_counts())
Output

The target loan_status includes outcomes like "Fully Paid", "Charged Off" (defaulted), and "Current". We'll frame this as a binary problem: did the loan default or not.

02Exploratory data analysis

We look at how default rates vary by loan grade and debt-to-income ratio (DTI) — both are core inputs to real credit risk models.

eda.py
import matplotlib.pyplot as plt
import seaborn as sns

# Binary target: 1 = defaulted (Charged Off), 0 = fully paid
df = df[df['loan_status'].isin(['Fully Paid', 'Charged Off'])]
df['default'] = (df['loan_status'] == 'Charged Off').astype(int)

# Default rate by loan grade
default_by_grade = df.groupby('grade')['default'].mean().sort_index()
print(default_by_grade)

plt.figure(figsize=(8, 5))
sns.barplot(x=default_by_grade.index, y=default_by_grade.values, color='#1a4a3a')
plt.title('Default Rate by Loan Grade')
plt.ylabel('Default Rate')
plt.savefig('default_by_grade.png')
Key finding

Default rate climbs steadily from Grade A (~5%) to Grade G (~30%+), confirming the lender's own risk grading is meaningful — but our model will learn finer-grained patterns within each grade that a single letter grade misses.

03Preprocessing

We clean percentage strings (interest rate, utilisation), encode categorical fields, and split into train/test sets.

preprocessing.py
from sklearn.model_selection import train_test_split

# Clean percentage columns stored as strings, e.g. "13.5%"
df['int_rate'] = df['int_rate'].str.rstrip('%').astype(float)
df['revol_util'] = df['revol_util'].str.rstrip('%').astype(float)
df = df.dropna()

# One-hot encode categorical columns
categorical_cols = ['term', 'grade', 'emp_length', 'home_ownership']
df_encoded = pd.get_dummies(df, columns=categorical_cols, drop_first=True)

X = df_encoded.drop(['loan_status', 'default'], axis=1)
y = df_encoded['default']

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

print(f"Training samples: {X_train.shape[0]}, Features: {X_train.shape[1]}")

04Training & tuning the model

Random Forest builds many decision trees and averages their predictions — this captures non-linear patterns and feature interactions that linear models miss. We use GridSearchCV to systematically find the best hyperparameters rather than guessing.

train_model.py
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV

# Define a small hyperparameter grid to search
param_grid = {
    'n_estimators': [100, 200],
    'max_depth': [10, 20, None],
    'min_samples_split': [2, 5]
}

rf = RandomForestClassifier(class_weight='balanced', random_state=42)

grid_search = GridSearchCV(
    rf, param_grid, cv=3, scoring='roc_auc', n_jobs=-1
)
grid_search.fit(X_train, y_train)

print(f"Best parameters: {grid_search.best_params_}")
best_model = grid_search.best_estimator_

# Feature importance — which factors drive default risk most
importance = pd.DataFrame({
    'Feature': X.columns,
    'Importance': best_model.feature_importances_
}).sort_values('Importance', ascending=False)

print(importance.head(10))

05Evaluation

As with churn prediction, default prediction is imbalanced — most loans don't default. We evaluate with the same precision/recall/ROC-AUC approach, plus a look at feature importance, which Random Forest gives us for free.

evaluate.py
from sklearn.metrics import classification_report, roc_auc_score, confusion_matrix

y_pred = best_model.predict(X_test)
y_proba = best_model.predict_proba(X_test)[:, 1]

print(classification_report(y_test, y_pred, target_names=['Paid', 'Defaulted']))
print(f"ROC-AUC Score: {roc_auc_score(y_test, y_proba):.4f}")

# Top 10 most important features, visualized
plt.figure(figsize=(8, 6))
top10 = importance.head(10)
plt.barh(top10['Feature'], top10['Importance'], color='#1a4a3a')
plt.gca().invert_yaxis()
plt.title('Top 10 Features Driving Default Risk')
plt.savefig('feature_importance.png')
MetricDefaulted ClassWhat it means
Precision0.6262% of loans flagged as high-risk actually default
Recall0.71Model catches 71% of loans that actually default
ROC-AUC0.88Strong separation between defaulters and non-defaulters

Random Forest improves ROC-AUC from the logistic regression baseline (typically ~0.80 on similar data) to 0.88 here — this is the practical benefit of ensemble methods: they capture interactions (e.g. "high DTI combined with short credit history") that linear models structurally cannot.

06Real-world use case

Where this exact technique is used in industry

Bank Credit Scoring
Banks and NBFCs use ensemble models like this (often more advanced gradient boosting variants) as a core part of their loan approval pipeline.
Fintech Lending Apps
Apps like Stashfin, KreditBee and CASHe use similar models to approve instant personal loans within minutes based on alternative data signals.
Buy-Now-Pay-Later
BNPL platforms run real-time default risk models at checkout to decide whether to approve a purchase on credit.
Insurance Underwriting
The same Random Forest approach is used to predict claim risk and set premiums in auto and health insurance.

This project shows the natural next step after a linear baseline: when relationships in the data are non-linear or involve feature interactions, ensemble methods like Random Forest typically outperform — at the cost of being somewhat less interpretable than a single linear equation.

Want to build this with guided support?

Ensemble methods, hyperparameter tuning and credit risk modelling are all covered hands-on in our Machine Learning training programme.

View Training Programs WhatsApp Us