Exact Shapley attributions for TuiML's tree models.
Classes
Exact per-prediction Shapley values for tree models, in polynomial time.
__init__( self, model: Any, background: Optional[np.ndarray] = None, feature_names: Optional[List[str]] = None, )
Overview
- Push the background data through the tree to record what fraction of it
- For a sample, walk every root-to-leaf path once, carrying the set of
- At each leaf, credit every feature on the path with its marginal
Theory
The Shapley value of feature j for prediction f(x) is
where f_x(S) is the prediction with only the features in S known and the rest marginalised over the background. Evaluating that directly costs O(2^F); carrying the subset-proportion bookkeeping down the tree instead collapses it to O(L D^2) for L leaves and depth D.
Efficiency is the property that makes the output trustworthy and testable: for every sample,
The attributions add up to the prediction exactly. This implementation is verified against a brute-force enumeration of all subsets and agrees to machine precision.
Parameters
model
DecisionTreeClassifier, DecisionTreeRegressor, RandomForestClassifier or RandomForestRegressor.
background
feature_names
Attributes
expected_value_
n_features_
Notes
Complexity. O(T L D^2) per sample for a forest of T trees, run in the shared C++ kernel tuiml._cpp_ext.shapley.tree_shap and parallel over samples. A forest costs the sum of its trees, so explaining a 500-tree forest is 500 times the work of explaining one.
This is the path-dependent variant. A feature is marginalised by following the background's branch proportions at each split, which respects the correlations the tree learned. The alternative interventional variant breaks those correlations deliberately and answers a subtly different causal question; the two disagree when features are correlated, and neither is wrong — they answer different questions.
Attribution is not causation. A large \phi_j says the model used that feature, not that the feature drives the outcome in the world. A model that leaks a proxy for the label will attribute confidently to it.
References
See Also
>>> import numpy as np
>>> from tuiml.explain import TreeExplainer
>>> from tuiml.algorithms.trees import DecisionTreeRegressor
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(200, 4))
>>> y = 3.0 * X[:, 0] - 2.0 * X[:, 1] + rng.normal(0, 0.1, 200)
>>> model = DecisionTreeRegressor(max_depth=5).fit(X, y)
>>> explainer = TreeExplainer(model, background=X)
>>> result = explainer.explain(X[:10])
>>> result.values.shape
(10, 4)
The attributions reconstruct the prediction exactly, which is what makes
them worth trusting:
>>> reconstructed = result.values.sum(axis=1) + explainer.expected_value_[0]
>>> bool(np.allclose(reconstructed, model.predict(X[:10])))
True
Methods
explain
(self, X: np.ndarray) -> Explanation
explain
(self, X: np.ndarray) -> Explanation
Compute Shapley values for each sample.
Parameters
X
Returns
explanation
values has shape (n_samples, n_features) for single-output models and (n_samples, n_features, n_outputs) otherwise. base_value holds expected_value_.