Skip to content

Implement LeverageSHAP approximator - #524

Merged
mmschlk merged 64 commits into
mmschlk:mainfrom
FabianK-Dev:leverageSHAP
Aug 10, 2026
Merged

Implement LeverageSHAP approximator#524
mmschlk merged 64 commits into
mmschlk:mainfrom
FabianK-Dev:leverageSHAP

Conversation

@FabianK-Dev

@FabianK-Dev FabianK-Dev commented May 22, 2026

Copy link
Copy Markdown
Contributor

Motivation and Context

In this PR we added the implementation of the LeverageSHAP approximator, based on the paper by Musco & Witter (2024). We built (1) a custom sampler for leverage score sampling (uniform size + paired sampling) (2) implemented the regression solver using the row-centering trick (Lemma 3.1) and (3) added different test cases. We originally opened a PR in our fork (FabianK-Dev#1) which I closed now and I am reopening here.

A few disclaimers / important notes:

    1. We already commited our summary src/shapiq/approximator/regression/our_impl_progress.md (as requsted in the project description) but this file is stil work-in-progress and not really ready for review, yet.
    1. There are still more unittests to come: I estimate we only covered around 50 % of unittests.
    1. We will improve existing docstrings and add more later on.

Public API Changes

  • No Public API changes
  • Yes, Public API changes (Details below)

Details: Added LeverageSHAP class to shapiq.approximator.regression.


How Has This Been Tested?

We added many unittests to cover the following things:

  • Budget constraints, exact recovery on additive games and deterministic behavior.
  • Tested with large n to verify the IS weight cancellation prevents float overflows.
  • Tested with highly skewed interaction games to ensure the lstsq solver maintains the efficiency axiom on ill-conditioned matrices.
  • Verified the reproducibility requirement by testing that the mean L2 error drops significantly when increasing the budget on a synthetic game.

You can run all new unittests using:
uv run pytest tests/shapiq/tests_unit/tests_approximators/test_approximator_leverageshap.py

Tests are passing:

uv run pytest tests/shapiq/tests_unit/tests_approximators/test_approximator_leverageshap.py
warning: `VIRTUAL_ENV=/home/fabian/Dokumente/Uni/LMU/Master/Semester2/Toolbox/.venv` does not match the project environment path `.venv` and will be ignored; use `--active` to target the active environment instead
====================================================================================== test session starts ======================================================================================
platform linux -- Python 3.13.5, pytest-9.0.2, pluggy-1.6.0
rootdir: /home/fabian/Dokumente/Uni/LMU/Master/Semester2/Toolbox/shapiq
configfile: pyproject.toml
plugins: xdist-3.8.0, cov-7.0.0, anyio-4.12.1
collected 15 items                                                                                                                                                                              

tests/shapiq/tests_unit/tests_approximators/test_approximator_leverageshap.py ...............                                                                                             [100%]

====================================================================================== 15 passed in 0.20s =======================================================================================

Checklist

We haven't completed all points on the checklist yet, as this is still an early PR where we ask for feedback but don't plan to merge it into the upstream repository, yet.

  • The changes have been tested locally.
  • Documentation has been updated (if the public API or usage changes).
  • An entry has been added to CHANGELOG.md (if relevant for users).
  • The code follows the project's style guidelines.
  • I have considered the impact of these changes on the public API.

42logos added 3 commits May 11, 2026 02:46
Forward-looking spec for the 3 new SV approximators (LeverageSHAP,
PolySHAP, OddSHAP). Approximator classes are looked up dynamically by
name, so the file auto-skips classes that have not yet been registered
in shapiq.approximator. As each implementation lands, the corresponding
parametrizations activate.

- Interface conformance (always required): index='SV', n_players,
  max_order/min_order, values shape and dtype, interaction_lookup.
- Numerical convergence vs ExactComputer (xfail strict=False): atol
  schedule by budget percentage.
- Determinism: same (n, random_state, budget, game) -> bit-identical
  output.

75 tests, all currently SKIP on main. Will activate as classes land.
Honors the cross-method testing platform promised to the tutor:
unified harness covering every SV approximator in shapiq (the
existing 11 — KernelSHAP, SVARM, Permutation*, ProxySPEX, ... — and
the 3 new ones from this project) instead of only the new line-up.

Approximator list is sourced dynamically from
shapiq.approximator.SV_APPROXIMATORS (canonical registry) plus the
3 new project names, deduplicated. Future shapiq additions land in
the harness automatically.

Split into two scopes:

  * test_interface_conformance — strict shape/dtype/index/lookup
    contract from the API spec. Applied ONLY to the 3 new
    approximators (the contract is ours; existing methods have
    different default output conventions like ProxySPEX defaulting
    to FBII and max_order=n).

  * test_numerical_convergence_vs_exact + test_determinism — apply
    to ALL SV approximators. Cross-method comparison against
    ExactComputer ground truth on identical SOUM games. xfail with
    strict=False so methods that do converge surface as XPASS;
    methods still under development surface as XFAIL.

Two robustness helpers:

  * _construct_or_skip — tries (n=, index='SV', max_order=1,
    random_state=) first (covers multi-index methods like SPEX,
    ProxySPEX, ProxySHAP, MSRBiased, kADDSHAP), then falls back to
    minimal signature for SV-only methods (KernelSHAP, OwenSamplingSV).

  * _safe_approximate — skips on ValueError raised by approximators
    that explicitly refuse a regime (e.g. SPEX 'Insufficient budget
    to compute the transform' at low budgets).

Results: 10 passed, 95 skipped, 90 xfailed, 23 xpassed. The 23
xpassed are existing shapiq SV methods that converge cleanly at
full budget on small SOUM — a useful baseline for the upcoming
benchmark report.
Drop-in framework that any teammate can merge into their feature branch
to run head-to-head benchmarks against ExactComputer across every SV
approximator in shapiq, then plot the standard SHAP-literature metric
curves. No source files are modified — adds a top-level benchmark/
package, a single test file, and a small in-place test-helper sys.path
hook. Does not touch pyproject.toml or any other upstream config.

Files added:

  * benchmark/__init__.py: makes the runner a proper Python package so
    invocation is 'python -m benchmark.performance'.

  * benchmark/_discovery.py: single source of truth for SV approximator
    discovery + SV-mode construction. Holds:
      - PROJECT_APPROXIMATOR_NAMES: LeverageSHAP, PolySHAP,
        PolySHAPKAdd / Partial / Prior, OddSHAP.
      - _SV_CONSTRUCT_OVERRIDES: per-class kwargs for non-standard
        constructors (PolySHAP variants need max_order /
        n_explanation_terms / q_prior).
      - construct_for_sv(): three-stage construction (override ->
        explicit SV signature -> minimal signature), returning
        (estimator, exc) so the caller can report the most informative
        exception. A ValueError from inside a matched signature wins
        over a TypeError from a signature mismatch.
      - safe_approximate(): catches ValueError and RuntimeError so
        sparse approximators that refuse a budget regime (SPEX,
        ProxySPEX, ...) skip the cell cleanly instead of crashing.

  * benchmark/performance.py: CLI runner that consumes _discovery,
    sweeps (method, game, budget, seed), records every cell in a
    long-format CSV, and emits one PNG per (game, metric) plus a
    runtime PNG. Seven metrics chosen from the union of LeverageSHAP,
    PolySHAP, OddSHAP and shapiq.benchmark.metrics literature:
    MSE / MAE / SSE / SAE / Precision@5 / Precision@10 / KendallTau.
    Includes a '--check' interface-probe mode that prints a
    constructibility table without running a sweep.

  * benchmark/README.md: usage doc covering merge workflow, --check,
    sweep CLI, output layout, CSV format, metric definitions, plot
    conventions, and notes on the multi-index approximators that need
    explicit (index='SV', max_order=1).

Files modified:

  * tests/shapiq/tests_unit/tests_approximators/test_approximators_vs_exact.py:
    now imports the shared helpers from benchmark._discovery via a
    tightly-scoped sys.path hook at the top of the file. Picks up the
    ValueError-priority construction and the RuntimeError-catch that
    the test file previously did not have. Interface conformance is
    now applied to the project's six new approximator names
    (LeverageSHAP, PolySHAP + 3 variants, OddSHAP), so Matthias's
    PolySHAP variants are no longer silently skipped by the contract
    check.

Verified locally:

  * pytest test_approximators_vs_exact.py: 10 passed, 170 skipped,
    87 xfailed, 26 xpassed. No failures.
  * python -m benchmark.performance --check: surfaces all 17 method
    names (11 existing on main + 6 project additions) correctly.
  * Drop-in compatibility verified by temporary merge into all three
    feature branches (oddshap_approximator, leverageSHAP, PolySHAP) —
    clean merge in each, --check picks up the local approximator.
@FabianK-Dev

Copy link
Copy Markdown
Contributor Author

Finally all tests pass again after:

  • Refactoring leverageshap.py to move its _solve() method into the base regression class in 274207e and 66c4e6c
  • Updating tests in 433fb7a to test a list of pre-defined seeds to prevent and accidental overfitting on seeds and fix tests accordingly in 348db3b
========================================== 1270 passed, 166 skipped, 124 xfailed, 32 xpassed, 5181 warnings in 956.40s (0:15:56) ==========================================

real    16m2,138s
user    119m51,705s
sys     0m37,665s

Theresa Geber and others added 24 commits June 8, 2026 13:57
…tions set, Z_list and probs_list and create all-true and all-false coalitions
…ferent game variables to avoid access counters interfering; Also compare metadata
…d tiny-n edge case and add comments to document and explain the test
…use its core claim was not reliable

With n = 6, a budget of 100 is above 2^n = 64, so the implementation enters the full-budget/exact regime.  In that regime, the result should be identical no matter which seed you use, so asserting that different seeds must differ is false and will fail even though the code is correct.
=> I lowered the budget to budget=20
…o test_exact_regime_seed_independence and test_stochastic_regime_seed_variability
…est_exact_matches_multiple_small_games, test_null_player_axiom and test_minimal_budget_sweep
@FabianK-Dev

Copy link
Copy Markdown
Contributor Author

@Advueu963 As requested in the last meeting, I have updated and expanded the benchmark notebook to evaluate our custom LeverageSHAP implementation (Algorithm 2) against the uniform KernelSHAP baseline across 25 configurations and 4 distinct datasets. The results demonstrate that our method surpasses KernelSHAP + np.zeros, with the custom implementation achieving a lower absolute L2 error and winning in 22 to 28 out of 30 budget steps across almost every configuration (please see the table below). Also, our Bernoulli sampling mechanism resolves the "zig-zag" pattern seen in 9 out of 25 baseline plots for KernelSHAP + np.zeros. I assume the "zig-zag" occurs when the KernelSHAP + np.zeros setup runs out of budget mid-layer.

Some configurations show extreme negative percentages in the relative "Avg Improvement" column. I think this caused when the baseline randomly hits a near-zero error in its symmetry valleys which then distorts the relative average. The fully cleaned and documented notebook is now ready for review. I also added a "Empirical Evaluation: Custom LeverageSHAP vs. Uniform Weighting Baseline" section which describes and tries to explain the observations. We also plot every single experiment (i.e. each line in the table below in the Jupyter NB).

image

You can also find the Jupyter NB exported in the PDF here:
reproduce_figure9_sampling_architecture.ipynb.pdf

…NBs will not be pushed to the shapiq repository according to the last meeting)
@FabianK-Dev

Copy link
Copy Markdown
Contributor Author

@Advueu963 I moved the notebooks located in notebooks/ folder to a PR on our fork because if I understood it correctly, as discussed with @pwhofman the notebooks, discussion files and 1-page summary will not be pushed to mmschlk/shapiq, thus I'm cleaning up this PR so it gets ready to be merged.

@mmschlk mmschlk left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! I only really have one issue with the static method of the solve_regression. I do not like that you re-export the function without any purpose. Delete this and then I also think this PR is good to go.

The discussion with the different sampling strategies is also very detailed!

Comment on lines +231 to +251
def solve_regression(
self,
X: np.ndarray,
y: np.ndarray,
kernel_weights: FloatVector,
*,
use_svd: bool = False,
) -> np.ndarray:
"""Solve the weighted regression system used by regression approximators.

Args:
X: The regression matrix of shape ``[n_coalitions, n_interactions]``.
y: The response vector for each coalition of shape ``[n_coalitions]``.
kernel_weights: The weights for the regression problem for each coalition.
use_svd: If ``True``, solve via the SVD-backed least-squares path directly.

Returns:
The solution to the regression problem.
"""
return solve_regression(X=X, y=y, kernel_weights=kernel_weights, use_svd=use_svd)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not like that you added a new static wrapper function to the real implementation in the module. While, yes having the function on the class level is a nice quality of life upgrade but also adds another maintenance burden which can rot over time. This is already the case haha as your docstring here is already out sync with the docstring in the real method (the docstring omits the SVD-fallback explanation and the [n_interactions] return shape that the module function documents). You are also not overriding the implementation here or any subclasses, so the addition is purely cosemetical.

If you really need to include it because of some thing I did not spot then the more honest way would be by bounding the variable to the staticmetho directly:

class Regression:
    solve_regression = staticmethod(solve_regression)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My call: just delete this re-export and use the module-level function. :)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good catch! I refactored the code according to your feedback and deleted the solve_regression() re-export in 23d8de4. We now use direct module-level function calls instead.

If possible, before merging allow me to quickly review all doc strings again and check the code for possible code smells. I wanna make sure the code is 100 % perfect before merging. I'll then also answer your question below and add the answer to the doc string in leverageshap.py so other users don't wonder why we use our own implementation instead of Coalition Sampler.

I'll notify you when I'm confident we're 100 % ready to merge.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No rush. We can do this easily after the meetings! :)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I like this evaluation. Can you tell me why your sampling is beating the Coalition Sampler exactly? Could you find out the root cause?

@FabianK-Dev

Copy link
Copy Markdown
Contributor Author

Nice work! I only really have one issue with the static method of the solve_regression. I do not like that you re-export the function without any purpose. Delete this and then I also think this PR is good to go.

The discussion with the different sampling strategies is also very detailed!

Thank you very much, I'll look at the feedback, changes, question, etc. now. 👍

@FabianK-Dev

Copy link
Copy Markdown
Contributor Author

(GitHub's UI doesn't allow replying directly below that comment so I comment here)

Yes, I like this evaluation. Can you tell me why your sampling is beating the Coalition Sampler exactly? Could you find out the root cause?

This one was a bit hard and it took me a while. But I think I might have found the root cause. I reproduced it in a Jupyter NB in this commit on our submission Draft PR on our own fork: 53900ed

Please look at this line in our custom sampler:

prob = 1.0 if full_count <= two_c else two_c / full_count

(https://github.com/FabianK-Dev/shapiq/blob/4e16c85788473e9bbbfe7448bc9af99a018d4be9/src/shapiq/approximator/regression/leverageshap.py#L283)

If, for a layer with size s the total number of all possible combinations $\binom{n}{s}$ is less than or equal to 2c, the probability is set to exactly 1. This means that all coalitions of this size (and their complements) are included in the sample 100% deterministically and completely.

For example: With a budget of m=100 and n=8, the parameter 2c is large enough that the layers s=1 (single players) and s=2 (pairs), as well as their complements s=7 and s=6, are fully enumerated.

This is better because the layers s = 1 and s = 2 contain the most important information about the main effects and pairwise interactions of the XGBoost model. Because our sampler captures them completely and deterministically, the sampling variance for these essential layers is exactly 0.

Coalition sampler selects coalitions in these small layers probabilistically. And that carries the risk of completely overlooking important single or pairwise coalitions or selecting them redundantly multiple times.

I reproduced this in the mentioned Jupyter NB:

Please look at this plot:
image
I compared LeverageSHAP (w/o Bernoulli) i.e. using CoalitonSampler vs. LeverageSHAP with our custom sampler vs. LeverageSHAP without "2c":

As you can see LeverageSHAP w/o Bernoulli and LeverageSHAP w/o 2c limit are nearly identical. In the NB I computed the L2 error for each method and computed the avg. win rate:
image

Summarized:
image

A win rate of 61,7 % doesn't sound too good but keep in mind we are comparing two already highly optimized sampling variants here. If we include KernelSHAP as a base-line/control you can see LeverageSHAP is generally performing very well (as expected):
image

So I believe the main "leverage" for our LeverageSHAP custom sampler implementation vs. CoalitionSampler is the calculation of 2c that includes all coalitions for coalitions sizes where the total number of combinations is small ((n over s) <= 2c​) deterministically. => Because then critical edge-layers like s = 1 and s = 2 are fully included.

What do you think?

@FabianK-Dev

Copy link
Copy Markdown
Contributor Author

The PR is now 100 % ready and we're confident that it can be merged as-is. 👍🎉

@mmschlk mmschlk left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice.

@mmschlk
mmschlk merged commit dd86841 into mmschlk:main Aug 10, 2026
14 checks passed
@mmschlk mmschlk mentioned this pull request Aug 14, 2026
@mmschlk mmschlk added this to the v1.7.0 milestone Aug 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

6 participants