"""Implementation of the TreeExplainer class.
The :class:`~shapiq.tree.explainer.TreeExplainer` computes path-dependent explanations with
the :class:`~shapiq.tree.quadrature.QuadratureTreeSHAP` algorithm and interventional
explanations with :class:`~shapiq.tree.interventional.computer.InterventionalTreeSHAPIQ`,
routing large interventional inputs through the optional Woodelf fast path.
"""
from __future__ import annotations
import importlib.util
import warnings
from typing import TYPE_CHECKING, Any, Literal, cast
import numpy as np
from shapiq.explainer.base import Explainer
from shapiq.interaction_values import InteractionValues, InteractionValuesBatch
from shapiq.tree.interventional.computer import InterventionalTreeSHAPIQ
from shapiq.utils.modules import safe_isinstance
from .base import TreeModel
from .quadrature import QuadratureTreeSHAP, QuadratureTreeSHAPIndices
from .validation import validate_tree_model
if TYPE_CHECKING:
from shapiq.typing import Model
TREE_MODES = Literal["pathdependent", "interventional"]
TREE_BACKENDS = Literal["auto", "woodelf", "shapiq"]
TreeExplainerIndices = Literal["SV", "SII", "k-SII", "BV", "BII", "STII", "FSII", "FBII"]
_WOODELF_INSTALL_HINT = "Install it with: pip install shapiq[tree]"
_WOODELF_REQUIRED = f"requires the optional 'woodelf-explainer' package. {_WOODELF_INSTALL_HINT}"
_WOODELF_INTERVENTIONAL_CUTOFF = 100_000
"""Interventional inputs with ``n_explained * n_reference`` at or above this route to Woodelf.
See :meth:`TreeExplainer._should_use_woodelf` for the measurements behind the value.
"""
class WoodelfNotAvailableWarning(UserWarning):
"""The explanation would be computed faster with the optional woodelf dependency.
Emitted when an input crosses :class:`TreeExplainer`'s interventional Woodelf cut-off but
the optional ``woodelf-explainer`` package is not installed, so the (slower) shapiq
implementation computes the explanation instead. Filter it with
``warnings.filterwarnings("ignore", category=WoodelfNotAvailableWarning)`` or silence it
for good by installing ``shapiq[tree]``.
"""
[docs]
class TreeExplainer(Explainer):
"""The TreeExplainer class for tree-based models.
The ``TreeExplainer`` is the model-specific explainer for tree-based models, capable of computing attributions and interactions
for both path-dependent and interventional modes (details below).
It supports various interaction indices and can leverage the optional Woodelf package for efficient computation on large datasets.
We support the following model types: ``scikit-learn`` decision trees, random forests, and gradient-boosted ensembles, as well as ``XGBoost``, ``LightGBM``, and ``CatBoost`` models, for both regression and classification tasks.
The model may be fitted both on categorical and numerical features (except for CatBoost, which requires categorical features to be encoded as integers).
Attributions and interactions are returned as :class:`~shapiq.interaction_values.InteractionValues` objects. That object is
indexed by feature tuples — ``values[(1,)]`` is the attribution of feature 1,
``values[(1, 2)]`` the interaction of features 1 and 2 — carries the expected model
output as ``baseline_value``, and offers plotting shortcuts such as
:meth:`~shapiq.interaction_values.InteractionValues.plot_force` and
:meth:`~shapiq.interaction_values.InteractionValues.plot_network`.
:meth:`explain_X` explains a whole data matrix at once, where we return a :class:`~shapiq.interaction_values.InteractionValuesBatch` object, for which ``values[(1,)]`` is a 1D array of the attributions of feature 1 for all explained instances, and ``values[(1, 2)]`` is a 1D array of the interactions of features 1 and 2 for all explained instances.
Two established tree-explanation modes :cite:t:`Lundberg.2020` differ in what "feature
:math:`i` is absent" means:
- In ``"pathdependent"`` mode (the default) an absent feature follows *both* children of
its split nodes, weighted by the share of training samples that went each way — the
background distribution is the one already stored in the tree, so no data is needed.
The computation uses Quadrature-TreeSHAP :cite:t:`Wettenstein.2026a` (whose first-order
case was independently derived in TreeGrad-Ranker :cite:t:`Li.2026`), implemented in
:class:`~shapiq.tree.quadrature.QuadratureTreeSHAP`: values and interactions are
Gauss-Legendre integrals of weighted Banzhaf interaction polynomials, numerically exact
in float64 at any tree depth. The algorithm descends from Linear TreeSHAP
:cite:t:`Yu.2022` and computes any-order Shapley interactions as introduced for trees
by TreeSHAP-IQ :cite:t:`Muschalik.2024a`; Banzhaf indices fall out of the same
polynomials by evaluation at participation probability 1/2. Supported indices:
``"SV"``, ``"SII"``, ``"k-SII"``, ``"BV"``, and ``"BII"``.
- In ``"interventional"`` mode an absent feature takes the values it has in a
``reference_dataset`` (background SHAP), computed by
:class:`~shapiq.tree.interventional.computer.InterventionalTreeSHAPIQ`, which
additionally supports the ``"STII"``, ``"FSII"``, and ``"FBII"`` indices. Large
interventional inputs are routed to the vectorized Woodelf and WOODELF-HD algorithms
:cite:t:`Nadel.2026` :cite:t:`Wettenstein.2026b` when the optional
``woodelf-explainer`` package is installed (``pip install shapiq[tree]``); the
``backend`` parameter overrides this routing.
The two computation classes (:class:`~shapiq.tree.quadrature.QuadratureTreeSHAP` and
:class:`~shapiq.tree.interventional.computer.InterventionalTreeSHAPIQ`) live in
:mod:`shapiq.tree` and can be used directly, as can the standalone reference algorithms
:class:`~shapiq.tree.treeshapiq.TreeSHAPIQ` and
:class:`~shapiq.tree.linear.computer.LinearTreeSHAP`.
Examples:
Shapley values for a random forest — the attributions sum to the prediction
(efficiency):
>>> import numpy as np
>>> from sklearn.ensemble import RandomForestRegressor
>>> from shapiq import TreeExplainer
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(500, 5))
>>> y = X[:, 0] + 2 * X[:, 1] * X[:, 2]
>>> model = RandomForestRegressor(n_estimators=20, random_state=0).fit(X, y)
>>> explainer = TreeExplainer(model, index="SV", max_order=1)
>>> shapley_values = explainer.explain(X[0])
>>> shapley_values[(1,)] # contribution of feature 1 to this prediction
-0.0993...
>>> float(shapley_values.values.sum()) # equals model.predict(X[:1])[0]
-0.2279...
Pairwise Shapley interactions (``k-SII`` of order 2) separate the learned
``X1 * X2`` synergy from the additive effects:
>>> explainer = TreeExplainer(model, index="k-SII", max_order=2)
>>> interactions = explainer.explain(X[0])
>>> abs(interactions[(1, 2)]) > 10 * abs(interactions[(0, 1)])
True
Interventional Shapley values against a background dataset:
>>> explainer = TreeExplainer(
... model,
... mode="interventional",
... reference_dataset=X[:100],
... )
>>> shapley_values = explainer.explain(X[0])
"""
def __init__(
self,
model: dict | TreeModel | list[TreeModel] | Model,
*,
mode: TREE_MODES = "pathdependent",
reference_dataset: np.ndarray | None = None,
max_order: int = 1,
min_order: int = 0,
index: TreeExplainerIndices = "SV",
class_index: int | None = None,
backend: TREE_BACKENDS = "auto",
**kwargs: Any, # noqa: ARG002
) -> None:
"""Initializes the TreeExplainer.
Args:
model: A tree-based model to explain.
mode: The mode of the explainer, either ``"pathdependent"`` or ``"interventional"``.
In ``"pathdependent"`` mode, the explainer computes path-dependent interaction
values with the Quadrature-TreeSHAP algorithm; in ``"interventional"`` mode, it
computes interventional interaction values against the ``reference_dataset``.
Defaults to ``"pathdependent"``.
max_order: The maximum order of interactions to be computed. Set to ``1`` for no
interactions (i.e, for Shapley values ``"SV"`` or Banzhaf values ``"BV"``). Any
value higher than ``1`` computes interaction values up to that order. Defaults to
``1``.
min_order: The minimum interaction order to keep in the returned
:class:`~shapiq.interaction_values.InteractionValues`. Must satisfy
``0 <= min_order <= max_order``. When ``min_order == 0`` the empty interaction
``()`` is included with the baseline value. When ``min_order >= 1`` all
interactions of order below ``min_order`` are filtered out of the result; the
underlying algorithm still computes them internally when required by aggregated
indices such as ``"k-SII"``. Defaults to ``0``.
index: The type of interaction to be computed. In ``"pathdependent"`` mode, the
indices ``["SV", "SII", "k-SII", "BV", "BII"]`` are supported. In
``"interventional"`` mode, further indices such as ``"STII"``, ``"FSII"``, or
``"FBII"`` can be computed. Defaults to ``"SV"``.
class_index: The class index of the model to explain. Defaults to ``None``, which will
set the class index to ``1`` per default for classification models and is ignored
for regression models.
reference_dataset: A dataset to be used for reference in the explanation. Required
when ``mode="interventional"``. Defaults to ``None``.
backend: Which implementation computes the explanations. With ``"auto"`` the
explainer computes path-dependent explanations with shapiq's
:class:`~shapiq.tree.quadrature.QuadratureTreeSHAP` and routes larger
interventional inputs to Woodelf (falling back to shapiq with a
:class:`WoodelfNotAvailableWarning` if the optional woodelf dependency is
missing). ``"woodelf"`` forces Woodelf and raises if it is not installed or
cannot handle the configuration; ``"shapiq"`` forces the shapiq
implementation. Defaults to ``"auto"``.
**kwargs: Additional keyword arguments are ignored.
"""
super().__init__(model, index=index, max_order=max_order)
if min_order < 0 or min_order > self._max_order:
msg = (
f"min_order={min_order} must satisfy 0 <= min_order <= max_order "
f"(max_order={self._max_order})."
)
raise ValueError(msg)
self._trees: list[TreeModel] = validate_tree_model(model, class_label=class_index)
self._n_trees = len(self._trees)
self._min_order: int = min_order
self._class_label: int | None = class_index
if mode == "interventional" and reference_dataset is None:
msg = (
"mode='interventional' requires a reference_dataset; pass one to "
"TreeExplainer(..., mode='interventional', reference_dataset=...)."
)
raise ValueError(msg)
self._mode: TREE_MODES = mode
self._reference_dataset: np.ndarray | None = reference_dataset
if backend not in ("auto", "woodelf", "shapiq"):
msg = f"backend='{backend}' must be one of 'auto', 'woodelf', or 'shapiq'."
raise ValueError(msg)
self.backend: TREE_BACKENDS = backend
if backend == "woodelf":
# forced means forced: fail fast instead of silently falling back later.
if importlib.util.find_spec("woodelf") is None:
msg = f"backend='woodelf' {_WOODELF_REQUIRED}"
raise ImportError(msg)
reason = self._woodelf_unsupported_reason()
if reason is not None:
msg = f"backend='woodelf' cannot be used: {reason}."
raise ValueError(msg)
if mode == "pathdependent" and self.index not in ("SV", "SII", "k-SII", "BV", "BII"):
msg = (
f"index='{self.index}' is not supported in 'pathdependent' mode; use "
"mode='interventional' with a reference_dataset."
)
raise ValueError(msg)
self._pathdependent_explainer: QuadratureTreeSHAP | None = None
self._interventional_explainer: InterventionalTreeSHAPIQ | None = None
self._explainers_initialized = False
self._baseline_value: float | None = None
@property
def baseline_value(self) -> float:
"""The empty prediction of the explained model, matching the explanation mode.
Computed lazily on first access and cached. In ``"pathdependent"`` mode this is the sum
of the per-tree (coverage-weighted) empty predictions; in ``"interventional"`` mode it is
the mean ensemble prediction over the reference dataset.
"""
if self._baseline_value is None:
if self.mode == "interventional":
if self._reference_dataset is None:
msg = (
"The interventional baseline value requires a reference_dataset; pass "
"one to TreeExplainer(..., mode='interventional', reference_dataset=...)."
)
raise ValueError(msg)
self._baseline_value = InterventionalTreeSHAPIQ.compute_empty_prediction(
self._trees, self._reference_dataset
)
else:
self._baseline_value = float(sum(tree.empty_prediction for tree in self._trees))
return self._baseline_value
@property
def mode(self) -> Literal["interventional", "pathdependent"]:
"""The mode of the explainer."""
return self._mode
def _init_explainers(self) -> None:
"""Build the shapiq explainers for the configured mode.
Runs lazily on the first explanation that is not routed to Woodelf, so the (potentially
expensive) shapiq explainers are never built when Woodelf handles the computation.
"""
if self.mode == "pathdependent":
self._pathdependent_explainer = QuadratureTreeSHAP(
model=self._trees,
max_order=self._max_order,
index=cast("QuadratureTreeSHAPIndices", self.index),
)
elif self.mode == "interventional":
if self._reference_dataset is None:
msg = (
"InterventionalTreeSHAPIQ requires a reference_dataset; pass one to "
"TreeExplainer(..., mode='interventional', reference_dataset=...)."
)
raise ValueError(msg)
self._interventional_explainer = InterventionalTreeSHAPIQ(
model=self._trees,
data=self._reference_dataset,
class_index=self._class_label,
max_order=self._max_order,
index=self.index,
)
# only mark initialized once the explainer was fully built: a constructor that raises
# must re-raise on retry, not leave the explainer half-built
self._explainers_initialized = True
def _should_use_woodelf(self, number_of_explained_instances: int) -> bool:
"""Decide whether Woodelf or the shapiq implementation computes the explanation.
Path-dependent explanations are computed by the shapiq quadrature kernel (unless
``backend="woodelf"`` forces Woodelf). In interventional mode the cut-off is
``n * m >= 100_000`` (:data:`_WOODELF_INTERVENTIONAL_CUTOFF`), where ``n`` is the
number of explained instances and ``m`` is the size of the reference dataset.
The value was re-measured after the improvements to
:class:`~shapiq.tree.interventional.computer.InterventionalTreeSHAPIQ` in
`PR #590 <https://github.com/mmschlk/shapiq/pull/590>`_ sped it up by ~2
orders of magnitude (the previous cut-off of ``100`` predates that). End-to-end
timings (explainer construction + ``explain_X``) of both backends over sklearn
random forests and XGBoost ensembles (50-300 trees, depth 3-8, 10-50 features,
400-25k leaves) for ``"SV"`` and order-2 ``"k-SII"`` put the break-even between
``n * m ~= 1e4`` (small ensembles, where Woodelf's fixed per-tree preprocessing
cost is also tiny) and ``n * m ~= 1e6`` (large forests). The penalty is
asymmetric: below the break-even, Woodelf's fixed preprocessing cost makes it up
to hundreds of times slower, while above it shapiq's per-instance loop trails by
a low single-digit factor near the boundary; ``1e5`` balances the worst-case
absolute losses across the measured model shapes and ``n``/``m`` splits.
This function should change when new capabilities are developed in Woodelf.
Args:
number_of_explained_instances: How many instances are about to be explained.
Returns:
``True`` if Woodelf should compute the explanation, ``False`` for shapiq.
"""
if self.backend == "shapiq":
return False
if self.backend == "woodelf":
return True
if self.mode != "interventional":
return False
if self._woodelf_unsupported_reason() is not None:
return False
if (
self._reference_dataset is not None
and len(self._reference_dataset) * number_of_explained_instances
>= _WOODELF_INTERVENTIONAL_CUTOFF
):
return self._woodelf_available()
return False
def _woodelf_unsupported_reason(self) -> str | None:
"""The reason why Woodelf cannot serve this explainer's configuration, if any.
Returns:
A human-readable reason, or ``None`` when Woodelf supports the configuration.
"""
if self.index not in ("SV", "BV", "SII", "k-SII", "BII"):
return f"index='{self.index}' is not supported by Woodelf"
cat_boost_classes = [
"catboost.core.CatBoostRegressor",
"catboost.core.CatBoostClassifier",
"catboost.core.CatBoost",
]
if any(safe_isinstance(self.model, catboost_cls) for catboost_cls in cat_boost_classes):
return "Woodelf does not support CatBoost models"
if isinstance(self.model, list | TreeModel):
return "Woodelf requires the original model object, not already-parsed trees"
return None
@staticmethod
def _woodelf_available() -> bool:
"""Return whether the optional woodelf dependency is installed.
Only called once the cut-offs decided for Woodelf, so a missing package warns the user
that the computation falls back to the (slower) shapiq implementation.
"""
if importlib.util.find_spec("woodelf") is not None:
return True
warnings.warn(
"This explanation would be computed substantially faster with the optional "
f"'woodelf-explainer' package. {_WOODELF_INSTALL_HINT}",
category=WoodelfNotAvailableWarning,
stacklevel=2,
)
return False
def _run_woodelf(self, X: np.ndarray) -> dict[tuple, np.ndarray]:
"""Compute Shapley or Banzhaf (interaction) values with Woodelf's ``hybrid_woodelf``.
Args:
X: The instances to explain as a 2-dimensional array of shape
``(n_instances, n_features)``.
Returns:
The values in the Woodelf format ``{interaction_tuple: ndarray of shape
(n_instances,)}``.
Raises:
ValueError: If the configuration is not supported by Woodelf (only reachable
with ``backend="woodelf"``; auto routing checks the configuration first).
"""
try:
import pandas as pd
from woodelf.core.cube_metric import (
BanzhafValues,
GeneralBanzhafInteractionValues,
GeneralShapleyInteractionValues,
ShapleyValues,
)
from woodelf.core.trees.parse_models import load_decision_tree_ensemble_model
from woodelf.woodelf_sparse import hybrid_woodelf
except ImportError as error:
msg = f"The Woodelf fast path {_WOODELF_REQUIRED}"
raise ImportError(msg) from error
consumer_dataset = pd.DataFrame(X)
background_dataset = None
if self._reference_dataset is not None and self.mode == "interventional":
background_dataset = pd.DataFrame(self._reference_dataset)
if self._index == "SV":
metric = ShapleyValues()
elif self._index == "BV":
metric = BanzhafValues()
elif self._index in ("SII", "k-SII"):
# k-SII is a pure aggregation of SII, Woodelf computes the SII base values and
# ``_aggregate_batched_sii_to_ksii`` makes them k-SII
metric = GeneralShapleyInteractionValues(max(self._min_order, 1), self._max_order)
elif self._index == "BII":
metric = GeneralBanzhafInteractionValues(max(self._min_order, 1), self._max_order)
else: # pre-validated in __init__ / _woodelf_unsupported_reason; defensive
msg = f"index='{self._index}' is not supported by Woodelf."
raise ValueError(msg)
class_index = self._class_label if self._class_label is not None else 1
loaded_model = load_decision_tree_ensemble_model(
self.model, range(X.shape[1]), class_index=class_index
)
# woodelf cannot compute path-dependent SHAP of order >= 3 on trees deeper than 16
# yet; only a forced backend='woodelf' routes path-dependent explanations here.
if self.mode == "pathdependent" and self.max_order >= 3 and loaded_model.max_depth > 16:
msg = (
"backend='woodelf' cannot compute path-dependent interactions of "
f"order >= 3 on trees deeper than 16 (tree depth: "
f"{loaded_model.max_depth}); use backend='auto' or backend='shapiq'."
)
raise ValueError(msg)
woodelf_result = hybrid_woodelf(
model=loaded_model,
consumer_data=consumer_dataset,
background_data=background_dataset,
metric=metric,
model_was_loaded=True,
)
if self._index in ("SV", "BV"):
return {(k,): v for k, v in woodelf_result.items()}
if self._index == "k-SII":
return self._aggregate_batched_sii_to_ksii(woodelf_result)
return woodelf_result
def _aggregate_batched_sii_to_ksii(
self, sii_result: dict[tuple, np.ndarray]
) -> dict[tuple, np.ndarray]:
"""Aggregate a batched Woodelf SII result into k-SII values.
k-SII is a linear aggregation of SII (the same one the shapiq explainers apply through
:class:`~shapiq.interaction_values.InteractionValues`), so it transfers unchanged onto
the per-row value arrays of the Woodelf format.
Args:
sii_result: Woodelf-format SII values ``{interaction_tuple: ndarray(n_instances,)}``.
Returns:
The k-SII values in the same format, restricted to orders
``max(min_order, 1) .. max_order``.
"""
from shapiq.game_theory.aggregation import aggregate_base_attributions
# min_order=1 describes the aggregation base, not the requested filter: the k-SII value
# of an interaction only depends on the SII values of its supersets, which Woodelf all
# computed, so the kept orders below are exact even when ``self._min_order > 1``.
aggregated, _, _ = aggregate_base_attributions(
interactions=sii_result,
index="SII",
order=self._max_order,
min_order=1,
baseline_value=self.baseline_value,
)
lowest = max(self._min_order, 1)
# the isinstance check drops the scalar ``()`` baseline entry
return {
subset: values
for subset, values in aggregated.items()
if isinstance(values, np.ndarray) and lowest <= len(subset) <= self._max_order
}
def _woodelf_result_to_batch(
self, woodelf_result: dict[tuple, np.ndarray], n_players: int, n_instances: int
) -> InteractionValuesBatch:
"""Wrap a Woodelf result in an :class:`~shapiq.interaction_values.InteractionValuesBatch`.
Args:
woodelf_result: Woodelf-format values
``{interaction_tuple: ndarray of shape (n_instances,)}``.
n_players: The number of features of the explained instances.
n_instances: The number of explained instances.
Returns:
The batch carrying this explainer's index, orders, and baseline value.
"""
return InteractionValuesBatch(
woodelf_result,
n_instances=n_instances,
n_players=n_players,
index=self._index,
max_order=self._max_order,
min_order=self._min_order,
baseline_value=self.baseline_value,
)
def _explain_function_pathdependent(
self,
x: np.ndarray,
**kwargs: Any, # noqa: ARG002
) -> InteractionValues:
"""Compute the explanation via the path-dependent quadrature explainer.
The :class:`~shapiq.tree.quadrature.QuadratureTreeSHAP` built in ``_init_explainers``
validates the model and aggregates over ensembles itself; this method only enforces
the requested ``min_order`` on the result.
Args:
x: The instance to explain as a 1-dimensional array.
**kwargs: Additional keyword arguments are ignored.
Returns:
The interaction values for the instance.
"""
if len(x.shape) != 1:
msg = "explain expects a single instance, not a batch."
raise TypeError(msg)
if self._pathdependent_explainer is None:
msg = "Path-dependent explainer is not initialized; mode must be 'pathdependent'."
raise RuntimeError(msg)
final_explanation = self._pathdependent_explainer.explain(x)
if self._min_order == 0 and final_explanation.min_order == 1:
final_explanation.min_order = 0
final_explanation.interactions[()] = float(final_explanation.baseline_value)
if self._min_order > final_explanation.min_order:
final_explanation = final_explanation.get_n_order(
min_order=self._min_order,
max_order=self._max_order,
)
return final_explanation
def _explain_function_interventionaltreeshapiq(
self,
x: np.ndarray,
**kwargs: Any, # noqa: ARG002
) -> InteractionValues:
"""Compute interaction values for ``x`` via the eagerly-built :class:`InterventionalTreeSHAPIQ`.
Args:
x: The instance to explain as a 1-dimensional array.
**kwargs: Additional keyword arguments are ignored.
Returns:
The interaction values for the instance.
"""
if self._interventional_explainer is None:
msg = "Interventional explainer is not initialized; mode must be 'interventional'."
raise RuntimeError(msg)
return self._interventional_explainer.explain_function(x)
[docs]
def explain_function( # type: ignore[override]
self,
x: np.ndarray,
*args: Any, # noqa: ARG002
**kwargs: Any,
) -> InteractionValues:
"""Computes the interaction index for a single instance.
The method used for computing the explanation depends on the specified mode and the
parameters of the explainer.
Args:
x: The instance to explain as a 1-dimensional array.
*args: Additional positional arguments are ignored.
**kwargs: Additional keyword arguments forwarded to the per-mode explain function.
Returns:
The computed interaction index for the instance.
"""
if self._should_use_woodelf(number_of_explained_instances=1):
woodelf_explanation = self._run_woodelf(np.array([x]))
return self._woodelf_result_to_batch(
woodelf_explanation, n_players=int(x.shape[0]), n_instances=1
)[0]
if not self._explainers_initialized:
self._init_explainers()
if self.mode == "pathdependent":
return self._explain_function_pathdependent(x, **kwargs)
return self._explain_function_interventionaltreeshapiq(x, **kwargs)
[docs]
def explain_X(
self,
X: np.ndarray,
*,
n_jobs: int | None = None,
random_state: int | None = None,
verbose: bool = False,
**kwargs: Any,
) -> InteractionValuesBatch:
"""Explain multiple instances at once, using Woodelf on larger interventional inputs.
The whole batch is computed in a single vectorized Woodelf run when the input crosses
the cut-offs (see :meth:`_should_use_woodelf`), and by the per-instance shapiq
computation otherwise. Either way the result is an
:class:`~shapiq.interaction_values.InteractionValuesBatch`: a sequence of one
:class:`~shapiq.interaction_values.InteractionValues` per instance (materialized lazily
on access), whose ``values`` attribute exposes the memory-efficient vectorized format
``{interaction_tuple: ndarray of shape (n_instances,)}`` directly.
Args:
X: A 2-dimensional matrix of inputs to be explained with shape
``(n_instances, n_features)``.
n_jobs: Number of jobs for the shapiq fallback's ``joblib.Parallel``. Defaults to
``None`` (no parallelization).
random_state: The random state to re-initialize the shapiq fallback with. Defaults to
``None``.
verbose: Whether to print a progress bar in the shapiq fallback. Defaults to
``False``.
**kwargs: Additional keyword-only arguments passed to the shapiq fallback.
Returns:
The interaction values of all instances in ``X`` as a batch.
"""
n_players = int(X.shape[1])
if self._should_use_woodelf(len(X)):
woodelf_result = self._run_woodelf(X)
return self._woodelf_result_to_batch(
woodelf_result, n_players=n_players, n_instances=len(X)
)
# initialize before joblib pickles this object, so workers don't re-construct
# the explainer (or re-emit construction-time warnings) once per task
if not self._explainers_initialized:
self._init_explainers()
shapiq_results = super().explain_X(
X, n_jobs=n_jobs, random_state=random_state, verbose=verbose, **kwargs
)
return InteractionValuesBatch.from_interaction_values(shapiq_results)