StaggeredDifferenceInDifferences#
- class causalpy.experiments.staggered_did.StaggeredDifferenceInDifferences[source]#
A class to analyse data from staggered adoption Difference-in-Differences settings.
This class implements the Borusyak, Jaravel, and Spiess (BJS, 2024) imputation estimator for staggered adoption settings. It fits a model on untreated observations only (pre-treatment periods for eventually-treated units plus all periods for never-treated units), then predicts counterfactual outcomes for all observations. Treatment effects are computed as the difference between observed and predicted outcomes for treated observations.
- Parameters:
data (
NativeDataFrame) – Panel data (unit x time observations) as any eager dataframe Narwhals supports, such as pandas, Polars, or PyArrow. Converted to pandas internally.formula (
str) – A statistical model formula. Recommended: “y ~ 1 + C(unit) + C(time)” for unit and time fixed effects.unit_variable_name (
str) – Name of the column identifying units.time_variable_name (
str) – Name of the column identifying time periods.treated_variable_name (
str) – Name of the column indicating treatment status (0/1). Defaults to “treated”.treatment_time_variable_name (
str|None) – Name of the column containing unit-level treatment time (G_i). If None, treatment time is inferred from the treated_variable_name column.never_treated_value (
Any) – Value indicating never-treated units in treatment_time column. Defaults to np.inf.model (
PyMCModel|RegressorMixin|None) – A model for the untreated outcome. Defaults to LinearRegression.event_window (
tuple[int,int] |None) – Tuple (min_event_time, max_event_time) to restrict event-time aggregation. If None, uses all available event-times.reference_event_time (
int) – Event-time index associated with plots (reserved for future use). Defaults to -1.
- Aggregated estimates live on the result bundle exposed through
- :attr:`result`
- Type:
att_group_timeandatt_event_timeDataFrames
- (each including an ``identified`` column; non-identified cells have
- ``NaN`` estimates), the raw counterfactual draws ``y_pred``, and the
- ``hdi_prob`` used during effect aggregation.
- non_identified_cohorts_#
Treatment cohorts with at least one non-identified post-treatment ATT(g, t).
- Type:
Notes
Estimate extraction
The Borusyak-Jaravel-Spiess imputation estimator fits the untreated outcome model using only observations that are not yet treated or never treated. It predicts each treated observation’s untreated potential outcome, subtracts that prediction from the observed outcome, and averages the resulting one-sided contrasts into group-time and event-time ATTs. Bayesian aggregation retains posterior uncertainty in
mu; OLS aggregation uses point predictions and standard-error approximations.Like Interrupted Time Series, this fit-predict-subtract procedure is a reduced-form estimator. The corresponding structural contrast is a saturated regression as in Wooldridge’s extended two-way fixed effects (ETWFE) framework, which CausalPy does not currently implement.
This estimator requires the following identifying assumptions:
Absorbing treatment: Once a unit receives treatment, it must remain treated in all subsequent periods. Treatment cannot be reversed or temporarily suspended. This is validated at runtime.
Parallel trends: In the absence of treatment, treated and control units would have followed parallel outcome trajectories.
No anticipation: Units do not change their behavior in anticipation of future treatment.
Untreated support at each calendar period: The time fixed effect \(\gamma_t\) for calendar period \(t\) is identified only if at least one unit is untreated in that period. Without never-treated units, post-treatment effects for the last-treated cohort (and any calendar periods where every unit is already treated) are not identified. CausalPy warns when this condition fails and marks the affected
ATT(g, t)andATT(e)cells as non-identified in the output tables.
Panel Balance: This implementation supports both balanced and unbalanced panel data. While balanced panels (where each unit is observed in every time period) are common in staggered DiD applications, the imputation-based approach of Borusyak et al. (2024) can accommodate unbalanced panels. The key requirement is that treatment timing is well-defined for each unit, not that all units are observed in all periods. Unit and observation counts in the summary output are computed without assuming balanced panels.
Lazy lifecycle
Construction only validates inputs and builds design matrices — no sampling happens. Call
fit()to sample posterior draws and populateresult, orsample_prior_predictive()to run a prior predictive check (inspect it viaplot(group="prior")).References
Borusyak, K., Jaravel, X., & Spiess, J. (2024). Revisiting Event Study Designs: Robust and Efficient Estimation. Review of Economic Studies.
Examples
>>> import causalpy as cp >>> from causalpy.data.simulate_data import generate_staggered_did_data >>> df = generate_staggered_did_data(n_units=30, n_time_periods=15, seed=42) >>> result = cp.StaggeredDifferenceInDifferences( ... df, ... formula="y ~ 1 + C(unit) + C(time)", ... unit_variable_name="unit", ... time_variable_name="time", ... treated_variable_name="treated", ... treatment_time_variable_name="treatment_time", ... model=cp.pymc_models.LinearRegression( ... sample_kwargs={ ... "tune": 100, ... "draws": 200, ... "chains": 2, ... "progressbar": False, ... } ... ), ... ).fit()
Methods
Construct the model graph without sampling anything.
Generate a decision-ready summary of causal effects for Staggered Difference-in-Differences.
StaggeredDifferenceInDifferences.fit(**kwargs)Run the posterior phase and populate
result.Generate a self-contained HTML report for this experiment.
Get event-time plotting data.
Validate the input data and parameters.
StaggeredDifferenceInDifferences.plot(*[, ...])Plot the staggered difference-in-differences event study.
Plot cohort-specific
ATT(g, t)trajectories.Ask the model to print its posterior coefficients.
StaggeredDifferenceInDifferences.sample_prior_predictive(...)Run the optional prior phase and populate
prior_result.Set optional maketables rendering options for this experiment.
Print summary of main results.
Attributes
has_prior_predictiveWhether the prior phase has run (draws, and bundle where kept).
idataReturn fitted DataTree when the model backend supports it.
is_builtWhether the model graph / fit design exists (no draws implied).
is_configureddesign matrices are ready.
is_fittedWhether posterior draws and the posterior result bundle exist.
modelThe underlying model instance.
prior_resultPrior-group result bundle; raises before prior sampling.
resultPosterior-group result bundle; raises before
fit().supports_bayessupports_olssupports_pymc_forecastlabelsdata- __init__(data, formula, unit_variable_name, time_variable_name, treated_variable_name='treated', treatment_time_variable_name=None, never_treated_value=inf, model=None, event_window=None, reference_event_time=-1)[source]#
- Parameters:
- Return type:
None
- classmethod __new__(*args, **kwargs)#