Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
51ab014
Added FBII
Advueu963 Mar 14, 2025
050c11b
Introduces game normalization for FBII in exact.
Advueu963 Mar 14, 2025
d5952f1
Added FBII to MC_Approximator
Advueu963 Mar 14, 2025
c198b24
Added FBI to Tabular Explainer
Advueu963 Mar 14, 2025
975d373
Update regression coefficient calculation for FBII & FSII.
Advueu963 Mar 14, 2025
99484b9
refactoring of regression weight calculation
Advueu963 Mar 14, 2025
ab1702f
fix fbii exact computation missing baseline_value addition
Advueu963 Mar 14, 2025
aa325d6
Added FBII
Advueu963 Mar 14, 2025
ad9fa06
Introduces game normalization for FBII in exact.
Advueu963 Mar 14, 2025
2aea87a
Added FBII to MC_Approximator
Advueu963 Mar 14, 2025
4708d10
Added FBI to Tabular Explainer
Advueu963 Mar 14, 2025
07e1ae6
Update regression coefficient calculation for FBII & FSII.
Advueu963 Mar 14, 2025
9e22e09
refactoring of regression weight calculation
Advueu963 Mar 14, 2025
54024f2
fix fbii exact computation missing baseline_value addition
Advueu963 Mar 14, 2025
6681a88
updated regression_coefficient calculation
Advueu963 Mar 15, 2025
fa1d292
Merge branch 'main' into 331-adding-faith-banzhaf-approximator
mmschlk Mar 17, 2025
a201d7e
improving approximation finalize result
Advueu963 Mar 17, 2025
d5cb74a
Consistent baseline/empty player value throughout InteractionValues.
Advueu963 Mar 18, 2025
574ee1f
Merge branch '331-adding-faith-banzhaf-approximator' into consistent_…
Advueu963 Mar 18, 2025
b6a2c66
Sampling weight initialisation for FBII and corresponding approximation
Advueu963 Mar 19, 2025
8ecdc52
Merge branch 'main' into 331-adding-faith-banzhaf-approximator
mmschlk Mar 19, 2025
d819796
renamed finalize function
mmschlk Mar 20, 2025
206ed4a
finalized explanations for TreeExplainer
mmschlk Mar 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- fixes a bug with xgboost where feature names where trees that did not contain all features would lead `TreeExplainer` to fail
- fixes a bug with `stacked_bar_plot` where the higher order interactions were inflated by the lower order interactions, thus wrongly showing the higher order interactions as higher than they are
- fixes a bug where `InteractionValues.get_subset()` returns a faulty `coalition_lookup` dictionary pointing to indices outside the subset of players [#336](https://github.com/mmschlk/shapiq/issues/336)
- updates default value of `TreeExplainer`'s `min_order` parameter from 1 to 0 to include the baseline value in the interaction values as per default
- adds the `RegressionFBII` approximator to estimate Faithful Banzhaf interactions via least squares regression [#333](https://github.com/mmschlk/shapiq/pull/333). Additionally, FBII support was introduced in TabularExplainer and MonteCarlo-Approximator.

### v1.2.2 (2025-03-11)
- changes python support to 3.10-3.13 [#318](https://github.com/mmschlk/shapiq/pull/318)
Expand Down
15 changes: 14 additions & 1 deletion shapiq/approximator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
from .permutation.sii import PermutationSamplingSII
from .permutation.stii import PermutationSamplingSTII
from .permutation.sv import PermutationSamplingSV
from .regression import InconsistentKernelSHAPIQ, KernelSHAP, KernelSHAPIQ, RegressionFSII, kADDSHAP
from .regression import (
InconsistentKernelSHAPIQ,
KernelSHAP,
KernelSHAPIQ,
RegressionFBII,
RegressionFSII,
kADDSHAP,
)

# contains all SV approximators
SV_APPROXIMATORS: list[Approximator.__class__] = [
Expand Down Expand Up @@ -57,6 +64,11 @@
SHAPIQ,
]

# contains all approximators that can be used for FBII
FBII_APPROXIMATORS: list[Approximator.__class__] = [
RegressionFBII,
]

__all__ = [
"PermutationSamplingSII",
"PermutationSamplingSTII",
Expand All @@ -77,6 +89,7 @@
"SII_APPROXIMATORS",
"STII_APPROXIMATORS",
"FSII_APPROXIMATORS",
"FBII_APPROXIMATORS",
]

# Path: shapiq/approximator/__init__.py
94 changes: 27 additions & 67 deletions shapiq/approximator/_base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
"""This module contains the base approximator classes for the shapiq package."""

import copy
import warnings
from abc import ABC, abstractmethod
from collections.abc import Callable

import numpy as np
from scipy.special import binom

from ..approximator.sampling import CoalitionSampler
from ..game_theory.indices import (
AVAILABLE_INDICES_FOR_APPROXIMATION,
get_computation_index,
is_empty_value_the_baseline,
is_index_aggregated,
)
from ..interaction_values import InteractionValues
from ..utils.sets import generate_interaction_lookup
Expand Down Expand Up @@ -143,13 +142,29 @@ def _init_sampling_weights(self) -> np.ndarray:
The weights for sampling subsets of size ``s`` in shape ``(n + 1,)``.
"""
weight_vector = np.zeros(shape=self.n + 1)
for coalition_size in range(0, self.n + 1):
if (coalition_size < self.max_order) or (coalition_size > self.n - self.max_order):
# prioritize these subsets
weight_vector[coalition_size] = self._big_M
else:
# KernelSHAP sampling weights
weight_vector[coalition_size] = 1 / (coalition_size * (self.n - coalition_size))
if self.index in ["FBII"]:

try:
for coalition_size in range(0, self.n + 1):
weight_vector[coalition_size] = binom(self.n, coalition_size) / 2**self.n
except OverflowError:
for coalition_size in range(0, self.n + 1):
weight_vector[coalition_size] = (
1
/ np.sqrt(2 * np.pi * 0.5)
* np.exp(-(coalition_size - self.n / 2) * +2 / (self.n / 2))
)
warnings.warn(
"The weights are approximated for n > 1000. While this is very close to the truth for sets for size in the region n/2, the approximation is not exact for size n or 0."
)
else:
for coalition_size in range(0, self.n + 1):
if (coalition_size < self.max_order) or (coalition_size > self.n - self.max_order):
# prioritize these subsets
weight_vector[coalition_size] = self._big_M
else:
# KernelSHAP sampling weights
weight_vector[coalition_size] = 1 / (coalition_size * (self.n - coalition_size))
sampling_weight = weight_vector / np.sum(weight_vector)
return sampling_weight

Expand All @@ -175,61 +190,6 @@ def _order_iterator(self) -> range:
"""
return range(self.min_order, self.max_order + 1)

def _finalize_result(
self,
result,
baseline_value: float,
*,
estimated: bool | None = None,
budget: int | None = None,
) -> InteractionValues:
"""Finalizes the result dictionary.

Args:
result: Interaction values.
baseline_value: Baseline value.
estimated: Whether interaction values were estimated.
budget: The budget for the approximation.

Returns:
The interaction values.

Raises:
ValueError: If the baseline value is not provided for SII and k-SII.
"""

if budget is None: # try to get budget from sampler (exclude from coverage)
budget = self._sampler.n_coalitions # pragma: no cover

if estimated is None:
estimated = False if budget >= 2**self.n else True

# set empty value as baseline value if necessary
if tuple() in self._interaction_lookup:
idx = self._interaction_lookup[tuple()]
empty_value = result[idx]
# only for SII empty value is not the baseline value
if empty_value != baseline_value and is_empty_value_the_baseline(self.index):
result[idx] = baseline_value

interactions = InteractionValues(
values=result,
estimated=estimated,
estimation_budget=budget,
index=self.approximation_index, # can be different from self.index
min_order=self.min_order,
max_order=self.max_order,
n_players=self.n,
interaction_lookup=copy.deepcopy(self.interaction_lookup),
baseline_value=baseline_value,
)

# if index needs to be aggregated
if is_index_aggregated(self.index):
interactions = self.aggregate_interaction_values(interactions)

return interactions

@staticmethod
def _calc_iteration_count(budget: int, batch_size: int, iteration_cost: int) -> tuple[int, int]:
"""Computes the number of iterations and the size of the last batch given the batch size and
Expand Down Expand Up @@ -313,6 +273,6 @@ def aggregate_interaction_values(
Returns:
The aggregated interaction values.
"""
from shapiq.game_theory.aggregation import aggregate_interaction_values
from shapiq.game_theory.aggregation import aggregate_base_interaction

return aggregate_interaction_values(base_interactions, order=order)
return aggregate_base_interaction(base_interactions, order=order)
19 changes: 16 additions & 3 deletions shapiq/approximator/marginals/owen.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import numpy as np

from ...interaction_values import InteractionValues
from ...interaction_values import InteractionValues, finalize_computed_interactions
from .._base import Approximator


Expand Down Expand Up @@ -101,8 +101,21 @@ def approximate(
idx = self._interaction_lookup[(player,)]
result_to_finalize[idx] = result[player]

return self._finalize_result(
result_to_finalize, baseline_value=empty_value, budget=used_budget, estimated=True
interaction = InteractionValues(
n_players=self.n,
values=result_to_finalize,
index=self.approximation_index,
interaction_lookup=self._interaction_lookup,
baseline_value=empty_value,
min_order=self.min_order,
max_order=self.max_order,
estimated=True,
estimation_budget=used_budget,
)

return finalize_computed_interactions(
interaction,
target_index=self.index,
)

@staticmethod
Expand Down
19 changes: 15 additions & 4 deletions shapiq/approximator/marginals/stratified.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import numpy as np

from ...interaction_values import InteractionValues
from ...interaction_values import InteractionValues, finalize_computed_interactions
from .._base import Approximator


Expand Down Expand Up @@ -37,13 +37,14 @@ def __init__(
self.iteration_cost: int = 2

def approximate(
self, budget: int, game: Callable[[np.ndarray], np.ndarray]
self, budget: int, game: Callable[[np.ndarray], np.ndarray], *args, **kwargs
) -> InteractionValues:
"""Approximates the Shapley values using ApproShapley.

Args:
budget: The number of game evaluations for approximation
game: The game function as a callable that takes a set of players and returns the value.
*args and **kwargs: Additional arguments not used.

Returns:
The estimated interaction values.
Expand Down Expand Up @@ -114,6 +115,16 @@ def approximate(
idx = self._interaction_lookup[(player,)]
result_to_finalize[idx] = result[player]

return self._finalize_result(
result_to_finalize, baseline_value=empty_value, budget=used_budget, estimated=True
interactions = InteractionValues(
n_players=self.n,
values=result_to_finalize,
index=self.approximation_index,
interaction_lookup=self._interaction_lookup,
baseline_value=float(empty_value),
min_order=self.min_order,
max_order=self.max_order,
estimated=True,
estimation_budget=used_budget,
)

return finalize_computed_interactions(interactions, target_index=self.index)
43 changes: 35 additions & 8 deletions shapiq/approximator/montecarlo/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from scipy.special import binom, factorial

from ...game_theory.indices import AVAILABLE_INDICES_MONTE_CARLO
from ...interaction_values import InteractionValues
from ...interaction_values import InteractionValues, finalize_computed_interactions
from ...utils.sets import powerset
from .._base import Approximator

Expand Down Expand Up @@ -50,7 +50,7 @@ def __init__(
f"Index {index} not available for Regression Approximator. Choose from "
f"{AVAILABLE_INDICES_MONTE_CARLO}."
)
if index == "FSII":
if index in ["FSII", "FBII"]:
top_order = True
super().__init__(
n,
Expand Down Expand Up @@ -99,10 +99,20 @@ def approximate(

baseline_value = float(game_values[self._sampler.empty_coalition_index])

return self._finalize_result(
result=shapley_interactions_values, baseline_value=baseline_value, budget=budget
interactions = InteractionValues(
shapley_interactions_values,
index=self.approximation_index,
n_players=self.n,
interaction_lookup=self.interaction_lookup,
min_order=self.min_order,
max_order=self.max_order,
baseline_value=baseline_value,
estimated=False if budget >= 2**self.n else True,
estimation_budget=budget,
)

return finalize_computed_interactions(interactions, target_index=self.index)

def monte_carlo_routine(
self,
game_values: np.ndarray,
Expand Down Expand Up @@ -386,8 +396,7 @@ def _stii_weight(self, coalition_size: int, interaction_size: int) -> float:
"""
if interaction_size == self.max_order:
return self.max_order / (self.n * binom(self.n - 1, coalition_size))
else:
return 1.0 * (coalition_size == 0)
return 1.0 * (coalition_size == 0)

def _fsii_weight(self, coalition_size: int, interaction_size: int) -> float:
"""Returns the FSII discrete derivative weight given the coalition size and interaction
Expand All @@ -411,8 +420,24 @@ def _fsii_weight(self, coalition_size: int, interaction_size: int) -> float:
* factorial(coalition_size + self.max_order - 1)
/ factorial(self.n + self.max_order - 1)
)
else:
raise ValueError("Lower order interactions are not supported.")
raise ValueError(f"Lower order interactions are not supported for {self.index}.")

def _fbii_weight(self, interaction_size: int) -> float:
"""Returns the FSII discrete derivative weight given the coalition size and interaction
size.

The representation is based on the FBII representation according to Theorem 17 by
`Tsai et al. (2023) <https://doi.org/10.48550/arXiv.2203.00870>`_.

Args:
interaction_size: The size of the interaction.

Returns:
The weight for the interaction type.
"""
if interaction_size == self.max_order:
return 1 / 2 ** (self.n - interaction_size)
raise ValueError(f"Lower order interactions are not supported for {self.index}.")

def _weight(self, index: str, coalition_size: int, interaction_size: int) -> float:
"""Returns the weight for each interaction type given coalition and interaction size.
Expand All @@ -429,6 +454,8 @@ def _weight(self, index: str, coalition_size: int, interaction_size: int) -> flo
return self._stii_weight(coalition_size, interaction_size)
elif index == "FSII":
return self._fsii_weight(coalition_size, interaction_size)
elif index == "FBII":
return self._fbii_weight(interaction_size)
elif index in ["SII", "SV"]:
return self._sii_weight(coalition_size, interaction_size)
elif index == "BII":
Expand Down
16 changes: 13 additions & 3 deletions shapiq/approximator/permutation/sii.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import numpy as np

from ...interaction_values import InteractionValues
from ...interaction_values import InteractionValues, finalize_computed_interactions
from ...utils.sets import powerset
from .._base import Approximator

Expand Down Expand Up @@ -143,6 +143,16 @@ def approximate(
# compute mean of interactions
result = np.divide(result, counts, out=result, where=counts != 0)

return self._finalize_result(
result, baseline_value=empty_value, budget=used_budget, estimated=True
interactions = InteractionValues(
n_players=self.n,
values=result,
index=self.approximation_index,
interaction_lookup=self._interaction_lookup,
baseline_value=empty_value,
min_order=self.min_order,
max_order=self.max_order,
estimated=True,
estimation_budget=used_budget,
)

return finalize_computed_interactions(interactions, target_index=self.index)
Loading