Model Explainability with SHAP and LIME #
Explainability bridges model predictions and stakeholder trust. Two widely adopted techniques are:
- SHAP (SHapley Additive exPlanations): game-theoretic, consistent feature attributions
- LIME (Local Interpretable Model-agnostic Explanations): local surrogate models around a prediction
This guide focuses on when to use each method, how to implement them safely, and how to operationalize explanations at scale.
Concepts and trade-offs #
- Local vs Global:
- Local: explain a single prediction (why this user was scored 0.82?)
- Global: aggregate local attributions to characterize model behavior across the dataset
- Model-agnostic vs Model-specific:
- LIME, SHAP KernelExplainer: model-agnostic, slower, approximate
- SHAP TreeExplainer, LinearExplainer, DeepExplainer: model-specific, faster, more accurate
- Baseline/background distribution:
- SHAP values are defined relative to a baseline (background data). A poor baseline skews attributions.
Guidance:
- Prefer SHAP for consistent, globally aggregatable explanations.
- Use LIME when you need quick, model-agnostic, locally faithful approximations and SHAP is too costly.
- Always document the background dataset and preprocessing assumptions.
SHAP: practical usage #
Tabular tree models (XGBoost/LightGBM/CatBoost) #
Fast and faithful via TreeExplainer.
# pip install shap xgboost lightgbm
import shap
import numpy as np
import xgboost as xgb
# Train model
model = xgb.XGBClassifier(
n_estimators=500, max_depth=6, learning_rate=0.05,
subsample=0.8, colsample_bytree=0.8, eval_metric="auc", tree_method="hist"
)
model.fit(X_train, y_train)
# SHAP explainer
explainer = shap.TreeExplainer(model)
# Use a small, representative background for speed when needed
X_bg = X_train[np.random.choice(X_train.shape[0], size=min(1000, len(X_train)), replace=False)]
shap_values = explainer(X_val) # or explainer.shap_values(X_val) for older shap versions
# Local explanation for one row
i = 0
shap.plots.waterfall(shap_values[i])
# Global feature importance (mean |SHAP|)
shap.plots.bar(shap_values, max_display=20)
# Dependence plot (feature effect with interactions)
shap.plots.scatter(shap_values[:, "age"], color=shap_values[:, "income"])
Notes:
- For classification, SHAP values can be on the log-odds scale depending on link. Use
explainer(..., output="probability")(newer APIs) if you need probability-space explanations. - Aggregate with care on highly correlated features (see Pitfalls).
Linear models #
import shap
import numpy as np
from sklearn.linear_model import LogisticRegression
clf = LogisticRegression(max_iter=5000).fit(X_train, y_train)
explainer = shap.LinearExplainer(clf, X_train, feature_perturbation="interventional")
sv = explainer(X_val)
shap.plots.bar(sv)
Model-agnostic KernelExplainer #
Works for arbitrary predict() functions; slower O(N × M) where N=background size, M=features.
import shap
import numpy as np
f = model.predict_proba # returns probabilities
X_bg = shap.sample(X_train, 200) # small representative background
explainer = shap.KernelExplainer(f, X_bg)
sv = explainer.shap_values(X_val[:50], nsamples="auto") # subset for latency
Production tips:
- Cache explanations for frequent queries.
- Precompute global summaries offline; compute local explanations on-demand with time budgets and fallbacks (e.g., top-K features only).
LIME: practical usage #
LIME learns a locally faithful linear surrogate around a specific prediction by perturbing inputs.
# pip install lime
import numpy as np
from lime.lime_tabular import LimeTabularExplainer
# Feature names and categorical metadata help readability
explainer = LimeTabularExplainer(
training_data=X_train,
feature_names=feature_names,
class_names=["no", "yes"],
discretize_continuous=True,
mode="classification"
)
row = X_val[0]
exp = explainer.explain_instance(
data_row=row,
predict_fn=model.predict_proba,
num_features=10,
top_labels=1
)
exp.show_in_notebook(show_table=True)
# Or export as list of (feature, weight)
explanation_weights = exp.as_list(label=1)
Guidance:
- LIME is sensitive to the perturbation distribution. Set
discretize_continuousand categorical masks to mimic realistic variations. - LIME explanations vary between runs; average multiple runs for stability if needed.
Pipelines and preprocessing #
Explanations must align with the features actually consumed by the model.
- If you trained with a Pipeline/ColumnTransformer, either:
- Explain in model-input space (post-transform). Keep track of transformed feature names (e.g., OHE columns).
- Or wrap the model with a function that applies the same preprocessing inside the explainer to ensure parity.
Example for scikit-learn Pipeline:
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
import shap
pre = ColumnTransformer([
("num", StandardScaler(), num_cols),
("cat", OneHotEncoder(handle_unknown="ignore"), cat_cols)
])
pipe = Pipeline([("pre", pre), ("clf", LogisticRegression(max_iter=2000))]).fit(X_train_df, y_train)
# SHAP on transformed inputs
X_trans = pipe.named_steps["pre"].transform(X_val_df)
explainer = shap.LinearExplainer(pipe.named_steps["clf"], pipe.named_steps["pre"].transform(X_train_df))
sv = explainer(X_trans)
For readability, map back transformed columns to original feature groups (e.g., sum |SHAP| over all OHE dummies of a base feature).
Aggregating to global insights #
- Mean absolute SHAP: global feature importance ranked by contribution magnitude.
- SHAP dependence plots: feature effect vs value; color by interacting feature to reveal interactions.
- Partial dependence (PDP) and ICE:
- PDP: average predicted response as a function of one feature (assumes feature independence)
- ICE: per-instance curves; reveal heterogeneity across data
Use both SHAP dependence and PDP/ICE to understand non-linearities and interactions.
Monitoring and governance #
- Track explanation drift: compare global SHAP importances and distributions over time. Large shifts may indicate feature drift or model changes.
- Persist explanation metadata: model version, baseline/background sample, feature schema, explainer parameters.
- Access control: explanations may leak sensitive feature influence. Gate via IAM and redact sensitive attributions when required.
Performance and cost #
- For TreeExplainer, explanations scale well; still cap per-request rows.
- For KernelExplainer/LIME, limit:
- Number of features (top-K)
- Background sample size
- Number of perturbations/samples
- Batch local explanations asynchronously; cache results.
Common pitfalls and remedies #
- Correlated features:
- SHAP distribution of credit among correlated features can be unstable. Prefer “interventional” SHAP settings, group correlated features, and report grouped importances.
- Poor background:
- Choose a representative baseline (stratified sample of training data). Document selection; avoid using a single zero vector unless meaningful.
- Preprocessing mismatch:
- Ensure the exact training-time transforms are applied. Fix with Pipelines or feature stores.
- Categorical explosion:
- One-hot expands features; aggregate attributions back to the base feature for readability.
- Leakage in explanations:
- If features inadvertently encode targets (leakage), SHAP/LIME will highlight them—fix the pipeline, not the explanation.
Minimal end-to-end example (tree model) #
import shap, numpy as np, xgboost as xgb
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score
X_train, X_val, y_train, y_val = train_test_split(X, y, stratify=y, test_size=0.2, random_state=42)
model = xgb.XGBClassifier(n_estimators=800, learning_rate=0.05, max_depth=6, subsample=0.8, colsample_bytree=0.8, eval_metric="auc", tree_method="hist")
model.fit(X_train, y_train)
y_score = model.predict_proba(X_val)[:, 1]
print("AUC:", roc_auc_score(y_val, y_score))
explainer = shap.TreeExplainer(model)
sv = explainer(X_val)
# Global
shap.plots.bar(sv, max_display=15)
# Local: first row
shap.plots.waterfall(sv[0])
Key takeaways #
- Use SHAP for consistent, aggregatable attributions; TreeExplainer for tree ensembles, Linear/Deep/KernelExplainer otherwise.
- Use LIME for quick, model-agnostic local explanations when SHAP is too heavy.
- Keep preprocessing inside the explanation path; choose and document a representative background distribution.
- Aggregate to global insights; monitor explanation drift in production; cache and budget expensive explanations.