Skill Tracer
Skills › Scientific Agent Skills › shap › workflows.md

workflows.md

598 lines · skills/shap/references/workflows.md at 5f2bc03

Open on GitHub
1# SHAP Workflows
2
3These workflows target SHAP 0.52.0 and the `shap.Explanation` API. Adapt model-specific code, but preserve the decisions about output, background, masking, validation, and reporting.
4

SHAP Workflows

These workflows target SHAP 0.52.0 and the shap.Explanation API. Adapt model-specific code, but preserve the decisions about output, background, masking, validation, and reporting.

Raw lines 1 to 4
5## Workflow 1: Reproducible Tabular Audit
6

Workflow 1: Reproducible Tabular Audit

Raw lines 5 to 6
7### 1. Freeze the predictive context
8
9Record:
11- dataset and split identifiers;
12- model and preprocessing artifact versions;
13- package versions;
14- feature schema and order;
15- output method/index/name/units;
16- background population and sampling rule;
17- explainer/masker configuration;
18- evaluation row selection.
20Do not begin interpretation until predictive performance is acceptable for the intended population.

1. Freeze the predictive context

Record:

  • dataset and split identifiers;
  • model and preprocessing artifact versions;
  • package versions;
  • feature schema and order;
  • output method/index/name/units;
  • background population and sampling rule;
  • explainer/masker configuration;
  • evaluation row selection.

Do not begin interpretation until predictive performance is acceptable for the intended population.

Raw lines 7 to 21
22### 2. Build the background from training/reference data
24```python
25rng_seed = 7
26background = shap.sample(X_train, 100, random_state=rng_seed)
27```
29Use a shared background for results intended for direct comparison.

2. Build the background from training/reference data

rng_seed = 7
background = shap.sample(X_train, 100, random_state=rng_seed)

Use a shared background for results intended for direct comparison.

Raw lines 22 to 30
31### 3. Explain held-out rows
33```python
34explainer = shap.Explainer(
35 model,
36 background,
37 algorithm="tree",
38 seed=rng_seed,
40all_outputs = explainer(X_test)
41```

3. Explain held-out rows

explainer = shap.Explainer(
    model,
    background,
    algorithm="tree",
    seed=rng_seed,
)
all_outputs = explainer(X_test)
Raw lines 31 to 42
43### 4. Select and validate the output
45```python
46if all_outputs.values.ndim == 3:
47 explanation = all_outputs[..., class_index]
48 expected = model.predict_proba(X_test)[:, class_index]
49else:
50 explanation = all_outputs
51 expected = model.predict(X_test)
53reconstructed = (
54 np.asarray(explanation.base_values)
55 + np.asarray(explanation.values).sum(axis=1)
57max_abs_error = float(np.max(np.abs(reconstructed - expected)))
58np.testing.assert_allclose(reconstructed, expected, rtol=1e-5, atol=1e-6)
59```
61This example is appropriate only when the explainer's selected output matches `predict` or `predict_proba`. For raw-margin models, compute the corresponding margin instead.

4. Select and validate the output

if all_outputs.values.ndim == 3:
    explanation = all_outputs[..., class_index]
    expected = model.predict_proba(X_test)[:, class_index]
else:
    explanation = all_outputs
    expected = model.predict(X_test)

reconstructed = (
    np.asarray(explanation.base_values)
    + np.asarray(explanation.values).sum(axis=1)
)
max_abs_error = float(np.max(np.abs(reconstructed - expected)))
np.testing.assert_allclose(reconstructed, expected, rtol=1e-5, atol=1e-6)

This example is appropriate only when the explainer's selected output matches predict or predict_proba. For raw-margin models, compute the corresponding margin instead.

Raw lines 43 to 62
63### 5. Move from global to local
65```python
66shap.plots.bar(explanation, max_display=20)
67shap.plots.beeswarm(explanation, max_display=20)
69global_importance = explanation.abs.mean(0)
70top_index = int(np.argmax(global_importance.values))
71top_feature = global_importance.feature_names[top_index]
72shap.plots.scatter(explanation[:, top_feature], color=explanation)
74shap.plots.waterfall(explanation[selected_row])
75```
77Select local rows using a documented criterion: high error, threshold proximity, representative quantile, or predeclared case.

5. Move from global to local

shap.plots.bar(explanation, max_display=20)
shap.plots.beeswarm(explanation, max_display=20)

global_importance = explanation.abs.mean(0)
top_index = int(np.argmax(global_importance.values))
top_feature = global_importance.feature_names[top_index]
shap.plots.scatter(explanation[:, top_feature], color=explanation)

shap.plots.waterfall(explanation[selected_row])

Select local rows using a documented criterion: high error, threshold proximity, representative quantile, or predeclared case.

Raw lines 63 to 78
79### 6. Publish an audit record
81Include:
83- model metric table;
84- global attribution distribution;
85- selected feature relationships;
86- local explanations with selection rules;
87- background and masking sensitivity;
88- additivity error;
89- non-causal and non-fairness caveats.

6. Publish an audit record

Include:

  • model metric table;
  • global attribution distribution;
  • selected feature relationships;
  • local explanations with selection rules;
  • background and masking sensitivity;
  • additivity error;
  • non-causal and non-fairness caveats.
Raw lines 79 to 90
91## Workflow 2: Regression
93```python
94explainer = shap.Explainer(regressor, background)
95explanation = explainer(X_test)
97prediction = regressor.predict(X_test)
98reconstructed = explanation.base_values + explanation.values.sum(axis=1)
99np.testing.assert_allclose(reconstructed, prediction, rtol=1e-5, atol=1e-6)
100```
102State whether the target was transformed. If the model predicts log target values, SHAP values are in log-target units unless the explained callable reverses the transform.
104For transformed targets, choose one:
106- explain the model's native output and label units accurately;
107- wrap the inverse-transformed prediction in a model-agnostic callable and explain that output;
108- provide both and explain why additive decompositions differ.

Workflow 2: Regression

explainer = shap.Explainer(regressor, background)
explanation = explainer(X_test)

prediction = regressor.predict(X_test)
reconstructed = explanation.base_values + explanation.values.sum(axis=1)
np.testing.assert_allclose(reconstructed, prediction, rtol=1e-5, atol=1e-6)

State whether the target was transformed. If the model predicts log target values, SHAP values are in log-target units unless the explained callable reverses the transform.

For transformed targets, choose one:

  • explain the model's native output and label units accurately;
  • wrap the inverse-transformed prediction in a model-agnostic callable and explain that output;
  • provide both and explain why additive decompositions differ.
Raw lines 91 to 109
110## Workflow 3: Binary Classification
112Binary model outputs vary:
114- scikit-learn tree classifiers commonly return one output per class;
115- XGBoost/LightGBM raw tree outputs commonly return one margin;
116- a model-agnostic `predict_proba` callable returns two columns.

Workflow 3: Binary Classification

Binary model outputs vary:

  • scikit-learn tree classifiers commonly return one output per class;
  • XGBoost/LightGBM raw tree outputs commonly return one margin;
  • a model-agnostic predict_proba callable returns two columns.
Raw lines 110 to 117
118### Explicit probability-space tree explanation
120```python
121explainer = shap.TreeExplainer(
122 model,
123 data=background,
124 feature_perturbation="interventional",
125 model_output="probability",
127all_outputs = explainer(X_test)
129if all_outputs.values.ndim == 3:
130 positive = all_outputs[..., positive_class_index]
131else:
132 positive = all_outputs
134expected = model.predict_proba(X_test)[:, positive_class_index]
135reconstructed = positive.base_values + positive.values.sum(axis=1)
136np.testing.assert_allclose(reconstructed, expected, rtol=1e-5, atol=1e-6)
137```
139If the model family exposes only one positive-class probability output, do not attempt a second class slice.

Explicit probability-space tree explanation

explainer = shap.TreeExplainer(
    model,
    data=background,
    feature_perturbation="interventional",
    model_output="probability",
)
all_outputs = explainer(X_test)

if all_outputs.values.ndim == 3:
    positive = all_outputs[..., positive_class_index]
else:
    positive = all_outputs

expected = model.predict_proba(X_test)[:, positive_class_index]
reconstructed = positive.base_values + positive.values.sum(axis=1)
np.testing.assert_allclose(reconstructed, expected, rtol=1e-5, atol=1e-6)

If the model family exposes only one positive-class probability output, do not attempt a second class slice.

Raw lines 118 to 140
141### Threshold-focused analysis
143```python
144probability = model.predict_proba(X_test)[:, positive_class_index]
145distance = np.abs(probability - decision_threshold)
146near_threshold = np.argsort(distance)[:25]
148shap.plots.heatmap(positive[near_threshold])
149```
151Threshold proximity is a legitimate predeclared selection rule. It does not make SHAP a recourse method.

Threshold-focused analysis

probability = model.predict_proba(X_test)[:, positive_class_index]
distance = np.abs(probability - decision_threshold)
near_threshold = np.argsort(distance)[:25]

shap.plots.heatmap(positive[near_threshold])

Threshold proximity is a legitimate predeclared selection rule. It does not make SHAP a recourse method.

Raw lines 141 to 152
153## Workflow 4: Multiclass or Multi-Output Models
155```python
156all_outputs = explainer(X_test)
157print(all_outputs.values.shape)
158print(all_outputs.output_names)
160per_output = {
161 str(name): all_outputs[..., index]
162 for index, name in enumerate(all_outputs.output_names)
164```
166For each output:
1681. validate against the corresponding model output;
1692. generate its own global summary;
1703. keep axis units and scales explicit;
1714. report class prevalence and performance.
173Do not:
175- use `all_outputs[class_index]` to select an output;
176- assume old list-of-arrays behavior;
177- average signed attributions across classes;
178- compare classes after using different backgrounds.
180For many outputs, explain only predeclared or top-ranked outputs:
182```python
183selected = explainer(
184 X_subset,
185 outputs=shap.Explanation.argsort.flip[:5],
187```
189The `outputs` option is explainer-dependent. Verify selected indexes/names, especially when top outputs can vary by row.

Workflow 4: Multiclass or Multi-Output Models

all_outputs = explainer(X_test)
print(all_outputs.values.shape)
print(all_outputs.output_names)

per_output = {
    str(name): all_outputs[..., index]
    for index, name in enumerate(all_outputs.output_names)
}

For each output:

  1. validate against the corresponding model output;
  2. generate its own global summary;
  3. keep axis units and scales explicit;
  4. report class prevalence and performance.

Do not:

  • use all_outputs[class_index] to select an output;
  • assume old list-of-arrays behavior;
  • average signed attributions across classes;
  • compare classes after using different backgrounds.

For many outputs, explain only predeclared or top-ranked outputs:

selected = explainer(
    X_subset,
    outputs=shap.Explanation.argsort.flip[:5],
)

The outputs option is explainer-dependent. Verify selected indexes/names, especially when top outputs can vary by row.

Raw lines 153 to 190
191## Workflow 5: Pipelines and Feature Names
193Choose whether to explain raw or transformed features.

Workflow 5: Pipelines and Feature Names

Choose whether to explain raw or transformed features.

Raw lines 191 to 194
195### Transformed-space, model-specific explanation
197```python
198X_train_t = preprocessor.transform(X_train)
199X_test_t = preprocessor.transform(X_test)
200feature_names = preprocessor.get_feature_names_out()
202explainer = shap.Explainer(
203 model,
204 X_train_t[:100],
205 feature_names=feature_names.tolist(),
207explanation = explainer(X_test_t)
208```
210Benefits: speed and specialized explainer support.
212Costs: one source feature may expand into many encoded columns. Aggregate one-hot levels only after preserving signed local contributions and documenting the aggregation:
214```python
215source_group_value = explanation.values[:, source_column_indices].sum(axis=1)
216```
218Sum signed values for a local additive group. For global importance, decide whether the quantity is:
220- mean absolute value of the summed group; or
221- sum of each encoded column's mean absolute value.
223They answer different questions.

Transformed-space, model-specific explanation

X_train_t = preprocessor.transform(X_train)
X_test_t = preprocessor.transform(X_test)
feature_names = preprocessor.get_feature_names_out()

explainer = shap.Explainer(
    model,
    X_train_t[:100],
    feature_names=feature_names.tolist(),
)
explanation = explainer(X_test_t)

Benefits: speed and specialized explainer support.

Costs: one source feature may expand into many encoded columns. Aggregate one-hot levels only after preserving signed local contributions and documenting the aggregation:

source_group_value = explanation.values[:, source_column_indices].sum(axis=1)

Sum signed values for a local additive group. For global importance, decide whether the quantity is:

  • mean absolute value of the summed group; or
  • sum of each encoded column's mean absolute value.

They answer different questions.

Raw lines 195 to 224
225### Raw-space, end-to-end explanation
227```python
228def positive_probability(frame):
229 frame = pd.DataFrame(frame, columns=raw_background.columns)
230 return pipeline.predict_proba(frame)[:, positive_class_index]
232masker = shap.maskers.Independent(raw_background, max_samples=100)
233explainer = shap.PermutationExplainer(
234 positive_probability,
235 masker,
236 feature_names=raw_background.columns.tolist(),
237 seed=7,
239explanation = explainer(
240 raw_test,
241 max_evals=(2 * raw_test.shape[1] + 1) * 5,
243```
245Benefits: source-level attribution.
247This fragment assumes numeric raw columns. Mixed categorical/object data require a tested domain masker or a numeric transformed representation; restoring DataFrame column names alone does not fix numeric masker invariance checks.
249Costs: slower model-agnostic evaluation and potentially unrealistic raw-column combinations.

Raw-space, end-to-end explanation

def positive_probability(frame):
    frame = pd.DataFrame(frame, columns=raw_background.columns)
    return pipeline.predict_proba(frame)[:, positive_class_index]

masker = shap.maskers.Independent(raw_background, max_samples=100)
explainer = shap.PermutationExplainer(
    positive_probability,
    masker,
    feature_names=raw_background.columns.tolist(),
    seed=7,
)
explanation = explainer(
    raw_test,
    max_evals=(2 * raw_test.shape[1] + 1) * 5,
)

Benefits: source-level attribution.

This fragment assumes numeric raw columns. Mixed categorical/object data require a tested domain masker or a numeric transformed representation; restoring DataFrame column names alone does not fix numeric masker invariance checks.

Costs: slower model-agnostic evaluation and potentially unrealistic raw-column combinations.

Raw lines 225 to 250
251## Workflow 6: Error and Loss Analysis
253Attributing predictions is not the same as attributing errors.

Workflow 6: Error and Loss Analysis

Attributing predictions is not the same as attributing errors.

Raw lines 251 to 254
255### Analyze errors as a cohort
257```python
258prediction = model.predict(X_test)
259error_mask = prediction != y_test.to_numpy()
261cohorts = {
262 "correct": explanation[~error_mask],
263 "incorrect": explanation[error_mask],
265shap.plots.bar(cohorts, max_display=20)
266```
268Include group sizes. Different attribution magnitudes do not identify the cause of error by themselves.

Analyze errors as a cohort

prediction = model.predict(X_test)
error_mask = prediction != y_test.to_numpy()

cohorts = {
    "correct": explanation[~error_mask],
    "incorrect": explanation[error_mask],
}
shap.plots.bar(cohorts, max_display=20)

Include group sizes. Different attribution magnitudes do not identify the cause of error by themselves.

Raw lines 255 to 269
270### Decompose a defined classification loss
272Do not assume `TreeExplainer(model_output="log_loss")` computes scikit-learn's cross entropy for every classifier. In 0.52.0, sklearn 1.9.1 gradient-boosting classifiers can have an unrecognized objective, and random forests can feed class probabilities into a logistic-margin loss transform. Test the actual per-row loss numerically; successful construction is insufficient.
274For numeric tabular inputs, a model-agnostic callable gives an explicit alternative. Explain each true-label cohort with that label held fixed; labels are not players in the feature game:
276```python
277epsilon = 1e-15 # The explained loss is explicitly clipped negative log likelihood.
278loss_explanations = {}
279for class_index, label in enumerate(model.classes_):
280 rows = X_test.loc[np.asarray(y_test) == label]
281 if rows.empty:
282 continue
284 def row_loss(batch, output_index=class_index):
285 frame = pd.DataFrame(batch, columns=X_train.columns)
286 probability = model.predict_proba(frame)[:, output_index]
287 return -np.log(np.clip(probability, epsilon, 1.0))
289 loss_explainer = shap.PermutationExplainer(
290 row_loss, background, seed=7,
291 )
292 loss_exp = loss_explainer(rows, max_evals=(2 * rows.shape[1] + 1) * 5)
293 np.testing.assert_allclose(
294 loss_exp.base_values + loss_exp.values.sum(axis=1),
295 row_loss(rows), rtol=1e-5, atol=1e-6,
296 )
297 loss_explanations[str(label)] = loss_exp
298```
300The following specialized path passed a tiny native binary XGBoost 3.4.1 fixture. For that numeric-only fixture the model was constructed with `enable_categorical=False`; XGBoost 3.4.1 defaults this flag to true, which SHAP 0.52.0 rejects on the interventional path even for purely numeric inputs. Preserve actual categorical semantics rather than disabling the flag on a model that uses categorical features:
302```python
303loss_explainer = shap.TreeExplainer(
304 binary_margin_model, data=background,
305 feature_perturbation="interventional", model_output="log_loss",
307loss_exp = loss_explainer(X_eval, y_encoded) # Labels encoded as 0/1.
308# 0.52.0 can return callable base_values. Materialize each label's baseline.
309loss_exp.base_values = np.asarray([
310 loss_explainer.expected_value(int(label)) for label in y_encoded
311])
312```
314Before plotting this path, compare the reconstruction with explicit per-row negative log likelihood from that exact model. Inspect any output axis and use only the intended output. Regression models with supported squared-error objectives can use squared loss under the same API name, so do not label their units as log loss.
316Loss attributions describe the model's loss under masking. A high value does not prove label error or the cause of poor performance; inspect residuals, labels, calibration, and the reference design separately.

Decompose a defined classification loss

Do not assume TreeExplainer(model_output="log_loss") computes scikit-learn's cross entropy for every classifier. In 0.52.0, sklearn 1.9.1 gradient-boosting classifiers can have an unrecognized objective, and random forests can feed class probabilities into a logistic-margin loss transform. Test the actual per-row loss numerically; successful construction is insufficient.

For numeric tabular inputs, a model-agnostic callable gives an explicit alternative. Explain each true-label cohort with that label held fixed; labels are not players in the feature game:

epsilon = 1e-15  # The explained loss is explicitly clipped negative log likelihood.
loss_explanations = {}
for class_index, label in enumerate(model.classes_):
    rows = X_test.loc[np.asarray(y_test) == label]
    if rows.empty:
        continue

    def row_loss(batch, output_index=class_index):
        frame = pd.DataFrame(batch, columns=X_train.columns)
        probability = model.predict_proba(frame)[:, output_index]
        return -np.log(np.clip(probability, epsilon, 1.0))

    loss_explainer = shap.PermutationExplainer(
        row_loss, background, seed=7,
    )
    loss_exp = loss_explainer(rows, max_evals=(2 * rows.shape[1] + 1) * 5)
    np.testing.assert_allclose(
        loss_exp.base_values + loss_exp.values.sum(axis=1),
        row_loss(rows), rtol=1e-5, atol=1e-6,
    )
    loss_explanations[str(label)] = loss_exp

The following specialized path passed a tiny native binary XGBoost 3.4.1 fixture. For that numeric-only fixture the model was constructed with enable_categorical=False; XGBoost 3.4.1 defaults this flag to true, which SHAP 0.52.0 rejects on the interventional path even for purely numeric inputs. Preserve actual categorical semantics rather than disabling the flag on a model that uses categorical features:

loss_explainer = shap.TreeExplainer(
    binary_margin_model, data=background,
    feature_perturbation="interventional", model_output="log_loss",
)
loss_exp = loss_explainer(X_eval, y_encoded)  # Labels encoded as 0/1.
# 0.52.0 can return callable base_values. Materialize each label's baseline.
loss_exp.base_values = np.asarray([
    loss_explainer.expected_value(int(label)) for label in y_encoded
])

Before plotting this path, compare the reconstruction with explicit per-row negative log likelihood from that exact model. Inspect any output axis and use only the intended output. Regression models with supported squared-error objectives can use squared loss under the same API name, so do not label their units as log loss.

Loss attributions describe the model's loss under masking. A high value does not prove label error or the cause of poor performance; inspect residuals, labels, calibration, and the reference design separately.

Raw lines 270 to 317
318## Workflow 7: Cohort and Fairness Investigation
320```python
321cohorts = explanation.cohorts(group_labels)
322shap.plots.bar(cohorts.abs.mean(0), max_display=20)
323```
325For each cohort also compute:
327- sample size;
328- outcome prevalence;
329- score distribution;
330- discrimination/performance;
331- calibration;
332- confusion-matrix rates at operational thresholds;
333- missingness and feature distribution.
335SHAP contributes descriptive model-use evidence. It does not replace a fairness framework.
337Avoid claims such as:
339- "protected attribute importance should be zero";
340- "the model is unbiased because group plots look similar";
341- "remove the proxy feature to guarantee fairness."
343Use a shared background for direct comparison. If a cohort-specific baseline is required, report both a shared-background and cohort-background analysis.

Workflow 7: Cohort and Fairness Investigation

cohorts = explanation.cohorts(group_labels)
shap.plots.bar(cohorts.abs.mean(0), max_display=20)

For each cohort also compute:

  • sample size;
  • outcome prevalence;
  • score distribution;
  • discrimination/performance;
  • calibration;
  • confusion-matrix rates at operational thresholds;
  • missingness and feature distribution.

SHAP contributes descriptive model-use evidence. It does not replace a fairness framework.

Avoid claims such as:

  • "protected attribute importance should be zero";
  • "the model is unbiased because group plots look similar";
  • "remove the proxy feature to guarantee fairness."

Use a shared background for direct comparison. If a cohort-specific baseline is required, report both a shared-background and cohort-background analysis.

Raw lines 318 to 344
345## Workflow 8: Model Comparison
347Before comparing attribution:
3491. evaluate models on identical rows;
3502. explain the same target/output and units;
3513. use the same background and masking semantics;
3524. align feature representations;
3535. validate each model separately.
355```python
356importance = {}
358for name, exp in explanations.items():
359 importance[name] = pd.Series(
360 np.abs(exp.values).mean(axis=0),
361 index=exp.feature_names,
362 name=name,
363 )
365importance_frame = pd.concat(importance.values(), axis=1)
366```
368Compare:
370- rank correlation;
371- signed feature distributions;
372- local agreement on identical rows;
373- stability across refits;
374- performance and calibration.
376Do not treat agreement as truth. Correlated features can cause equivalent models to distribute credit differently.

Workflow 8: Model Comparison

Before comparing attribution:

  1. evaluate models on identical rows;
  2. explain the same target/output and units;
  3. use the same background and masking semantics;
  4. align feature representations;
  5. validate each model separately.
importance = {}

for name, exp in explanations.items():
    importance[name] = pd.Series(
        np.abs(exp.values).mean(axis=0),
        index=exp.feature_names,
        name=name,
    )

importance_frame = pd.concat(importance.values(), axis=1)

Compare:

  • rank correlation;
  • signed feature distributions;
  • local agreement on identical rows;
  • stability across refits;
  • performance and calibration.

Do not treat agreement as truth. Correlated features can cause equivalent models to distribute credit differently.

Raw lines 345 to 377
378## Workflow 9: Feature Engineering
380Use SHAP to generate hypotheses, then validate them out of sample.
3821. Fit a baseline in a training fold.
3832. Inspect scatter plots and interaction candidates.
3843. Propose a transformation based on domain knowledge.
3854. Build the feature inside cross-validation.
3865. Compare predictive metrics and calibration on untouched data.
3876. Recompute explanations with the same reference design.
389Avoid engineering features after repeatedly inspecting the final test set.
391For a nonlinear pattern:
393```python
394shap.plots.scatter(explanation[:, feature_name], color=explanation)
395```
397For tree interactions:
399```python
400interaction_explainer = shap.TreeExplainer(
401 model, model_output="raw", feature_perturbation="tree_path_dependent",
403interaction = interaction_explainer.shap_interaction_values(X_subset)
404```
406Use interaction values as model diagnostics, not proof of real-world synergy.

Workflow 9: Feature Engineering

Use SHAP to generate hypotheses, then validate them out of sample.

  1. Fit a baseline in a training fold.
  2. Inspect scatter plots and interaction candidates.
  3. Propose a transformation based on domain knowledge.
  4. Build the feature inside cross-validation.
  5. Compare predictive metrics and calibration on untouched data.
  6. Recompute explanations with the same reference design.

Avoid engineering features after repeatedly inspecting the final test set.

For a nonlinear pattern:

shap.plots.scatter(explanation[:, feature_name], color=explanation)

For tree interactions:

interaction_explainer = shap.TreeExplainer(
    model, model_output="raw", feature_perturbation="tree_path_dependent",
)
interaction = interaction_explainer.shap_interaction_values(X_subset)

Use interaction values as model diagnostics, not proof of real-world synergy.

Raw lines 378 to 407
408## Workflow 10: Time Series
410Random train/test splits and random backgrounds can leak future information.
4121. Use temporal or rolling-origin validation.
4132. Build lag/rolling features using past data only.
4143. Choose background rows from an allowed historical reference window.
4154. Explain a fixed forecast horizon and output.
4165. preserve time ordering in monitoring plots.
4176. compare seasonal regimes separately.
419```python
420background = X_train.loc[reference_start:reference_end]
421evaluation = X_test.loc[forecast_start:forecast_end]
422explanation = explainer(evaluation)
423```
425Lag features are highly dependent. Compare independent and grouped/domain-constrained maskers, and report instability. A lag attribution is not the causal effect of changing the historical outcome.
427For sequence models that consume a time-by-channel tensor, define whether a "feature" is:
429- one time point;
430- one channel;
431- one time-channel cell;
432- a grouped time window.
434Use a masker consistent with that unit.

Workflow 10: Time Series

Random train/test splits and random backgrounds can leak future information.

  1. Use temporal or rolling-origin validation.
  2. Build lag/rolling features using past data only.
  3. Choose background rows from an allowed historical reference window.
  4. Explain a fixed forecast horizon and output.
  5. preserve time ordering in monitoring plots.
  6. compare seasonal regimes separately.
background = X_train.loc[reference_start:reference_end]
evaluation = X_test.loc[forecast_start:forecast_end]
explanation = explainer(evaluation)

Lag features are highly dependent. Compare independent and grouped/domain-constrained maskers, and report instability. A lag attribution is not the causal effect of changing the historical outcome.

For sequence models that consume a time-by-channel tensor, define whether a "feature" is:

  • one time point;
  • one channel;
  • one time-channel cell;
  • a grouped time window.

Use a masker consistent with that unit.

Raw lines 408 to 435
436## Workflow 11: Text and Images
438Use domain maskers and constrain outputs:
440```python
441# Text
442text_masker = shap.maskers.Text(tokenizer)
443text_explainer = shap.Explainer(
444 text_model_fn,
445 text_masker,
446 algorithm="partition",
447 output_names=class_names,
449text_exp = text_explainer(text_rows)
451# Image
452image_masker = shap.maskers.Image("inpaint_telea", image_shape)
453image_explainer = shap.Explainer(
454 image_model_fn,
455 image_masker,
456 output_names=class_names,
458image_exp = image_explainer(
459 images,
460 max_evals=500,
461 batch_size=50,
462 outputs=shap.Explanation.argsort.flip[:3],
464```
466See [modalities.md](modalities.md) for model wrappers and interpretation cautions.

Workflow 11: Text and Images

Use domain maskers and constrain outputs:

# Text
text_masker = shap.maskers.Text(tokenizer)
text_explainer = shap.Explainer(
    text_model_fn,
    text_masker,
    algorithm="partition",
    output_names=class_names,
)
text_exp = text_explainer(text_rows)

# Image
image_masker = shap.maskers.Image("inpaint_telea", image_shape)
image_explainer = shap.Explainer(
    image_model_fn,
    image_masker,
    output_names=class_names,
)
image_exp = image_explainer(
    images,
    max_evals=500,
    batch_size=50,
    outputs=shap.Explanation.argsort.flip[:3],
)

See modalities.md for model wrappers and interpretation cautions.

Raw lines 436 to 467
468## Workflow 12: Stability and Uncertainty
470Repeat the analysis across:
472- model refits or cross-validation folds;
473- background samples;
474- approximation seeds/budgets;
475- plausible maskers;
476- evaluation cohorts.
478Example background sensitivity:
480```python
481importance_runs = []
483for seed in range(10):
484 background = shap.sample(X_train, 100, random_state=seed)
485 masker = shap.maskers.Independent(background, max_samples=len(background))
486 exp = shap.Explainer(model, masker)(X_test)
487 if exp.values.ndim == 3:
488 exp = exp[..., class_index]
489 importance_runs.append(np.abs(exp.values).mean(axis=0))
491importance_runs = np.stack(importance_runs)
492importance_mean = importance_runs.mean(axis=0)
493importance_low = np.quantile(importance_runs, 0.025, axis=0)
494importance_high = np.quantile(importance_runs, 0.975, axis=0)
495```
497These intervals describe background-sampling variation for a fixed model, not total statistical uncertainty.

Workflow 12: Stability and Uncertainty

Repeat the analysis across:

  • model refits or cross-validation folds;
  • background samples;
  • approximation seeds/budgets;
  • plausible maskers;
  • evaluation cohorts.

Example background sensitivity:

importance_runs = []

for seed in range(10):
    background = shap.sample(X_train, 100, random_state=seed)
    masker = shap.maskers.Independent(background, max_samples=len(background))
    exp = shap.Explainer(model, masker)(X_test)
    if exp.values.ndim == 3:
        exp = exp[..., class_index]
    importance_runs.append(np.abs(exp.values).mean(axis=0))

importance_runs = np.stack(importance_runs)
importance_mean = importance_runs.mean(axis=0)
importance_low = np.quantile(importance_runs, 0.025, axis=0)
importance_high = np.quantile(importance_runs, 0.975, axis=0)

These intervals describe background-sampling variation for a fixed model, not total statistical uncertainty.

Raw lines 468 to 498
499## Workflow 13: Production Explanation Service
501Production requirements:
503- version model, preprocessing, SHAP, background, masker, output, and feature schema together;
504- enforce input column order and dtype;
505- cap request rows and explanation budgets;
506- return output name, units, baseline, contributions, and reconstruction error;
507- avoid exposing sensitive raw feature values;
508- monitor latency and failure rates;
509- test explanations after every model-library or SHAP upgrade.
511Prefer rebuilding the explainer from trusted, versioned components in a controlled process. Python pickle/joblib artifacts can execute code; never deserialize an untrusted model or explainer.
513Example response construction after validation:
515```python
516def format_local_explanation(local_exp, prediction, top_n=10):
517 values = np.asarray(local_exp.values)
518 order = np.argsort(np.abs(values))[::-1][:top_n]
520 reconstructed = float(np.asarray(local_exp.base_values) + values.sum())
521 return {
522 "prediction": float(prediction),
523 "base_value": float(np.asarray(local_exp.base_values)),
524 "reconstructed_output": reconstructed,
525 "max_reconstruction_error": abs(reconstructed - float(prediction)),
526 "output_name": local_exp.output_names,
527 "contributions": [
528 {
529 "feature": local_exp.feature_names[index],
530 "shap_value": float(values[index]),
531 }
532 for index in order
533 ],
534 }
535```
537Do not truncate contributions before checking reconstruction; omitted values still matter.

Workflow 13: Production Explanation Service

Production requirements:

  • version model, preprocessing, SHAP, background, masker, output, and feature schema together;
  • enforce input column order and dtype;
  • cap request rows and explanation budgets;
  • return output name, units, baseline, contributions, and reconstruction error;
  • avoid exposing sensitive raw feature values;
  • monitor latency and failure rates;
  • test explanations after every model-library or SHAP upgrade.

Prefer rebuilding the explainer from trusted, versioned components in a controlled process. Python pickle/joblib artifacts can execute code; never deserialize an untrusted model or explainer.

Example response construction after validation:

def format_local_explanation(local_exp, prediction, top_n=10):
    values = np.asarray(local_exp.values)
    order = np.argsort(np.abs(values))[::-1][:top_n]

    reconstructed = float(np.asarray(local_exp.base_values) + values.sum())
    return {
        "prediction": float(prediction),
        "base_value": float(np.asarray(local_exp.base_values)),
        "reconstructed_output": reconstructed,
        "max_reconstruction_error": abs(reconstructed - float(prediction)),
        "output_name": local_exp.output_names,
        "contributions": [
            {
                "feature": local_exp.feature_names[index],
                "shap_value": float(values[index]),
            }
            for index in order
        ],
    }

Do not truncate contributions before checking reconstruction; omitted values still matter.

Raw lines 499 to 538
539## Workflow 14: Attribution Monitoring
541Attribution drift can reflect:
543- input drift;
544- model-version change;
545- background change;
546- feature pipeline change;
547- genuine changes in model use.
549Keep the model and reference fixed when detecting input-driven attribution drift.
551Numeric summaries:
553```python
554def attribution_summary(exp):
555 values = np.asarray(exp.values)
556 return {
557 "mean": values.mean(axis=0),
558 "mean_abs": np.abs(values).mean(axis=0),
559 "q05": np.quantile(values, 0.05, axis=0),
560 "q50": np.quantile(values, 0.50, axis=0),
561 "q95": np.quantile(values, 0.95, axis=0),
562 }
563```
565Monitor:
567- output and baseline distributions;
568- attribution distributions, not only means;
569- model performance when labels arrive;
570- missingness and schema;
571- group-specific behavior.
573Set thresholds from historical variability and operational consequences, not arbitrary percentage changes.

Workflow 14: Attribution Monitoring

Attribution drift can reflect:

  • input drift;
  • model-version change;
  • background change;
  • feature pipeline change;
  • genuine changes in model use.

Keep the model and reference fixed when detecting input-driven attribution drift.

Numeric summaries:

def attribution_summary(exp):
    values = np.asarray(exp.values)
    return {
        "mean": values.mean(axis=0),
        "mean_abs": np.abs(values).mean(axis=0),
        "q05": np.quantile(values, 0.05, axis=0),
        "q50": np.quantile(values, 0.50, axis=0),
        "q95": np.quantile(values, 0.95, axis=0),
    }

Monitor:

  • output and baseline distributions;
  • attribution distributions, not only means;
  • model performance when labels arrive;
  • missingness and schema;
  • group-specific behavior.

Set thresholds from historical variability and operational consequences, not arbitrary percentage changes.

Raw lines 539 to 574
575## Review Checklist
577- [ ] The explained output and units are explicit.
578- [ ] Background comes from a defensible reference population.
579- [ ] The masker matches feature structure.
580- [ ] Multi-output results are sliced correctly.
581- [ ] Additivity is checked against the correct model output.
582- [ ] Local rows and cohorts use documented selection rules.
583- [ ] Correlation and background sensitivity are evaluated.
584- [ ] Performance accompanies explanations.
585- [ ] Fairness, causal, and recourse claims are separated.
586- [ ] Version and schema metadata are retained.
587- [ ] No untrusted model/explainer artifact is deserialized.

Review Checklist

  • [ ] The explained output and units are explicit.
  • [ ] Background comes from a defensible reference population.
  • [ ] The masker matches feature structure.
  • [ ] Multi-output results are sliced correctly.
  • [ ] Additivity is checked against the correct model output.
  • [ ] Local rows and cohorts use documented selection rules.
  • [ ] Correlation and background sensitivity are evaluated.
  • [ ] Performance accompanies explanations.
  • [ ] Fairness, causal, and recourse claims are separated.
  • [ ] Version and schema metadata are retained.
  • [ ] No untrusted model/explainer artifact is deserialized.
Raw lines 575 to 588
589## Sources
591- SHAP examples: https://shap.readthedocs.io/en/latest/index.html
592- Tabular examples: https://shap.readthedocs.io/en/latest/tabular_examples.html
593- API examples: https://shap.readthedocs.io/en/latest/api_examples.html
594- TreeExplainer: https://shap.readthedocs.io/en/latest/generated/shap.TreeExplainer.html
595- Explanation: https://shap.readthedocs.io/en/latest/generated/shap.Explanation.html
596- Causal interpretation caution: https://shap.readthedocs.io/en/latest/example_notebooks/overviews/Be%20careful%20when%20interpreting%20predictive%20models%20in%20search%20of%20causal%20insights.html
598- Released source / upstream contract: https://github.com/shap/shap/blob/v0.52.0/shap/explainers/_tree.py

Sources

  • SHAP examples: https://shap.readthedocs.io/en/latest/index.html
  • Tabular examples: https://shap.readthedocs.io/en/latest/tabular_examples.html
  • API examples: https://shap.readthedocs.io/en/latest/api_examples.html
  • TreeExplainer: https://shap.readthedocs.io/en/latest/generated/shap.TreeExplainer.html
  • Explanation: https://shap.readthedocs.io/en/latest/generated/shap.Explanation.html
  • Causal interpretation caution: https://shap.readthedocs.io/en/latest/example_notebooks/overviews/Be%20careful%20when%20interpreting%20predictive%20models%20in%20search%20of%20causal%20insights.html
  • Released source / upstream contract: https://github.com/shap/shap/blob/v0.52.0/shap/explainers/_tree.py
Raw lines 589 to 598