Skip to content

feat: add dynamic regression (time-varying coefficients)#7

Merged
YuminosukeSato merged 5 commits into
mainfrom
feat/dynamic-regression
Mar 22, 2026
Merged

feat: add dynamic regression (time-varying coefficients)#7
YuminosukeSato merged 5 commits into
mainfrom
feat/dynamic-regression

Conversation

@YuminosukeSato
Copy link
Copy Markdown
Owner

@YuminosukeSato YuminosukeSato commented Mar 22, 2026

Summary

Add Dynamic Regression (time-varying coefficients β_t), equivalent to R CausalImpact's AddDynamicRegression.

β_t = β_{t-1} + η_t, η_t ~ N(0, diag(σ²_β))

Regression coefficients follow a random walk over time. This provides better counterfactual predictions than static regression when the pre-intervention relationship undergoes structural changes.

Changes

Rust (src/)

  • kalman.rs: Multivariate FFBS (Durbin-Koopman simulation smoother). Cholesky-solve based backward pass for numerical stability (avoids explicit matrix inversion)
  • sampler.rs: Extended Block Gibbs loop via run_single_chain_dynamic() — samples β_t, σ²_β and propagates random walk predictions in post-period
  • lib.rs: Added dynamic_regression=false PyO3 argument

Python (python/)

  • options.py: Added ModelOptions.dynamic_regression: bool = False field
  • main.py: Added default to DEFAULT_MODEL_ARGS, passthrough in _run_sampler

Tests

  • test_dynamic_regression.py: 16 tests (option validation 2, basic behavior 3, boundary 5, statistical quality 3, integration 3)
  • Rust tests: 6 new tests (kalman 3 + sampler 3)
  • All 193 existing tests pass with no regressions

Design Decisions

  • Spike-and-slab is automatically disabled when dynamic_regression=True (variable selection is incompatible with time-varying coefficients)
  • β_t / σ²_β are internal variables, not exposed in V1 (YAGNI)
  • σ²_β prior: InvGamma(16, 16 × (y_sd/√T_pre)²), matching R AddDynamicRegression defaults
  • init_beta_var = 100 (sufficiently vague prior for standardized data)
  • Static/dynamic paths are fully separated via run_single_chain_static / run_single_chain_dynamic — zero risk of regression

Usage

ci = CausalImpact(
    df, pre_period, post_period,
    model_args={"dynamic_regression": True}
)

Test plan

  • cargo fmt --check && cargo clippy -- -D warnings passes
  • cargo test — 26 tests pass
  • ruff check && ruff format --check passes
  • .venv/bin/pytest tests/ -v — 193 tests pass
  • End-to-end verification with structural break data

Formatting-only changes applied by ruff format.
No behavioral changes.
Implement multivariate FFBS (Durbin-Koopman simulation smoother) for
dynamic regression coefficients β_t that follow a random walk:
  β_t = β_{t-1} + η_t, η_t ~ N(0, diag(σ²_β))

Key components:
- kalman.rs: dynamic_beta_smoother() with Cholesky-solve backward pass
  for numerical stability (avoids explicit matrix inversion)
- sampler.rs: run_single_chain_dynamic() with full Block Gibbs loop:
  Step 1: y_adj = y - x'β_t → sample μ_t (existing smoother)
  Step 2: y_adj = y - μ_t → sample β_t (new FFBS)
  Step 3-4: sample σ²_obs, σ²_level (existing)
  Step 5: sample σ²_β per covariate (new inv-gamma)
  Predictions: random walk β propagation in post-period
- lib.rs: dynamic_regression=false PyO3 argument
- Spike-and-slab disabled when dynamic_regression=true

Includes 6 new Rust tests (3 kalman + 3 sampler).
- ModelOptions: add dynamic_regression: bool = False with validation
- DEFAULT_MODEL_ARGS: add "dynamic_regression": False
- _run_sampler: pass dynamic_regression to Rust run_gibbs_sampler
16 tests across 5 categories:
- Options validation (2): default=False, True accepted
- Basic behavior (3): predictions shape, gamma empty, False=existing
- Boundary (5): k=0, k=1, k=2, T_pre=2, k=T_pre-1
- Statistical (3): constant beta, structural break, no NaN
- Integration (3): end-to-end, summary/inferences, plot

Also update test_options.py expected_keys for dynamic_regression.
- summary.py: adopt origin/main's R-compatible summary format
- test_analysis.py: adopt origin/main's docstring removal
@YuminosukeSato YuminosukeSato merged commit 96bf6dd into main Mar 22, 2026
13 checks passed
@YuminosukeSato YuminosukeSato deleted the feat/dynamic-regression branch March 22, 2026 16:33
YuminosukeSato added a commit that referenced this pull request Mar 23, 2026
feat: add dynamic regression (time-varying coefficients)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant