SECTION 1: LEARNING OBJECTIVES

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

  • Understand the regulatory imperative for explainable AI in banking – SR 11-7, EU AI Act, GDPR “right to explanation,” and Fair Lending requirements.

  • Distinguish between global and local interpretability and understand when each is appropriate.

  • Apply SHAP (SHapley Additive exPlanations) to explain individual predictions of any machine learning model.

  • Apply LIME (Local Interpretable Model-agnostic Explanations) as an alternative to SHAP for local explanations.

  • Interpret feature importance from tree-based models (Random Forest, XGBoost) and linear models.

  • Generate partial dependence plots (PDP) and individual conditional expectation (ICE) plots to visualise feature effects.

  • Understand the concept of counterfactual explanations and their application in credit decisions.

  • Implement a comprehensive XAI dashboard for a credit scoring model, including SHAP, LIME, and PDP.

  • Develop a framework for documenting model explanations in a regulatory-compliant manner.


SECTION 2: WHY EXPLAINABLE AI IN FINANCE?

2.1 The Regulatory Imperative
 
 
Regulation Requirement Implication for AI
SR 11-7 (US) Model validation must include conceptual soundness and ongoing monitoring. Black-box models require additional scrutiny and explanation.
EU AI Act High-risk AI systems must be transparent and explainable. Credit scoring, risk assessment, and recruitment are high-risk.
GDPR (Art. 22) Right to explanation for automated decisions. Individuals have the right to understand how decisions affecting them were made.
Fair Lending (ECOA, FHA) Lending decisions must not discriminate. Models must be auditable for disparate impact.
Basel III Internal models must be validated and documented. Model risk management includes explainability.
2.2 Why Black-Box Models Are Problematic
  • Trust: Stakeholders (customers, regulators, management) need to trust model decisions.

  • Validation: Regulators require that models be validated, which is difficult with black-box models.

  • Bias detection: Disparate impact can only be detected if we understand model behaviour.

  • Error diagnosis: If a model makes a mistake, we need to understand why.

  • Business alignment: Model decisions must align with business logic and domain knowledge.

2.3 Types of Interpretability
 
 
Type Definition Example
Global Interpretability Understanding the overall model behaviour. Feature importance, PDPs show how features affect predictions on average.
Local Interpretability Understanding why a specific prediction was made. SHAP/LIME for an individual loan application.
Model-Specific Techniques that work only for certain models. Feature importance from Random Forest, coefficients from linear models.
Model-Agnostic Techniques that work for any model. SHAP, LIME, PDP (apply to any black-box model).
Post-hoc Explanations after the model is trained. Most XAI techniques (SHAP, LIME).
Ante-hoc Interpretable by design. Linear regression, decision trees (intrinsically interpretable).

SECTION 3: SHAP – SHAPLEY ADDITIVE EXPLANATIONS

SHAP is based on game theory’s Shapley values, which fairly distribute the “payout” (prediction) among the features.

3.1 The Shapley Value

In cooperative game theory, the Shapley value assigns a contribution to each player:

ϕi=∑S⊆F∖{i}∣S∣!(∣F∣−∣S∣−1)!∣F∣![v(S∪{i})−v(S)]

where:

  • F = set of all features

  • S = subset of features

  • v(S) = model prediction using only features in S

Interpretation: ϕi is the average contribution of feature i across all possible feature subsets.

Properties of Shapley values:

  • Efficiency: ∑ϕi=f(x)−E[f(x)] (the sum of contributions equals the difference between the prediction and the average prediction).

  • Symmetry: If two features contribute equally, they receive equal Shapley values.

  • Dummy: Features that don’t contribute get a Shapley value of zero.

  • Additivity: For an ensemble, Shapley values are additive.

3.2 SHAP in Practice (TreeExplainer, KernelExplainer)
 
 
Explainer Description Best For
TreeExplainer Optimised for tree-based models (Random Forest, XGBoost, LightGBM). Fastest; uses the structure of trees.
KernelExplainer Model-agnostic; uses sampling and linear regression. Any model; slower but works everywhere.
DeepExplainer For deep learning models. Neural networks (TensorFlow, PyTorch).
GradientExplainer Faster approximation for deep learning. Large neural networks.

SHAP output types:

  • Force plot: Shows the contribution of each feature pushing the prediction higher or lower.

  • Summary plot: Shows feature importance and impact across all samples.

  • Dependence plot: Shows how a feature’s SHAP value changes with the feature value.


SECTION 4: LIME – LOCAL INTERPRETABLE MODEL-AGNOSTIC EXPLANATIONS

LIME explains individual predictions by approximating the black-box model with a simple, interpretable model (e.g., linear regression) in the local neighbourhood.

How LIME works:

  1. Perturb: Generate a set of perturbed samples around the instance to be explained.

  2. Weight: Weight samples by their proximity to the original instance.

  3. Predict: Get predictions from the black-box model on the perturbed samples.

  4. Fit: Train a simple model (e.g., linear regression) on the perturbed samples.

  5. Explain: Interpret the coefficients of the simple model.

LIME vs SHAP:

 
 
Aspect LIME SHAP
Approach Local surrogate model Game-theoretic Shapley values
Consistency Can be inconsistent Consistent (mathematically grounded)
Speed Faster (for small samples) Slower (especially KernelExplainer)
Interpretability Intuitive (linear coefficients) More rigorous (Shapley values)
Global explanations No (local only) Yes (global via aggregation)
Regulatory acceptance Moderate Higher (mathematically sound)

SECTION 5: PARTIAL DEPENDENCE PLOTS (PDP) AND ICE PLOTS

PDP shows the marginal effect of one or two features on the predicted outcome.

f^S(xS)=1n∑i=1nf^(xS,xC(i))

  • Fix the feature(s) of interest at a grid of values.

  • Average the predictions over all other features.

  • Plot the average prediction against the feature value.

ICE (Individual Conditional Expectation): Shows the effect for each individual observation (instead of average).

Use case: Understanding the relationship between credit score and default probability (monotonic? non-linear? threshold effect?).


SECTION 6: IMPLEMENTATION IN PYTHON – XAI DASHBOARD

python
# ===================================================================
# MODULE 6, LESSON 3: EXPLAINABLE AI (XAI) FOR REGULATORY COMPLIANCE
# ===================================================================

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score
import shap
import lime
import lime.lime_tabular
from sklearn.inspection import partial_dependence
import warnings
warnings.filterwarnings('ignore')

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

print("="*70)
print("EXPLAINABLE AI (XAI) FOR FINANCIAL MODELS")
print("="*70)

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

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

# Generate synthetic credit data
n_samples = 5000
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
employment_years = np.random.gamma(3, 5, n_samples).clip(0, 30)
age = np.random.normal(45, 12, n_samples).clip(22, 75)

# Create non-linear relationships
log_odds = (-4.5 + 
            0.04 * dti - 
            0.005 * credit_score + 
            0.01 * (loan_amount/1000) + 
            0.02 * employment_years -
            0.01 * age +
            # Non-linear term: DTI squared (higher DTI has increasing effect)
            0.0003 * dti**2 +
            # Interaction: credit_score * dti
            -0.00001 * credit_score * dti)

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

# Create DataFrame
df = pd.DataFrame({
    'income': income,
    'dti': dti,
    'credit_score': credit_score,
    'loan_amount': loan_amount,
    'employment_years': employment_years,
    'age': age,
    'default': default
})

feature_names = ['income', 'dti', 'credit_score', 'loan_amount', 'employment_years', 'age']

# Train-test split
X = df[feature_names]
y = df['default']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# Standardise (for LIME and linear models)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print(f"Training samples: {len(X_train)}")
print(f"Test samples: {len(X_test)}")
print(f"Default rate: {y_train.mean():.2%}")

# ----------------------------------------------------------------
# PART B: TRAIN BLACK-BOX MODEL (RANDOM FOREST)
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART B: Training Random Forest Model")
print("-"*60)

rf_model = RandomForestClassifier(
    n_estimators=100,
    max_depth=8,
    min_samples_split=50,
    min_samples_leaf=20,
    random_state=42,
    n_jobs=-1
)
rf_model.fit(X_train, y_train)

# Evaluate
train_auc = roc_auc_score(y_train, rf_model.predict_proba(X_train)[:, 1])
test_auc = roc_auc_score(y_test, rf_model.predict_proba(X_test)[:, 1])

print(f"Training AUC: {train_auc:.4f}")
print(f"Test AUC: {test_auc:.4f}")

# ----------------------------------------------------------------
# PART C: GLOBAL INTERPRETABILITY – FEATURE IMPORTANCE
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART C: Global Interpretability – Feature Importance")
print("-"*60)

# Built-in feature importance (Gini importance)
feature_importance = pd.DataFrame({
    'feature': feature_names,
    'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)

print("Feature Importance (Gini):")
print(feature_importance.to_string(index=False))

# Visualise
fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(feature_importance['feature'], feature_importance['importance'], color='blue', alpha=0.7)
ax.set_xlabel('Importance')
ax.set_title('Global Feature Importance – Random Forest')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('global_feature_importance.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART D: SHAP – GLOBAL AND LOCAL EXPLANATIONS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART D: SHAP Explanations")
print("-"*60)

# Create SHAP explainer (TreeExplainer for Random Forest)
explainer_shap = shap.TreeExplainer(rf_model)
shap_values = explainer_shap.shap_values(X_test)

# For binary classification, use the positive class SHAP values
shap_values_pos = shap_values[1] if isinstance(shap_values, list) else shap_values

# Global SHAP summary
fig, ax = plt.subplots(figsize=(12, 7))
shap.summary_plot(shap_values_pos, X_test, feature_names=feature_names, show=False)
plt.title('SHAP Summary Plot – Feature Impact on Default Probability', fontsize=14)
plt.tight_layout()
plt.savefig('shap_summary_plot.png', dpi=300)
plt.show()

# SHAP bar plot (mean absolute SHAP values)
fig, ax = plt.subplots(figsize=(10, 6))
shap.summary_plot(shap_values_pos, X_test, feature_names=feature_names, plot_type='bar', show=False)
plt.title('SHAP Feature Importance (Mean |SHAP Value|)', fontsize=14)
plt.tight_layout()
plt.savefig('shap_bar_plot.png', dpi=300)
plt.show()

# SHAP dependence plot for the most important feature (credit_score)
# Find the most important feature
mean_shap = np.abs(shap_values_pos).mean(axis=0)
most_important_idx = np.argmax(mean_shap)
most_important_feature = feature_names[most_important_idx]

fig, ax = plt.subplots(figsize=(10, 6))
shap.dependence_plot(most_important_idx, shap_values_pos, X_test, 
                     feature_names=feature_names, show=False)
plt.title(f'SHAP Dependence Plot – {most_important_feature}', fontsize=14)
plt.tight_layout()
plt.savefig('shap_dependence_plot.png', dpi=300)
plt.show()

print(f"Most important feature: {most_important_feature}")

# Local SHAP explanation (force plot for a specific prediction)
# Choose a specific test sample
sample_idx = 5
sample = X_test.iloc[sample_idx:sample_idx+1]
sample_shap = explainer_shap.shap_values(sample)

fig, ax = plt.subplots(figsize=(14, 3))
shap.force_plot(explainer_shap.expected_value[1] if isinstance(explainer_shap.expected_value, list) else explainer_shap.expected_value, 
                sample_shap[1] if isinstance(sample_shap, list) else sample_shap, 
                sample, feature_names=feature_names, matplotlib=True, show=False)
plt.title('SHAP Force Plot – Individual Prediction Explanation', fontsize=12)
plt.tight_layout()
plt.savefig('shap_force_plot.png', dpi=300)
plt.show()

print(f"Prediction for sample {sample_idx}: {rf_model.predict_proba(sample)[0, 1]:.3f}")

# ----------------------------------------------------------------
# PART E: LIME – LOCAL EXPLANATIONS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART E: LIME Local Explanations")
print("-"*60)

# Create LIME explainer (use scaled data for LIME)
explainer_lime = lime.lime_tabular.LimeTabularExplainer(
    X_train_scaled,
    feature_names=feature_names,
    class_names=['No Default', 'Default'],
    mode='classification',
    discretize_continuous=True,
    discretize_continuous_method='quantile',
    n_jobs=1
)

# Explain the same sample as above
sample_scaled = X_test_scaled[sample_idx:sample_idx+1].flatten()
exp = explainer_lime.explain_instance(
    sample_scaled,
    lambda x: rf_model.predict_proba(scaler.inverse_transform(x)),
    num_features=len(feature_names),
    top_labels=1
)

# Visualise LIME explanation
fig = exp.as_pyplot_figure()
plt.title('LIME Explanation – Individual Prediction', fontsize=12)
plt.tight_layout()
plt.savefig('lime_explanation.png', dpi=300)
plt.show()

print("LIME Explanation (top features):")
for feature, weight in exp.local_exp[1]:  # class 1 (default)
    print(f"  {feature_names[feature]}: {weight:.4f}")

# ----------------------------------------------------------------
# PART F: PARTIAL DEPENDENCE PLOTS (PDP)
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART F: Partial Dependence Plots (PDP)")
print("-"*60)

# Create PDP for credit_score
fig, ax = plt.subplots(figsize=(12, 8))

# PDP for credit_score
from sklearn.inspection import PartialDependenceDisplay
PartialDependenceDisplay.from_estimator(
    rf_model,
    X_test,
    features=['credit_score', 'dti'],
    kind='average',
    grid_resolution=20,
    ax=ax,
    feature_names=feature_names
)
plt.suptitle('Partial Dependence Plots – Credit Score and DTI', fontsize=14)
plt.tight_layout()
plt.savefig('pdp_plots.png', dpi=300)
plt.show()

# PDP with ICE (Individual Conditional Expectation)
fig, ax = plt.subplots(figsize=(12, 6))
PartialDependenceDisplay.from_estimator(
    rf_model,
    X_test,
    features=['credit_score'],
    kind='both',  # Both PDP and ICE
    grid_resolution=20,
    ax=ax,
    feature_names=feature_names,
    ice_lines_kw={'alpha': 0.1, 'color': 'gray'},
    pd_line_kw={'color': 'blue', 'linewidth': 3}
)
plt.title('PDP and ICE for Credit Score', fontsize=14)
plt.tight_layout()
plt.savefig('pdp_with_ice.png', dpi=300)
plt.show()

# ----------------------------------------------------------------
# PART G: COUNTERFACTUAL EXPLANATIONS
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART G: Counterfactual Explanations")
print("-"*60)

print("""
Counterfactual explanations answer: "What would need to change for the prediction to be different?"

For example, if a loan application is rejected, a counterfactual explanation might say:
"If your credit score were 720 instead of 680, your application would have been approved."

This is powerful for:
  - Providing actionable feedback to rejected applicants.
  - Demonstrating compliance with fair lending regulations.
  - Building trust with customers.

Simple counterfactual search algorithm:
  1. Start with the original instance.
  2. Iteratively adjust features (within plausible bounds).
  3. Check if the prediction changes.
  4. Return the minimal set of changes.

Algorithm for credit_score counterfactual:
""")

def find_counterfactual(model, instance, feature_names, target_class=0, max_iter=100):
    """
    Simple counterfactual search.
    Adjust credit_score until prediction changes from default to no-default.
    """
    # Copy the instance
    cf = instance.copy()
    original_prob = model.predict_proba(cf.values.reshape(1, -1))[0, 1]
    print(f"Original instance: Default probability = {original_prob:.3f}")
    
    # Try to find a counterfactual by increasing credit_score
    for i in range(max_iter):
        cf['credit_score'] = instance['credit_score'] + i * 2  # Increase by 2 each step
        prob = model.predict_proba(cf.values.reshape(1, -1))[0, 1]
        if prob < 0.5:  # No-default
            print(f"Counterfactual found! credit_score: {instance['credit_score']:.0f} → {cf['credit_score']:.0f}")
            print(f"New default probability: {prob:.3f}")
            return cf, i
    
    print("Counterfactual not found within max iterations.")
    return None, None

# Use the same sample instance
sample_df = pd.DataFrame([X_test.iloc[sample_idx].values], columns=feature_names)
cf, iterations = find_counterfactual(rf_model, sample_df, feature_names)

# ----------------------------------------------------------------
# PART H: COMPREHENSIVE XAI DASHBOARD
# ----------------------------------------------------------------

print("\n" + "-"*60)
print("PART H: XAI Dashboard – Summary for a Single Prediction")
print("-"*60)

def xai_dashboard(model, X_data, instance_idx, feature_names):
    """
    Generate a comprehensive XAI dashboard for a single prediction.
    """
    instance = X_data.iloc[instance_idx:instance_idx+1]
    pred_prob = model.predict_proba(instance)[0, 1]
    pred_class = "Default" if pred_prob >= 0.5 else "No Default"
    
    # SHAP explanation
    shap_values = explainer_shap.shap_values(instance)
    shap_vals = shap_values[1] if isinstance(shap_values, list) else shap_values
    
    # Create dashboard
    print("\n" + "="*70)
    print(f"XAI DASHBOARD – Instance {instance_idx}")
    print("="*70)
    print(f"\nPrediction: {pred_class} (Probability: {pred_prob:.3f})")
    
    print("\nFeature Values:")
    for feature in feature_names:
        print(f"  {feature}: {instance[feature].values[0]:.2f}")
    
    print("\nSHAP Contributions:")
    # Sort by absolute SHAP value
    shap_df = pd.DataFrame({
        'feature': feature_names,
        'shap_value': shap_vals.flatten(),
        'feature_value': instance.values.flatten()
    }).sort_values('shap_value', ascending=False)
    
    for _, row in shap_df.iterrows():
        direction = "↑ (increases risk)" if row['shap_value'] > 0 else "↓ (decreases risk)"
        print(f"  {row['feature']:>15}: {row['shap_value']:+.4f} → {direction}")
    
    print("\nExplanation Summary:")
    # Identify the top positive and negative contributors
    top_positive = shap_df[shap_df['shap_value'] > 0].nlargest(2, 'shap_value')
    top_negative = shap_df[shap_df['shap_value'] < 0].nsmallest(2, 'shap_value')
    
    if not top_positive.empty:
        print("  Risk-increasing factors:")
        for _, row in top_positive.iterrows():
            print(f"    • {row['feature']}: {row['feature_value']:.2f} (contributes +{row['shap_value']:.3f})")
    
    if not top_negative.empty:
        print("  Risk-decreasing factors:")
        for _, row in top_negative.iterrows():
            print(f"    • {row['feature']}: {row['feature_value']:.2f} (contributes {row['shap_value']:.3f})")
    
    return shap_df

# Run dashboard for a sample instance
dashboard = xai_dashboard(rf_model, X_test, sample_idx, feature_names)

# ----------------------------------------------------------------
# PART I: REGULATORY COMPLIANCE CHECKLIST
# ----------------------------------------------------------------

print("\n" + "="*70)
print("PART I: Regulatory Compliance Checklist for XAI")
print("="*70)

checklist = [
    "✅ Model documentation includes feature importance (global interpretability).",
    "✅ Individual predictions can be explained (local interpretability).",
    "✅ SHAP or LIME is used for complex models.",
    "✅ Calibration plots show model probabilities match actual outcomes.",
    "✅ Disparate impact analysis has been conducted across protected groups.",
    "✅ Counterfactual explanations are available for rejected applications.",
    "✅ Model monitoring includes drift detection (PSI, CSI).",
    "✅ Regular model validation includes XAI review.",
    "✅ Explanation reports are stored for audit purposes.",
    "✅ Stakeholders have been trained on interpreting explanations."
]

for item in checklist:
    print(item)

print("\nRecommended XAI Framework for Banking:")
print("""
1. For all models: Provide global feature importance and PDPs.
2. For high-risk models (credit scoring): Provide SHAP for all predictions.
3. For customer-facing decisions: Provide LIME or counterfactual explanations.
4. For regulatory submissions: Include SHAP summary plots and feature importance.
5. For ongoing monitoring: Track SHAP distributions over time (drift in explanations).
""")

SECTION 7: COMPARISON OF XAI TECHNIQUES

 
 
Technique Type Speed Interpretability Mathematical Rigour Regulatory Acceptance
Feature Importance Global Fast High Moderate High
PDP Global Medium High Moderate High
SHAP Global/Local Slow Very High Very High Very High
LIME Local Fast High Moderate Moderate
Counterfactuals Local Medium Very High Low High (actionable)
ICE Global/Local Medium High Moderate Moderate

SECTION 8: SUMMARY FOR THE DATA PRACTITIONER

  • Explainable AI (XAI) is a regulatory requirement, not a luxury, in banking.

  • SHAP is the gold standard for model-agnostic explanations, based on game-theoretic Shapley values.

  • LIME is a faster alternative for local explanations, especially for smaller models.

  • Global interpretability (feature importance, PDP) is essential for model validation.

  • Local interpretability (SHAP, LIME) is essential for individual decisions (e.g., loan rejections).

  • Counterfactual explanations provide actionable feedback and are highly valued by regulators.

  • Always document explanations and store them for audit purposes.

  • Monitor explanations over time to detect model drift and changing behaviour.


SECTION 9: RECOMMENDED NEXT STEPS

  1. Apply SHAP and LIME to a real credit scoring model.

  2. Build an XAI dashboard for your organisation’s models.

  3. Conduct a disparate impact analysis using SHAP.

  4. Implement counterfactual generation for loan application decisions.

  5. Learn about model validation frameworks that incorporate XAI.

  6. Prepare for the next lesson on Algorithmic Trading and Reinforcement Learning.


[END OF LESSON 3 – MODULE 6]

Â