Skip to content

feat(granger): Bayesian Granger causality with Toda-Yamamoto robustness - #226

Merged
thomaspinder merged 6 commits into
mainfrom
feat/154-granger-toda-yamamoto
Jul 29, 2026
Merged

feat(granger): Bayesian Granger causality with Toda-Yamamoto robustness#226
thomaspinder merged 6 commits into
mainfrom
feat/154-granger-toda-yamamoto

Conversation

@thomaspinder

Copy link
Copy Markdown
Owner

Summary

Stacked on feat/140-stationarity-diagnostics (#197) — it consumes the frozen IntegrationOrderResult.order / .d_max / .inconclusive contract pinned on the issue.

  • FittedVAR.granger_causality(cause, effect, *, rope=None, standardize=True, test_lags=None): labelled posterior causal-strength summaries — per-lag coefficient posteriors and the headline posterior of the joint coefficient norm with median/HDI, standardised by sd(cause)/sd(effect) by default. The probability statement is p_rope = P(‖b‖ < rope | data) with a user-supplied region of practical equivalence and no default — the docstring states plainly why P(no causality) is unavailable under continuous priors and what would be needed to get one (spike-and-slab), per the issue's explicit honesty requirement. ADR-0010 records the stance (including why a Savage-Dickey Bayes factor was rejected: it exports the prior tightness into what reads as evidence).
  • toda_yamamoto(data, cause, effect, ...): lag-augmented mode — fits ConjugateVAR(lags=p + d) (closed-form, milliseconds) but tests only the first p lags; n_lags_tested vs n_lags_fitted metadata makes the split explicit and the augmented block structurally cannot enter the statistic (single result-construction site). The integration-order decision is consumed per the contract: non-empty inconclusive → hard refusal naming the variables and explaining floor-vs-finding, with an explicit d= integer as the only override (recorded augmentation_source="user"); injectable integration_order_result=; works without statsmodels when d= is given.
  • Docs: how-to with a CO2–temperature example whose limits section names the omitted forcings, the bidirectional physical coupling, and exactly what a small p_rope licenses; reference page; CONTEXT.md terms; ADR-0010.

Closes #154

Review

Planned by a Fable-tier planner; independently reviewed (verdict: approve). The reviewer re-derived the lag-major indexing and cross-checked it with an out-of-suite simulation, audited the honesty paragraph claim-by-claim against the implementation, and verified the refusal wording against _stationarity's actual inconclusive semantics. Its one P2 (a sentence conflating the event with its probability) is fixed in the final commit.

Note: the beartype forward-ref failure on FittedVAR methods that surfaced during testing is pre-existing and already fixed on main by #209 — it resolves when this stack rebases.

Tests

50 tests, 100% branch coverage on the engine: exact-index pins (including transposed-dims realignment and a lag_matrices cross-check), directionality (true edge p_rope 0.000 vs null edge 0.968), a 40-query null-calibration sweep, the full TY contract (refusal/override/injection/end-to-end on an I(1) system with statsmodels), and a slow NUTS smoke. Branch fast suite: 641 passed. ruff/ty/docs build clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV

@codecov-commenter

codecov-commenter commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.4%. Comparing base (41e5234) to head (f69edaf).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##            main    #226     +/-   ##
=======================================
+ Coverage   95.2%   95.4%   +0.1%     
=======================================
  Files         45      46      +1     
  Lines       3317    3431    +114     
  Branches     420     437     +17     
=======================================
+ Hits        3161    3275    +114     
  Misses       111     111             
  Partials      45      45             

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@thomaspinder
thomaspinder force-pushed the feat/140-stationarity-diagnostics branch from 56c9fdc to 3f9ac15 Compare July 29, 2026 13:58
@thomaspinder
thomaspinder changed the base branch from feat/140-stationarity-diagnostics to main July 29, 2026 20:55
@thomaspinder
thomaspinder force-pushed the feat/154-granger-toda-yamamoto branch from 5ecae59 to fb09b5e Compare July 29, 2026 21:38
thomaspinder and others added 6 commits July 29, 2026 23:55
`FittedVAR.granger_causality(cause, effect)` reports the posterior of the
Euclidean norm of the tested lag coefficients of `cause` in the `effect`
equation, plus the per-lag posteriors behind it. A magnitude, not a test
statistic: nothing divides through by the posterior covariance, so a small
effect stays distinguishable from an imprecise one.

An optional `rope` adds `p_rope = P(||b|| < rope | data)` — practical
negligibility at a threshold the analyst names, deliberately with no
default. It is not the probability of no causality: `b = 0` has probability
zero under continuous coefficient priors, so that quantity needs a
spike-and-slab prior Impulso does not fit. The `GrangerCausalityResult`
docstring carries the full statement.

The engine lives in a new private `_granger.py` (the `conditional_forecast`
delegation precedent), and its extraction is pinned by tests against a
hand-built posterior with every entry distinct, cross-checked against
`lag_matrices` so the lag-major layout cannot drift between the two
consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
`toda_yamamoto(data, cause, effect)` runs the Toda-Yamamoto (1995)
procedure for possibly-integrated systems: fit the VAR in levels with
`p + d` lags, test only the first `p`. The augmented lags are never tested
and the reported test lag order is never silently changed to match the fit
— the result carries `n_lags_tested` and `n_lags_fitted` separately, with
`augmentation` and `augmentation_source` recording where the extra lags
came from.

`d` comes from `integration_order` unless the caller pins it. Honouring the
consumer contract frozen by #140/#197: when the diagnostics leave anything
in `inconclusive`, `d_max` is a floor rather than a finding, so this
refuses to run — naming the variables, pointing at `.summary()` and at the
`d=` override — rather than under-augmenting silently. An explicit `d`
skips the diagnostics entirely, so the route also works without
statsmodels installed.

The fit uses the closed-form conjugate estimator, since augmentation
inflates the lag order; exogenous regressors it cannot consume are refused
with a message naming the manual `VAR(lags=p + d)` route.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
New how-to covering the fitted-model query, how to read `summary()`, why
there is no probability of no causality, the Toda-Yamamoto happy path plus
its refusal and the `d=` override, the manual NUTS route, and a worked
carbon-dioxide/temperature example with an explicit statement of what it
does and does not license (predictive precedence not intervention; omitted
forcings; bidirectional physical coupling; annual aggregation). Cross-links
with the climate-pitfalls page both ways.

New reference page for `toda_yamamoto`; `GrangerCausalityResult` added to
the results page. CONTEXT gains three terms — Granger causality, ROPE,
Toda-Yamamoto augmentation — plus the two relationships that place the
query on `FittedVAR` and wire the augmentation to the integration-order
contract.

ADR-0010 records the decision: the norm of the tested coefficients as the
headline with an analyst-supplied ROPE, against the rejected alternatives
(a Wald quadratic headline, a fixed default epsilon, spike-and-slab, and
Savage-Dickey).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
A transposed posterior must be realigned by its canonical dim names, and
an unlabelled one must fall back to the positional (chain, draw, var,
coeff) convention — the same contract `dynamic_multiplier` relies on.
Neither path was exercised. Takes `_granger.py` to full branch coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
…event

The honesty paragraph's one imprecise sentence read the probability as an
assertion of practical negligibility; a p_rope of 0.02 says no such thing.

Refs #154

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
… columns)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Egjd7ToFeb9TQqFnfRQZxV
@thomaspinder
thomaspinder force-pushed the feat/154-granger-toda-yamamoto branch from fb09b5e to f69edaf Compare July 29, 2026 21:57
@thomaspinder
thomaspinder merged commit 3dade89 into main Jul 29, 2026
9 checks passed
@thomaspinder thomaspinder added the enhancement New feature or request label Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: add Bayesian Granger causality with Toda-Yamamoto robustness

2 participants