Exact estimation with estimand plugins

Business context: Before trusting approximate samplers on production prompts, teams need ground-truth attributions on tiny player sets. Exact enumeration is the reference path for estimator validation and compliance sign-off on toy models.

Goal: Walk through every public exact-estimation API in nlp-shap 0.1.5 — coalition enumeration, estimand delegation, budget guards, and plugin resolution.

Prerequisites: nlp-shap 0.1.5+, Python 3.12.

Theory: See the exact estimation theory page.

Expected output: Seven coalitions for three players; Shapley and Banzhaf diverge on a majority game; Shapley matches coefficients on an additive game.

from itertools import product

from nlp_shap import (
    BanzhafAggregator,
    Estimand,
    ExactEstimator,
    ShapleyAggregator,
)
from nlp_shap.domain.coalition import CoalitionMask
from nlp_shap.domain.players import PlayerSet
from nlp_shap.plugins import PluginGroup, PluginRegistry, register_builtin_plugins

Coalition enumeration

Business: Exact runs must declare how many model calls will occur — exponential in player count, but fully predictable for audit planning.

Technical: ExactEstimator.iter_masks lazily yields 2^n - 1 masks, excluding the grand coalition that backends often precompute. Materialize with list(...) only for tiny player sets.

player_set = PlayerSet(player_ids=("p0", "p1", "p2"))
estimator = ExactEstimator()

first_mask = next(ExactEstimator.iter_masks(player_set))
sampled_masks = list(
    estimator.sample_masks(
        player_set,
        budget_fraction=1.0,
        include_minimal_masks=False,
        seed=0,
    )
)
print("num_coalitions:", ExactEstimator.num_coalitions(player_set.num_players))
print("sampled:", len(sampled_masks))
print("first mask (lazy):", first_mask.present)
num_coalitions: 7
sampled: 7
first mask (lazy): (False, False, False)

Majority game — estimand divergence

Business: Reporting Banzhaf values under a Shapley label invalidates fairness claims; exact enumeration makes the estimand choice explicit on identical payoffs.

Technical: estimate_attributions delegates to ShapleyAggregator or BanzhafAggregator without embedding weights in the estimator.

all_masks = [
    tuple(bits) for bits in product([False, True], repeat=player_set.num_players)
]


def majority_payoff(mask: tuple[bool, ...]) -> float:
    return 1.0 if sum(mask) >= 2 else 0.0


payoffs = [majority_payoff(mask) for mask in all_masks]
coalition_masks = tuple(CoalitionMask.from_sequence(mask) for mask in all_masks)

shapley = estimator.estimate_attributions(
    coalition_masks,
    payoffs,
    ShapleyAggregator(),
)
banzhaf = estimator.estimate_attributions(
    coalition_masks,
    payoffs,
    BanzhafAggregator(),
)

print("Shapley:", shapley)
print("Banzhaf:", banzhaf)
print("diverge:", shapley != banzhaf)
Shapley: [0.3333333333333333, 0.3333333333333333, 0.3333333333333333]
Banzhaf: [0.5, 0.5, 0.5]
diverge: True

Additive game — Shapley ground truth

Business: Linear utility games are the sanity baseline for any new estimator — coefficients should recover exactly before scaling to LLM prompts.

Technical: Supply all 2^n coalitions (including the grand coalition) when aggregating so marginal lookups remain complete.

coefficients = (1.0, 2.0)
two_player_masks = [
    tuple(bits) for bits in product([False, True], repeat=len(coefficients))
]
additive_payoffs = [
    sum(coef for coef, present in zip(coefficients, mask, strict=True) if present)
    for mask in two_player_masks
]
additive_masks = tuple(CoalitionMask.from_sequence(mask) for mask in two_player_masks)

additive_shapley = estimator.estimate_attributions(
    additive_masks,
    additive_payoffs,
    ShapleyAggregator(),
)
print("coefficients:", coefficients)
print("Shapley:", additive_shapley)
coefficients: (1.0, 2.0)
Shapley: [1.0, 2.0]

Budget guard and plugin resolution

Business: Partial budgets belong to approximate estimators; exact mode must fail fast rather than silently under-sample.

Technical: register_builtin_plugins plus load_entry_points(PluginGroup.ESTIMATORS) resolves the exact entry point.

try:
    estimator.sample_masks(
        PlayerSet(player_ids=("p0",)),
        budget_fraction=0.5,
        include_minimal_masks=False,
        seed=0,
    )
except ValueError as exc:
    print("budget guard:", exc)

registry = PluginRegistry()
register_builtin_plugins(registry)
registry.load_entry_points(PluginGroup.ESTIMATORS)
resolved = registry.resolve(PluginGroup.ESTIMATORS, "exact")
print("plugin name:", resolved.name)
print("Shapley estimand:", ShapleyAggregator().estimand is Estimand.SHAPLEY)
budget guard: exact estimator requires budget_fraction == 1.0
plugin name: exact
Shapley estimand: True