Interpretability Fundamentals
Model-Specific vs Model-Agnostic Interpretability
Read aloud is unavailable in this browser.
The Generality-Efficiency Tradeoff
When explaining a gradient boosting model's prediction, you have a choice:
Option A: Use TreeSHAP—a method designed specifically for tree-based models that computes exact Shapley values in polynomial time by exploiting tree structure.
Option B: Use KernelSHAP—a general method that works with any model but requires many model evaluations and provides only approximate values.
Both methods produce SHAP values with the same interpretation. But TreeSHAP is:
- 100x faster than KernelSHAP
- Exact rather than approximate
- More numerically stable
The catch? TreeSHAP only works for tree-based models. KernelSHAP works for everything.
This is the model-specific vs. model-agnostic tradeoff—the third major dimension of interpretability methods.
By the end of this page, you will understand: the precise distinction between model-specific and model-agnostic methods, the advantages and limitations of each approach, detailed techniques for major model families (linear, tree, neural network), how to choose between specificity and generality, and practical implementation considerations.
Model-Agnostic Methods: Universal Interpretability
Model-agnostic methods treat the model as a black box—they only require the ability to query the model for predictions. These methods work with any model type, making them universally applicable.
The core principle: perturb inputs, observe output changes, and infer feature importance from the relationship between perturbations and predictions.
- •Permutation Importance — Shuffle feature values and measure prediction change. Works with any model; no assumptions about internal structure.
- •LIME — Fit local linear model to perturbations. Treats model as oracle; only needs predict() function.
- •KernelSHAP — Approximate Shapley values using weighted regression. Model-agnostic implementation of SHAP.
- •Partial Dependence Plots — Average predictions over feature values. Only requires batch prediction capability.
- •ICE Plots — Individual prediction curves for each instance. Pure input-output analysis.
- •Counterfactual Explanations — Search for nearby inputs with different predictions. Only needs model predictions.
- •Anchors — Find sufficient conditions for predictions. Perturbation-based rule finding.
- •Universal applicability — Same method for any model
- •Flexibility — Easy to swap models without changing explanation pipeline
- •Standardization — Consistent explanation format across projects
- •Reduced expertise — No need to understand model internals
- •Future-proof — Works with models not yet invented
- •Computational cost — Many model queries required
- •Approximation error — Cannot access exact computations
- •Inefficiency — Ignores exploitable model structure
- •Perturbation issues — May create unrealistic inputs
- •Limited depth — Cannot inspect internal representations
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
import numpy as npfrom sklearn.inspection import permutation_importance, PartialDependenceDisplayimport lime.lime_tabularimport shap # Model-agnostic methods work with ANY model# All they need is a predict() or predict_proba() function class AnyModel: """This could be ANY model - the methods don't care""" def __init__(self, model): self.model = model def predict(self, X): return self.model.predict(X) def predict_proba(self, X): return self.model.predict_proba(X) # Wrap your model (could be sklearn, tensorflow, pytorch, custom, etc.)wrapped_model = AnyModel(your_model) # ============================================# Method 1: Permutation Importance (Global, Model-Agnostic)# ============================================# Only needs: model.predict() or model.score()print("📊 Permutation Importance")result = permutation_importance( wrapped_model.model, # Any model with predict X_test, y_test, n_repeats=30, scoring='accuracy', random_state=42) for name, importance in sorted( zip(feature_names, result.importances_mean), key=lambda x: -x[1])[:5]: print(f" {name}: {importance:.4f}") # ============================================# Method 2: LIME (Local, Model-Agnostic)# ============================================# Only needs: model.predict_proba()print("📊 LIME Local Explanation")lime_explainer = lime.lime_tabular.LimeTabularExplainer( X_train, feature_names=feature_names, class_names=['Class 0', 'Class 1'], mode='classification') # Explain any single instanceinstance = X_test[0]explanation = lime_explainer.explain_instance( instance, wrapped_model.predict_proba, # Just needs predict_proba num_features=5) print(f"Instance prediction: {wrapped_model.predict([instance])[0]}")for feature, weight in explanation.as_list(): print(f" {feature}: {weight:+.4f}") # ============================================# Method 3: KernelSHAP (Local+Global, Model-Agnostic)# ============================================# Only needs: model.predict() or model.predict_proba()print("📊 KernelSHAP Explanation")kernel_explainer = shap.KernelExplainer( wrapped_model.predict_proba, # Just needs callable shap.sample(X_train, 100) # Background samples) # Note: This is SLOW compared to model-specific SHAP variantsshap_values = kernel_explainer.shap_values(X_test[:10], nsamples=500) print("Top features by mean |SHAP|:")importance = np.abs(shap_values[1]).mean(axis=0)for name, imp in sorted(zip(feature_names, importance), key=lambda x: -x[1])[:5]: print(f" {name}: {imp:.4f}") # ============================================# Key Point: Same code works for ANY model# ============================================# To use with a different model, just change the wrapped_model# Everything else stays exactly the same # Example models this code works with unchanged:# - RandomForest, GradientBoosting, XGBoost, LightGBM, CatBoost# - LogisticRegression, SVM, KNN# - Neural Networks (sklearn, keras, pytorch)# - Custom models, API-based models, ensemble models# - Pre-trained models you can't access internals ofModel-agnostic methods often rely on perturbing inputs and observing output changes. But perturbation can create unrealistic inputs: shuffling 'age' might produce 5-year-olds with PhDs. Methods like LIME and KernelSHAP are sensitive to perturbation distribution. Using realistic perturbations (e.g., conditional sampling) improves explanation quality.
Model-Specific Methods: Exploiting Structure
Model-specific methods exploit knowledge of the model's internal structure to compute explanations more efficiently, more accurately, or to reveal information impossible to obtain from pure input-output analysis.
The key insight: different model architectures offer different opportunities for interpretation. A method designed for tree ensembles can exploit the discrete, hierarchical structure of trees. A method for neural networks can use gradients and activations.
| Model Type | Available Methods | Exploits |
|---|---|---|
| Linear Models | Coefficients, statistical tests, odds ratios | Linear structure, closed-form solutions |
| Decision Trees | Tree paths, split points, Gini importance | Hierarchical structure, discrete splits |
| Tree Ensembles | TreeSHAP, feature importance, tree visualization | Additive tree structure, efficient traversal |
| Neural Networks | Gradients, activations, attention, LRP | Differentiability, layer structure |
| GAMs | Shape plots, per-feature contributions | Additive structure, explicit functions |
| Bayesian Models | Posterior distributions, credible intervals | Probabilistic structure, uncertainty |
The efficiency advantage:
Model-specific methods can be dramatically more efficient. Consider computing SHAP values:
| Method | Complexity | Notes |
|---|---|---|
| Exact Shapley | O(2^n) | Exponential in features |
| KernelSHAP | O(n² × samples) | Many model evaluations |
| TreeSHAP | O(TLD²) | T=trees, L=leaves, D=depth |
For a gradient boosting model with 100 trees, 20 features, max depth 6:
- KernelSHAP: ~10,000+ model evaluations per instance
- TreeSHAP: ~100 × 64 × 36 ≈ 230,000 operations (much faster in practice)
TreeSHAP is typically 100-1000x faster than KernelSHAP for tree models, with exact rather than approximate values.
The depth advantage:
Model-specific methods can access internal representations that black-box methods cannot:
- Neural network activations reveal learned features at different layers
- Attention weights show which inputs the model attends to
- Tree splits reveal exact decision boundaries
- Bayesian posteriors provide uncertainty quantification
This internal information is invisible to model-agnostic approaches.
Model-specific methods require expertise in that model type. They can't be transferred when you change models. They may become obsolete as architectures evolve. And they may give a false sense of completeness—internal representations don't always correspond to human concepts.
Deep Dive: Linear Model Interpretability
Linear models (linear regression, logistic regression, linear SVMs) are the gold standard of interpretability. Their structure directly exposes feature effects.
Model form:
Linear Regression: ŷ = β₀ + β₁x₁ + β₂x₂ + ... + βₙxₙ
Logistic Regression: P(y=1) = σ(β₀ + β₁x₁ + β₂x₂ + ... + βₙxₙ)
Interpretation:
- Coefficient magnitude indicates effect strength
- Coefficient sign indicates effect direction
- Statistical tests (t-test, p-values) assess significance
- Confidence intervals quantify uncertainty
For logistic regression:
- Coefficients are log-odds ratios
- exp(βᵢ) = odds ratio for one-unit increase in xᵢ
- exp(βᵢ) = 2.0 means the odds double when xᵢ increases by 1
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
import numpy as npimport pandas as pdfrom sklearn.linear_model import LogisticRegressionfrom sklearn.preprocessing import StandardScalerimport statsmodels.api as sm # ============================================# Method 1: Raw Coefficients# ============================================scaler = StandardScaler()X_train_scaled = scaler.fit_transform(X_train)X_test_scaled = scaler.transform(X_test) log_reg = LogisticRegression(random_state=42)log_reg.fit(X_train_scaled, y_train) print("📊 Logistic Regression Coefficients (Standardized Features)")print("="*60)print(f"Intercept: {log_reg.intercept_[0]:.4f}")print("Feature coefficients (log-odds scale):")for name, coef in sorted( zip(feature_names, log_reg.coef_[0]), key=lambda x: -abs(x[1])): odds_ratio = np.exp(coef) effect = "increases" if coef > 0 else "decreases" print(f" {name}: β={coef:+.4f}, OR={odds_ratio:.3f}") print(f" → 1σ increase in {name} {effect} odds by {abs(odds_ratio-1)*100:.1f}%") # ============================================# Method 2: Statistical Inference with Statsmodels# ============================================print("📊 Full Statistical Analysis")print("="*60) # Add constant for interceptX_train_sm = sm.add_constant(X_train_scaled)logit_model = sm.Logit(y_train, X_train_sm)result = logit_model.fit(disp=0) print(result.summary2().tables[1].to_string()) # Key outputs:# - Coef: coefficient value# - Std.Err: standard error (uncertainty)# - z: z-statistic (coefficient / std.err)# - P>|z|: p-value for significance test# - [0.025, 0.975]: 95% confidence interval # ============================================# Method 3: Feature Importance Decomposition# ============================================print("📊 Prediction Decomposition for Single Instance")print("="*60) instance_idx = 0instance_scaled = X_test_scaled[instance_idx]prediction = log_reg.predict_proba([instance_scaled])[0, 1] contributions = log_reg.coef_[0] * instance_scaledlog_odds = log_reg.intercept_[0] + contributions.sum() print(f"Instance {instance_idx}:")print(f" P(y=1) = {prediction:.4f}")print(f" Log-odds = {log_odds:.4f}")print(f" Intercept contribution: {log_reg.intercept_[0]:.4f}")print(" Feature contributions to log-odds:")for name, value, contrib in sorted( zip(feature_names, instance_scaled, contributions), key=lambda x: -abs(x[2]))[:5]: direction = "↑" if contrib > 0 else "↓" print(f" {name} (z={value:.2f}): {contrib:+.4f} {direction}") # Verify: contributions sum to predictionpredicted_log_odds = log_reg.intercept_[0] + contributions.sum()predicted_prob = 1 / (1 + np.exp(-predicted_log_odds))print(f" Verification: predicted P = {predicted_prob:.4f} (matches)") # ============================================# Advantages of Linear Interpretability# ============================================# 1. EXACT feature contributions (no approximation)# 2. Statistical inference (p-values, confidence intervals)# 3. Global interpretation = local interpretation (linear = additive)# 4. Comparison across features (standardized coefficients)# 5. Established theory, regulatory acceptanceRaw coefficients are hard to compare across features with different scales. A coefficient of 0.01 for income ($) and 0.5 for age (years) doesn't mean age matters 50x more—the units differ. Always standardize features before interpreting coefficient magnitudes, or report standardized coefficients explicitly.
Deep Dive: Tree-Based Model Interpretability
Tree-based models (decision trees, random forests, gradient boosting) are among the most interpretable complex models. Single trees are directly readable; ensembles have powerful specific methods.
Single Decision Tree Interpretation
For a single decision tree, the model IS the explanation. Each prediction has a unique path from root to leaf, consisting of human-readable conditions.
Path extraction:
IF credit_score <= 680 AND debt_ratio > 0.4 AND income <= 45000
THEN predict: HIGH_RISK (confidence: 0.82)
Interpretation methods:
- Tree visualization — Plot the full tree structure
- Decision path extraction — Get rules for specific predictions
- Gini/entropy importance — Which features reduce impurity most?
- Node sample counts — How many training examples at each node?
- Leaf value distributions — What's the prediction uncertainty at each leaf?
Limitations:
- Deep trees become hard to interpret (too many rules)
- Trees are unstable (small data changes = different tree)
- Single trees often underperform ensembles
- Axis-aligned splits may not capture true boundaries
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
import shapimport numpy as npfrom sklearn.tree import DecisionTreeClassifier, export_textfrom sklearn.ensemble import GradientBoostingClassifierimport xgboost as xgb # ============================================# Single Tree: Direct Interpretation# ============================================print("📊 Single Decision Tree Interpretation")print("="*60) tree = DecisionTreeClassifier(max_depth=4, random_state=42)tree.fit(X_train, y_train) # Print tree rulesprint("Decision Rules:")print(export_text(tree, feature_names=list(feature_names), max_depth=3)) # Path for specific predictioninstance_idx = 0instance = X_test[instance_idx:instance_idx+1]prediction = tree.predict(instance)[0]proba = tree.predict_proba(instance)[0] # Get decision pathpath = tree.decision_path(instance)node_indicators = path.toarray()[0]active_nodes = np.where(node_indicators)[0] print(f"Prediction path for instance {instance_idx}:")print(f" Final prediction: {prediction} (P={proba[prediction]:.3f})")print(" Decision path:")for node_id in active_nodes: if tree.tree_.children_left[node_id] != -1: # Not a leaf feat = feature_names[tree.tree_.feature[node_id]] threshold = tree.tree_.threshold[node_id] value = instance[0, tree.tree_.feature[node_id]] direction = "≤" if value <= threshold else ">" print(f" Node {node_id}: {feat} = {value:.2f} {direction} {threshold:.2f}") else: print(f" Leaf {node_id}: predict class {np.argmax(tree.tree_.value[node_id])}") # ============================================# Tree Ensemble: TreeSHAP# ============================================print("📊 TreeSHAP for Gradient Boosting")print("="*60) # Train ensemblegb_model = GradientBoostingClassifier(n_estimators=100, random_state=42)gb_model.fit(X_train, y_train) # TreeSHAP - MUCH faster than KernelSHAPimport timeexplainer = shap.TreeExplainer(gb_model) start = time.time()shap_values = explainer.shap_values(X_test[:100])tree_time = time.time() - startprint(f"TreeSHAP time for 100 instances: {tree_time:.2f}s") # Compare with KernelSHAP (would be ~100x slower)print(f"Estimated KernelSHAP time: ~{tree_time * 100:.0f}s") # Global importance from TreeSHAPprint("Global Feature Importance (mean |SHAP|):")importance = np.abs(shap_values[1]).mean(axis=0)for name, imp in sorted(zip(feature_names, importance), key=lambda x: -x[1])[:5]: print(f" {name}: {imp:.4f}") # ============================================# Interaction Effects# ============================================print("📊 SHAP Interaction Analysis")print("="*60) # Compute interaction values (more expensive)interaction_values = explainer.shap_interaction_values(X_test[:50]) # Sum absolute interaction valuesn_features = len(feature_names)interaction_matrix = np.abs(interaction_values[1]).mean(axis=0) print("Top feature interactions:")pairs = []for i in range(n_features): for j in range(i+1, n_features): pairs.append((feature_names[i], feature_names[j], interaction_matrix[i, j])) for f1, f2, strength in sorted(pairs, key=lambda x: -x[2])[:5]: print(f" {f1} × {f2}: {strength:.4f}") # Interpretation: high values mean these features interact# Their joint effect differs from sum of individual effectsDeep Dive: Neural Network Interpretability
Neural networks are inherently opaque, but their differentiable structure enables powerful model-specific interpretation methods. These methods leverage gradients, activations, and architectural properties.
- •Gradient-based Saliency — Compute ∂output/∂input. High gradient magnitude = sensitive input regions. Simple but noisy.
- •Integrated Gradients — Integrate gradients along path from baseline to input. Satisfies key axioms. Addresses gradient saturation.
- •GradCAM — For CNNs: use gradients into last conv layer to produce class-discriminative localization maps. Widely used for image classification.
- •Layer-wise Relevance Propagation (LRP) — Propagate output relevance backward through layers. Each layer distributes relevance to inputs proportionally.
- •Attention Visualization — For transformers: visualize attention weights as proxy for importance. Intuitive but not always faithful.
- •Activation Maximization — Find inputs that maximize specific neurons. Reveals what features neurons detect.
- •DeepLIFT — Compare activations to reference to decompose output difference. Efficient, handles ReLU networks well.
- •Probing Classifiers — Train simple classifiers on intermediate representations to detect learned concepts.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
import torchimport torch.nn as nnimport numpy as np # ============================================# Setup: Simple neural network# ============================================class SimpleNN(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim): super().__init__() self.fc1 = nn.Linear(input_dim, hidden_dim) self.fc2 = nn.Linear(hidden_dim, hidden_dim) self.fc3 = nn.Linear(hidden_dim, output_dim) self.relu = nn.ReLU() def forward(self, x): x = self.relu(self.fc1(x)) x = self.relu(self.fc2(x)) return self.fc3(x) model = SimpleNN(input_dim=20, hidden_dim=64, output_dim=2)model.load_state_dict(torch.load('model.pt'))model.eval() # ============================================# Method 1: Simple Gradient Saliency# ============================================def simple_gradient_saliency(model, input_tensor, target_class): """Compute gradient of output w.r.t. input""" input_tensor = input_tensor.clone().requires_grad_(True) output = model(input_tensor) # Compute gradient of target class output w.r.t. input model.zero_grad() output[0, target_class].backward() # Gradient magnitude as saliency saliency = input_tensor.grad.abs() return saliency.detach().numpy() instance = torch.tensor(X_test[0:1], dtype=torch.float32)target = model(instance).argmax().item()saliency = simple_gradient_saliency(model, instance, target) print("📊 Gradient Saliency")for name, sal in sorted(zip(feature_names, saliency[0]), key=lambda x: -x[1])[:5]: print(f" {name}: {sal:.4f}") # ============================================# Method 2: Integrated Gradients# ============================================def integrated_gradients(model, input_tensor, baseline, target_class, steps=100): """Compute integrated gradients from baseline to input""" # Generate interpolated inputs scaled_inputs = [baseline + (float(i) / steps) * (input_tensor - baseline) for i in range(steps + 1)] scaled_inputs = torch.cat(scaled_inputs, dim=0) scaled_inputs.requires_grad_(True) # Forward pass outputs = model(scaled_inputs) # Compute gradients model.zero_grad() outputs[:, target_class].sum().backward() gradients = scaled_inputs.grad # Integrate (average gradients × input difference) avg_gradients = gradients.mean(dim=0, keepdim=True) integrated_grads = (input_tensor - baseline) * avg_gradients return integrated_grads.detach().numpy() baseline = torch.zeros_like(instance) # Common choice: zero baselineig = integrated_gradients(model, instance, baseline, target) print("📊 Integrated Gradients")for name, val in sorted(zip(feature_names, ig[0]), key=lambda x: -abs(x[1]))[:5]: direction = "↑" if val > 0 else "↓" print(f" {name}: {val:+.4f} {direction}") # ============================================# Method 3: Using Captum Library# ============================================from captum.attr import IntegratedGradients, DeepLift, LayerGradCam # Integrated Gradients with Captumig_captum = IntegratedGradients(model)attributions = ig_captum.attribute(instance, baseline, target=target) print("📊 Integrated Gradients (Captum)")for name, attr in sorted(zip(feature_names, attributions[0].numpy()), key=lambda x: -abs(x[1]))[:5]: print(f" {name}: {attr:+.4f}") # DeepLiftdl = DeepLift(model)dl_attr = dl.attribute(instance, baseline, target=target) print("📊 DeepLift")for name, attr in sorted(zip(feature_names, dl_attr[0].numpy()), key=lambda x: -abs(x[1]))[:5]: print(f" {name}: {attr:+.4f}") # ============================================# CNN: GradCAM for image classification# ============================================# For CNNs, GradCAM provides spatial localization# from captum.attr import LayerGradCam# # grad_cam = LayerGradCam(cnn_model, cnn_model.layer4) # Target conv layer# attributions = grad_cam.attribute(image_tensor, target=predicted_class)# # Result: heatmap showing which image regions matter for this predictionFor transformer models, attention weights are often visualized as explanations. However, research has shown that attention weights don't reliably indicate feature importance. Attention can be manipulated without changing predictions, and gradient-based methods often disagree with attention patterns. Use attention visualization for intuition, not as ground truth.
Choosing Between Model-Specific and Model-Agnostic
The choice between model-specific and model-agnostic methods depends on your constraints, requirements, and the models you're working with.
| Factor | Favors Model-Specific | Favors Model-Agnostic |
|---|---|---|
| Computational budget | Tight budget; need efficiency | Budget allows many model queries |
| Explanation quality | Need exact, faithful explanations | Approximate explanations acceptable |
| Model stability | Model architecture is fixed | May change models frequently |
| Team expertise | Deep expertise in specific model types | General ML skills; model-type-agnostic |
| Internal access | Full access to model internals | Model is an API or black box |
| Interpretability depth | Need internal representations | Input-output relationships suffice |
| Comparison needs | Compare within model family | Compare across different model types |
- •Is a model-specific method available for your model? If not, you must use model-agnostic. For novel architectures, custom methods, or API-based models, agnostic is the only option.
- •Do you have full model access? Gradient-based neural network methods require backpropagation capability. TreeSHAP needs tree structure. Without access, use agnostic methods.
- •Is computational speed critical? For real-time explanations, model-specific methods (TreeSHAP, gradients) are often essential. KernelSHAP and LIME may be too slow.
- •Do you need exact or approximate explanations? For regulatory or legal purposes, exact TreeSHAP may be required. For general insight, approximate LIME suffices.
- •Will you change model architectures? If model selection is ongoing, model-agnostic methods provide consistent explanations across experiments.
In practice, many teams use both: model-specific methods for production (efficiency) and model-agnostic methods for validation (consistency checking). When TreeSHAP and KernelSHAP disagree significantly, it suggests implementation issues or unusual data. Use agreement between methods as a sanity check.
Implementation Considerations
Practical implementation of interpretability methods involves considerations beyond algorithm selection. Library choices, performance optimization, and infrastructure integration matter.
| Library | Best For | Key Features |
|---|---|---|
| shap | SHAP values, tree models | TreeSHAP, DeepSHAP, KernelSHAP, visualizations |
| lime | LIME explanations | Tabular, text, image; fast local explanations |
| captum | PyTorch neural networks | Integrated gradients, DeepLift, GradCAM, more |
| alibi | Production deployment | Counterfactuals, anchors, trust scores |
| interpret | Glass-box models | EBM, global/local viz, comparison |
| tf-explain | TensorFlow/Keras | Grad visualization, smoothgrad, activation maps |
| eli5 | Quick debugging | Permutation importance, simple API |
Performance Optimization Strategies
- •Caching: Pre-compute and cache background data for SHAP explainers. Reuse explainers across prediction batches.
- •Sampling: For global analysis, sample representative instances rather than explaining every prediction. Use stratified sampling for subgroup analysis.
- •Batching: Compute explanations in batches. TreeSHAP and neural network methods can parallelize across instances.
- •Approximation tolerance: Use fewer KernelSHAP samples (nsamples=100) for fast approximate explanations. Increase samples for high-stakes decisions.
- •Lazy computation: Only compute explanations when requested (on-demand). Don't explain every prediction if most don't need it.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
import shapimport numpy as npfrom functools import lru_cacheimport joblib class ProductionExplainer: """ Production-ready explainer with caching, batching, and efficiency optimizations. """ def __init__(self, model, X_background, feature_names): self.model = model self.feature_names = feature_names # Pre-compute SHAP explainer (expensive) print("Initializing TreeExplainer (one-time cost)...") self.explainer = shap.TreeExplainer(model) # Pre-compute expected value self.expected_value = self.explainer.expected_value # Cache for frequently requested explanations self.cache = {} def _instance_hash(self, instance): """Create hashable key for instance caching""" return tuple(instance.flatten().tolist()) def explain_single(self, instance, use_cache=True): """Explain single instance with optional caching""" key = self._instance_hash(instance) if use_cache and key in self.cache: return self.cache[key] shap_values = self.explainer.shap_values(instance.reshape(1, -1)) # Handle binary classification if isinstance(shap_values, list): shap_values = shap_values[1] # Positive class result = { 'shap_values': shap_values[0], 'base_value': float(self.expected_value[1] if isinstance(self.expected_value, list) else self.expected_value), 'features': { name: {'value': float(instance[i]), 'shap': float(shap_values[0, i])} for i, name in enumerate(self.feature_names) }, 'top_features': self._get_top_features(shap_values[0], instance) } if use_cache: self.cache[key] = result return result def explain_batch(self, instances, progress=False): """Efficient batch explanation""" shap_values = self.explainer.shap_values(instances) if isinstance(shap_values, list): shap_values = shap_values[1] results = [] for i in range(len(instances)): results.append({ 'shap_values': shap_values[i], 'top_features': self._get_top_features(shap_values[i], instances[i]) }) return results def _get_top_features(self, shap_values, instance, n=5): """Get top n features by SHAP magnitude""" sorted_idx = np.argsort(-np.abs(shap_values))[:n] return [ { 'feature': self.feature_names[i], 'value': float(instance[i]), 'shap': float(shap_values[i]), 'direction': 'positive' if shap_values[i] > 0 else 'negative' } for i in sorted_idx ] def get_global_importance(self, X_sample): """Compute global feature importance from sample""" shap_values = self.explainer.shap_values(X_sample) if isinstance(shap_values, list): shap_values = shap_values[1] importance = np.abs(shap_values).mean(axis=0) return { name: float(imp) for name, imp in sorted( zip(self.feature_names, importance), key=lambda x: -x[1] ) } def save(self, path): """Serialize explainer for deployment""" joblib.dump({ 'model': self.model, 'feature_names': self.feature_names, 'expected_value': self.expected_value }, path) # Usageexplainer = ProductionExplainer(model, X_train[:100], feature_names) # Single prediction with explanationinstance = X_test[0]prediction = model.predict([instance])[0]explanation = explainer.explain_single(instance) print(f"Prediction: {prediction}")print("Top contributing features:")for feat in explanation['top_features']: print(f" {feat['feature']}: {feat['shap']:+.4f} ({feat['direction']})")KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.