Skip to content

Per-equation exog_prior_scale and a truncated own lag for one latent series with lags=1 (0.1.4) - #398

Merged
thomaspinder merged 5 commits into
mainfrom
issue-393
Nov 4, 2026
Merged

thomaspinder merged 5 commits into
mainfrom
issue-393

Conversation

@thomaspinder

@thomaspinder thomaspinder commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Implements spec #393: the two root fixes behind pymc-labs/pymc-marketing#3293 (review items P1.1 and P1.2), for release 0.1.4.

Closes #393
Closes #394
Closes #395

Per-equation exog_prior_scale (#394)

VAR.exog_prior_scale takes a float or a tuple with one entry per endogenous equation, in endog_names order, mirroring MinnesotaPrior.own_lag_mean. A field validator requires every entry to be positive and finite, so the scalar and the tuple are validated the same way (a scalar inf, which gt=0 let through, is now rejected). The tuple's length is checked where the exogenous prior is built, against the number of endogenous variables. Row i of the prior sd is scale_i * sigma_i / s_j; the scalar path gives the same numbers as before. fit, prior_predictive and build_in_model all go through that construction. ADR-0012 records the amendment; the docstring drops the stale "applies only to fit".

Truncated own lag for one latent series with lags=1 (#395)

For exactly one latent endogenous series with lag order 1 the latent block is the own lag B[0, 0] and the stationary region is |B[0, 0]| < 1, so the constraint is now exact and smooth: latent_own_lag is the own lag's Minnesota Normal truncated to (−1, 1) under PyMC's interval transform, B_free is a flat Normal over every other lag coefficient, and B is a Deterministic assembled from the two under its existing name and (var, coeff) dims. No Potential and no initial value are registered in that case; every other case (no latent series, lags > 1, several latent series) is unchanged. The posterior is the same truncated prior the Potential imposed, up to its normalising constant.

Consequences: the own lag starts at 0 whatever its prior mean, so the host model is clonable for any own-lag mean, Impulso's default of 1 included; jitter stays inside the region; prior predictive draws of the own lag are truncated, so prior-predictive latent paths are stationary; no eig in the graph, so the JAX backend differentiates the model for this case (#378 stays open for the Potential cases). On the slow one-latent sampling setup under the default own-lag mean of 1, the Potential gave 268–300 divergent draws of 400 on three seeds; the truncated own lag gives none, with R-hat at most 1.011 on the own lag.

_register_coefficients owns B and the latent-block constraint in both forms, written as a free block plus a constrained block so that a general parametrisation of the latent block (#397) replaces the constrained block without a second rewrite. ADR-0017 records the decision; ADR-0016's initial-value, Potential and bounded-own-lag bullets point at it; the glossary entry for the stationarity constraint says how it is enforced in each case.

Commits

Tests first (strict xfail), then the implementation that unmarks them, per ticket: four commits.

Verification

  • uv run python -m pytest -m "not slow" -q, ruff check, ruff format --check src tests, ty check: clean on the merged branch; all slow tests in tests/test_build_in_model.py pass.
  • Known, pre-existing on main: ruff format --check . fails on 11 Markdown files (ruff 0.17.2 formats Python blocks in Markdown); none are touched here. The existing JAX test for two latent series with lags=2 fails on main too when JAX is installed (JAX samplers fail on models with latent series: the latent_stationarity gradient goes through eig #378); CI skips it.

🤖 Generated with Claude Code

thomaspinder and others added 2 commits November 3, 2026 22:35
Strict-xfail tests for a tuple `exog_prior_scale`: through the graph
`build_in_model` registers (each row of `B_exog`'s prior sd scales with
its own entry, the latent equation gets its own scale, a wrong-length
tuple raises naming the expected length), through `fit`, and through
`VAR` construction (non-positive and non-finite entries rejected) and
its JSON round trip.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`VAR.exog_prior_scale` takes a float or a tuple with one entry per
endogenous equation, in `endog_names` order. A field validator requires
every entry to be positive and finite, so the scalar and the tuple are
validated the same way. The tuple's length is checked where the
exogenous prior is built, against the number of endogenous variables;
row `i` of the prior sd is `scale_i * sigma_i / s_j`, and the scalar
path is unchanged. `fit`, `prior_predictive` and `build_in_model` all go
through that construction. The docstring drops the stale "applies only
to `fit`" and ADR-0012 records the per-equation form.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@thomaspinder thomaspinder added enhancement New feature or request breaking-change Breaking changes labels Nov 3, 2026
@codecov

codecov Bot commented Nov 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

thomaspinder and others added 3 commits November 3, 2026 22:49
New tests, strict-xfail where the current graph cannot pass them:
TestTruncatedOwnLag (B a Deterministic of B_free and latent_own_lag, a
smooth boundary, jitter inside the region without a retry, no initial
value for own-lag means 1 and -0.9, a truncated prior predictive, JAX,
and a slow run under the default own-lag mean), and the (1, 1) cell of
the joint-density reference, which now also checks the coefficients'
density against the Minnesota prior less the truncation constant.

Passing already, so unmarked: the own-lag mean 0 case of the
initial-value test, and FittedVAR.from_posterior on a synthetic
posterior in the new layout.

The Potential tests move off the (lags=1, one latent) cell: the
inside/outside grid drops it, the jitter-retry test uses two latent
series, and the non-finite-block test uses (2, 1) and (1, 2).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ne latent series with lags=1 (#395)

For one latent series with lag order 1 the latent block is the own lag
B[0, 0] alone and its stationary region is |B[0, 0]| < 1. build_in_model
now registers the own lag as latent_own_lag, its Minnesota Normal
truncated to (-1, 1) under PyMC's interval transform, every other lag
coefficient as the free Normal block B_free, and B as a Deterministic
assembled from the two under its existing name and (var, coeff) dims.
No Potential and no initial value are registered in that case, so the
boundary is smooth, the own lag starts at 0 whatever its prior mean,
jitter stays inside the region, prior predictive draws are stationary,
the host model is always clonable, and there is no eig in the graph.
The posterior equals the truncated prior the Potential imposed. Every
other case is unchanged.

_register_coefficients owns B and the latent-block constraint in both
forms. The build_in_model docstring tells the two cases apart, ADR-0017
records the decision, ADR-0016 points at it, and the glossary entry
says how the constraint is enforced in each case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review fixes: `B_free` is described as one-dimensional rather than
"flat", which reads as uninformative in a Bayesian library; ADR-0017
names #397 as the follow-up for the general latent block; ADR-0016's
"what it does not do" bullet says the constrained case is exempt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@thomaspinder
thomaspinder marked this pull request as ready for review November 3, 2026 21:56
@thomaspinder
thomaspinder merged commit ee197f4 into main Nov 4, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Breaking changes enhancement New feature or request

Projects

None yet

1 participant