SECTION 1: LEARNING OBJECTIVES

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

  • Understand the end‑to‑end lifecycle of a machine learning model in a banking environment – from development to production deployment.

  • Distinguish between model development, validation, deployment, and monitoring phases.

  • Apply model serialisation and deployment strategies using tools like Pickle, Joblib, and ONNX.

  • Design a model monitoring framework that tracks performance, data drift, and concept drift.

  • Understand the regulatory requirements for model monitoring under SR 11‑7, including triggers for model revalidation.

  • Implement drift detection using Population Stability Index (PSI) and Characteristic Stability Index (CSI).

  • Evaluate model performance decay using time‑based performance tracking.

  • Understand the role of MLOps in ensuring model reliability, reproducibility, and governance.


SECTION 2: THE MODEL LIFECYCLE IN BANKING

A model in a production environment follows a structured lifecycle:

text
┌─────────────────────────────────────────────────────────────────┐
│                    MODEL LIFECYCLE (SR 11-7)                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. DEVELOPMENT                                                 │
│     ├── Data collection and preparation                        │
│     ├── Feature engineering                                    │
│     ├── Model training and tuning                              │
│     ├── Model selection and documentation                     │
│     └── Initial validation                                     │
│                                                                 │
│  2. VALIDATION (Independent Review)                            │
│     ├── Conceptual soundness                                   │
│     ├── Data quality assessment                               │
│     ├── Performance assessment (AUC, KS, etc.)                │
│     ├── Robustness testing (stress testing)                   │
│     └── Documentation review                                   │
│                                                                 │
│  3. DEPLOYMENT                                                 │
│     ├── Model packaging and serialisation                     │
│     ├── Integration with production systems                    │
│     ├── API development                                        │
│     └── User training                                          │
│                                                                 │
│  4. MONITORING (Ongoing)                                       │
│     ├── Performance tracking (AUC, KS over time)              │
│     ├── Data drift detection (PSI, CSI)                       │
│     ├── Concept drift detection                               │
│     ├── Alerting and reporting                                │
│     └── Trigger for revalidation                               │
│                                                                 │
│  5. REVALIDATION / RETRAINING                                  │
│     ├── Periodic revalidation (annually or as triggered)      │
│     ├── Model retraining (with new data)                      │
│     ├── Re-documentation                                      │
│     └── Re-deployment                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Key insight: Model development is only 20% of the effort. The remaining 80% is validation, deployment, and monitoring.


SECTION 3: MODEL SERIALISATION AND DEPLOYMENT

Before a model can be used in production, it must be serialised (saved to disk) and deployed (made accessible for predictions).

3.1 Serialisation Methods in Python
 
 
Method Advantages Disadvantages Use Case
Pickle Built‑in; simple; supports most objects. Security concerns; version compatibility issues. Quick prototyping, internal tools.
Joblib Optimised for large NumPy arrays. Less flexible than Pickle. Large models (Random Forest, XGBoost).
ONNX Cross‑platform; works across frameworks. Limited support for some models. Production deployments requiring interoperability.
MLflow Full lifecycle management; tracks metrics. Requires setup. Enterprise MLOps.
Cloud-specific (SageMaker, Vertex AI) Managed services; scalability. Vendor lock‑in. Cloud‑native deployments.
3.2 Deployment Strategies
 
 
Strategy Description Financial Example
Batch (Offline) Predictions generated periodically (daily/weekly). Monthly credit scoring for loan origination.
Real‑time (Online) Predictions generated on‑demand via API. Fraud detection during transactions.
Shadow Mode Model runs in parallel with existing system; outputs not used for decisions. Testing new fraud model before live deployment.
Canary Deployment Gradually roll out to a small percentage of traffic. Phased rollout of a new credit model.
A/B Testing Split traffic between old and new models to compare performance. Testing new marketing response model.
3.3 Regulatory Requirements for Deployment
  • Version control: Every model version must be tracked with its training data, hyperparameters, and performance metrics.

  • Testing: Must test the model in a staging environment with production‑like data before live deployment.

  • Rollback plan: Clear procedure to revert to the previous model if issues arise.

  • Documentation: Deployment logs, validation reports, and business impact assessments.


SECTION 4: MODEL MONITORING – THE CRITICAL SAFEGUARD

Once deployed, models must be continuously monitored for:

4.1 Performance Decay (Model Drift)

The model’s predictive accuracy may decline over time due to changes in the underlying relationships.

Metrics to track:

  • AUC / KS – measure discrimination power.

  • Gini Coefficient – alternative to KS.

  • Precision / Recall – business‑specific metrics.

  • Calibration – predicted vs. observed probabilities.

Triggers for revalidation:

  • AUC drops by > 5% over a quarter.

  • KS drops below the validation threshold.

  • Calibration is significantly off (Hosmer‑Lemeshow test fails).

4.2 Data Drift (Covariate Shift)

The distribution of input features changes over time. Even if the model is still good, it may make decisions based on a different data landscape.

Detection methods:

  • Population Stability Index (PSI): Measures the shift in the distribution of a single feature or the entire model score.

    • PSI < 0.1 → Stable

    • PSI 0.1‑0.25 → Moderate shift (investigate)

    • PSI > 0.25 → Significant shift (action required)

  • Characteristic Stability Index (CSI): Similar to PSI but focuses on feature‑by‑feature.

  • Kolmogorov‑Smirnov (KS) for features: Compares feature distributions across time periods.

Financial example:

  • A credit model trained on 2020 data may see a shift in income distribution after a post‑COVID recovery. The PSI triggers a review.

4.3 Concept Drift

The relationship between features and the target changes. This is more severe than data drift.

Example: The relationship between credit score and default may change during a recession (defaults increase even for high credit scores).

Detection:

  • Monitor performance drift (AUC decline) – this is the ultimate indicator of concept drift.

  • Use survival analysis to detect changes in hazard rates.


SECTION 5: IMPLEMENTATION IN PYTHON – MODEL MONITORING DASHBOARD

python
# ===================================================================
# MODULE 4, LESSON 6: MODEL MONITORING – DRIFT DETECTION
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.metrics import roc_auc_score, confusion_matrix
from sklearn.preprocessing import KBinsDiscretizer
import warnings
warnings.filterwarnings('ignore')

print("="*70)
print("MODEL MONITORING – DRIFT DETECTION AND PERFORMANCE TRACKING")
print("="*70)

# ----------------------------------------------------------------
# PART A: GENERATE SYNTHETIC MONITORING DATA
# ----------------------------------------------------------------

# Simulate model scores and actual outcomes over 12 quarters (3 years)
np.random.seed(42)
n_periods = 12
n_samples = 500

# Simulate 4 features (like income, DTI, credit_score, loan_amount)
features = ['income', 'dti', 'credit_score', 'loan_amount']

# Create baseline data (Period 0)
baseline_data = pd.DataFrame({
    'income': np.random.gamma(5, 15, n_samples) + 20,
    'dti': np.random.beta(2, 5, n_samples) * 60,
    'credit_score': np.random.normal(700, 50, n_samples).clip(550, 850),
    'loan_amount': np.random.gamma(4, 50, n_samples) + 50
})

# Simulate a model score (0-1) based on the features
log_odds = -4.5 + 0.04 * baseline_data['dti'] - 0.005 * baseline_data['credit_score']
baseline_score = 1 / (1 + np.exp(-log_odds))
baseline_score = np.clip(baseline_score, 0.01, 0.99)
baseline_default = np.random.binomial(1, baseline_score)

# Store baseline
baseline_df = baseline_data.copy()
baseline_df['score'] = baseline_score
baseline_df['default'] = baseline_default

# Simulate drift in features and performance over time
periodic_dfs = []

for period in range(1, n_periods + 1):
    # Simulate drift: income shifts up, credit_score shifts down slightly, DTI increases over time
    drift_factor = period * 0.02  # gradually increasing drift
    
    # Add drift to features
    period_data = baseline_data.copy()
    period_data['income'] = period_data['income'] * (1 + 0.005 * period)
    period_data['dti'] = period_data['dti'] * (1 + 0.01 * period)
    period_data['credit_score'] = period_data['credit_score'] - 2 * period
    period_data['loan_amount'] = period_data['loan_amount'] * (1 + 0.005 * period)
    
    # Score (model still uses original coefficients, leading to performance decay)
    log_odds_drift = -4.5 + 0.04 * period_data['dti'] - 0.005 * period_data['credit_score']
    score = 1 / (1 + np.exp(-log_odds_drift))
    score = np.clip(score, 0.01, 0.99)
    
    # Actual outcome may have concept drift (relationship changes)
    # In later periods, defaults become more likely for a given score
    default_prob = score * (1 + 0.05 * period / 4)  # gradual increase in default rate
    default_prob = np.clip(default_prob, 0.01, 0.95)
    default = np.random.binomial(1, default_prob)
    
    period_df = period_data.copy()
    period_df['score'] = score
    period_df['default'] = default
    period_df['period'] = period
    periodic_dfs.append(period_df)

all_data = pd.concat([baseline_df.assign(period=0), *periodic_dfs], ignore_index=True)

# Compute period-level metrics
period_metrics = []
for period in sorted(all_data['period'].unique()):
    data = all_data[all_data['period'] == period]
    auc = roc_auc_score(data['default'], data['score'])
    default_rate = data['default'].mean()
    period_metrics.append({
        'period': period,
        'auc': auc,
        'default_rate': default_rate,
        'n_samples': len(data)
    })

metrics_df = pd.DataFrame(period_metrics)
print("\nModel Performance Over Time:")
print(metrics_df.to_string(index=False))

# ----------------------------------------------------------------
# PART B: POPULATION STABILITY INDEX (PSI) – FEATURE DRIFT
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: POPULATION STABILITY INDEX (PSI) – Feature Drift")
print("-"*60)

def calculate_psi(expected, actual, bins=10):
    """
    Calculate Population Stability Index between expected and actual distributions.
    PSI = sum((actual_i - expected_i) * ln(actual_i / expected_i))
    """
    # Bin the data
    expected_bins = np.percentile(expected, np.linspace(0, 100, bins+1))
    expected_counts = np.histogram(expected, bins=expected_bins)[0]
    actual_counts = np.histogram(actual, bins=expected_bins)[0]
    
    # Convert to proportions
    expected_prop = expected_counts / len(expected)
    actual_prop = actual_counts / len(actual)
    
    # Add small epsilon to avoid division by zero
    expected_prop = np.clip(expected_prop, 1e-10, 1)
    actual_prop = np.clip(actual_prop, 1e-10, 1)
    
    # Calculate PSI
    psi = np.sum((actual_prop - expected_prop) * np.log(actual_prop / expected_prop))
    return psi

# Baseline (period 0) as reference
baseline_scores = all_data[all_data['period'] == 0]['score']

# Calculate PSI for each period
psi_results = []
for period in sorted(all_data['period'].unique())[1:]:
    actual_scores = all_data[all_data['period'] == period]['score']
    psi = calculate_psi(baseline_scores, actual_scores)
    psi_results.append({'period': period, 'psi': psi})
psi_df = pd.DataFrame(psi_results)

print("\nPSI by Period (Score Distribution):")
print(psi_df.to_string(index=False))

# Interpret PSI
for _, row in psi_df.iterrows():
    psi_val = row['psi']
    if psi_val < 0.1:
        status = "STABLE"
        action = "No action required."
    elif psi_val < 0.25:
        status = "MODERATE"
        action = "Investigate feature distributions."
    else:
        status = "SIGNIFICANT"
        action = "Revalidate model or retrain."
    print(f"Period {row['period']}: PSI={psi_val:.4f}{status}{action}")

# ----------------------------------------------------------------
# PART C: FEATURE-LEVEL DRIFT DETECTION
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Feature-Level Drift (PSI per feature)")
print("-"*60)

# Baseline data (period 0)
baseline_full = all_data[all_data['period'] == 0][['income', 'dti', 'credit_score', 'loan_amount']]

# Recent data (period 11)
recent_full = all_data[all_data['period'] == 11][['income', 'dti', 'credit_score', 'loan_amount']]

feature_psi = {}
for feature in baseline_full.columns:
    psi = calculate_psi(baseline_full[feature], recent_full[feature])
    feature_psi[feature] = psi

feature_psi_df = pd.DataFrame(list(feature_psi.items()), columns=['Feature', 'PSI'])
print(feature_psi_df.to_string(index=False))

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

for i, feature in enumerate(['income', 'dti', 'credit_score', 'loan_amount']):
    ax = axes[i // 2, i % 2]
    ax.hist(baseline_full[feature], bins=30, alpha=0.5, label='Baseline', edgecolor='black')
    ax.hist(recent_full[feature], bins=30, alpha=0.5, label='Recent', edgecolor='black')
    ax.set_title(f'{feature} (PSI={feature_psi[feature]:.4f})')
    ax.set_xlabel(feature)
    ax.set_ylabel('Frequency')
    ax.legend()
    ax.grid(True, alpha=0.3)

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

# ----------------------------------------------------------------
# PART D: PERFORMANCE MONITORING DASHBOARD
# -----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: Performance Monitoring Dashboard")
print("-"*60)

# Create dashboard visualisation
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 1. AUC over time
ax = axes[0, 0]
ax.plot(metrics_df['period'], metrics_df['auc'], 'bo-', linewidth=2, markersize=8)
ax.axhline(metrics_df['auc'].iloc[0], color='green', linestyle='--', 
           label=f'Baseline AUC: {metrics_df["auc"].iloc[0]:.3f}')
ax.axhline(metrics_df['auc'].iloc[0] - 0.05, color='red', linestyle='--', 
           label='Warning threshold (AUC - 5%)')
ax.set_xlabel('Time Period (Quarter)')
ax.set_ylabel('AUC')
ax.set_title('Model Performance (AUC) Over Time')
ax.legend()
ax.grid(True, alpha=0.3)

# 2. Default Rate over time
ax = axes[0, 1]
ax.bar(metrics_df['period'], metrics_df['default_rate'], color='orange', alpha=0.7)
ax.set_xlabel('Time Period (Quarter)')
ax.set_ylabel('Default Rate')
ax.set_title('Actual Default Rate Over Time')
ax.grid(True, alpha=0.3)

# 3. PSI over time
ax = axes[1, 0]
ax.plot(psi_df['period'], psi_df['psi'], 'ro-', linewidth=2, markersize=8)
ax.axhline(0.1, color='green', linestyle='--', label='Stable (PSI < 0.1)')
ax.axhline(0.25, color='orange', linestyle='--', label='Moderate (PSI 0.1-0.25)')
ax.axhline(0.25, color='red', linestyle='--', label='Significant (PSI > 0.25)')
ax.fill_between(psi_df['period'], 0, psi_df['psi'], alpha=0.3, color='red', 
                where=(psi_df['psi'] > 0.25))
ax.fill_between(psi_df['period'], 0, psi_df['psi'], alpha=0.3, color='orange', 
                where=(psi_df['psi'] > 0.1) & (psi_df['psi'] <= 0.25))
ax.set_xlabel('Time Period (Quarter)')
ax.set_ylabel('PSI')
ax.set_title('Population Stability Index (Score Drift)')
ax.legend()
ax.grid(True, alpha=0.3)

# 4. Confusion Matrix for current period
ax = axes[1, 1]
current_data = all_data[all_data['period'] == 11]
threshold = 0.5
y_pred = (current_data['score'] >= threshold).astype(int)
cm = confusion_matrix(current_data['default'], y_pred)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Pred No Default', 'Pred Default'],
            yticklabels=['Actual No Default', 'Actual Default'],
            ax=ax)
ax.set_title(f'Confusion Matrix (Period 11, Threshold={threshold})')

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

# ----------------------------------------------------------------
# PART E: ALERTING AND ACTION TRIGGERS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: Alerting System – Trigger Conditions")
print("-"*60)

# Define alert thresholds
ALERT_CONFIG = {
    'auc_drop': 0.05,        # 5% drop in AUC
    'psi_threshold': 0.25,   # Significant drift
    'psi_warning': 0.10,     # Moderate drift
    'default_rate_change': 0.10  # 10% change in default rate
}

# Check each period for alerts
alerts = []
for period in sorted(all_data['period'].unique())[1:]:
    period_metrics = metrics_df[metrics_df['period'] == period].iloc[0]
    baseline_metrics = metrics_df[metrics_df['period'] == 0].iloc[0]
    
    # AUC drop
    auc_drop = baseline_metrics['auc'] - period_metrics['auc']
    if auc_drop > ALERT_CONFIG['auc_drop']:
        alerts.append({
            'period': period,
            'type': 'PERFORMANCE_DECAY',
            'detail': f'AUC dropped from {baseline_metrics["auc"]:.3f} to {period_metrics["auc"]:.3f} (drop: {auc_drop:.3f})'
        })
    
    # PSI
    psi_val = psi_df[psi_df['period'] == period]['psi'].iloc[0]
    if psi_val > ALERT_CONFIG['psi_threshold']:
        alerts.append({
            'period': period,
            'type': 'SIGNIFICANT_DRIFT',
            'detail': f'PSI = {psi_val:.3f} (threshold: {ALERT_CONFIG["psi_threshold"]})'
        })
    elif psi_val > ALERT_CONFIG['psi_warning']:
        alerts.append({
            'period': period,
            'type': 'MODERATE_DRIFT',
            'detail': f'PSI = {psi_val:.3f} (warning threshold: {ALERT_CONFIG["psi_warning"]})'
        })
    
    # Default rate change
    default_change = abs(period_metrics['default_rate'] - baseline_metrics['default_rate']) / baseline_metrics['default_rate']
    if default_change > ALERT_CONFIG['default_rate_change']:
        alerts.append({
            'period': period,
            'type': 'DEFAULT_RATE_SHIFT',
            'detail': f'Default rate changed from {baseline_metrics["default_rate"]:.3f} to {period_metrics["default_rate"]:.3f} ({default_change:.1%})'
        })

# Display alerts
if alerts:
    alert_df = pd.DataFrame(alerts)
    print("\nALERTS TRIGGERED:")
    print(alert_df.to_string(index=False))
else:
    print("\nNo alerts triggered. Model is stable.")

print("\nRecommended Actions:")
print("  • PERFORMANCE_DECAY → Revalidate model; consider retraining.")
print("  • SIGNIFICANT_DRIFT → Investigate data sources; update preprocessing.")
print("  • MODERATE_DRIFT → Monitor closely; plan for upcoming revalidation.")
print("  • DEFAULT_RATE_SHIFT → Review business conditions; adjust risk appetite.")

# ----------------------------------------------------------------
# PART F: MODEL LIFECYCLE DOCUMENTATION
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART F: MODEL LIFECYCLE DOCUMENTATION TEMPLATE")
print("="*70)

documentation_template = """
--- MODEL DOCUMENTATION (SR 11-7 COMPLIANCE) ---

1. MODEL IDENTIFICATION
   - Model Name: Credit Default Predictor v3.2
   - Model Type: XGBoost Classifier
   - Development Date: 2025-12-01
   - Deployment Date: 2026-02-15
   - Owner: Credit Risk Analytics Team

2. DATA & FEATURES
   - Training Data: Loans originated 2022-2024 (48 months)
   - Features Used: income, age, dti, credit_score, loan_amount
   - Data Source: Internal Core Banking System (DWH)

3. MODEL PERFORMANCE (At Deployment)
   - AUC: 0.84
   - KS: 0.42
   - Gini: 0.68
   - Calibration: Hosmer-Lemeshow p-value = 0.12

4. PERFORMANCE MONITORING
   - Current AUC: {current_auc:.3f}
   - AUC Drop: {auc_drop:.3f}
   - Current PSI: {current_psi:.3f}
   - Status: {status}

5. NEXT STEPS
   - Next Validation Date: {next_validation}
   - Retraining Trigger: AUC < 0.75 OR PSI > 0.25
   - Action Plan: {action_plan}
"""

# Fill template with latest data
latest_period = 11
current_auc = metrics_df[metrics_df['period'] == latest_period]['auc'].iloc[0]
auc_drop = metrics_df[metrics_df['period'] == 0]['auc'].iloc[0] - current_auc
current_psi = psi_df[psi_df['period'] == latest_period]['psi'].iloc[0] if latest_period in psi_df['period'].values else 0

if current_psi < 0.1 and auc_drop < 0.05:
    status = "STABLE – No action required"
    action_plan = "Continue monitoring; quarterly review"
elif current_psi < 0.25 or auc_drop < 0.05:
    status = "MODERATE – Investigate"
    action_plan = "Perform detailed feature analysis; plan validation in 3 months"
else:
    status = "SIGNIFICANT – Action required"
    action_plan = "Immediate model validation; consider retraining"

print(documentation_template.format(
    current_auc=current_auc,
    auc_drop=auc_drop,
    current_psi=current_psi,
    status=status,
    next_validation="2026-09-15" if status != "SIGNIFICANT" else "2026-06-15",
    action_plan=action_plan
))

SECTION 6: REGULATORY REQUIREMENTS FOR MODEL MONITORING

 
 
Regulation Requirement Implementation
SR 11-7 Models must be monitored for performance and drift; triggers for revalidation. Monthly performance tracking; PSI/CSI reports; quarterly validation reports.
ECOA / Fair Lending Monitor for disparate impact over time. Track model outcomes across protected groups; PSI by group.
IFRS 9 / CECL Expected losses must reflect current conditions. Incorporate recent default rates into ECL calculations; adjust if drift detected.
GDPR Right to explanation; regular auditing. Maintain documentation of model changes; SHAP/LIME explanations.
Basel III Capital models must be validated and monitored. Regular performance reports; independent review.

SECTION 7: SUMMARY FOR THE DATA PRACTITIONER

  • Model deployment is not the end – it’s the beginning of the monitoring lifecycle.

  • Performance decay, data drift, and concept drift are the three primary threats to model reliability.

  • PSI is the industry‑standard metric for detecting data drift (PSI < 0.1 = stable).

  • AUC/KS must be tracked over time to detect performance decay.

  • Alerts should trigger revalidation or retraining based on predefined thresholds.

  • Documentation is essential for regulatory compliance and business continuity.

  • MLOps tools (MLflow, Seldon, Kubeflow) automate many of these monitoring tasks in enterprise environments.


SECTION 8: RECOMMENDED NEXT STEPS

  1. Set up a monitoring framework for a model you’ve built (log metrics, PSI, feature distributions).

  2. Learn about MLflow for tracking model versions and metrics.

  3. Explore Seldon Core or Kubeflow for production model deployment.

  4. Understand Drift Detection with Deep Learning (for more complex models).

  5. Prepare for the next module on Advanced Topics: Deep Learning and NLP in Finance.


[END OF LESSON 6 – MODULE 4]