SHAP Support
SHAP explanations help audit fitted CartoBoost models after training. For taxi-trip studies, use them to ask which modeled covariates contributed to a prediction: distance, hour, pickup/dropoff memberships, graph-derived columns, neural embedding columns, or fitted tree weights.
SHAP is an explanation layer, not a new model and not proof of causality. It is most useful after the validation design is fixed, because explanations inherit the same feature-generation choices, split protocol, and data limitations as the model being explained.
Install the optional dependency before using SHAP:
uv add shap
For a source checkout:
uv sync --group dev
When To Use It
Use SHAP when you need to:
- compare the contribution of route distance, hour, zone memberships, and spatial features for individual taxi predictions;
- inspect whether sparse pickup/dropoff IDs dominate a model unexpectedly;
- audit graph or neural feature-generation blocks after they have been appended as dense columns;
- verify additive prediction decomposition for debugging and reporting.
Feature names matter. If graph or neural embeddings are appended as generated columns, SHAP explains those generated columns, not the standalone graph or neural training process that produced them.
Basic Usage
import shap
from cartoboost import CartoBoostRegressor
model = CartoBoostRegressor(n_estimators=50, learning_rate=0.1, max_depth=3)
model.fit(X_train, y_train)
explainer = shap.Explainer(model, X_train)
explanation = explainer(X_test)
This is SHAP's general model-callable route. It is appropriate for dense column-level explanations, but its selected algorithm depends on SHAP and the feature count. For a model with many columns, pass a deliberately sized, representative background sample and choose a SHAP algorithm that fits the latency and fidelity you need.
explanation is a shap.Explanation, so it works with SHAP plotting helpers:
shap.plots.beeswarm(explanation)
shap.plots.waterfall(explanation[0])
CartoBoost also provides convenience helpers:
explanation = model.explain_shap(X_test)
explainer = model.make_shap_explainer()
# Preserve interventional/background-based semantics when desired.
explanation = model.explain_shap(X_test, background=X_train)
The module-level helpers are available from
cartoboost.explain; estimator methods are the preferred application
interface because they preserve CartoBoost's fitted feature metadata.
Background-Free Feature Contributions
For conventional hard axis-aligned regression trees with constant leaves,
CartoBoost computes exact path-dependent TreeSHAP in Rust. The fitted node
covers represent the training distribution, so serving does not retain a
background dataset, construct a Python explainer, or require the optional
shap package.
contributions = model.predict(X_test, pred_contrib=True)
# Equivalent explicit spelling:
contributions = model.predict_feature_contributions(X_test)
feature_names = model.feature_name_
feature_values = contributions[:, :-1]
base_value = contributions[:, -1]
reconstructed = base_value + feature_values.sum(axis=1)
The array follows LightGBM's layout: one column per original fitted feature, then the expected prediction in the final column. Internally expanded categorical columns are summed back to their original DataFrame feature. The base value is the initial prediction plus the cover-weighted expected output of every tree; it is constant across rows.
This native surface is path-dependent SHAP. Passing an explicit background=
to the rich helpers retains the existing interventional/background-based SHAP
semantics instead.
Additive Weight Decomposition
By default, SHAP decomposes predictions over input features. CartoBoost can also decompose fitted additive prediction weights: the initial prediction and one component per fitted tree.
explanation = model.explain_shap(
X_test,
background=X_train,
decomposition="weights",
)
The explanation feature names are init_prediction, tree_0, tree_1, and so
on. This is an exact closed-form SHAP decomposition: CartoBoost centers each
component on its background mean and uses the background prediction as the base
value. It runs directly from native additive values and never selects SHAP's
PermutationExplainer, even for models with hundreds of trees. The raw
additive matrix is also available directly:
additive = model.predict_additive_values(X_test)
prediction = additive.sum(axis=1)
The direct explainer also provides the conventional SHAP values array when a
pipeline stores arrays rather than shap.Explanation objects:
explainer = model.make_shap_explainer(
X_train,
decomposition="weights",
)
component_shap_values = explainer.shap_values(X_test)
For each row and component, this is component_value - mean(background_component). explainer.expected_value is the sum of the
background component means. Consequently, expected_value + component_shap_values.sum(axis=1) exactly reconstructs model.predict(X) up
to floating-point precision.
Choosing A Decomposition
| Need | Use | Runtime behavior | Attribution meaning |
|---|---|---|---|
| Which input columns moved this prediction without serving background data? | predict(X, pred_contrib=True) or background-free default feature explanation | Native Rust path-dependent TreeSHAP for hard dense axis trees. | Original fitted input columns plus a final base-value column. |
| Which input columns moved this prediction relative to a chosen background? | Default feature explanation with background=... | Native TreeExplainer for dense axis trees; SHAP's general adapter for structured routing. | Input columns, including any generated dense columns. |
| Store a fast, exact audit trail across a large ensemble | decomposition="weights" | One native additive-values call; no permutation sampling or repeated prediction calls. | Initial prediction plus each fitted tree contribution. |
| Analyze sparse-set IDs | Default decomposition with both sparse_sets and background_sparse_sets | SHAP runs on CartoBoost's binary sparse-ID representation. | Dense columns and active sparse IDs such as pickup_zone=132. |
Weight decomposition is not a substitute for feature attribution: it answers
which fitted ensemble components contributed, rather than which original
columns caused routing through those components. It is the recommended path
when an existing pipeline previously stored a TreeExplainer-style per-tree
audit and needs predictable latency.
TreeExplainer Support
For fitted dense models whose selected trees use ordinary axis-aligned splits
and constant leaves, background-free helpers use the same native Rust
contribution kernel as pred_contrib=True. The helper only formats those
values as a standard shap.Explanation. With an explicit background,
CartoBoost exports the ensemble to SHAP's TreeExplainer and preserves the
existing interventional behavior.
explainer = model.make_shap_explainer()
explanation = explainer(X_test)
feature_values = explainer.shap_values(X_test)
The model object itself is still not passed to shap.TreeExplainer(model):
SHAP only recognizes its built-in library classes at that entry point. Use the
CartoBoost helper, which selects the native tree backend automatically.
CartoBoost's structured splitters, sparse-set routing, fuzzy routing, and
linear leaves have semantics beyond an axis-aligned constant-leaf ensemble.
They remain supported by the default feature SHAP path, but are not converted
lossily into a TreeExplainer model. Their helper call uses the general SHAP
adapter with the supplied background data; choose its algorithm and background
size explicitly when runtime matters. Use decomposition="weights" for the
always-exact, low-latency tree-component audit across every supported CartoBoost
model.
Sparse Sets
Models trained with sparse_sets= can be explained through the CartoBoost
helper. Sparse IDs are exposed to SHAP as binary features named column=id.
This makes pickup/dropoff memberships auditable while preserving the model's
sparse-list training contract.
explanation = model.explain_shap(
X_test,
background=X_train,
sparse_sets={"taxi_zones": taxi_zones_test},
background_sparse_sets={"taxi_zones": taxi_zones_train},
)
For reusable explainers, pass the background sparse sets when creating the explainer:
explainer = model.make_shap_explainer(
X_train,
sparse_sets={"taxi_zones": taxi_zones_train},
)
Additivity Check
For regression, SHAP values should add back to the model prediction:
prediction = model.predict(X_test)
reconstructed = explanation.base_values + explanation.values.sum(axis=1)
This additivity property is the main sanity check for dense and sparse-set explanations.
Current Limits
- CartoBoost estimators are callable after fitting, so
shap.Explainer(model, background)works directly for dense prediction workflows. decomposition="weights"is CartoBoost's fast exact explanation path. It attributes predictions to the initial value and fitted tree components, not to original input columns. Use the default feature decomposition when you need column-level attributions.- Background-free native feature contributions support identity-output models
whose fitted trees contain only hard axis-aligned splits and constant leaves.
Structured or periodic splits, sparse-list routing, fuzzy routing, linear
leaves, and transformed
log_l2output fail clearly; use background-based SHAP or the per-tree weight decomposition for those models. - Calling
shap.TreeExplaineron a CartoBoost estimator directly is not supported; use the CartoBoost helper. - Dense Python, NumPy, and pandas inputs are supported through existing estimator input handling.
- Sparse-set models are supported through CartoBoost helpers because they need the sparse-ID encoding described above.
- SHAP explains generated graph or neural columns only after those columns are part of the model input; standalone graph and neural artifacts have their own modeling contracts.