SECTION 1: LEARNING OBJECTIVES

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

  • Understand the three pillars of credit risk – Probability of Default (PD), Loss Given Default (LGD), and Exposure at Default (EAD).

  • Explain the role of PD, LGD, and EAD in calculating Expected Loss (EL) and regulatory capital under Basel III.

  • Apply the IRB (Internal Ratings-Based) approach to estimate PD, LGD, and EAD.

  • Understand the IFRS 9 and CECL frameworks for expected credit loss (ECL) provisioning.

  • Distinguish between the 12-month and lifetime ECL under IFRS 9.

  • Apply the Vasicek model for calculating unexpected loss and economic capital.

  • Implement PD estimation using logistic regression and survival analysis.

  • Estimate LGD and EAD using statistical techniques and business rules.

  • Use Python to calculate Expected Loss and perform sensitivity analysis.

  • Understand the business and regulatory implications of credit risk modelling.


SECTION 2: THE THREE COMPONENTS OF CREDIT RISK

2.1 Definitions
 
 
Component Symbol Definition Unit
Probability of Default PD The likelihood that a borrower will default within a given time horizon (usually 1 year). Percentage
Loss Given Default LGD The proportion of exposure that will be lost if default occurs (after recoveries). Percentage
Exposure at Default EAD The total exposure (outstanding balance + undrawn commitments) at the time of default. Currency (e.g., $)
2.2 Expected Loss (EL)

EL=PD×LGD×EAD

This is the average loss expected over the credit cycle.

Example: A loan with:

  • PD = 2%

  • LGD = 45%

  • EAD = $100,000

EL = 0.02 × 0.45 × 100,000 = $900

2.3 Unexpected Loss (UL) – Economic Capital

Unexpected loss is the volatility around the expected loss – the amount of capital needed to cover losses at a high confidence level (e.g., 99.9%).

The Vasicek model (used in Basel II/III) relates UL to PD, LGD, and the asset correlation ρ:

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

where Φ is the standard normal CDF.

Regulatory capital formula (Basel III):

K=LGD×[Φ(Φ−1(PD)+ρΦ−1(0.999)1−ρ)−PD]×EAD


SECTION 3: PD ESTIMATION

3.1 Methods for Estimating PD
 
 
Method Description Pros / Cons
Historical Default Rates Use historical default frequencies by rating grade. Simple but backward-looking.
Logistic Regression Model default as a function of borrower characteristics. Industry standard; interpretable.
Survival Analysis (Cox Proportional Hazards) Model time to default; handles censoring. More sophisticated; good for time-dependent data.
Machine Learning XGBoost, Random Forest for PD prediction. Higher accuracy; less interpretable.
Merton Model (Structural) Option-theoretic approach using equity volatility. Used for public companies.
3.2 Logistic Regression for PD Estimation

We use the same framework as in Module 4, Lesson 1. The output is the probability of default (PD).

Key considerations:

  • PD must be monotonic with respect to risk drivers (e.g., higher DTI → higher PD).

  • PD estimates must be validated using backtesting and calibration.

  • For regulatory purposes, PD must be “through-the-cycle” (TTC) or “point-in-time” (PIT), depending on the model purpose.

3.3 PD Calibration

PD estimates from models must be calibrated to historical default rates:

PDcalibrated=PDmodel×Historical Default RateModel Average PD

Example: If the model predicts an average PD of 2.5% but the historical default rate is 2.0%, we scale all PDs by 2.0/2.5 = 0.8.


SECTION 4: LGD ESTIMATION

4.1 Definition and Components

LGD = 1 – Recovery Rate (RR)

Components affecting LGD:

  • Seniority: Secured loans have lower LGD.

  • Collateral: Higher collateral coverage → lower LGD.

  • Recovery period: Longer recovery → higher costs → higher LGD.

  • Economic conditions: LGD is higher during recessions.

  • Jurisdiction: Legal environment affects recovery.

4.2 Estimating LGD
 
 
Method Description Use Case
Workout LGD Track losses on defaulted loans through the recovery process. Most common for corporate and retail.
Market LGD Use market prices of distressed debt. Used for liquid securities.
Regression Models Predict LGD using borrower and loan characteristics. Helps differentiate LGD across segments.
Theoretical Models Use structural models (e.g., Leland-Toft). Rarely used in practice.

Workout LGD calculation:

LGD=1−Total Recoveries−Workout CostsEAD

Regulatory requirements:

  • LGD must be estimated under downturn conditions (Basel III).

  • LGD estimates must be validated and recalibrated periodically.

4.3 LGD in Practice
 
 
Loan Type Typical LGD Range
Secured Residential Mortgage 20-40%
Secured Auto Loan 30-50%
Unsecured Personal Loan 60-80%
Corporate Senior Secured 35-55%
Corporate Unsecured 60-80%
Sovereign 10-30%

SECTION 5: EAD ESTIMATION

5.1 Definition

EAD is the exposure amount at the time of default. For term loans, it is usually the outstanding balance. For revolving facilities (credit cards, lines of credit), it includes:

  • Drawn amount (current balance)

  • Undrawn commitment × Credit Conversion Factor (CCF)

EAD=Drawn+Undrawn×CCF

5.2 Credit Conversion Factors (CCF)
 
 
Facility Type CCF
Undrawn commitments (general) 75%
Revolving retail (credit cards) 75%
Trade finance 20%
Commercial letters of credit 20%

Regulatory (Basel) CCFs are standardised for the Standardised Approach. Under the IRB approach, banks may estimate their own CCFs.

5.3 EAD Estimation Methods
 
 
Method Description
Current Balance Use the outstanding balance at default (for term loans).
Drawdown Modelling Model the drawdown behaviour of borrowers before default.
Regulatory CCF Use prescribed CCFs (simpler but less accurate).

Drawdown modelling example:
For credit cards, drawdowns increase before default as borrowers use available credit. This is captured by:

EAD=Balancet−1+Drawdowndefault


SECTION 6: IFRS 9 AND CECL – EXPECTED CREDIT LOSS (ECL)

6.1 Overview

Both IFRS 9 (International) and CECL (US – Current Expected Credit Loss) require banks to recognise expected credit losses upfront, rather than waiting for a loss event.

Key difference from incurred loss models: Under the old model (IAS 39), losses were recognised when they became probable. Under IFRS 9/CECL, losses are expected over the life of the asset.

6.2 IFRS 9 – Three-Stage Approach
 
 
Stage Definition Impairment Interest Recognition
Stage 1 No significant increase in credit risk (SICR) since origination. 12-month ECL Gross carrying amount
Stage 2 Significant increase in credit risk (SICR) since origination. Lifetime ECL Gross carrying amount
Stage 3 Credit-impaired (already in default). Lifetime ECL Net carrying amount (net of ECL)
6.3 ECL Calculation

ECL=PD12m×LGD×EAD(Stage 1)

\text{ECL} = \text{PD}_{lifetime} \times \text{LGD} \times \text{EAD} \quad \text{(Stages 2 & 3)}

Key difference: PD is over 12 months vs. over the remaining lifetime of the asset (which may be >12 months).

6.4 Discounting

ECL must be discounted at the effective interest rate (EIR) of the asset.

ECLdiscounted=∑t=1TECLt(1+EIR)t


SECTION 7: IMPLEMENTATION IN PYTHON

python
# ===================================================================
# MODULE 5, LESSON 4: CREDIT RISK MODELLING – PD, LGD, EAD
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from scipy.stats import norm
import warnings
warnings.filterwarnings('ignore')

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

print("="*70)
print("CREDIT RISK MODELLING – PD, LGD, EAD AND IFRS 9")
print("="*70)

# ----------------------------------------------------------------
# PART A: GENERATE LOAN PORTFOLIO DATA
# ----------------------------------------------------------------

n_loans = 10000

# Borrower characteristics
income = np.random.gamma(4, 20, n_loans) + 30  # $000s
dti = np.random.beta(2, 4, n_loans) * 50
credit_score = np.random.normal(700, 50, n_loans).clip(550, 850)
loan_amount = np.random.gamma(4, 40, n_loans) + 30  # $000s
term = np.random.choice([12, 24, 36, 48, 60, 72], n_loans, p=[0.1, 0.1, 0.2, 0.2, 0.2, 0.2])

# Collateral type (0 = unsecured, 1 = secured)
collateral = np.random.binomial(1, 0.6, n_loans)

# Generate PD using a logistic model
log_odds_pd = -4.0 + 0.05*dti - 0.006*credit_score + 0.005*(loan_amount/10) - 0.3*collateral
pd_true = 1/(1+np.exp(-log_odds_pd))
default = np.random.binomial(1, pd_true)

# Generate LGD (depends on collateral, recovery environment, and economic conditions)
# LGD = 1 - Recovery Rate
lgd_base = np.where(collateral == 1, 0.35, 0.65)  # secured lower LGD
lgd_noise = np.random.normal(0, 0.1, n_loans).clip(-0.15, 0.15)
lgd = (lgd_base + lgd_noise).clip(0.05, 0.95)

# EAD = loan amount + undrawn (if revolving)
ead = loan_amount  # For term loans, EAD = loan amount

# Create DataFrame
df = pd.DataFrame({
    'income': income,
    'dti': dti,
    'credit_score': credit_score,
    'loan_amount': loan_amount,
    'term': term,
    'collateral': collateral,
    'default': default,
    'lgd': lgd,
    'ead': ead,
    'pd_true': pd_true
})

print("Portfolio Summary:")
print(f"  Total Loans: {len(df):,}")
print(f"  Default Rate: {df['default'].mean():.2%}")
print(f"  Average PD: {df['pd_true'].mean():.2%}")
print(f"  Average LGD: {df['lgd'].mean():.2%}")
print(f"  Average EAD: ${df['ead'].mean():,.2f}")

# ----------------------------------------------------------------
# PART B: PD ESTIMATION USING LOGISTIC REGRESSION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: PD Estimation (Logistic Regression)")
print("-"*60)

# Train-test split
X = df[['income', 'dti', 'credit_score', 'loan_amount', 'term', 'collateral']]
y = df['default']
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# Standardise
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# Logistic Regression
model_pd = LogisticRegression(random_state=42)
model_pd.fit(X_train_scaled, y_train)

# Predict PDs
pd_pred_train = model_pd.predict_proba(X_train_scaled)[:, 1]
pd_pred_test = model_pd.predict_proba(X_test_scaled)[:, 1]

# Evaluate
auc_pd = roc_auc_score(y_test, pd_pred_test)
print(f"PD Model AUC: {auc_pd:.4f}")

# Feature coefficients
coef_df = pd.DataFrame({
    'Feature': X.columns,
    'Coefficient': model_pd.coef_[0]
}).sort_values('Coefficient', ascending=False)
print("\nPD Model Coefficients:")
print(coef_df.to_string(index=False))

print("\nInterpretation:")
print("  • DTI and loan amount increase PD (positive coefficients).")
print("  • Credit score, income, and collateral decrease PD (negative coefficients).")

# ----------------------------------------------------------------
# PART C: LGD ESTIMATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: LGD Estimation")
print("-"*60)

# For demonstration, we use the true LGD (in practice, estimated from recovery data)

# LGD by collateral type
lgd_by_collateral = df.groupby('collateral')['lgd'].agg(['mean', 'std', 'count'])
print("\nLGD by Collateral Type:")
print(lgd_by_collateral)

# LGD segmentation
df['lgd_segment'] = pd.cut(df['lgd'], bins=[0, 0.2, 0.4, 0.6, 0.8, 1], 
                           labels=['0-20%', '20-40%', '40-60%', '60-80%', '80-100%'])

fig, ax = plt.subplots(figsize=(10, 6))
lgd_counts = df['lgd_segment'].value_counts().sort_index()
ax.bar(lgd_counts.index, lgd_counts.values, color='blue', alpha=0.7)
ax.set_xlabel('LGD Segment')
ax.set_ylabel('Number of Loans')
ax.set_title('LGD Distribution', fontsize=12)
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('lgd_distribution.png', dpi=300)
plt.show()

# LGD Regression (predict LGD from features)
print("\nLGD Regression (OLS) – Predicting LGD from Borrower Characteristics")
from sklearn.linear_model import LinearRegression
model_lgd = LinearRegression()
model_lgd.fit(X_train_scaled, df.loc[X_train.index, 'lgd'])
lgd_pred = model_lgd.predict(X_test_scaled)

# Evaluate
from sklearn.metrics import mean_absolute_error, mean_squared_error
mae_lgd = mean_absolute_error(df.loc[X_test.index, 'lgd'], lgd_pred)
rmse_lgd = np.sqrt(mean_squared_error(df.loc[X_test.index, 'lgd'], lgd_pred))
print(f"LGD Model MAE: {mae_lgd:.4f}")
print(f"LGD Model RMSE: {rmse_lgd:.4f}")

# ----------------------------------------------------------------
# PART D: EAD ESTIMATION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: EAD Estimation")
print("-"*60)

# For term loans, EAD = loan amount
# For revolving, we would model drawdowns
print("For term loans, EAD = Outstanding Balance.")
print(f"Total EAD: ${df['ead'].sum():,.2f}")

# Simulate a revolving portfolio with CCF
revolving_indicator = np.random.binomial(1, 0.3, n_loans)
df['is_revolving'] = revolving_indicator.astype(bool)
df['undrawn'] = np.where(df['is_revolving'], df['ead'] * np.random.uniform(0.2, 0.8, n_loans), 0)
df['ccf'] = np.where(df['is_revolving'], np.random.uniform(0.3, 0.8, n_loans), 0.75)
df['ead_adjusted'] = df['ead'] + df['undrawn'] * df['ccf']

print("\nEAD for Revolving Facilities:")
print(df[df['is_revolving']][['ead', 'undrawn', 'ccf', 'ead_adjusted']].head(5).round(2))

# ----------------------------------------------------------------
# PART E: EXPECTED LOSS CALCULATION
# ----------------------------------------------------------------

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

# Calculate Expected Loss for each loan
df['pd'] = pd_pred_train if len(pd_pred_train) == len(df.loc[X_train.index]) else pd_pred_test
# For this demo, use the actual PD from the model on the training set
# We'll use the full dataset predictions for demonstration
df['pd_model'] = model_pd.predict_proba(scaler.transform(X))[:, 1]
df['el'] = df['pd_model'] * df['lgd'] * df['ead_adjusted']

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

# Aggregate by segment
el_by_collateral = df.groupby('collateral').agg({
    'el': 'sum',
    'ead_adjusted': 'sum',
    'pd_model': 'mean',
    'lgd': 'mean'
})
el_by_collateral['el_rate'] = el_by_collateral['el'] / el_by_collateral['ead_adjusted']
print("\nEL by Collateral Type:")
print(el_by_collateral.round(4))

# ----------------------------------------------------------------
# PART F: ECONOMIC CAPITAL – UNEXPECTED LOSS (VASICEK MODEL)
# -----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Economic Capital (Unexpected Loss)")
print("-"*60)

def vasicek_capital(pd, lgd, ead, rho=0.12, confidence=0.999):
    """
    Calculate regulatory capital using the Vasicek model.
    """
    z = norm.ppf(confidence)
    pd_adj = norm.cdf((norm.ppf(pd) + np.sqrt(rho) * z) / np.sqrt(1 - rho))
    capital = lgd * (pd_adj - pd) * ead
    return capital

# Apply to each loan
df['ul'] = vasicek_capital(df['pd_model'], df['lgd'], df['ead_adjusted'])
df['ul'] = np.maximum(df['ul'], 0)  # No negative capital

total_ul = df['ul'].sum()
print(f"Total Unexpected Loss (99.9% confidence): ${total_ul:,.2f}")
print(f"Economic Capital Ratio: {total_ul / df['ead_adjusted'].sum():.2%}")

# ----------------------------------------------------------------
# PART G: IFRS 9 – ECL CALCULATION (STAGE 1 vs STAGE 2)
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART G: IFRS 9 – ECL Calculation")
print("-"*60)

# Define SICR threshold (significant increase in credit risk)
sicr_threshold = 0.5  # PD increase of 50% or more from origination
df['pd_origination'] = df['pd_model'] * np.random.uniform(0.5, 0.9, n_loans)  # Simulated origination PD
df['pd_increase'] = (df['pd_model'] - df['pd_origination']) / df['pd_origination']
df['stage'] = 1  # Default Stage 1
df.loc[df['pd_increase'] > sicr_threshold, 'stage'] = 2  # Stage 2
df.loc[df['default'] == 1, 'stage'] = 3  # Stage 3 (defaulted)

# Lifetime PD for Stage 2/3 (approximation: multiply 12-month PD by average life)
avg_loan_life = 5  # years
df['pd_lifetime'] = df['pd_model'] * avg_loan_life
df['pd_lifetime'] = np.minimum(df['pd_lifetime'], 1)  # Cap at 1

# ECL calculation by stage
def calculate_ecl(row):
    if row['stage'] == 1:
        return row['pd_model'] * row['lgd'] * row['ead_adjusted']  # 12-month ECL
    else:  # Stages 2 and 3
        return row['pd_lifetime'] * row['lgd'] * row['ead_adjusted']  # Lifetime ECL

df['ecl_ifrs9'] = df.apply(calculate_ecl, axis=1)

print("\nIFRS 9 ECL by Stage:")
stage_summary = df.groupby('stage').agg({
    'ecl_ifrs9': 'sum',
    'ead_adjusted': 'sum',
    'pd_model': 'mean',
    'count': 'size'
})
stage_summary['ecl_rate'] = stage_summary['ecl_ifrs9'] / stage_summary['ead_adjusted']
print(stage_summary.round(4))

print(f"\nTotal IFRS 9 ECL Provision: ${df['ecl_ifrs9'].sum():,.2f}")

# Compare with CECL (lifetime ECL for all loans)
df['ecl_cecl'] = df['pd_lifetime'] * df['lgd'] * df['ead_adjusted']
print(f"Total CECL ECL Provision: ${df['ecl_cecl'].sum():,.2f}")
print(f"Difference (CECL - IFRS 9): ${(df['ecl_cecl'].sum() - df['ecl_ifrs9'].sum()):,.2f}")

# ----------------------------------------------------------------
# PART H: VISUALISATION AND BUSINESS INSIGHTS
# ----------------------------------------------------------------

fig, axes = plt.subplots(2, 2, figsize=(14, 12))

# PD Distribution
ax = axes[0, 0]
ax.hist(df['pd_model'], bins=50, alpha=0.7, edgecolor='black', color='blue')
ax.axvline(df['pd_model'].mean(), color='red', linestyle='--', label=f"Mean PD: {df['pd_model'].mean():.2%}")
ax.set_xlabel('Probability of Default (PD)')
ax.set_ylabel('Frequency')
ax.set_title('PD Distribution')
ax.legend()
ax.grid(True, alpha=0.3)

# EL by Stage
ax = axes[0, 1]
stage_el = df.groupby('stage')['ecl_ifrs9'].sum()
ax.bar(stage_el.index, stage_el.values, color=['green', 'orange', 'red'])
ax.set_xlabel('IFRS 9 Stage')
ax.set_ylabel('Total ECL ($)')
ax.set_title('ECL by Stage')
ax.grid(True, alpha=0.3)

# LGD vs PD Scatter
ax = axes[1, 0]
scatter = ax.scatter(df['pd_model'], df['lgd'], c=df['ead_adjusted'], cmap='viridis', alpha=0.5, s=10)
ax.set_xlabel('PD')
ax.set_ylabel('LGD')
ax.set_title('PD vs LGD (colored by EAD)')
plt.colorbar(scatter, ax=ax, label='EAD ($)')
ax.grid(True, alpha=0.3)

# EL vs EAD
ax = axes[1, 1]
ax.scatter(df['ead_adjusted'], df['el'], alpha=0.3, s=5, color='blue')
ax.set_xlabel('EAD ($)')
ax.set_ylabel('Expected Loss ($)')
ax.set_title('EL vs EAD')
ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('credit_risk_analysis.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART I: SUMMARY REPORT
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART I: CREDIT RISK SUMMARY REPORT")
print("="*70)

report = pd.DataFrame({
    'Metric': [
        'Number of Loans',
        'Total Exposure (EAD)',
        'Average PD',
        'Average LGD',
        'Expected Loss (EL)',
        'EL / EAD Ratio',
        'Unexpected Loss (UL)',
        'UL / EAD Ratio',
        'IFRS 9 ECL Provision',
        'CECL Provision',
        'Stage 1 Loans',
        'Stage 2 Loans',
        'Stage 3 Loans'
    ],
    'Value': [
        f"{len(df):,}",
        f"${df['ead_adjusted'].sum():,.2f}",
        f"{df['pd_model'].mean():.2%}",
        f"{df['lgd'].mean():.2%}",
        f"${df['el'].sum():,.2f}",
        f"{df['el'].sum() / df['ead_adjusted'].sum():.2%}",
        f"${df['ul'].sum():,.2f}",
        f"{df['ul'].sum() / df['ead_adjusted'].sum():.2%}",
        f"${df['ecl_ifrs9'].sum():,.2f}",
        f"${df['ecl_cecl'].sum():,.2f}",
        f"{(df['stage'] == 1).sum():,}",
        f"{(df['stage'] == 2).sum():,}",
        f"{(df['stage'] == 3).sum():,}"
    ]
})
print(report.to_string(index=False))

SECTION 8: REGULATORY REQUIREMENTS SUMMARY

 
 
Regulation Requirement Key Element
Basel III IRB approach for PD, LGD, EAD; capital calculation. Vasicek model; downturn LGD.
IFRS 9 Expected Credit Loss (ECL); three-stage approach. 12-month vs lifetime ECL.
CECL (US) Lifetime ECL for all loans (no staging). Full lifetime loss recognition.
SR 11-7 PD, LGD, EAD models must be validated. Independent model validation.
ECB / EBA Model validation and benchmarking. Stress testing of credit models.

SECTION 9: SUMMARY FOR THE DATA PRACTITIONER

  • PD, LGD, and EAD are the three pillars of credit risk modelling.

  • Expected Loss (EL) = PD × LGD × EAD – the average loss over the cycle.

  • Regulatory capital is based on Unexpected Loss (UL) – the 99.9% quantile of the loss distribution (Vasicek model).

  • IFRS 9 uses a three-stage approach: 12-month ECL (Stage 1) vs lifetime ECL (Stages 2 & 3).

  • CECL is simpler but more conservative – lifetime ECL for all loans.

  • PD models (logistic regression) must be well-calibrated and validated.

  • LGD and EAD are often estimated using historical data and business rules.

  • Credit risk modelling is heavily regulated – documentation and validation are essential.


SECTION 10: RECOMMENDED NEXT STEPS

  1. Apply PD, LGD, and EAD estimation to a real loan portfolio.

  2. Implement the Vasicek model for economic capital calculation.

  3. Learn about Credit Portfolio Models (e.g., CreditMetrics, KMV).

  4. Explore Counterparty Credit Risk (CVA, DVA, and Basel).

  5. Study the Transition Matrix approach for rating migrations.

  6. Prepare for the next lesson on Operational Risk and Basel III.


[END OF LESSON 4 – MODULE 5]