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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ The format is based on Keep a Changelog.

## [Unreleased]

### Removed

- DATE decomposition (`ci.decompose()`, `DateDecomposition`, `EffectComponent`).
The linear trend basis poses confounding risk with seasonal patterns; removed
to prevent misinterpretation.
- Retrospective mode (`mode="retrospective"`): treatment indicator covariate approach.
Removed along with DATE decomposition.

### Added

- Horseshoe prior as alternative to spike-and-slab via `ModelOptions(prior_type='horseshoe')`
Expand Down
9 changes: 0 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,6 @@ Evidence per implementation (all verified from source code, not documentation cl
| 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"` |
| DATE decomposition | Extended | Decomposes effects into spot/persistent/trend (arXiv:2602.00836) |
| Retrospective mode | Extended | Treatment indicators as covariates; effects from beta posteriors (arXiv:2602.00836) |
| Placebo test | Extended | Null distribution from pre-period splits |
| Horseshoe prior | Extended | Continuous shrinkage alternative to spike-and-slab (Kohns & Bhattacharjee 2022) |
| Conformal inference | Extended | Distribution-free prediction intervals |
Expand All @@ -220,8 +218,6 @@ Features that go beyond R's CausalImpact. These have no R equivalent.

| Feature | Method | What it does | Reference |
|---|---|---|---|
| DATE decomposition | `ci.decompose()` | Decomposes causal effect into spot, persistent, and trend | Schaffe-Odeleye et al. (2026), arXiv:2602.00836 |
| Retrospective mode | `mode="retrospective"` | Treatment indicators as covariates; effects extracted from beta posteriors | Schaffe-Odeleye et al. (2026), arXiv:2602.00836 |
| Placebo test | `ci.run_placebo_test()` | Validates effect against null distribution from pre-period splits | |
| Conformal inference | `ci.run_conformal_analysis()` | Distribution-free prediction intervals | Vovk et al. (2005) |
| DTW control selection | `select_controls()` | Automatic covariate selection via Dynamic Time Warping | Sakoe & Chiba (1978) |
Expand Down Expand Up @@ -255,8 +251,6 @@ Features that go beyond R's CausalImpact. These have no R equivalent.
| `dynamic_regression` | `False` | Enable time-varying regression coefficients (random-walk beta) |
| `state_model` | `"local_level"` | `"local_level"` or `"local_linear_trend"` |
| `prior_type` | `"spike_slab"` | `"spike_slab"` or `"horseshoe"` (continuous shrinkage for dense DGP) |
| `mode` | `"forward"` | `"forward"` (counterfactual prediction) or `"retrospective"` (treatment indicators as covariates) |

#### Methods and Properties

| Name | Returns | Description |
Expand All @@ -268,7 +262,6 @@ Features that go beyond R's CausalImpact. These have no R equivalent.
| `summary_stats` | `dict` | Aggregate statistics (effect mean, CI, p-value, etc.) |
| `posterior_inclusion_probs` | `ndarray \| None` | Posterior inclusion probability per covariate (spike-and-slab only) |
| `posterior_shrinkage` | `ndarray \| None` | Mean shrinkage factor per covariate (horseshoe only) |
| `decompose(alpha=None)` | `DateDecomposition` | DATE decomposition into spot/persistent/trend components |
| `run_placebo_test(...)` | `PlaceboTestResults` | Placebo test for effect validation |
| `run_conformal_analysis(...)` | `ConformalResults` | Distribution-free conformal prediction intervals |

Expand All @@ -295,8 +288,6 @@ python/causal_impact/
analysis.py # CausalAnalysis: effect computation, CI, p-values
summary.py # SummaryFormatter: tabular and narrative reports
plot.py # Plotter: matplotlib visualization
decomposition.py # DATE decomposition (spot/persistent/trend)
retrospective.py # Retrospective attribution mode

src/ (Rust)
lib.rs # PyO3 entry point: run_gibbs_sampler()
Expand Down
65 changes: 1 addition & 64 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ ci = CausalImpact(data, pre_period, post_period, model_args=None, alpha=0.05)
|---|---|---|
| `summary(output="summary", digits=2)` | `str` | Tabular summary of causal effects. Set `output="report"` for narrative form. |
| `report()` | `str` | Narrative interpretation of results (shortcut for `summary(output="report")`) |
| `plot(metrics=None)` | `Figure` | Matplotlib figure with original/pointwise/cumulative panels. Pass a list like `["original", "cumulative"]` to select panels. Add `"decomposition"` for the DATE panel. |
| `decompose(alpha=None)` | `DateDecomposition` | DATE decomposition of pointwise effects into spot/persistent/trend. Call before plotting with `"decomposition"` metric. |
| `plot(metrics=None)` | `Figure` | Matplotlib figure with original/pointwise/cumulative panels. Pass a list like `["original", "cumulative"]` to select panels. |
| `run_placebo_test(n_placebos=None, min_pre_length=3)` | `PlaceboTestResults` | Validates the causal effect against a null distribution from pre-period splits. |
| `run_conformal_analysis(alpha=None)` | `ConformalResults` | Distribution-free prediction intervals via split conformal inference. |

Expand Down Expand Up @@ -64,19 +63,6 @@ ci = CausalImpact(data, pre_period, post_period, model_args=opts)
| `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. |

### Analysis Mode

`mode` controls forward vs retrospective analysis. Pass via `model_args` dict (not `ModelOptions`).

| Value | Description |
|---|---|
| `"forward"` (default) | Counterfactual prediction: fit on pre-period, predict post-period |
| `"retrospective"` | Treatment indicators as covariates: fit on entire series |

```python
ci = CausalImpact(data, pre, post, model_args={"mode": "retrospective"})
```

## `CausalImpactResults`

Returned by `ci._results`. A frozen dataclass containing all computed quantities.
Expand Down Expand Up @@ -138,7 +124,6 @@ print(ci.posterior_shrinkage) # mean(kappa_j), 0=included 1=shrunk
### Incompatible combinations

- `prior_type='horseshoe'` + `dynamic_regression=True` raises `ValueError`
- `prior_type='horseshoe'` + `mode='retrospective'` raises `ValueError`

### References

Expand All @@ -150,54 +135,6 @@ print(ci.posterior_shrinkage) # mean(kappa_j), 0=included 1=shrunk

## Beyond R Extensions

### Retrospective Mode

In retrospective mode (`mode="retrospective"`), treatment indicator columns
(spot, persistent, trend) are added as covariates and the BSTS model is fit on
the entire time series. Treatment effects are extracted directly from the beta
posteriors for the treatment columns, rather than from counterfactual predictions.

Key differences from forward mode:

- The model fits on all data (pre + post), not just pre-period
- Spike-and-slab variable selection is auto-disabled
- `ci._decomposition` is populated automatically (no need to call `ci.decompose()`)
- `ci.decompose()` is still available but uses the forward-mode OLS projection

Reference: Schaffe-Odeleye et al. (2026), arXiv:2602.00836.

### `DateDecomposition`

Returned by `ci.decompose()` or auto-populated in retrospective mode.
A frozen dataclass containing the DATE decomposition results.

Reference: Schaffe-Odeleye et al. (2026), arXiv:2602.00836.

| Field | Type | Description |
|---|---|---|
| `spot` | `EffectComponent` | Immediate impulse effect at intervention |
| `persistent` | `EffectComponent` | Sustained level shift from intervention onward |
| `trend` | `EffectComponent \| None` | Linearly growing/decaying effect. `None` when `T_post < 3`. |
| `residual_mean` | `ndarray` | Mean residual trajectory after decomposition |
| `design_matrix` | `ndarray` | The D matrix used for OLS projection |
| `alpha` | `float` | Significance level used for credible intervals |

### `EffectComponent`

Each component of the DATE decomposition.

| Field | Type | Description |
|---|---|---|
| `name` | `str` | Component name: `"spot"`, `"persistent"`, or `"trend"` |
| `coefficient` | `float` | Posterior mean of the OLS coefficient |
| `ci_lower` | `float` | Lower credible interval bound on coefficient |
| `ci_upper` | `float` | Upper credible interval bound on coefficient |
| `samples` | `ndarray` | Per-sample trajectories, shape `(n_samples, T_post)` |
| `coefficients` | `ndarray` | OLS coefficient per sample, shape `(n_samples,)` |
| `mean` | `ndarray` | Posterior mean trajectory, shape `(T_post,)` |
| `lower` | `ndarray` | Lower CI on trajectory, shape `(T_post,)` |
| `upper` | `ndarray` | Upper CI on trajectory, shape `(T_post,)` |

### `PlaceboTestResults`

Returned by `ci.run_placebo_test()`. Validates the effect against a null distribution.
Expand Down
2 changes: 0 additions & 2 deletions docs/compatibility-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@ Features that extend beyond R CausalImpact. No R equivalent exists.

| Feature | Method | Description |
|---|---|---|
| DATE decomposition | `ci.decompose()` | Decomposes causal effect into spot/persistent/trend (arXiv:2602.00836) |
| Retrospective mode | `mode="retrospective"` | Treatment indicators as covariates; effects from beta posteriors (arXiv:2602.00836) |
| Placebo test | `ci.run_placebo_test()` | Validates effect against null distribution from pre-period splits |
| Conformal inference | `ci.run_conformal_analysis()` | Distribution-free prediction intervals (Vovk et al., 2005) |
| DTW control selection | `select_controls()` | Automatic covariate selection via Dynamic Time Warping |
123 changes: 1 addition & 122 deletions docs/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,131 +437,10 @@ print(ci.posterior_inclusion_probs)
| No Effect (negative control) | 0.0 | -0.55 | [-1.50, 0.48] | 0.118 | No (correct) |
| Multiple Covariates | 5.0 | 6.53 | [3.64, 9.67] | 0.001 | Yes |

---

## 7. DATE Decomposition (Spot / Persistent / Trend)

Decompose the estimated causal effect into three interpretable components.
This example uses synthetic data with a known persistent effect of +3 and
a trend of +0.1 per time step.

Reference: Schaffe-Odeleye et al. (2026), arXiv:2602.00836.

```python
rng = np.random.default_rng(42)

n_pre, n_post = 100, 30
n = n_pre + n_post
x = np.zeros(n)
x[0] = 100
for t in range(1, n):
x[t] = 0.999 * x[t - 1] + rng.normal(0, 1)

y = 1.2 * x + rng.normal(0, 1, size=n)
# Inject a persistent shift + trend
y[n_pre:] += 3.0 + 0.1 * np.arange(n_post)

dates = pd.date_range("2020-01-01", periods=n, freq="D")
data = pd.DataFrame({"y": y, "x": x}, index=dates)
pre_period = ["2020-01-01", "2020-04-09"]
post_period = ["2020-04-10", "2020-05-09"]

ci = CausalImpact(data, pre_period, post_period, model_args={"seed": 42})
dec = ci.decompose()

print(f"Spot: {dec.spot.coefficient:+.2f} [{dec.spot.ci_lower:+.2f}, {dec.spot.ci_upper:+.2f}]")
print(f"Persistent: {dec.persistent.coefficient:+.2f} [{dec.persistent.ci_lower:+.2f}, {dec.persistent.ci_upper:+.2f}]")
if dec.trend is not None:
print(f"Trend: {dec.trend.coefficient:+.2f} [{dec.trend.ci_lower:+.2f}, {dec.trend.ci_upper:+.2f}]")
```

### Interpreting the results

| Component | True value | What it means |
|---|---|---|
| Spot | 0.0 | No immediate one-time impact |
| Persistent | 3.0 | Permanent baseline shift |
| Trend | 0.1 | Effect grows by 0.1 per time step |

### Plotting with decomposition

```python
fig = ci.plot(metrics=["original", "pointwise", "cumulative", "decomposition"])
fig.savefig("example_decomposition.png", dpi=150, bbox_inches="tight")
```

The fourth panel shows the three components with credible intervals.

---

## 8. Retrospective Attribution Mode

In retrospective mode, treatment indicator columns (spot, persistent, trend)
are added as covariates and the model is fit on the entire time series.
Treatment effects are extracted directly from the beta posteriors.

This approach avoids the counterfactual prediction step of forward mode and
instead estimates treatment effects within a single model fit.

```python
import numpy as np
import pandas as pd
from causal_impact import CausalImpact

rng = np.random.default_rng(42)

n_pre, n_post = 200, 50
n = n_pre + n_post
y = np.cumsum(rng.normal(0, 0.3, n)) + 50
y += rng.normal(0, 0.5, n)
# Inject a persistent level shift of +5 at intervention
y[n_pre:] += 5.0

dates = pd.date_range("2020-01-01", periods=n, freq="D")
data = pd.DataFrame({"y": y}, index=dates)
pre_period = [dates[0], dates[n_pre - 1]]
post_period = [dates[n_pre], dates[-1]]

ci = CausalImpact(
data, pre_period, post_period,
model_args={
"niter": 2000, "nwarmup": 1000, "seed": 42,
"mode": "retrospective",
"prior_level_sd": 0.001,
},
)

# Decomposition is auto-populated in retrospective mode
dec = ci._decomposition
print(f"Spot: {dec.spot.coefficient:+.2f}")
print(f"Persistent: {dec.persistent.coefficient:+.2f}")
if dec.trend is not None:
print(f"Trend: {dec.trend.coefficient:+.2f}")

# Standard summary and plot still work
print(ci.summary())
fig = ci.plot()
```

### When to use retrospective mode

- Forward mode (default): Use when you have a clear pre/post split and want counterfactual predictions. Standard CausalImpact workflow.
- Retrospective mode: Use when you want to decompose the treatment effect into spot/persistent/trend directly from the model, or when the treatment timing is known and you want a single-model-fit approach.

### Note on `prior_level_sd`

In retrospective mode, the local level state (random walk) and the persistent
treatment indicator (step function) are partially collinear. Setting
`prior_level_sd` to a small value (e.g., 0.001) constrains the state variation
and forces the model to attribute level shifts to the treatment columns.

---

## Summary of All Examples

All eight examples produce correct results:
All six examples produce correct results:

- When there is a true effect, the 95% credible interval contains the true value
- When there is no effect, the model correctly reports non-significance
- Results are consistent with the R CausalImpact package
- Retrospective mode extracts treatment components directly from beta posteriors
Loading
Loading