Estimator comparison: budget, variance, and accuracy¶
Business context: Attribution quality must be judged against exact Shapley with realistic player counts and tight call budgets — not from a single seed.
Goal: (1) Benchmark eight players with at most 20 coalition evaluations; (2) sweep budget.fraction up to 0.5 where that cap allows; (3) explain why Monte Carlo can show high error with tiny variance; (4) plot the relative L1 error of each estimator against the budget fraction once the cap is lifted.
Prerequisites: mllm-shap with the [viz] extra (matplotlib for the final plot), Python 3.12 or newer.
Theory: Approximation methods.
from collections.abc import Callable
from functools import partial
import matplotlib.pyplot as plt
import numpy as np
from mllm_shap import (
CoalitionMask,
ComplementaryEstimator,
ExactEstimator,
MonteCarloEstimator,
NeymanEstimator,
PlayerSet,
ShapleyAggregator,
)
MAX_EVALUATIONS = 20
SEED_COUNT = 32
Eight players, cap ≤ 20 (primary benchmark)¶
Business: Eight tokens is a realistic sentence slice. We cap coalition evaluations at 20 and ladder toward that maximum.
Technical: At n = 8 a budget.fraction of 0.5 would imply 128 calls — above the cap — so we convert target caps {8, 12, 16, 20} into equivalent fractions (~0.03–0.08). The cap column reports the coalitions each estimator actually requested, which can exceed the target: Monte Carlo with include_minimal_masks=True always evaluates the empty coalition, the n singletons and the grand coalition (n + 2 = 10), and complementary and Neyman-CC never go below 2n + 2 = 18 masks (the grand/empty pair and one singleton pair ({i}, N - {i}) per player).
NUM_PLAYERS_EIGHT = 8
TARGET_CAPS = (8, 12, 16, 20)
exact_eight = estimate(ExactEstimator, NUM_PLAYERS_EIGHT, 1.0, 0)
scale_eight = sum(abs(value) for value in exact_eight)
print(
f"exact Shapley ({NUM_PLAYERS_EIGHT} players): {[round(v, 3) for v in exact_eight]}"
)
print(f"reference L1 scale: {scale_eight:.3f}")
for target_cap in TARGET_CAPS:
assert target_cap <= MAX_EVALUATIONS
fraction = fraction_for_cap(NUM_PLAYERS_EIGHT, target_cap)
compare(
f"\ntarget cap {target_cap} (fraction={fraction:.3f})",
NUM_PLAYERS_EIGHT,
fraction,
)
exact Shapley (8 players): [1.9, 3.8, 5.7, 7.6, 9.5, 11.4, 13.3, 15.2]
reference L1 scale: 68.400
target cap 8 (fraction=0.031)
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 10 27.300 0.000 0.399
complementary 18 21.714 0.000 0.317
neyman_cc 18 21.714 0.000 0.317
target cap 12 (fraction=0.047)
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 12 26.855 1.119 0.393
complementary 18 21.714 0.000 0.317
neyman_cc 18 21.714 0.000 0.317
target cap 16 (fraction=0.062)
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 16 25.407 1.968 0.371
complementary 18 21.714 0.000 0.317
neyman_cc 18 21.714 0.000 0.317
target cap 20 (fraction=0.078)
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 20 23.845 2.404 0.349
complementary 20 25.485 1.413 0.373
neyman_cc 20 36.851 1.925 0.539
Why Monte Carlo shows high error but tiny variance¶
Business: A narrow error band is not proof of accuracy — MC can be confidently wrong.
Technical:
include_minimal_masks=Trueemits the same empty/singleton prefix on every seed. When the budget allows no more than that prefix and the grand coalition (n + 2calls), the sample is deterministic: the zero spread at target cap 8 above.The Shapley aggregator only learns from pairs where both
SandS ∪ {i}were sampled. It averages the observed marginals per coalition size and rescales over the sizes it saw, so with few resolved pairs the estimate rests on a handful of strata.
def shapley_marginal_coverage(masks: list[CoalitionMask], num_players: int):
"""Count the sampled pairs (S, S with i) out of all sampled S without i."""
seen = {mask.present for mask in masks}
pairs = [
(mask.present, i)
for mask in masks
for i in range(num_players)
if not mask.present[i]
]
resolved = sum(
(*present[:i], True, *present[i + 1 :]) in seen for present, i in pairs
)
return resolved, len(pairs)
analysis_fraction = fraction_for_cap(NUM_PLAYERS_EIGHT, 12)
def mc_sample(seed: int, minimal: bool = True) -> list[CoalitionMask]:
"""Monte Carlo masks at the 12-coalition analysis budget."""
sampler = MonteCarloEstimator().sample_masks(
players(NUM_PLAYERS_EIGHT), analysis_fraction, minimal, seed
)
return list(sampler)
prefix = NUM_PLAYERS_EIGHT + 1
same_prefix = [m.present for m in mc_sample(0)[:prefix]] == [
m.present for m in mc_sample(99)[:prefix]
]
print(f"minimal prefix identical across seeds: {same_prefix}")
resolved, possible = shapley_marginal_coverage(mc_sample(0), NUM_PLAYERS_EIGHT)
print(
f"Shapley marginals resolved: {resolved}/{possible} "
f"({100 * resolved / possible:.1f}%) at cap={len(mc_sample(0))}"
)
for minimal in (True, False):
errors = [
l1_error(
estimate(MonteCarloEstimator, 8, analysis_fraction, seed, minimal),
exact_eight,
)
for seed in range(SEED_COUNT)
]
print(
f"include_minimal_masks={minimal} cap={len(mc_sample(0, minimal))} "
f"mean L1={np.mean(errors):.3f} std L1={np.std(errors):.3f}"
)
minimal prefix identical across seeds: True
Shapley marginals resolved: 9/69 (13.0%) at cap=12
include_minimal_masks=True cap=12 mean L1=26.855 std L1=1.119
include_minimal_masks=False cap=12 mean L1=57.034 std L1=8.773
Fraction sweep 0.1–0.5 at cap ≤ 20 (five players)¶
Business: Product configs often speak in budget.fraction, not coalition counts. At eight players, fraction = 0.5 would exceed 20 calls, so we repeat the sweep on five players where 0.1–0.5 all stay within the cap.
Technical: Same nonlinear game and estimators; exact reference recomputed for n = 5.
NUM_PLAYERS_FIVE = 5
BUDGET_FRACTIONS = (0.1, 0.2, 0.3, 0.4, 0.5)
exact_five = estimate(ExactEstimator, NUM_PLAYERS_FIVE, 1.0, 0)
print(
f"exact Shapley ({NUM_PLAYERS_FIVE} players): {[round(v, 3) for v in exact_five]}"
)
for budget_fraction in BUDGET_FRACTIONS:
for make in (MonteCarloEstimator, ComplementaryEstimator):
assert requested(make, NUM_PLAYERS_FIVE, budget_fraction) <= MAX_EVALUATIONS
compare(
f"\nbudget.fraction = {budget_fraction:.1f}", NUM_PLAYERS_FIVE, budget_fraction
)
print("\nexact reference: L1 = 0.000 (by definition)")
exact Shapley (5 players): [1.6, 3.2, 4.8, 6.4, 8.0]
budget.fraction = 0.1
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 7 6.800 0.000 0.283
complementary 12 4.800 0.000 0.200
neyman_cc 12 4.800 0.000 0.200
budget.fraction = 0.2
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 7 6.800 0.000 0.283
complementary 12 4.800 0.000 0.200
neyman_cc 12 4.800 0.000 0.200
budget.fraction = 0.3
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 9 5.518 0.980 0.230
complementary 12 4.800 0.000 0.200
neyman_cc 12 4.800 0.000 0.200
budget.fraction = 0.4
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 12 3.430 1.301 0.143
complementary 12 4.800 0.000 0.200
neyman_cc 12 4.800 0.000 0.200
budget.fraction = 0.5
estimator cap mean L1 std L1 mean rel
-----------------------------------------------
mc 16 1.985 0.904 0.083
complementary 16 5.250 1.317 0.219
neyman_cc 16 5.250 1.317 0.219
exact reference: L1 = 0.000 (by definition)
Error versus budget (cap lifted)¶
Business: Choosing budget.fraction for an expensive backend needs the whole curve, not one point: how much accuracy each extra coalition buys.
Technical: Eight players, the same game, the exact reference from all 256 coalitions, and budget fractions from 0.1 to 1.0 without the 20-call cap. Each point is the mean relative L1 error (L1 divided by the L1 norm of the exact values) over eight seeds. At 1.0 every estimator sees the full table and must return the exact values.
SWEEP_FRACTIONS = (0.1, 0.2, 0.3, 0.5, 0.75, 1.0)
SWEEP_SEEDS = 8
LABELS = {
"mc": "Monte Carlo",
"complementary": "Complementary",
"neyman_cc": "Neyman-CC",
}
relative_errors = {
LABELS[name]: [
float(
np.mean([
l1_error(estimate(make, NUM_PLAYERS_EIGHT, fraction, seed), exact_eight)
for seed in range(SWEEP_SEEDS)
])
)
/ scale_eight
for fraction in SWEEP_FRACTIONS
]
for name, make in ESTIMATORS.items()
}
for label, errors in relative_errors.items():
print(f"{label:>13}: " + " ".join(f"{error:.3f}" for error in errors))
figure, axis = plt.subplots(figsize=(6, 3.5))
for label, errors in relative_errors.items():
axis.plot(SWEEP_FRACTIONS, errors, marker="o", label=label)
axis.set_xlabel("budget.fraction")
axis.set_ylabel("relative L1 error vs exact Shapley")
axis.set_title(f"{NUM_PLAYERS_EIGHT} players, mean over {SWEEP_SEEDS} seeds")
axis.legend()
plt.show()
Monte Carlo: 0.289 0.134 0.086 0.044 0.018 0.000
Complementary: 0.288 0.373 0.239 0.095 0.047 0.000
Neyman-CC: 0.373 0.188 0.122 0.066 0.028 0.000
Reading the results¶
Business: with at most 20 calls on eight players, complementary and Neyman-CC are closest to exact Shapley at caps 8 to 16, at the cost of their 18-call floor. At exactly 20 calls each, Monte Carlo is closest and Neyman-CC furthest. A zero spread across seeds at the smallest caps means a fixed, incomplete sample, not convergence.
Technical: complementary and Neyman-CC first evaluate the empty and grand pair and the eight singleton pairs (18 calls), so at caps 8 to 16 they evaluate the same coalitions on every seed. With 20 calls Neyman-CC never reaches phase two. On the five-player ladder both sit on their 2n + 2 = 12 floor up to 0.4. On the lifted-cap curve Monte Carlo with the stratified aggregator has the lowest error from 0.2 on, and all three reach the exact values at 1.0. The ranking depends on the game.