Skip to content

statspai.causal_impact

causal_impact

Causal Impact module for StatsPAI.

Estimates the causal effect of an intervention on a time series by constructing a synthetic counterfactual from control series using a structural time-series model (regression + AR(1) state, fit by plug-in estimates and the Kalman filter).

Same estimand and interface idea as Google's R CausalImpact package, but a different, frequentist model: R CausalImpact fits a Bayesian local-level + spike-and-slab regression by MCMC, so the numbers differ.

References

Brodersen, K.H., Gallusser, F., Koehler, J., Remy, N., and Scott, S.L. (2015). "Inferring Causal Impact Using Bayesian Structural Time-Series Models." Annals of Applied Statistics, 9(1), 247-274. [@brodersen2015inferring]

CausalImpactEstimator

Causal Impact estimator using a structural time-series model.

Fits a regression + AR(1) state-space model on the pre-intervention period, then forecasts the counterfactual into the post-intervention period. The high-level :func:causal_impact wrapper is the usual entry point; instantiate this class directly only when you want to drive .fit() yourself.

Parameters:

Name Type Description Default
data DataFrame

Time-series data, one row per period.

required
y str

Outcome (intervened) series.

required
time str

Time column used for ordering.

required
intervention_time Any

First post-intervention period (inclusive).

required
covariates list of str

Control series unaffected by the intervention.

None
alpha float

Significance level for the credible intervals.

0.05
n_seasons int

Not used (no seasonal component); a value > 1 warns.

None
n_boot int

Bootstrap draws per pass for the critical value; 0 for the t quantile (see :func:causal_impact).

199
seed int

Seed of the bootstrap draws.

0

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 60
>>> week = np.arange(n)
>>> ad_spend = rng.normal(100, 5, size=n)
>>> sales = 2.0 * ad_spend + rng.normal(0, 3, size=n)
>>> sales[40:] += 15.0
>>> df = pd.DataFrame({'week': week, 'sales': sales,
...                    'ad_spend': ad_spend})
>>> est = sp.CausalImpactEstimator(df, y='sales', time='week',
...                                intervention_time=40,
...                                covariates=['ad_spend'])
>>> result = est.fit()
>>> (est.n_pre, est.n_post)
(40, 20)
>>> bool(result.estimate > 0)
True

fit

fit() -> CausalResult

Fit the structural time-series model and estimate causal impact.

causal_impact

causal_impact(data: DataFrame, y: str, time: str, intervention_time: Any, covariates: Optional[List[str]] = None, alpha: float = 0.05, n_seasons: Optional[int] = None, weights: Optional[str] = None, n_boot: int = 199, seed: int = 0) -> CausalResult

Estimate the causal impact of an intervention on a time series.

Parameters:

Name Type Description Default
data DataFrame

Time-series data (one row per time period).

required
y str

Outcome variable (the series that was intervened upon).

required
time str

Time column (used for ordering; can be int, date, etc.).

required
intervention_time any

First period of the intervention (inclusive).

required
covariates list of str

Control time series (not affected by the intervention). If None, uses a local-level model without covariates.

None
alpha float

Significance level for the (frequentist) prediction intervals.

0.05
n_seasons int

Accepted for API compatibility but currently not used: the model has no seasonal component.

None
weights None

Not supported, and refused with MethodIncompatibility rather than ignored: the data are one series, one row per period, so there is no sample of units for sampling weights to describe, and the forecast-error variance has no weighted form. To weight the units that make up an aggregate series, build the series as a weighted mean first.

None
n_boot int

Parametric-bootstrap draws per pass for the critical value of the average / cumulative effect (bias-corrected bootstrap-t, see Notes); 0 uses the t quantile alone.

199
seed int

Seed of those draws (the result is reproducible).

0

Returns:

Type Description
CausalResult
Notes

estimate is the average post-period effect and se the standard deviation of the error of the forecast actually made, u_post - L u_pre, which accounts for estimating the regression coefficients exactly; the error covariance is an AR(1)-plus-noise model fitted by REML on the pre-period, and intervals and p-values use a t distribution with n_pre - p - 3 degrees of freedom (model_info['df']). The uncertainty of that covariance is carried by the critical value of the average / cumulative effect: a two-pass parametric bootstrap-t (n_boot draws each) that first corrects the small-sample bias of the REML AR(1) coefficient and noise share, then simulates the studentised forecast error with the covariance re-fitted on every draw. model_info['crit'] is the larger of that quantile and the t quantile, and the p-value the larger of the bootstrap share and the t p-value, so the interval is never shorter than the t one. The t quantile alone covered 89.5% for a nominal 95% at n_pre = 40, rho = 0.9 (see tests/reference_parity/ test_causal_impact_uncertainty.py for the calibrated figures); the pointwise bands in detail keep the t quantile. Until 2026-10-19 the SE took all parameters as known and covered 57.5%-90.5% in the same designs; that number is kept as model_info['se_avg_conditional']. The detail column effect_se carries the pointwise version (predicted_se stays the filter's conditional one-step value). This is a frequentist regression + AR(1) state-space model, not the Bayesian local-level + spike-and-slab model of R CausalImpact; the numbers are not comparable one for one.

Examples:

Effect of an intervention introduced at week 40, recovered against a counterfactual built from an ad-spend control series:

>>> import numpy as np
>>> import pandas as pd
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 60
>>> week = np.arange(n)
>>> ad_spend = rng.normal(100, 5, size=n)
>>> sales = 2.0 * ad_spend + rng.normal(0, 3, size=n)
>>> sales[40:] += 15.0          # intervention lifts sales from week 40
>>> df = pd.DataFrame({'week': week, 'sales': sales,
...                    'ad_spend': ad_spend})
>>> result = sp.causal_impact(df, y='sales', time='week',
...                           intervention_time=40,
...                           covariates=['ad_spend'])
>>> result.n_obs
60
>>> bool(result.estimate > 0)        # positive average impact recovered
True
References

brodersen2015inferring

impactplot

impactplot(result: CausalResult, type: str = 'all', ax: Any = None, figsize: tuple[float, float] = (12, 9), title: Optional[str] = None) -> Any

Causal Impact visualization (Google-style 3-panel plot).

Parameters:

Name Type Description Default
result CausalResult

Result from causal_impact().

required
type str

'all': 3-panel (original + pointwise + cumulative). 'original': actual vs counterfactual. 'pointwise': pointwise causal effect. 'cumulative': cumulative effect.

'all'
ax matplotlib Axes

Only for single-panel types.

None
figsize tuple
(12, 9)
title str
None

Returns:

Type Description
(fig, ax) or (fig, axes)

Examples:

>>> import numpy as np
>>> import pandas as pd
>>> import matplotlib
>>> matplotlib.use('Agg')
>>> import statspai as sp
>>> rng = np.random.default_rng(0)
>>> n = 60
>>> week = np.arange(n)
>>> ad_spend = rng.normal(100, 5, size=n)
>>> sales = 2.0 * ad_spend + rng.normal(0, 3, size=n)
>>> sales[40:] += 15.0
>>> df = pd.DataFrame({'week': week, 'sales': sales,
...                    'ad_spend': ad_spend})
>>> result = sp.causal_impact(df, y='sales', time='week',
...                           intervention_time=40,
...                           covariates=['ad_spend'])
>>> fig, axes = sp.impactplot(result, type='all')
>>> len(axes)
3
>>> fig.savefig('impact.png')