Source code for nlp_shap.estimation.exact

"""Exact coalition enumeration estimator."""

from __future__ import annotations

from collections.abc import Iterator, Sequence

from ..domain.coalition import CoalitionMask
from ..domain.conversation import ConversationSnapshot
from ..domain.players import PlayerSet
from ..protocols.estimand import EstimandAggregator


[docs] class ExactEstimator: """Enumerate all coalitions and delegate attribution to an estimand plugin.""" def __init__(self) -> None: self._snapshot: ConversationSnapshot | None = None @property def name(self) -> str: """Return the registered estimator identifier.""" return "exact"
[docs] @staticmethod def num_coalitions(num_players: int) -> int: """Return the number of coalitions evaluated by exact enumeration.""" if num_players < 1: msg = "num_players must be at least 1" raise ValueError(msg) return int(2**num_players - 1)
[docs] @staticmethod def iter_mask_ints(num_players: int) -> Iterator[int]: """Lazily yield coalition bitmasks except the all-present coalition.""" grand_mask = (1 << num_players) - 1 for value in range(1 << num_players): if value == grand_mask: continue yield value
[docs] @staticmethod def mask_int_to_present(mask_int: int, num_players: int) -> tuple[bool, ...]: """Decode a coalition bitmask into presence flags.""" return tuple(bool((mask_int >> index) & 1) for index in range(num_players))
[docs] def bind_snapshot(self, snapshot: ConversationSnapshot) -> None: """Attach the conversation snapshot under explanation.""" self._snapshot = snapshot
[docs] def sample_masks( self, player_set: PlayerSet, budget_fraction: float, include_minimal_masks: bool, seed: int, ) -> Iterator[CoalitionMask]: """Yield every coalition mask except the grand coalition.""" if budget_fraction != 1.0: msg = "exact estimator requires budget_fraction == 1.0" raise ValueError(msg) _ = include_minimal_masks, seed return self.iter_masks(player_set)
[docs] @staticmethod def iter_masks(player_set: PlayerSet) -> Iterator[CoalitionMask]: """Lazily yield coalition masks except the all-present coalition.""" num_players = player_set.num_players for mask_int in ExactEstimator.iter_mask_ints(num_players): present = ExactEstimator.mask_int_to_present(mask_int, num_players) yield CoalitionMask.from_sequence(present)
[docs] def estimate_attributions( self, masks: Sequence[CoalitionMask], payoffs: Sequence[float], aggregator: EstimandAggregator, ) -> list[float]: """Aggregate coalition payoffs with the selected estimand plugin.""" return aggregator.aggregate([mask.present for mask in masks], payoffs)