SECTION 1: LEARNING OBJECTIVES

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

  • Calculate Expected Loss (EL) using PD, LGD, and EAD for the loan portfolio.

  • Calculate Economic Capital using the Vasicek model.

  • Apply the selected model to predict PD for the entire portfolio.

  • Perform stress testing using macroeconomic scenarios.

  • Quantify the business impact – reduced defaults, cost savings, revenue uplift.

  • Calculate ROI for the digital lending transformation project.

  • Present findings to stakeholders in a clear, actionable format.


SECTION 2: RISK CALCULATION FRAMEWORK

2.1 Key Risk Metrics
 
 
Metric Formula Description
Probability of Default (PD) Model prediction Likelihood of default within 1 year.
Loss Given Default (LGD) 1 – Recovery Rate Proportion of exposure lost on default.
Exposure at Default (EAD) Loan amount + undrawn commitments Total exposure at default.
Expected Loss (EL) PD × LGD × EAD Average loss expected.
Unexpected Loss (UL) Vasicek model Capital required to cover losses at 99.9% confidence.
2.2 Vasicek Model for Economic Capital

UL99.9%=LGD×EAD×[Φ(Φ−1(PD)+ρΦ−1(0.999)1−ρ)−PD]

where:

  • Φ = standard normal CDF

  • ρ = asset correlation (0.12 for corporate, 0.04 for retail)

2.3 Stress Testing

Scenarios:

  • Baseline: Current economic conditions.

  • Adverse: Moderate recession (GDP -2%, unemployment +3%).

  • Severely Adverse: Severe recession (GDP -5%, unemployment +6%).

  • Regulatory: CCAR/DFAST prescribed scenarios.


SECTION 3: IMPLEMENTATION IN PYTHON – RISK ANALYTICS

python
# ===================================================================
# MODULE 9, LESSON 4: RISK ANALYTICS, STRESS TESTING, AND BUSINESS IMPACT
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy.stats import norm
import joblib
import warnings
warnings.filterwarnings('ignore')

# Set style
sns.set_style("whitegrid")
np.random.seed(42)

print("="*70)
print("CAPSTONE PROJECT – RISK ANALYTICS AND STRESS TESTING")
print("="*70)

# ----------------------------------------------------------------
# PART A: LOAD DATA AND MODEL
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART A: Loading Data and Model")
print("-"*60)

# Load the test data (using full dataset for risk calculation)
loan_apps = pd.read_csv('loan_applications.csv')
feature_names = ['applicant_age', 'income', 'credit_score', 'dti', 'loan_amount', 
                 'loan_term', 'employment_years', 'home_owner', 'marital_status', 
                 'education', 'loan_to_income', 'dti_credit_interaction', 
                 'dti_squared', 'loan_amount_log']

# Create features (same as before)
df = loan_apps.copy()
df['loan_to_income'] = df['loan_amount'] / df['income']
df['dti_credit_interaction'] = df['dti'] * df['credit_score'] / 1000
df['dti_squared'] = df['dti'] ** 2
df['loan_amount_log'] = np.log(df['loan_amount'] + 1)

# One-hot encode categorical features
X_full = pd.get_dummies(df[feature_names], columns=['marital_status', 'education'])

# Load the best model (XGBoost)
model = joblib.load('xgboost_model.pkl')
scaler = joblib.load('scaler.pkl')

# Standardise
X_full_scaled = scaler.transform(X_full)

# Predict PDs
pd_pred = model.predict_proba(X_full_scaled)[:, 1]
df['pd'] = pd_pred

print(f"PD predictions generated for {len(df)} loans.")
print(f"Average PD: {df['pd'].mean():.4f}")
print(f"Median PD: {df['pd'].median():.4f}")

# ----------------------------------------------------------------
# PART B: EXPECTED LOSS CALCULATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Expected Loss Calculation")
print("-"*60)

# Assign LGD based on home_owner (secured vs unsecured)
# Home owners generally have lower LGD (secured by property)
df['lgd'] = np.where(df['home_owner'] == 1, 
                     np.random.normal(0.35, 0.08, len(df)).clip(0.10, 0.70),
                     np.random.normal(0.65, 0.10, len(df)).clip(0.20, 0.90))

# EAD = loan_amount (for term loans)
df['ead'] = df['loan_amount']

# Expected Loss
df['el'] = df['pd'] * df['lgd'] * df['ead']

# Portfolio totals
total_exposure = df['ead'].sum()
total_el = df['el'].sum()
avg_pd = df['pd'].mean()
avg_lgd = df['lgd'].mean()

print(f"Portfolio Summary:")
print(f"  Total Exposure (EAD): ${total_exposure:,.2f}")
print(f"  Total Expected Loss (EL): ${total_el:,.2f}")
print(f"  EL as % of Exposure: {total_el/total_exposure*100:.2f}%")
print(f"  Average PD: {avg_pd:.4f}")
print(f"  Average LGD: {avg_lgd:.4f}")

# EL by segment
el_by_purpose = df.groupby('purpose').agg({
    'el': 'sum',
    'ead': 'sum',
    'pd': 'mean',
    'lgd': 'mean',
    'default': 'mean'
}).sort_values('el', ascending=False)

print("\nEL by Loan Purpose:")
print(el_by_purpose.round(4))

# ----------------------------------------------------------------
# PART C: ECONOMIC CAPITAL (VASICEK MODEL)
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Economic Capital (Vasicek Model)")
print("-"*60)

def vasicek_ul(pd, lgd, ead, rho=0.12, confidence=0.999):
    """
    Calculate unexpected loss using the Vasicek model.
    """
    z = norm.ppf(confidence)
    pd_adj = norm.cdf((norm.ppf(pd) + np.sqrt(rho) * z) / np.sqrt(1 - rho))
    ul = lgd * (pd_adj - pd) * ead
    return np.maximum(ul, 0)

# Calculate UL for each loan
df['ul'] = vasicek_ul(df['pd'], df['lgd'], df['ead'])

total_ul = df['ul'].sum()
print(f"Total Unexpected Loss (99.9%): ${total_ul:,.2f}")
print(f"UL as % of Exposure: {total_ul/total_exposure*100:.2f}%")
print(f"Capital Buffer Required: ${total_ul:,.2f}")

# ----------------------------------------------------------------
# PART D: STRESS TESTING
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Stress Testing")
print("-"*60)

def apply_macro_stress(df, gdp_shock, unemp_shock):
    """
    Apply macroeconomic shocks to PD calculations.
    """
    df_stressed = df.copy()
    # Simulate stress impact on PD
    # GDP growth reduces PD; unemployment increases PD
    # Scale factor based on the model's sensitivity to macro factors
    pd_shift = -1.5 * gdp_shock + 3.0 * unemp_shock
    # Apply shift in log-odds space
    log_odds = np.log(df_stressed['pd'] / (1 - df_stressed['pd']))
    log_odds_shifted = log_odds + pd_shift
    df_stressed['pd_stressed'] = 1 / (1 + np.exp(-log_odds_shifted))
    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

# Define scenarios
scenarios = {
    'Baseline': {'gdp': 0.0, 'unemp': 0.0, 'description': 'Current economic conditions'},
    'Adverse': {'gdp': -0.02, 'unemp': 0.03, 'description': 'Moderate recession'},
    'Severely Adverse': {'gdp': -0.05, 'unemp': 0.06, 'description': 'Severe recession'},
    'Regulatory (CCAR)': {'gdp': -0.04, 'unemp': 0.04, 'description': 'Regulatory stress scenario'},
}

# Run stress tests
stress_results = []
for name, params in scenarios.items():
    df_stressed = apply_macro_stress(df, params['gdp'], params['unemp'])
    el_stressed = df_stressed['el_stressed'].sum()
    pd_avg_stressed = df_stressed['pd_stressed'].mean()
    
    stress_results.append({
        'Scenario': name,
        'Description': params['description'],
        'EL': el_stressed,
        'EL Increase %': (el_stressed / total_el - 1) * 100,
        'Average PD': pd_avg_stressed
    })

stress_df = pd.DataFrame(stress_results)
print("Stress Test Results:")
print(stress_df.round(2).to_string(index=False))

# Visualise stress test results
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# EL by scenario
ax = axes[0]
scenario_names = stress_df['Scenario'].tolist()
el_values = stress_df['EL'].values / 1e6
ax.bar(scenario_names, el_values, color=['green', 'orange', 'red', 'purple'], alpha=0.7)
ax.set_xlabel('Scenario')
ax.set_ylabel('Expected Loss ($M)')
ax.set_title('Expected Loss Under Stress Scenarios')
for i, v in enumerate(el_values):
    ax.text(i, v + 0.1, f'${v:.1f}M', ha='center', va='bottom', fontweight='bold')
ax.grid(True, alpha=0.3)

# PD distribution under stress
ax = axes[1]
for scenario in ['Baseline', 'Adverse', 'Severely Adverse']:
    df_stressed = apply_macro_stress(df, 
                                     scenarios[scenario]['gdp'], 
                                     scenarios[scenario]['unemp'])
    ax.hist(df_stressed['pd_stressed'], bins=30, alpha=0.4, label=scenario, density=True)
ax.set_xlabel('Probability of Default')
ax.set_ylabel('Density')
ax.set_title('PD Distribution Under Stress')
ax.legend()
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('stress_test_results.png', dpi=300, bbox_inches='tight')
plt.show()
print("Stress test visualisation saved as 'stress_test_results.png'")

# ----------------------------------------------------------------
# PART E: BUSINESS IMPACT AND ROI
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Business Impact and ROI Calculation")
print("-"*60)

# Assumptions (simulated)
current_default_rate = 0.06  # 6% current default rate
new_default_rate = df['default'].mean()  # Model-based default rate
reduction_in_defaults = current_default_rate - new_default_rate

# Portfolio size
portfolio_size = total_exposure

# Benefits
reduced_losses = portfolio_size * reduction_in_defaults
cost_savings = 2_000_000  # $2M annual operational savings
revenue_uplift = 3_000_000  # $3M additional revenue from faster approvals

# Investment
total_investment = 5_000_000  # $5M project cost

# Annual benefits
annual_benefits = reduced_losses + cost_savings + revenue_uplift

# ROI over 3 years
roi_3yr = (annual_benefits * 3 - total_investment) / total_investment * 100

print(f"Business Impact Summary:")
print(f"  Current Default Rate: {current_default_rate*100:.2f}%")
print(f"  New Default Rate: {new_default_rate*100:.2f}%")
print(f"  Reduction in Defaults: {reduction_in_defaults*100:.2f}%")
print(f"  Portfolio Size: ${portfolio_size:,.2f}")
print(f"  Reduced Losses (Annual): ${reduced_losses:,.2f}")
print(f"  Cost Savings (Annual): ${cost_savings:,.2f}")
print(f"  Revenue Uplift (Annual): ${revenue_uplift:,.2f}")
print(f"  Total Annual Benefits: ${annual_benefits:,.2f}")
print(f"  Project Investment: ${total_investment:,.2f}")
print(f"  3-Year ROI: {roi_3yr:.1f}%")

# ----------------------------------------------------------------
# PART F: EXECUTIVE SUMMARY
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Executive Summary for Stakeholders")
print("-"*60)

executive_summary = f"""
--- EXECUTIVE SUMMARY: DIGITAL LENDING TRANSFORMATION ---

1. PROJECT SUMMARY
   Atlantic Bank's digital lending transformation project has successfully 
   implemented an AI-powered underwriting platform for personal loans.

2. MODEL PERFORMANCE
   - Best Model: XGBoost (tuned)
   - AUC: {auc_best:.3f}
   - KS Statistic: {ks_best:.3f}
   - Calibration: Within acceptable limits

3. PORTFOLIO RISK
   - Total Exposure: ${total_exposure:,.0f}
   - Average PD: {avg_pd*100:.2f}%
   - Expected Loss: ${total_el:,.0f}
   - Economic Capital (99.9%): ${total_ul:,.0f}

4. STRESS TEST RESULTS
   - Adverse Scenario: EL increases by {stress_df[stress_df['Scenario']=='Adverse']['EL Increase %'].values[0]:.1f}%
   - Severely Adverse: EL increases by {stress_df[stress_df['Scenario']=='Severely Adverse']['EL Increase %'].values[0]:.1f}%

5. BUSINESS IMPACT
   - Default Rate Reduction: {reduction_in_defaults*100:.2f}%
   - Annual Benefits: ${annual_benefits:,.0f}
   - 3-Year ROI: {roi_3yr:.1f}%

6. RECOMMENDATIONS
   - Deploy the XGBoost model for internal risk assessment.
   - Use Logistic Regression for regulatory submissions.
   - Implement SHAP for model explainability.
   - Continue monitoring for drift and model performance.

7. NEXT STEPS
   - Phase 2: Commercial loan expansion
   - Phase 3: Integration with other lending products
"""

print(executive_summary)

# ----------------------------------------------------------------
# PART G: SAVE RESULTS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART G: Saving Results")
print("-"*60)

# Save risk results
df[['application_id', 'pd', 'lgd', 'ead', 'el', 'ul', 'default']].to_csv('risk_results.csv', index=False)
stress_df.to_csv('stress_results.csv', index=False)

print("Risk results saved as 'risk_results.csv'")
print("Stress results saved as 'stress_results.csv'")
print("Executive summary generated.")

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

SECTION 4: SUMMARY FOR THE DATA PRACTITIONER

  • Expected Loss (EL) = PD × LGD × EAD – calculated for the entire portfolio.

  • Economic Capital = Unexpected Loss (UL) using the Vasicek model at 99.9% confidence.

  • Stress testing shows the portfolio’s resilience under adverse scenarios.

  • Business impact includes reduced defaults, cost savings, and revenue uplift.

  • ROI of 3-year project is positive, justifying the investment.

  • Executive summary communicates key findings to stakeholders.


SECTION 5: RECOMMENDED NEXT STEPS

  1. Review the risk analytics and stress test results.

  2. Prepare for Lesson 5: Model Deployment and Monitoring.

  3. Consider additional scenarios for stress testing.

  4. Explore the use of SHAP for model explainability.