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:
-
Perturb:Â Generate a set of perturbed samples around the instance to be explained.
-
Weight:Â Weight samples by their proximity to the original instance.
-
Predict:Â Get predictions from the black-box model on the perturbed samples.
-
Fit:Â Train a simple model (e.g., linear regression) on the perturbed samples.
-
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
# =================================================================== # 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
-
Apply SHAP and LIME to a real credit scoring model.
-
Build an XAI dashboard for your organisation’s models.
-
Conduct a disparate impact analysis using SHAP.
-
Implement counterfactual generation for loan application decisions.
-
Learn about model validation frameworks that incorporate XAI.
-
Prepare for the next lesson on Algorithmic Trading and Reinforcement Learning.
[END OF LESSON 3 – MODULE 6]