Skip to content

Add linear-Gaussian SCM games for causal Shapley values - #600

Open
bongho wants to merge 1 commit into
mmschlk:mainfrom
bongho:feat/linear-gaussian-scm
Open

bongho wants to merge 1 commit into
mmschlk:mainfrom
bongho:feat/linear-gaussian-scm

Conversation

@bongho

@bongho bongho commented Sep 21, 2026

Copy link
Copy Markdown

Motivation and Context

Closes #599.

Causal Shapley values (Heskes et al., 2020) are not available in a pure-Python stack today. The paper's code supplement was folded into the R package shapr; the Python entry point pyshapr is an rpy2 wrapper and needs a working R installation. The same request has been open on shap (shap#3914) since 2024-11 with no assignee.

Following @Advueu963's answer on #599, this adds the value function to shapiq_games, in the shape #551 established. It starts at the synthetic end, because that is where the answer is checkable: on a known SCM all three value functions a Shapley explanation can be built on are available in closed form, so the game is its own ground truth.

LinearGaussianSCM takes a linear model on Gaussian features and evaluates one of three value functions on it:

mode v(S) question it answers
marginal E[f(x*_S, X_S̄)] what does knowing x*_S add, if I know nothing else?
conditional E[f(X) | X_S = x*_S] what does knowing x*_S add, given everything else?
causal E[f(X) | do(X_S = x*_S)] what happens if I set X_S = x*_S?

The causal structure is taken the way shapr takes it: a causal_ordering of feature groups plus one confounding flag per group, which together specify the causal chain graph. The non-intervened members of a group are drawn conditional on every preceding group, plus the intervened members of the same group only when that group is not confounded. That is the rule shapr implements in get_S_causal_steps (R/asymmetric_and_causal_Shapley.R).

One thing worth noting for review: in the linear-Gaussian case the three value functions are fully determined by the coefficients, the explanation point, the feature mean and covariance, and the chain graph. The structural coefficients are not needed, so the class takes a covariance matrix rather than an SCM object.

ConfoundedChainSCM is a three-feature preset where the three semantics disagree in sign:

X1 = U + noise
X2 = U + noise            # X1 and X2 share a hidden common cause
X3 = 2*X1 + X2 + noise    # X3 is downstream of both
f  = X1 + 2*X2 + 3*X3     # explained at x* = (1, 1, 1), f(x*) = 6
X1 X2 X3 sum
marginal 1.00 2.00 3.00 6.00
conditional 3.95 4.38 -2.33 6.00
causal 4.00 3.50 -1.50 6.00

X3 is the largest positive contributor under the first row and pushes the prediction down under the other two. Against its unconditional mean of zero, 1 is above average; against what its parents predict for it (2·1 + 1·1 = 3), it came in two units low. Conditional and causal then part ways on X2, because intervening on X1 cuts the confounded path between them while conditioning on it does not.

Public API Changes

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

Two new names exported from shapiq_games.synthetic:

  • LinearGaussianSCM(coefficients, x_explain, cov, *, mean=None, causal_ordering=None, confounding=None, mode="causal", normalize=True)
  • ConfoundedChainSCM(*, mode="causal", normalize=True)

Nothing existing changes. docs/source/references.bib gains a heskes2020 entry and examples/causal/README.txt gains one sentence.

How Has This Been Tested?

The value functions were derived by hand in closed form and cross-checked two ways that do not share code: a standalone NumPy script enumerating all 8 coalitions, and shapiq.ExactComputer run over the game in this PR. Both give the table above to 4 decimal places, and all three satisfy efficiency (sum 6.00 = f(x*)).

tests/shapiq_games/tests_unit/test_linear_gaussian_scm.py adds 17 tests: the three closed-form attributions, efficiency for each, two structural identities that must hold on any correct implementation (marginal == causal on the sink node, conditional == causal once every ancestor is fixed), the effect of flipping the confounding flag, the degenerate single-group case collapsing to the observational value function, and argument validation.

$ uv run pytest tests/shapiq_games -q
17 passed in 0.02s

$ uv run pytest tests/shapiq/tests_unit/tests_games tests/shapiq/tests_unit/tests_game_theory -q
129 passed

pyproject.toml already lists tests/shapiq_games in testpaths and the directory did not exist yet, so these are the first tests in it. I did not touch .github/workflows/ci.yml, which currently runs tests/shapiq and tests/shapiq_benchmark explicitly — happy to add the path if you want it in CI.

uv run pre-commit run --all-files passes every hook except ty, which fails identically on a clean main (22 error[...] diagnostics, none in the files this PR touches; they are the optional woodelf imports in src/shapiq/tree/explainer.py and friends).

The example was run end to end and reproduces the table.

Checklist

  • 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.

Three notes for you, none blocking:

  1. On validating against shapr. Add causal Shapley values (Heskes et al., 2020) #599 said I would check the numbers against shapr in R. I dropped that and used the closed form instead. shapr's causal Shapley is a Monte Carlo estimator, so agreement with it would have been agreement to within sampling error; the analytic value is the stronger reference, and it keeps the test suite free of an R dependency. The shapr algorithm is still the source for the rule, just not for the numbers.

  2. On where the causal value function should live long term. While building this I noticed shapiq.imputer already has GaussianImputer and GaussianCopulaImputer, which is the conditional value function on Gaussian features, and that Imputer subclasses Game. So a CausalImputer subclassing GaussianImputer would reuse that machinery and would plug into TabularExplainer(imputer=...), giving causal Shapley values on a user's own fitted model rather than only on a synthetic benchmark. That felt like a separate PR, and I did not want to relocate what you had just pointed me at. Would you want that as a follow-up?

  3. On the CHANGELOG heading. The file currently starts at ## v1.7.0 and I could not find an unreleased-section convention anywhere in its history, so I opened a ## Unreleased heading. Say the word if you would rather the entry went somewhere else, or nowhere until release time.

@bongho

bongho commented Sep 28, 2026

Copy link
Copy Markdown
Author

@Advueu963 one small thing: the CI run is sitting at action_required since this is my first PR here, so no checks have actually executed yet. Whenever you get a moment, approving the workflow run would let the test results show up. No rush on the review itself.

@bongho

bongho commented Sep 28, 2026

Copy link
Copy Markdown
Author

Thanks for approving the run, @Advueu963 — that surfaced a real problem on my side.

Code Quality Checks failed because the branch was formatted at line-length 88; this repo sets 100, so ruff-format rewrote three files. Fixed in 8bf1bb3 — formatting only, no behaviour change. ruff and ruff-format both pass now and the game's tests still pass 17/17.

The push queued a new run that is sitting at action_required again, since the approval is per-run for a first-time contributor. One more approval and the checks should go green.

@codecov

codecov Bot commented Oct 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@mmschlk mmschlk added the games 🎲 All issues that are linked to games label Oct 6, 2026
@mmschlk mmschlk self-assigned this Oct 6, 2026
@mmschlk

mmschlk commented Oct 7, 2026

Copy link
Copy Markdown
Owner

@bongho, @Advueu963 told me about the conversations you guys had and the work you are doing with the lab. We are currently in the midst of rewriting the shapiq_Games and benchmark machinery in #616 and I would like to get the rewrite in first before brining this in. Would you be open to adapt this PR after we merged the other one in? I‘d ping you here?

@bongho

bongho commented Oct 8, 2026

Copy link
Copy Markdown
Author

Thanks @mmschlk — that order makes sense, and yes, I'm happy to adapt this after #616 lands.

Looking at #616, it reworks src/shapiq_games/__init__.py and adds _base.py, so the synthetic/ surface this PR builds on is going to move anyway. Resolving the current conflict now would only give you something to review against the old structure.

I'll leave this open and wait for your ping. When it comes I'll rebase onto the new layout, re-run the oracle cross-check (numpy closed form vs ExactComputer, all 8 coalitions), and update the example and CHANGELOG to match.

No rush — the rewrite looks like a big one.

@bongho
bongho force-pushed the feat/linear-gaussian-scm branch from 8b8850e to d83aa0f Compare October 19, 2026 00:31
@bongho

bongho commented Oct 19, 2026

Copy link
Copy Markdown
Author

@mmschlk #616 is in, so I rebased this onto the new layout. Thanks for the heads-up on the ordering, reviewing against the old shapiq_games would indeed have been wasted effort.

What the adaptation changed:

  • LinearGaussianSCM and ConfoundedChainSCM are exported from shapiq_games directly, next to the other synthetic games, and listed in the games-synthetic API reference.
  • both are registered in the GAMES table of tests/shapiq_games/test_contract.py, so they are held to the game contract like every other family. Both are centered.
  • the example moved out of examples/causal/ into examples/technicalities/, beside plot_imputers.py and plot_conditional_imputer.py. That group already draws the interventional/observational line, and this example extends it with the causal value function, so it reads as the third case rather than a group of its own. Registered in EXAMPLE_ORDER right after the conditional imputer.
  • the citation key follows the new bibliography convention: heskes2020 is now Heskes.2020.
  • the three commits are squashed into one.

What I ran on the rebased branch:

  • pytest tests/shapiq_games/test_linear_gaussian_scm.py: 17 passed. That includes the oracle cross-check, the closed form against ExactComputer over all 8 coalitions for each of the three value functions.
  • pytest tests/shapiq_games/test_contract.py -k "linear_gaussian_scm or confounded_chain_scm": 12 passed.
  • pytest tests/shapiq_games: 328 passed, 8 skipped, 2 failed. The two failures are test_path_dependent_game_of_xgboost_matches_tree_shap and test_tree_games_explain_the_raw_scores_of_lightgbm_boosters. They fail identically on a clean origin/main in this checkout, so they are a local xgboost/lightgbm version mismatch, not something this PR introduces.
  • the gallery example runs to completion and reproduces the numbers in its title: X3 is +3.00 under the marginal value function and -2.33 under the conditional one.
  • pre-commit run --files over the 10 changed files: ruff and ruff-format pass. The ty hook fails here on woodelf, optuna, smac and ConfigSpace imports in files this PR does not touch, and no diagnostic points at any file in this change.

No rush on the review, I know v1.8.0 still has items open.

@bongho
bongho force-pushed the feat/linear-gaussian-scm branch from d83aa0f to 4c0bcd2 Compare October 21, 2026 12:14
@bongho

bongho commented Oct 21, 2026

Copy link
Copy Markdown
Author

Rebased onto v1.8.0. The only conflict was the CHANGELOG, since the release turned ## Development into ## v1.8.0, so I moved the entry into a new ## Development section at the top. No code changes.

tests/shapiq_games/test_linear_gaussian_scm.py and test_contract.py pass locally (207 passed). The new run will need an approval again whenever you get to it, no rush.

Adds LinearGaussianSCM and ConfoundedChainSCM to shapiq_games.synthetic.
Both evaluate the marginal, the conditional and the causal value function
of Heskes et al. (2020) in closed form on one linear-Gaussian
data-generating process, so the three semantics can be compared on the
same model and the same point without sampling or a random seed.

A linear model on Gaussian features keeps every conditional expectation
linear, which is what makes the closed form possible. That also makes the
games exact references for benchmarking approximators: no seed, no
approximation error, and the Shapley values follow from the eight
coalitions of the three-feature example exactly.

The gallery example sits with the imputers in technicalities, where the
marginal/conditional distinction is already drawn, and extends it with
the causal value function: the same feature is +3.00 marginally and -2.33
conditionally.
@bongho
bongho force-pushed the feat/linear-gaussian-scm branch from 4c0bcd2 to 00dae92 Compare October 27, 2026 12:01

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

games 🎲 All issues that are linked to games

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Add causal Shapley values (Heskes et al., 2020)

2 participants