Machine Learning · Insper

Explainability

Which features does the model rely on, and why this prediction?

Every method worked out by hand first, then called from a library that must give the same numbers

A good prediction nobody can read

  • Random forests, gradient boosting and neural networks won the last lessons.
  • A boosted ensemble of 200 trees predicts well, but "why this number?" has no answer you could write on a page.

global

which features does the model rely on, overall?

local

why did it make this prediction for this row?

Running example: California housing, the median house value of a district from its income, its houses and its position. The lab: why was this loan applicant flagged as high risk?

Why explain a model?

debugging

a model that looks too good usually is: a leaked column shows up at the top of every importance ranking

trust

a doctor, a credit analyst or a judge will not act on a number they cannot question

regulation

LGPD (Lei 13.709/2018, art. 20): the right to ask for a review of automated decisions, with "clear and adequate information" about the criteria; GDPR art. 22 points the same way

science

sometimes the model is a tool to learn about the world, and the explanation is the result

The toolbox of this lesson

methodquestion it answersscope
coefficients, tree rulesthe model itselfglobal
impurity importance (MDI)which features did the training splits use most?global, trees
permutation importancehow much does the score depend on each feature?global
partial dependence (PDP)how does the average prediction change with one feature?global
ICE curves... and for each row?local, many rows
SHAP valueshow much did each feature move this prediction from the average?local → global
LIMEwhich linear model imitates the black box around this row?local

Model-specific methods read the internals; model-agnostic ones only call predict.

The path of this lesson

  1. Readable firstcoefficients, tree rules, the price of readability
  2. Global: what matterspermutation importance, partial dependence, ICE
  3. Local: one predictionSHAP values, LIME
  4. Read with carecausation, right for the wrong reasons, a checklist
Each stage answers one question: do we need a black box at all? → which features does the model rely on, and how? → why this prediction, for this row? → what can an explanation honestly claim?
Stage 1 · readable first

First: do you need a black box?

Three candidates on the same five folds, from fully readable to not readable at all.

Predict. A linear regression, a depth-3 tree and gradient boosting with 200 trees. How much R² do the two readable models give up?

Readability has a price, here

linear regression

0.602
CV R² ± 0.017

tree, depth 3

0.526
CV R² ± 0.016

gradient boosting

0.823
CV R² ± 0.007 · test 0.825
Twenty-two points of R² is too much to give away: here the black box wins. The gap is not always there: in the lab, on credit data, a logistic regression and a random forest are statistically tied, and then the interpretable model should be used (Rudin, 2019).

What the readable models say

eight coefficients

$10,000 more income → +$44,140 (with the others fixed); one degree north → −$41,300.

rooms −11.65 and bedrooms +63.55 have opposite signs: they are correlated (0.85), so "one more bedroom at a fixed number of rooms" means converting a room.

eight rules

Anyone can follow a district down the tree:

income above $78,000 → $457,900.

From now on model is the black box: 200 trees with up to 16 leaves each. The rest of the lesson opens it.
Stage 2 · which features does the model rely on, and how?

Permutation importance: shuffle one column

  1. Take held-out data and the model's score on it.
  2. Shuffle one column: its values stay, its link with the target and with the rest of each row breaks.
  3. Measure the drop in the score. Repeat for every column (Breiman, 2001).

for col in X_test.columns:
    X_perm = X_test.copy()
    X_perm[col] = rng.permutation(X_perm[col].to_numpy())
    drops[col] = base - r2_score(y_test, model.predict(X_perm))
  
Predict. R² is 0.825 with nothing shuffled. Can shuffling one column cost more than 0.825?

Two rankings of the same model

permutationMDI
latitude1.3460.103
longitude1.1870.117
income0.5320.567
occupancy0.1340.130
rooms0.0560.024
population0.0030.009

The by-hand loop gives the same story: income 0.539, population 0.002.

Shuffling latitude "costs" 1.330: R² from 0.825 to about −0.5, much worse than predicting the average for everybody.

The explanation comes in three slides.

Held-out data, not training data

A forest with two noise columns: a continuous one and a coin flip.

A random forest with two noise columns: MDI gives the continuous noise 0.018 against 0.002 for the coin flip; permutation on the training set gives it 0.016, on the test set 0.000
MDI: noise 0.018, coin 0.002. It rewards many distinct values.
permutation on train: noise 0.016. Fooled the same way.
permutation on test: noise −0.000. The number to report.

Correlated features (1): shared credit

Add an exact copy of income and refit the forest.

MDIperm. on test
income alone0.5160.754
income0.2620.196
income_copy0.2590.196
both shuffled together0.748

Shuffle one copy and the trees that split on the other still work.

Together the two copies get 0.392, half of what the information is worth.

A "useless" feature may just have a substitute: permute correlated groups together.

Correlated features (2): impossible rows

  • Latitude and longitude: correlation −0.92, because California is a diagonal strip.
  • Shuffle latitude alone: a San Francisco longitude gets a San Diego latitude, a point in the Pacific.
45%
of the shuffled districts are more than 0.25° (about 25 km) from any real one

The model never saw such places. Part of the 1.330 measures latitude; part measures how badly the model behaves off the map.

Fix: treat the correlated pair as one feature, "location".

Partial dependence: in which direction, with what shape?

Set feature \(j\) to a value \(v\) in every row, keep the rest, average the predictions (Friedman, 2001):

\[ \mathrm{PD}_j(v) = \frac{1}{n}\sum_{i=1}^{n} \hat f\bigl(x_{i,1}, \dots,\, v,\, \dots, x_{i,d}\bigr) \]

by_hand = [model.predict(X_test.assign(income=v)).mean() for v in grid]
  
Predict. As median income grows from $15,000 to $100,000, does the predicted value grow steadily, or does it saturate?

The PDP of income, by hand

income ($10k)1.523456810
average prediction ($1k)142.6150.6173.7205.1229.5280.6358.6379.8

flat at the bottom

1.5 → 2 adds only 8.0

steep in the middle

5 → 8 adds 129.1

flattening again

8 → 10 adds 21.2, partly the data's cap ($500,001)

partial_dependence(..., method='brute') returns the same eight numbers.

ICE: one curve per row

ICE curves for income: every district gains when income rises from $20k to $80k, on average +208k, but from +139k to +269k depending on the district

Income from $20,000 to $80,000: +$208,000 on average (the PDP), but from +$139,000 to +$269,000 district by district (5th to 95th percentile).

Parallel ICE curves: the PDP tells the whole story. Fanning out: the model learned interactions, and the average hides them.

The extrapolation trap

Map of California districts with the points a latitude PDP creates; most moved copies fall in the Pacific or the desert
latitude set to33°35°37°39°41°
rows off the map87%35%18%58%62%
The PDP value at 33° is mostly the model's guess about places that do not exist. Check ICE curves and the data's range; plot correlated pairs jointly; ALE plots avoid this (Apley & Zhu, 2020).
Stage 3 · from global to local

SHAP: why this prediction, for me?

Split one prediction into contributions, one per feature, that add up exactly:

\[ \hat f(x) = \underbrace{\phi_0}_{\text{base value}} + \phi_1 + \phi_2 + \dots + \phi_d \]
  • Features are players; the prediction is the team's payout (Shapley, 1953; Lundberg & Lee, 2017).
  • Value of a coalition \(S\): set the features in \(S\) to the row's values, take the others from a background dataset, average.
  • \(\phi_j\): the average of \(v(S \cup \{j\}) - v(S)\) over the coalitions \(j\) can join, every order of arrival equally likely.

A toy model, small enough for a calculator

\[ f = 100 + 80\,\text{rich} + 60\,\text{coastal} + 40\,\text{rich}\cdot\text{coastal} + 20\,\text{big} \quad (\$1{,}000) \]
  • Background: four districts, worth 120, 180, 160 and 100. So \(v(\varnothing) = 140\).
  • We explain a rich, coastal, not big district: \(f(x) = 280\).
Predict. Being rich and coastal is worth 40 more than the two separately. How should Shapley split those 40?

Follow the arrows where "rich" joins

The 8 coalitions of the toy model with their values; the four arrows where rich joins a coalition contribute +70, +90, +70, +90

\(v(\{\text{rich}\})\): set rich = 1 in the four districts: 200, 180, 280, 180, average 210.

\[ \phi_{\text{rich}} = \tfrac13(210-140) + \tfrac16(285-195) + \tfrac16(205-135) + \tfrac13(280-190) = 80 \]

Every number makes sense

rich: 80

main effect 80 × (1 − ¼) = 60, plus half of the interaction, 20

coastal: 65

45 + 20: the interaction is split equally

big: −5

20 × (0 − ¼): not big, while a quarter of the background is

80 + 65 − 5 = 140 = 280 − 140. The only attribution with these four properties:

efficiency (they add up) · symmetry · dummy (no effect → 0) · additivity (a sum of models → a sum of values)

shap.TreeExplainer on a tree that reproduces the formula returns [80, 65, −5] and base value 140.

Simulator: every coalition, every arrival order

Predict. Turn coastal off. Does the interaction still split in half? Does the waterfall still land on the prediction?

On the real model: a San Francisco district

  • 8 features: \(2^8 = 256\) coalitions × 100 background districts = 25,600 predictions.
  • Brute force and TreeExplainer agree to the sixth decimal (largest difference 2e-06).
\[ \underbrace{207.39}_{\text{base value}} + \underbrace{222.35}_{\sum \phi} = 429.74 = \texttt{model.predict} \]
With 20 features brute force would need a million coalitions per row. TreeExplainer gets the same exact numbers by walking each tree once per row (Lundberg et al., 2020).

Reading a waterfall

Waterfalls for two districts: San Francisco, predicted 430k from a base of 207k; Fresno, predicted 271k

San Francisco (430k): longitude +185.6 and latitude −64.6, so location +121.0; low occupancy +66.3; old houses +34.2; an ordinary income, +2.6.

Fresno (271k): a high income (7.22) adds +145.2; location −88.3.

The same feature pushes up in one row and down in another: a local explanation is about this row.

Global: local explanations, stacked

Beeswarm of 300 districts: high incomes push the prediction up, high occupancy and northern latitudes push it down

300 districts × 8 SHAP values. Mean |SHAP|, in $1,000: latitude 76.6, longitude 64.1, income 44.2, occupancy 22.5 ...

The beeswarm shows direction too: high incomes on the right, high occupancies and northern latitudes on the left.

Three rankings, three questions

MDI

income first (0.567)

what did training use?

permutation

latitude first, a drop larger than 1, inflated by impossible rows

what does the score need?

mean |SHAP|

latitude first, 76.6 against 44.2 for income, in $1,000

how far does each feature move the predictions?

Choose the question before the method.

LIME: a linear model around one row

  1. perturb: thousands of points around the row
  2. predict: ask the black box about each
  3. weight: the closer, the heavier (kernel width \(w\))
  4. fit a weighted linear model
San Franciscoby handlime
income42.142.0
occupancy−49.9−48.9
latitude−89.3−91.8
longitude−53.6−55.5

In $1,000 per standard deviation (Ribeiro, Singh & Guestrin, 2016).

SHAP or LIME? They answer different questions

Predict. Same district, same feature: SHAP gives income +2.6, LIME gives +42.1 per standard deviation. Which one is wrong?

SHAP

how much does this district's actual income move its prediction away from the average? Its income is close to average: not much.

"why is this house expensive?"

LIME

what would happen if income changed? The local slope, which is steep.

"what would change the price?"

How local is "local"?

width0.51.02.125.0std, 5 seeds
income54.754.842.138.21.5
house_age−9.91.31.31.01.3
occupancy−71.0−67.4−49.9−45.00.9
One-feature LIME: the model's staircase curve of price against income, and three local lines with slopes +93k, +74k and +56k
The seeds matter little; the kernel width matters a lot, and house_age even changes sign. The default \(0.75\sqrt{d} = 2.12\) is a convention, not a law.
Stage 4 · what can an explanation honestly claim?

Explanation ≠ causation

  • "Around this district, one standard deviation more occupancy lowers the predicted price by about $50,000" is a true statement about the model.
  • It does not mean that moving more people into a house makes it cheaper: crowded households are a marker of poorer neighborhoods.
  • For auditing: if a credit model leans on the postcode, it may use it as a proxy for income or ethnicity.
To claim that a feature causes an outcome you need experiments or causal inference, not SHAP.

Right for the wrong reasons

Hospital A: patients too sick to stand get the portable scanner. Hospital B: scanners assigned at random.

Predict. A model trained on hospital A scores 93.4% on a validation split of A. What does it score on hospital B?

hospital B

53.8%
barely better than a coin

permutation importance

scanner 0.393; medical markers 0.012 at most

markers only

73.0% on A, 74.7% on B

No validation on A could have warned you. The explanation did; only domain knowledge says the reliance will not survive.

A checklist for explaining a model

  1. Try an interpretable model first; if it is as good on the same folds, use it.
  2. Compute importances on held-out data.
  3. Find correlated groups and permute or explain them together.
  4. Look at ICE curves, not only the PDP; check that the plotted values are inside the data.
  5. Use SHAP for local explanations; state the background you used.
  6. When a feature dominates, ask when is this value recorded? (leakage).
  7. Never read an explanation as a causal claim.
Explanations are estimates with choices inside (background, kernel width, correlated features), and they can be gamed (Slack et al., 2020): evidence to check, not a certificate.
Lab · about a minute of computing

Explaining credit decisions

  • German credit: 1,000 loans, 20 attributes, 30% went bad.
  • Scorecard or forest? Logistic regression ROC AUC 0.771, random forest 0.785: a fold-by-fold difference of +0.014 ± 0.023, a tie.
  • Open the forest: MDI against permutation, PDP and ICE, SHAP globally and for the riskiest applicant.
  • Does the model need personal_status? Then plant a leak (collection letters, sent after a loan goes bad): CV ROC AUC 0.785 → 0.927, and the explanations find it.

Download the lab notebook and open it in Jupyter or Colab (File → Upload notebook).

Summary

  1. Before explaining a black box, check whether a readable model is as good.
  2. Permutation importance on held-out data; correlated features share credit and create impossible rows.
  3. PDP for the average shape, ICE for the rows behind it; beware extrapolation.
  4. SHAP: contributions that add up exactly to prediction − base value.
  5. LIME: the local slope, sensitive to the kernel width.
  6. An explanation describes the model, not the world.