Machine learning models keep getting more powerful, but let's be honest — most of them are basically black boxes. When a random forest predicts a loan default or a gradient boosting model flags a suspicious transaction, the first question from stakeholders is always: but why? That's where SHAP (SHapley Additive exPlanations) comes in. It gives you mathematically grounded explanations for any model's predictions, and it does so in a way that actually makes sense.
In this guide, I'll walk you through SHAP from installation to production-ready interpretability workflows. You'll learn how to explain both classification and regression models built with scikit-learn, create visualizations that non-technical stakeholders can actually understand, and avoid the pitfalls that trip up most people. Everything here includes working code you can copy and run right away.
What Are SHAP Values and Why Do They Matter?
SHAP values come from cooperative game theory, of all places. The concept was developed by Lloyd Shapley back in 1953 to answer what sounds like a simple question: if a group of players work together to produce some outcome, how do you fairly split the reward?
In ML terms, your features are the "players," the prediction is the "game," and the "payout" is the gap between the model's prediction for a given instance and the baseline (average) prediction. SHAP assigns each feature a contribution score that satisfies three key mathematical properties:
- Additivity — The sum of all SHAP values for a prediction equals the difference between that prediction and the baseline value.
- Consistency — If a model changes so that a feature's contribution increases, its SHAP value never decreases.
- Null player — A feature with zero impact on the prediction always gets a SHAP value of zero.
These guarantees are what make SHAP the gold standard for model interpretability. Unlike simpler methods like permutation importance, SHAP gives you both local (per-prediction) and global (across-the-dataset) explanations — with mathematically rigorous fairness baked in.
Why SHAP Matters in 2026
Here's the thing: the EU AI Act becomes fully applicable in August 2026. Article 86 grants individuals a right to explanation of AI-driven decisions that adversely affect them, and high-risk AI systems must be designed so deployers can interpret their output. SHAP isn't just a nice-to-have anymore — for organizations in regulated industries like finance, healthcare, and hiring, it's essentially a compliance requirement.
Installation and Setup
SHAP 0.50 is the latest release as of early 2026, supporting Python 3.9 through 3.13. Here's what you need:
pip install shap==0.50.0 scikit-learn pandas matplotlib
Quick sanity check:
import shap
print(shap.__version__) # 0.50.0
A heads-up if you're upgrading from an older version: SHAP 0.50 changed how multi-output models return values (now np.ndarray instead of list), and there are fixes for incorrect SHAP values with missing data in newer scikit-learn tree models. Worth checking the release notes before upgrading.
Explaining a Regression Model Step by Step
Let's get hands-on with a complete regression example. We'll use the California Housing dataset and a random forest — nothing exotic, just a solid starting point.
Step 1: Load and Prepare the Data
import shap
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
# Load dataset
data = fetch_california_housing(as_frame=True)
X, y = data.data, data.target
# Split into train and test sets
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
print(f"Training samples: {X_train.shape[0]}")
print(f"Test samples: {X_test.shape[0]}")
print(f"Features: {list(X.columns)}")
Step 2: Train the Model
# Train a Random Forest Regressor
model = RandomForestRegressor(
n_estimators=200,
max_depth=12,
min_samples_leaf=5,
random_state=42,
n_jobs=-1
)
model.fit(X_train, y_train)
print(f"R² score: {model.score(X_test, y_test):.4f}")
Step 3: Compute SHAP Values
shap.Explainer is smart enough to pick the most efficient algorithm for your model type automatically. For tree-based models, it uses TreeExplainer under the hood, which computes exact SHAP values in polynomial time — way faster than the exponential-time general algorithm.
# Create the SHAP explainer
# For tree models, this automatically uses TreeExplainer
explainer = shap.Explainer(model, X_train)
# Compute SHAP values for the test set
shap_values = explainer(X_test)
print(f"SHAP values shape: {shap_values.shape}")
print(f"Base value (expected prediction): {shap_values.base_values[0]:.4f}")
The base_values represent the average model prediction across the training set. Every individual prediction breaks down like this: base_value + sum(shap_values) = prediction. Simple, right?
Step 4: Verify Additivity
One of SHAP's strongest guarantees is additivity, and you can actually verify it yourself. I'd recommend doing this at least once — it's satisfying to see the math check out perfectly:
# Check additivity for the first test sample
prediction = model.predict(X_test.iloc[[0]])[0]
shap_sum = shap_values[0].base_values + shap_values[0].values.sum()
print(f"Model prediction: {prediction:.4f}")
print(f"SHAP base + sum: {shap_sum:.4f}")
print(f"Match: {np.isclose(prediction, shap_sum)}")
SHAP Visualization Toolkit
SHAP ships with a genuinely impressive set of plots. Here are the five you'll use most, along with when each one makes sense.
Waterfall Plot — Decomposing a Single Prediction
This is the plot you'll reach for when a business stakeholder asks "why did the model make this decision?" The waterfall shows how each feature pushes a prediction from the base value to the final output.
# Explain why the model predicted a high value for sample 0
shap.plots.waterfall(shap_values[0], max_display=10)
How to read it: the bottom starts at E[f(X)] (the average prediction). Each bar shows a feature's push — red pushes higher, blue pushes lower. The final value f(x) at the top is what the model actually predicted.
Beeswarm Plot — Global Feature Impact
The beeswarm is probably the most information-dense visualization SHAP offers. Each dot is one instance, the x-axis shows the SHAP value, and the color tells you the feature's actual value (red = high, blue = low). Features get sorted by overall importance.
# Global view: how each feature affects predictions across all samples
shap.plots.beeswarm(shap_values, max_display=10)
What makes this plot so powerful is that it reveals not just which features matter, but how their values relate to predictions. For instance, you might see that high MedInc (median income) values consistently push predictions upward — a relationship that's completely invisible in a standard feature importance bar chart.
Bar Plot — Mean Absolute SHAP Values
Need something clean for an executive summary? The bar plot shows mean absolute SHAP values per feature. It's simple and gets the point across fast:
# Simple feature importance ranking
shap.plots.bar(shap_values, max_display=10)
Force Plot — Interactive Prediction Breakdown
Force plots show a "tug-of-war" between features pushing the prediction up or down from the base value:
# Force plot for a single prediction
shap.plots.force(shap_values[0])
You can also stack multiple predictions to spot patterns across samples:
# Force plot for multiple predictions (interactive in Jupyter)
shap.plots.force(shap_values[:100])
Dependence Plot — Feature Interaction Effects
Dependence plots show how a feature's value relates to its SHAP value, automatically coloring by the strongest interacting feature:
# How does MedInc affect predictions, and what interacts with it?
shap.plots.scatter(shap_values[:, "MedInc"], color=shap_values)
These are especially useful for spotting nonlinear relationships and interaction effects that traditional partial dependence plots tend to miss.
Explaining a Classification Model
Classification works a bit differently because you're dealing with multiple classes. Here's a complete example with a breast cancer classifier:
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import GradientBoostingClassifier
# Load data
cancer = load_breast_cancer(as_frame=True)
X, y = cancer.data, cancer.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train a gradient boosting classifier
clf = GradientBoostingClassifier(
n_estimators=200,
max_depth=4,
learning_rate=0.1,
random_state=42
)
clf.fit(X_train, y_train)
print(f"Accuracy: {clf.score(X_test, y_test):.4f}")
# Compute SHAP values
explainer = shap.Explainer(clf, X_train)
shap_values = explainer(X_test)
# Waterfall for a single prediction
shap.plots.waterfall(shap_values[0], max_display=12)
For binary classifiers, SHAP values correspond to the log-odds of the positive class. Positive SHAP value? The feature pushes toward malignant. Negative? It pushes toward benign. Straightforward once you know the convention.
Multi-Class Classification
With more than two classes, SHAP values gain an extra dimension — you get one set of values per class:
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier
# Load multi-class data
iris = load_iris(as_frame=True)
X, y = iris.data, iris.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)
# Train classifier
clf_multi = RandomForestClassifier(n_estimators=100, random_state=42)
clf_multi.fit(X_train, y_train)
# Compute SHAP values
explainer = shap.Explainer(clf_multi, X_train)
shap_values = explainer(X_test)
# Bar plot showing importance across all classes
shap.plots.bar(shap_values, max_display=6)
Choosing the Right SHAP Explainer
SHAP has several specialized explainer classes, each optimized for different model types. Picking the right one can make a huge difference in both accuracy and speed.
| Explainer | Best For | Speed | Exactness |
|---|---|---|---|
TreeExplainer |
Decision trees, random forests, XGBoost, LightGBM, CatBoost | Very fast | Exact |
LinearExplainer |
Linear regression, logistic regression, Ridge, Lasso | Very fast | Exact |
KernelExplainer |
Any model (model-agnostic) | Slow | Approximate |
DeepExplainer |
Deep learning (TensorFlow, PyTorch) | Medium | Approximate |
Explainer (auto) |
Automatically selects the best explainer | Varies | Varies |
My recommendation: just use shap.Explainer(model, X_train) in most cases. It inspects the model type and dispatches to the most efficient algorithm automatically. You only need a specific explainer when you want fine-grained control over computation parameters.
Using TreeExplainer Directly
# Direct TreeExplainer for maximum control
tree_explainer = shap.TreeExplainer(
model,
data=X_train, # Background dataset for interventional SHAP
feature_perturbation="interventional" # Accounts for feature correlations
)
shap_values_tree = tree_explainer(X_test)
The feature_perturbation parameter is worth understanding. The "interventional" mode breaks feature correlations and measures the causal effect of each feature individually, while the default "tree_path_dependent" mode is faster but can attribute importance to correlated features that aren't truly driving the prediction.
Using KernelExplainer for Any Model
When you're working with something that doesn't have a specialized explainer — an SVM, a custom ensemble, or whatever else — KernelExplainer is your fallback:
from sklearn.svm import SVR
# Train an SVR model
svr = SVR(kernel="rbf", C=10)
svr.fit(X_train, y_train)
# KernelExplainer works with any model
# Use a small background sample for efficiency
background = shap.sample(X_train, 100)
kernel_explainer = shap.KernelExplainer(svr.predict, background)
# Compute SHAP values (slower than TreeExplainer)
shap_values_kernel = kernel_explainer.shap_values(X_test.iloc[:50])
Performance tip: KernelExplainer is computationally expensive. For large datasets, subsample both the background data and the instances you want to explain. Seriously — don't try to run this on your full dataset unless you enjoy waiting.
SHAP with XGBoost, LightGBM, and CatBoost
These three are the workhorses of production ML (if you've read our gradient boosting comparison guide, you already know this). Here's how to use SHAP with each one:
XGBoost
import xgboost as xgb
xgb_model = xgb.XGBRegressor(n_estimators=200, max_depth=6, random_state=42)
xgb_model.fit(X_train, y_train)
explainer = shap.Explainer(xgb_model, X_train)
shap_values = explainer(X_test)
shap.plots.beeswarm(shap_values)
LightGBM
import lightgbm as lgb
lgb_model = lgb.LGBMRegressor(n_estimators=200, max_depth=6, random_state=42)
lgb_model.fit(X_train, y_train)
explainer = shap.Explainer(lgb_model, X_train)
shap_values = explainer(X_test)
shap.plots.beeswarm(shap_values)
CatBoost
from catboost import CatBoostRegressor
cb_model = CatBoostRegressor(iterations=200, depth=6, random_seed=42, verbose=0)
cb_model.fit(X_train, y_train)
explainer = shap.Explainer(cb_model, X_train)
shap_values = explainer(X_test)
shap.plots.beeswarm(shap_values)
All three have native TreeSHAP integration, so shap.Explainer automatically takes the optimized path. One nice fix in SHAP 0.50: it resolves issues with special characters in CatBoost feature names that caused errors in earlier versions.
Building an Interpretability Report
In real-world projects, you'll often need to produce a structured report that documents model behavior — for auditors, stakeholders, or your future self. Here's a reusable function I've found useful:
def generate_shap_report(model, X_train, X_test, sample_idx=0, top_n=10):
"""Generate a comprehensive SHAP interpretability report."""
import matplotlib.pyplot as plt
explainer = shap.Explainer(model, X_train)
shap_values = explainer(X_test)
# 1. Global feature importance
print("=" * 60)
print("GLOBAL FEATURE IMPORTANCE (Mean |SHAP|)")
print("=" * 60)
mean_abs_shap = np.abs(shap_values.values).mean(axis=0)
importance_df = pd.DataFrame({
"Feature": X_test.columns,
"Mean |SHAP|": mean_abs_shap
}).sort_values("Mean |SHAP|", ascending=False).head(top_n)
print(importance_df.to_string(index=False))
# 2. Generate plots
fig, axes = plt.subplots(1, 2, figsize=(18, 6))
plt.sca(axes[0])
shap.plots.bar(shap_values, max_display=top_n, show=False)
axes[0].set_title("Global Feature Importance")
plt.sca(axes[1])
shap.plots.waterfall(shap_values[sample_idx], max_display=top_n, show=False)
axes[1].set_title(f"Prediction Breakdown (Sample {sample_idx})")
plt.tight_layout()
plt.savefig("shap_report.png", dpi=150, bbox_inches="tight")
plt.show()
# 3. Prediction breakdown
print(f"\nPREDICTION BREAKDOWN (Sample {sample_idx})")
print(f"Base value: {shap_values[sample_idx].base_values:.4f}")
print(f"Prediction: {model.predict(X_test.iloc[[sample_idx]])[0]:.4f}")
return shap_values
# Usage
shap_values = generate_shap_report(model, X_train, X_test)
Performance Tips for Large Datasets
Computing SHAP values on large datasets can get slow fast. Here are the strategies that actually matter:
1. Subsample the Background Data
# Instead of using the entire training set as background
# Use a representative subsample
background = shap.sample(X_train, 200) # 200 samples is usually sufficient
explainer = shap.Explainer(model, background)
2. Use shap.kmeans for Summarized Backgrounds
# Summarize the background data using k-means clustering
background_summary = shap.kmeans(X_train, 50)
kernel_explainer = shap.KernelExplainer(model.predict, background_summary)
3. Explain a Subset of Test Data
# Don't compute SHAP values for millions of test samples
# A random sample of 500-1000 is enough for global patterns
sample_indices = np.random.choice(len(X_test), size=500, replace=False)
shap_values = explainer(X_test.iloc[sample_indices])
4. Leverage TreeExplainer's Speed
TreeExplainer runs in O(TLD²) time, where T is the number of trees, L is the max number of leaves, and D is the max depth. For a random forest with 200 trees and max depth 12, that's orders of magnitude faster than KernelExplainer. If you're using tree-based models (and in 2026, you probably are for tabular data), this is essentially free.
Common Pitfalls and How to Avoid Them
Pitfall 1: Confusing Correlation with Causation
This one catches people all the time. SHAP values show how the model uses features — not their real-world causal effect. If two features are correlated, the model might lean on either one, and SHAP will faithfully reflect that choice rather than true causal importance.
# Check feature correlations before interpreting SHAP
correlation_matrix = X_train.corr()
high_corr = (correlation_matrix.abs() > 0.8) & (correlation_matrix != 1.0)
print("Highly correlated feature pairs:")
for col in correlation_matrix.columns:
correlated = high_corr.index[high_corr[col]].tolist()
if correlated:
print(f" {col} <-> {correlated}")
Pitfall 2: Ignoring the Background Dataset
The background dataset defines what "absence" of a feature means. A poorly chosen background can produce misleading results. Always use your training data (or a representative subsample) as the background — never test data. I've seen this mistake in production code more times than I'd like to admit.
Pitfall 3: Over-Relying on Global Importance
A feature with medium global importance might be critically important for specific subgroups. Always combine global plots (beeswarm) with local explanations (waterfall) for high-stakes predictions. The aggregate view can hide important patterns.
Pitfall 4: Not Monitoring SHAP Drift
In production, you should periodically recompute SHAP values on fresh data. If feature importance shifts significantly, it's a strong signal of concept drift — and your model probably needs retraining.
# Compare SHAP importance between two time periods
def compare_shap_importance(shap_values_period1, shap_values_period2, feature_names):
imp1 = np.abs(shap_values_period1.values).mean(axis=0)
imp2 = np.abs(shap_values_period2.values).mean(axis=0)
comparison = pd.DataFrame({
"Feature": feature_names,
"Period 1": imp1,
"Period 2": imp2,
"Change (%)": ((imp2 - imp1) / imp1 * 100)
}).sort_values("Change (%)", ascending=False)
print("Features with largest importance shifts:")
print(comparison.head(5).to_string(index=False))
return comparison
SHAP vs. Other Interpretability Methods
SHAP isn't the only game in town for model interpretability. Here's how it stacks up against the alternatives:
| Method | Type | Pros | Cons |
|---|---|---|---|
| SHAP | Local + Global | Mathematically rigorous, consistent, rich visualizations | Can be slow for large datasets or KernelExplainer |
| LIME | Local | Fast, intuitive, model-agnostic | Inconsistent across runs, no mathematical guarantees |
| Permutation Importance | Global | Simple, works with any model, built into scikit-learn | No local explanations, biased by correlated features |
| Partial Dependence Plots | Global | Easy to understand, shows marginal effects | Assumes feature independence, no per-prediction insight |
| Feature Importance (built-in) | Global | Very fast, no extra computation | Biased toward high-cardinality features, varies by algorithm |
For most use cases in 2026, SHAP is the recommended default. Reach for LIME when you need quick one-off local explanations, and permutation importance when a rough global ranking is all you need.
Frequently Asked Questions
What are SHAP values in simple terms?
SHAP values measure how much each feature contributes to a specific prediction compared to the average prediction. Think of it like splitting a restaurant bill fairly — each feature gets credit proportional to what it actually brought to the table. A SHAP value of +0.5 means that feature pushed the prediction 0.5 units above average, while -0.3 means it pulled it 0.3 units below.
How long does it take to compute SHAP values?
It depends entirely on the explainer. TreeExplainer handles tree-based models (random forests, XGBoost, LightGBM) in seconds, even with hundreds of thousands of rows. KernelExplainer, the model-agnostic option, can take minutes to hours depending on your dataset size. For typical scikit-learn workflows, SHAP adds negligible overhead.
Can SHAP explain deep learning models?
Yes. SHAP includes DeepExplainer for TensorFlow and PyTorch models, plus GradientExplainer for gradient-based approximations. For transformer models and NLP tasks, it also supports partition-based explanations that work with the Hugging Face transformers library.
What's the difference between SHAP and LIME?
Both explain individual predictions, but the key difference is guarantees. SHAP provides mathematically guaranteed consistency and additivity, while LIME approximates using a local surrogate model. SHAP is deterministic (same inputs, same explanation every time), while LIME can give slightly different results across runs because of random perturbation sampling. SHAP also extends naturally to global interpretability through aggregation — LIME doesn't.
Is SHAP required for EU AI Act compliance?
The EU AI Act doesn't name specific tools, but it does require that high-risk AI systems provide interpretable outputs and that affected individuals can get explanations of AI-driven decisions. SHAP is one of the most widely used tools for meeting these transparency requirements, and you'll find it referenced frequently in compliance frameworks and audit reports.