SECTION 1: LEARNING OBJECTIVES

By the end of this lesson, you will be able to:

  • Understand the fundamentals of credit risk in digital banking.

  • Identify the key components of credit risk – PD, LGD, EAD.

  • Apply machine learning models for credit scoring and default prediction.

  • Use alternative data for credit risk assessment.

  • Implement model validation for credit risk models.

  • Understand the regulatory framework – Basel III, IFRS 9, CECL.

  • Measure model performance using appropriate metrics.

  • Develop a credit risk strategy for a digital bank.


SECTION 2: UNDERSTANDING CREDIT RISK

2.1 What is Credit Risk?

Credit Risk is the risk of loss arising from a borrower’s failure to repay a loan or meet contractual obligations. It is one of the most significant risks faced by banks.

2.2 Key Components of Credit Risk
 
 
Component Description Formula
Probability of Default (PD) Likelihood of default within a given time horizon. Probability estimate.
Loss Given Default (LGD) Proportion of exposure lost on default. 1 – Recovery Rate.
Exposure at Default (EAD) Total exposure at the time of default. Outstanding + undrawn commitments.
Expected Loss (EL) Average loss expected. PD × LGD × EAD.
Unexpected Loss (UL) Volatility around expected loss. Statistical measure.
2.3 Credit Risk Drivers
 
 
Driver Description Impact
Credit Score Borrower’s creditworthiness. Higher score → lower risk.
Debt-to-Income Ratio Debt relative to income. Higher ratio → higher risk.
Loan-to-Value Ratio Loan amount relative to asset value. Higher LTV → higher risk.
Employment History Stability of employment. Stable → lower risk.
Payment History Past repayment behaviour. Consistent → lower risk.
Economic Conditions Macroeconomic factors. Recession → higher risk.

SECTION 3: AI FOR CREDIT RISK MODELLING

3.1 Traditional vs AI Credit Scoring
 
 
Aspect Traditional Scoring AI-Powered Scoring
Data Sources Credit bureau, application data. Traditional + alternative data.
Model Approach Logistic regression, scorecards. ML/DL, XGBoost, Neural Networks.
Interpretability High (coefficients). Moderate to Low (SHAP required).
Speed Batch processing. Real-time scoring.
Inclusivity Limited to credit history. Includes unbanked/underbanked.
Accuracy Good. Better.
3.2 Alternative Data Sources
 
 
Data Source Description Use Case
Telecom Data Phone bill payments. Assess payment reliability.
Utility Payments Electricity, water, gas. Income stability.
Rent Payments Rental history. Credit history for thin files.
Mobile Data App usage, call patterns. Behavioural scoring.
Transaction Data Spending patterns. Income verification.
Social Media Professional network. Identity verification.

SECTION 4: MODEL VALIDATION AND REGULATORY FRAMEWORK

4.1 Model Validation Framework
 
 
Activity Description Tools/Methods
Conceptual Soundness Review theoretical basis. Literature review, peer review.
Data Quality Assess data completeness, accuracy. Data profiling, quality checks.
Performance Testing Evaluate model accuracy. AUC, KS, calibration.
Stability Testing Test performance over time. Out-of-time validation.
Benchmarking Compare with alternative models. Simpler models, industry benchmarks.
Fairness Testing Test for disparate impact. Disparate impact analysis.
4.2 Regulatory Framework
 
 
Regulation Requirement Impact
Basel III Capital adequacy for credit risk. IRB approach, validation.
IFRS 9 / CECL Expected credit loss provisioning. PD, LGD, EAD estimates.
ECOA / Fair Lending Non-discrimination. Fairness testing.
SR 11-7 Model risk management. Validation, governance.
GDPR / CCPA Data privacy. Data protection, consent.

SECTION 5: IMPLEMENTATION IN PYTHON – CREDIT RISK MODELLING

python
# ===================================================================
# MODULE 4, LESSON 7: CREDIT RISK MODELLING WITH AI
# ===================================================================

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from sklearn.metrics import (roc_auc_score, classification_report, confusion_matrix,
                             roc_curve, mean_squared_error, r2_score)
from scipy.stats import norm
import warnings
warnings.filterwarnings('ignore')

print("="*70)
print("CREDIT RISK MODELLING WITH AI")
print("="*70)

# ----------------------------------------------------------------
# PART A: GENERATE CREDIT DATA
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART A: Generating Credit Data")
print("-"*60)

np.random.seed(42)
n_loans = 10000

# Generate borrower features
credit_data = pd.DataFrame({
    'loan_id': range(1, n_loans + 1),
    'age': np.random.normal(45, 15, n_loans).clip(18, 80).astype(int),
    'income': np.random.gamma(5, 20, n_loans) + 20,
    'credit_score': np.random.normal(700, 50, n_loans).clip(550, 850).astype(int),
    'dti': np.random.beta(2, 5, n_loans) * 60,
    'loan_amount': np.random.gamma(4, 50, n_loans) + 30,
    'loan_term': np.random.choice([12, 24, 36, 48, 60, 72], n_loans),
    'employment_years': np.random.gamma(3, 5, n_loans).clip(0, 30).astype(int),
    'home_owner': np.random.binomial(1, 0.65, n_loans),
    'marital_status': np.random.choice([0, 1, 2], n_loans, p=[0.35, 0.45, 0.20]),
    'education': np.random.choice([0, 1, 2, 3], n_loans, p=[0.15, 0.25, 0.35, 0.25]),
    'previous_defaults': np.random.choice([0, 1, 2], n_loans, p=[0.8, 0.15, 0.05])
})

# Generate default based on features
log_odds = (-4.5 + 0.04 * credit_data['dti'] - 0.005 * credit_data['credit_score']
            + 0.01 * (credit_data['loan_amount']/1000) + 0.02 * credit_data['employment_years']
            - 0.01 * credit_data['age'] + 0.3 * credit_data['home_owner']
            - 0.5 * credit_data['marital_status'] - 0.3 * credit_data['education']
            + 0.2 * credit_data['previous_defaults'])

# Add non-linearity
log_odds += 0.0003 * credit_data['dti']**2
log_odds -= 0.00001 * credit_data['credit_score'] * credit_data['dti']

prob_default = 1 / (1 + np.exp(-log_odds))
credit_data['default'] = np.random.binomial(1, prob_default)

# Generate LGD based on home ownership
credit_data['lgd'] = np.where(credit_data['home_owner'] == 1,
                             np.random.normal(0.35, 0.08, n_loans).clip(0.10, 0.70),
                             np.random.normal(0.65, 0.10, n_loans).clip(0.20, 0.90))

# EAD = loan amount
credit_data['ead'] = credit_data['loan_amount']

print(f"Generated {len(credit_data)} loan applications")
print(f"Default rate: {credit_data['default'].mean():.2%}")
print(f"Average LGD: {credit_data['lgd'].mean():.2%}")

# ----------------------------------------------------------------
# PART B: EXPLORATORY ANALYSIS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Exploratory Analysis")
print("-"*60)

# Default rate by key features
default_summary = credit_data.groupby('default').agg({
    'credit_score': 'mean',
    'dti': 'mean',
    'income': 'mean',
    'loan_amount': 'mean'
}).round(2)

print("Default Summary:")
print(default_summary)

# Visualise
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Credit Score vs Default
ax = axes[0, 0]
sns.boxplot(data=credit_data, x='default', y='credit_score', ax=ax)
ax.set_xticklabels(['No Default', 'Default'])
ax.set_title('Credit Score by Default')

# DTI vs Default
ax = axes[0, 1]
sns.boxplot(data=credit_data, x='default', y='dti', ax=ax)
ax.set_xticklabels(['No Default', 'Default'])
ax.set_title('DTI by Default')

# Income vs Loan Amount
ax = axes[1, 0]
scatter = ax.scatter(credit_data['income'], credit_data['loan_amount'],
                     c=credit_data['default'], cmap='RdYlGn', alpha=0.5, s=10)
ax.set_xlabel('Income ($000s)')
ax.set_ylabel('Loan Amount ($000s)')
ax.set_title('Income vs Loan Amount')
plt.colorbar(scatter, ax=ax, label='Default')

# Correlation Heatmap
ax = axes[1, 1]
numeric_cols = ['age', 'income', 'credit_score', 'dti', 'loan_amount',
                'loan_term', 'employment_years', 'default']
corr = credit_data[numeric_cols].corr()
sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm', ax=ax)
ax.set_title('Correlation Matrix')

plt.tight_layout()
plt.savefig('credit_risk_eda.png', dpi=300, bbox_inches='tight')
plt.show()
print("Credit risk EDA visualisation saved as 'credit_risk_eda.png'")

# ----------------------------------------------------------------
# PART C: FEATURE ENGINEERING
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Feature Engineering")
print("-"*60)

# Create engineered features
credit_data['loan_to_income'] = credit_data['loan_amount'] / credit_data['income']
credit_data['dti_credit_interaction'] = credit_data['dti'] * credit_data['credit_score'] / 1000
credit_data['dti_squared'] = credit_data['dti'] ** 2
credit_data['credit_score_category'] = pd.cut(credit_data['credit_score'],
                                             bins=[550, 600, 650, 700, 750, 850],
                                             labels=['Poor', 'Fair', 'Good', 'Very Good', 'Excellent'])

# Features for modelling
features = ['age', 'income', 'credit_score', 'dti', 'loan_amount', 'loan_term',
            'employment_years', 'home_owner', 'marital_status', 'education',
            'previous_defaults', 'loan_to_income', 'dti_credit_interaction',
            'dti_squared']

X = credit_data[features]
y = credit_data['default']

# Encode categorical features
X_encoded = pd.get_dummies(X, columns=['marital_status', 'education'], drop_first=True)

print(f"Features: {len(X_encoded.columns)}")

# ----------------------------------------------------------------
# PART D: MODEL TRAINING AND EVALUATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Model Training and Evaluation")
print("-"*60)

# Train-test split
X_train, X_test, y_train, y_test = train_test_split(X_encoded, y, test_size=0.3, random_state=42)

# Scale features
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 1. Logistic Regression (Baseline)
lr_model = LogisticRegression(class_weight='balanced', max_iter=1000, random_state=42)
lr_model.fit(X_train_scaled, y_train)
y_pred_lr_proba = lr_model.predict_proba(X_test_scaled)[:, 1]
auc_lr = roc_auc_score(y_test, y_pred_lr_proba)

# 2. Random Forest
rf_model = RandomForestClassifier(n_estimators=100, max_depth=10, class_weight='balanced', random_state=42)
rf_model.fit(X_train_scaled, y_train)
y_pred_rf_proba = rf_model.predict_proba(X_test_scaled)[:, 1]
auc_rf = roc_auc_score(y_test, y_pred_rf_proba)

# 3. XGBoost
scale_pos_weight = len(y_train[y_train==0]) / len(y_train[y_train==1])
xgb_model = XGBClassifier(n_estimators=100, max_depth=6, learning_rate=0.1,
                         scale_pos_weight=scale_pos_weight,
                         random_state=42, use_label_encoder=False, eval_metric='logloss')
xgb_model.fit(X_train_scaled, y_train)
y_pred_xgb_proba = xgb_model.predict_proba(X_test_scaled)[:, 1]
auc_xgb = roc_auc_score(y_test, y_pred_xgb_proba)

print("Model Performance (AUC):")
print(f"  Logistic Regression: {auc_lr:.4f}")
print(f"  Random Forest: {auc_rf:.4f}")
print(f"  XGBoost: {auc_xgb:.4f}")

# Best model = XGBoost
y_pred = (y_pred_xgb_proba >= 0.5).astype(int)
print("\nXGBoost Classification Report:")
print(classification_report(y_test, y_pred, target_names=['No Default', 'Default']))

# Confusion Matrix
cm = confusion_matrix(y_test, y_pred)
print("\nConfusion Matrix:")
print(pd.DataFrame(cm, columns=['Pred No Default', 'Pred Default'],
                   index=['Actual No Default', 'Actual Default']))

# Feature Importance
importance_xgb = pd.DataFrame({
    'Feature': X_encoded.columns,
    'Importance': xgb_model.feature_importances_
}).sort_values('Importance', ascending=False)

print("\nTop 10 Credit Risk Predictors:")
print(importance_xgb.head(10).to_string(index=False))

# ----------------------------------------------------------------
# PART E: CALIBRATION AND STRESS TESTING
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Calibration and Stress Testing")
print("-"*60)

# Calibration plot
def calibration_plot(y_true, y_pred, n_bins=10):
    """Create a calibration plot."""
    df = pd.DataFrame({'y_true': y_true, 'y_pred': y_pred})
    df['bin'] = pd.qcut(df['y_pred'], q=n_bins, labels=False, duplicates='drop')
    cal_data = df.groupby('bin').agg(
        observed=('y_true', 'mean'),
        predicted=('y_pred', 'mean'),
        count=('y_true', 'count')
    ).reset_index()
    return cal_data

cal_data = calibration_plot(y_test, y_pred_xgb_proba)

fig, ax = plt.subplots(figsize=(8, 6))
ax.plot(cal_data['predicted'], cal_data['observed'], 'bo-', linewidth=2, markersize=8, label='Model')
ax.plot([0, 1], [0, 1], 'r--', linewidth=1, label='Perfect Calibration')
ax.set_xlabel('Mean Predicted Probability')
ax.set_ylabel('Observed Default Rate')
ax.set_title('Calibration Plot - XGBoost')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('credit_calibration.png', dpi=300, bbox_inches='tight')
plt.show()
print("Calibration plot saved as 'credit_calibration.png'")

# Expected Loss calculation
credit_data['pd'] = xgb_model.predict_proba(scaler.transform(X_encoded))[:, 1]
credit_data['el'] = credit_data['pd'] * credit_data['lgd'] * credit_data['ead']

print(f"Total Expected Loss: ${credit_data['el'].sum():,.2f}")
print(f"Average EL per Loan: ${credit_data['el'].mean():,.2f}")

# Stress testing (simulated)
def stress_test(df, gdp_shock, unemp_shock):
    """Simulate stress testing on credit portfolio."""
    df_stressed = df.copy()
    # Apply macroeconomic shocks
    log_odds = np.log(df_stressed['pd'] / (1 - df_stressed['pd']))
    log_odds += -1.5 * gdp_shock + 3.0 * unemp_shock
    df_stressed['pd_stressed'] = 1 / (1 + np.exp(-log_odds))
    df_stressed['pd_stressed'] = df_stressed['pd_stressed'].clip(0.001, 0.999)
    df_stressed['el_stressed'] = df_stressed['pd_stressed'] * df_stressed['lgd'] * df_stressed['ead']
    return df_stressed

scenarios = {
    'Baseline': {'gdp': 0.0, 'unemp': 0.0},
    'Adverse': {'gdp': -0.02, 'unemp': 0.03},
    'Severe': {'gdp': -0.05, 'unemp': 0.06}
}

stress_results = []
for name, shocks in scenarios.items():
    df_stressed = stress_test(credit_data, shocks['gdp'], shocks['unemp'])
    el_stressed = df_stressed['el_stressed'].sum()
    stress_results.append({
        'Scenario': name,
        'EL': el_stressed,
        'Increase %': (el_stressed / credit_data['el'].sum() - 1) * 100
    })

stress_df = pd.DataFrame(stress_results)
print("\nStress Test Results:")
print(stress_df.to_string(index=False))

# ----------------------------------------------------------------
# PART F: REGULATORY METRICS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Regulatory Metrics")
print("-"*60)

# KS Statistic
from scipy.stats import ks_2samp
scores_good = y_pred_xgb_proba[y_test == 0]
scores_bad = y_pred_xgb_proba[y_test == 1]
ks_stat, _ = ks_2samp(scores_good, scores_bad)

# Gini Coefficient
gini = 2 * auc_xgb - 1

print(f"KS Statistic: {ks_stat:.4f}")
print(f"Gini Coefficient: {gini:.4f}")
print(f"AUC: {auc_xgb:.4f}")

# Regulatory benchmarks
regulatory_benchmarks = {
    'Metric': ['AUC', 'KS', 'Gini', 'Calibration (H-L)'],
    'Acceptable': ['> 0.75', '> 0.30', '> 0.50', 'p > 0.05'],
    'Good': ['> 0.80', '> 0.35', '> 0.60', 'p > 0.10'],
    'Excellent': ['> 0.85', '> 0.40', '> 0.70', 'p > 0.20']
}

benchmark_df = pd.DataFrame(regulatory_benchmarks)
print("\nRegulatory Benchmarks:")
print(benchmark_df.to_string(index=False))

# ----------------------------------------------------------------
# PART G: SUMMARY AND RECOMMENDATIONS
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART G: Summary and Recommendations")
print("="*70)

print("""
Credit Risk Modelling with AI – Key Takeaways:

1. Credit risk includes PD, LGD, EAD, Expected Loss, and Unexpected Loss.
2. AI models (XGBoost, Random Forest) outperform traditional logistic regression.
3. Alternative data improves credit risk assessment for underserved populations.
4. Model validation: conceptual soundness, data quality, performance, stability.
5. Regulatory framework: Basel III, IFRS 9/CECL, ECOA/Fair Lending.
6. Key metrics: AUC, KS, Gini, calibration, stress testing.
7. Explainability (SHAP) is essential for regulatory compliance.

Recommendations:
  - Use ensemble models (XGBoost) for credit scoring.
  - Incorporate alternative data for financial inclusion.
  - Regularly validate and monitor credit risk models.
  - Ensure model explainability for regulatory compliance.
  - Conduct stress testing under multiple scenarios.
  - Maintain clear documentation for regulatory submissions.
""")

print("="*70)
print("END OF LESSON 7 – MODULE 4")
print("="*70)

SECTION 6: SUMMARY FOR THE DATA PRACTITIONER

  • Credit risk includes Probability of Default (PD), Loss Given Default (LGD), Exposure at Default (EAD), Expected Loss (EL), and Unexpected Loss (UL).

  • AI models (XGBoost, Random Forest) outperform traditional logistic regression for credit scoring.

  • Alternative data (telecom, utility, rent, mobile data) improves credit risk assessment for underserved populations.

  • Model validation requires conceptual soundness, data quality, performance, stability, and fairness testing.

  • Regulatory framework includes Basel III, IFRS 9/CECL, ECOA/Fair Lending, and SR 11-7.

  • Key metrics include AUC, KS Statistic, Gini Coefficient, calibration, and stress testing results.

  • Explainability (SHAP) is essential for regulatory compliance and fair lending.


SECTION 7: RECOMMENDED NEXT STEPS

  1. Use ensemble models (XGBoost) for credit scoring.

  2. Incorporate alternative data for financial inclusion.

  3. Regularly validate and monitor credit risk models.

  4. Ensure model explainability for regulatory compliance.

  5. Conduct stress testing under multiple scenarios.

  6. Maintain clear documentation for regulatory submissions.

  7. Prepare for Lesson 8: Data Governance and Ethics in Banking.


[END OF LESSON 7 – MODULE 4]

Â