Skip to content

07 · Model Interpretability

A model that predicts well but can't explain why is a liability in regulated domains (credit, healthcare, hiring) and a debugging nightmare everywhere else. This module covers three complementary tools: permutation importance, partial dependence, and SHAP values.

Setup

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor

data = fetch_california_housing(as_frame=True)
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)

rf = RandomForestRegressor(n_estimators=300, random_state=42, n_jobs=-1)
rf.fit(X_train, y_train)
print(f"R^2: {rf.score(X_test, y_test):.3f}")   # ~0.805

Permutation importance

feature_importances_ (Module 01/02) measures how often a feature was used for splitting — but that can be biased toward high-cardinality features. Permutation importance instead measures the actual performance drop when a feature's values are shuffled, breaking its relationship to the target.

from sklearn.inspection import permutation_importance
import numpy as np

result = permutation_importance(rf, X_test, y_test, n_repeats=10, random_state=42, n_jobs=-1)
order = np.argsort(result.importances_mean)[::-1]
for i in order[:5]:
    print(f"{X.columns[i]:12s} {result.importances_mean[i]:.3f} +/- {result.importances_std[i]:.3f}")
# MedInc       0.891 +/- 0.021
# AveOccup     0.079 +/- 0.006
# Latitude     0.075 +/- 0.005
# Longitude    0.068 +/- 0.006
# HouseAge     0.021 +/- 0.002

The number reported is the drop in R² caused by shuffling that one column — directly interpretable, model-agnostic (works for any fitted estimator), and computed on held-out X_test, unlike feature_importances_.

Partial dependence: what happens as one feature changes

Importance says how much a feature matters; partial dependence shows how — the shape of the relationship.

from sklearn.inspection import PartialDependenceDisplay
import matplotlib.pyplot as plt

PartialDependenceDisplay.from_estimator(rf, X_train, features=["MedInc", "HouseAge"])
plt.savefig("pdp.png")

For MedInc (median income), the curve rises steeply then flattens — predicted house value increases with income, but with diminishing returns past a point, a pattern invisible from the importance number alone.

SHAP values: per-prediction attribution

Importance and PDP describe the model globally. SHAP (SHapley Additive exPlanations) explains one specific prediction: how much each feature pushed this prediction above or below the average.

import shap

explainer = shap.TreeExplainer(rf)
sample = X_test.iloc[[0]]
shap_values = explainer(sample)

print("base value (avg prediction):", shap_values.base_values[0])
print("this prediction:", rf.predict(sample)[0])
for name, val in zip(X.columns, shap_values.values[0]):
    print(f"{name:12s} {val:+.3f}")
# base value (avg prediction): 2.07
# this prediction: 3.41
# MedInc       +1.02
# AveOccup     +0.18
# Latitude     +0.09
# ...

The SHAP values for one row sum (plus the base value) to exactly that row's prediction: base_value + Σ shap_values = prediction — every unit of "why is this prediction what it is" is accounted for.

Cheat sheet

Question Tool
Which features matter overall? permutation_importance
What shape is the relationship? PartialDependenceDisplay
Why did the model predict this for this row? shap.TreeExplainer
Fast but potentially biased importance model.feature_importances_

How It Actually Works

Permutation importance measures a causal-ish counterfactual, not a correlation. For feature j, the procedure takes the test set, randomly shuffles only column j across rows (so each row now has a wrong, random value for feature j but correct values for everything else), computes the model's score on this corrupted data, and reports the drop from the original score, averaged over n_repeats=10 independent shuffles (the +/- std reflects shuffle-to-shuffle variance). Because shuffling specifically destroys the statistical relationship between column j and the target while leaving every other column's relationship intact, the resulting score drop isolates the marginal, model-relied-upon value of that one feature — mechanically different from feature_importances_, which just counts how often and how effectively a feature reduced impurity during tree-building (Module 05's Gini mechanism) and can overweight high-cardinality features simply because they offer more possible split thresholds to try.

Partial dependence averages out every other feature to isolate one. For a target feature (say MedInc) and a grid of values v_1, ..., v_k spanning its observed range, the partial dependence at v_i is computed by taking every row in the dataset, overwriting its MedInc value with v_i (keeping all its other features as they actually are), running the model on this modified dataset, and averaging the predictions. Repeating this for each grid value and plotting the resulting curve shows how the model's average prediction moves as MedInc moves, with the influence of every other feature averaged away by construction — which is precisely why it can reveal a shape (steep-then-flat) that a single importance number cannot.

SHAP values are Shapley values from cooperative game theory, applied to features as "players" and the prediction as the "payout." Conceptually, for a given row, SHAP considers every possible subset (coalition) of features, measures how much adding feature j to a subset changes the model's output (averaged over subsets and orderings, weighted so that all orderings of adding features are treated fairly), and assigns that averaged marginal contribution to feature j as its SHAP value. TreeExplainer computes this exactly and efficiently for tree ensembles by walking each tree's structure rather than literally enumerating all 2^30 feature subsets. The resulting values have one mathematically guaranteed property that makes them trustworthy for individual explanations: efficiency — they sum exactly to prediction - base_value, so there's no leftover "unexplained" contribution, which is what makes base_value + Σ shap_values = prediction an equality rather than an approximation.

Exercise

Compute permutation importance for rf on the training set instead of the test set. Compare the ranking and magnitudes against the test-set version above, and explain — using the "shuffling destroys the true relationship" mechanism — why training-set permutation importance tends to overstate the importance of features the model has overfit to.