Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,3 @@ rustflags = ["-C", "target-cpu=native"]

[target.aarch64-apple-darwin]
rustflags = ["-C", "target-cpu=native"]

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "target-cpu=native"]
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ z-ai/
.claude/
*.DS_Store
site/
.cargo/
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ Tests run on every commit with seed-fixed MCMC for deterministic reproduction.
|---|---|---|
| `point_effect_mean` | ±3% relative | Passing on core scenarios |
| `cumulative_effect_total` | ±3% relative | Passing on core scenarios |
| `ci_lower` / `ci_upper` | Tight parity | `±1.5%` no-covariates, `±1%` covariates, explicit Phase 2 acceptance `±3%`, seasonal fixture `±5%` |
| `ci_lower` / `ci_upper` | Tight parity | `±1%` no-covariates, `±1%` covariates, explicit Phase 2 acceptance `±3%`, seasonal `±1%` |
| `p_value` | Significance match | Classification at alpha=0.05 |

### What is matching R and what is not
Expand All @@ -118,10 +118,13 @@ Tests run on every commit with seed-fixed MCMC for deterministic reproduction.
| Spike-and-slab variable selection | Matching | Coordinate-wise sampling with StudentSpikeSlabPrior defaults (`expected.r2=0.8`, `prior.df=50`, `prior.information.weight=0.01`, `diagonal.shrinkage=0.5`) |
| expected.model.size | Matching | Unified default `2` in `CausalImpact` and `ModelOptions` |
| expected.r2 = 0.8, prior.df = 50 | Matching | Same documented residual variance prior defaults as BoomSpikeSlab / bsts |
| Seasonal component (`nseasons`, `season_duration`) | Supported | R-compatible API with seasonal fixture coverage |
| Seasonal component (`nseasons`, `season_duration`) | Matching | State-space model matching R bsts `AddSeasonal()` (±1% CI parity) |
| Dynamic regression | Supported | Time-varying coefficients via random-walk FFBS; `dynamic_regression=True` |
| Local linear trend | Supported | Opt in with `state_model="local_linear_trend"` |

Matching = CI-enforced numerical equivalence with R bsts (±3% or tighter).
Supported = Feature implemented, no R parity fixture yet.

Covariate CI bounds are enforced twice: the legacy parity fixture remains tighter than
Phase 2 requirements, and a separate Phase 2 acceptance test keeps the threshold at `±3%`.

Expand Down
4 changes: 2 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ ci = CausalImpact(data, pre_period, post_period, model_args=opts)
| `expected_model_size` | `int` | 2 | Expected number of active covariates for spike-and-slab prior |
| `dynamic_regression` | `bool` | `False` | Enable time-varying regression coefficients |
| `state_model` | `str` | `"local_level"` | `"local_level"` or `"local_linear_trend"` |
| `nseasons` | `int \| None` | `None` | Seasonal cycle count |
| `season_duration` | `int \| None` | `None` | Duration of each seasonal block; defaults to 1 when `nseasons` is set |
| `nseasons` | `int \| None` | `None` | Seasonal cycle count. `nseasons=1` is equivalent to no seasonal component. |
| `season_duration` | `int \| None` | `None` | Duration of each seasonal block; defaults to 1 when `nseasons` is set. Requires `nseasons` to be set. |

## `CausalImpactResults`

Expand Down
6 changes: 3 additions & 3 deletions docs/compatibility-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Comparison of features between R CausalImpact (bsts 1.4.1) and this Python imple
|---|---|---|---|
| Local level | Yes | Yes | Identical algorithm |
| Local linear trend | Yes | Yes | `state_model="local_linear_trend"` |
| Seasonality | Yes | Yes | R-compatible API with seasonal fixture coverage |
| Seasonality | Yes | Yes | State-space model matching R bsts `AddSeasonal()` |
| Dynamic regression | Yes | Yes | `dynamic_regression=True` |
| Regression (static) | Yes | Yes | Identical algorithm |

Expand All @@ -18,7 +18,7 @@ Comparison of features between R CausalImpact (bsts 1.4.1) and this Python imple
|---|---|---|---|
| niter | Yes | Yes | Same default (1000) |
| nseasons | Yes | Yes | `ModelOptions.nseasons` or `model_args["nseasons"]` |
| season.duration | Yes | Yes | `ModelOptions.season_duration` or `model_args["season.duration"]` |
| season.duration | Yes | Yes | `ModelOptions.season_duration` or `model_args["season_duration"]` (R compat: `"season.duration"`) |
| prior.level.sd | Yes | Yes | Same default (0.01) |
| standardize.data | Yes | Yes | Same default (True) |
| expected.model.size | Yes | Yes | Unified default `2` |
Expand Down Expand Up @@ -71,7 +71,7 @@ Comparison of features between R CausalImpact (bsts 1.4.1) and this Python imple
|---|---|---|
| point_effect_mean | ±3% relative | Passing |
| cumulative_effect_total | ±3% relative | Passing |
| ci_lower / ci_upper | Tight parity (`±1.5%` no-cov, `±1%` covariates, `±5%` seasonal) | Passing |
| ci_lower / ci_upper | Tight parity (`±1%` no-cov, `±1%` covariates, `±1%` seasonal) | Passing |
| p_value significance | Match at alpha=0.05 | Passing |

Tests run against R CausalImpact 1.4.1 fixtures on every PR.
32 changes: 32 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,38 @@ Posterior prob. of a causal effect: 99.95%
True effect = 8.0, estimated = 7.57. The 95% CI [5.99, 9.18] contains the true
value.

### How it works

The seasonal component uses a state-space model matching R bsts `AddSeasonal()`.
The state vector is `[μ_t, s_1(t), ..., s_{S-1}(t)]` where `S = nseasons`.
Seasonal states evolve via a sum-to-zero transition: the next season equals
the negative sum of the previous `S-1` seasons plus noise.

### Using `season_duration`

Set `season_duration > 1` when each season spans multiple time steps.
The seasonal transition only fires at season boundaries
(`t % season_duration == 0`); in between, the seasonal state is frozen.

```python
# Using the same data from the seasonal example above

# Daily data with 7-day weekly pattern (default: season_duration=1)
ci = CausalImpact(
data, pre_period, post_period,
model_args={"nseasons": 7, "season_duration": 1, "seed": 42},
)

# Monthly data with quarterly pattern (each quarter = 3 months)
ci = CausalImpact(
data, pre_period, post_period,
model_args={"nseasons": 4, "season_duration": 3, "seed": 42},
)
```

When `season_duration` is omitted, it defaults to 1 (every time step is a new
season). `nseasons=1` is equivalent to no seasonal component.

---

## 4. Dynamic Regression
Expand Down
4 changes: 3 additions & 1 deletion docs/migration-from-r.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,9 @@ Key differences:

## Numerical Equivalence

This library verifies ±3% agreement with R CausalImpact on point estimates and cumulative effects across multiple test scenarios, including a seasonal fixture. Tests run on every PR.
This library verifies ±3% agreement with R CausalImpact on point estimates and cumulative effects across multiple test scenarios. Tests run on every PR.

The seasonal model uses the same state-space algorithm as R bsts `AddSeasonal()`, achieving ±1% CI parity on the seasonal fixture.

Differences arise from independent RNG implementations (R's `set.seed` vs Rust's `ChaCha8Rng`), not from algorithmic differences.

Expand Down
Loading
Loading