bjs
Estimate causal effects in staggered treatment adoption using imputation-based difference-in-differences, with event studies and pre-trend diagnostics that handle heterogeneous effects.
Instructions
Borusyak, Jaravel & Spiess (2024) imputation DID estimator. Do NOT use when: no never-treated or not-yet-treated observations remain -- the imputation model cannot be fit; the design is a simple 2x2 -- use sp.did(method='2x2'). Assumptions: Conditional parallel trends between treated and comparison groups absent treatment; No anticipation of treatment before its onset; Treatment effects may be heterogeneous across cohorts and time (no homogeneity required). Pre-conditions: Panel or repeated cross-section with a unit (or group) identifier and a time identifier; At least one never-treated or not-yet-treated comparison group; Pre-treatment periods to assess parallel trends. Failure modes: Pre-treatment event-study coefficients are jointly non-zero (pre-trend violation) -> Use honest DiD bounds to quantify robustness to trend violations, or condition on covariates; Two-way fixed-effects estimate is contaminated by 'forbidden' comparisons / negative weights -> Use a heterogeneity-robust estimator (Callaway-Sant'Anna, Borusyak et al., Gardner two-stage). Alternatives: sp.callaway_santanna, sp.did, sp.honest_did. Typical minimum N: 100.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| y | Yes | Outcome variable name. | |
| fe | No | Which fixed effects the Y(0) model carries, replacing the default two-way ``unit + time``. Mirrors Stata ``did_imputation``'s ``fe()``. Each entry is one fixed effect, written either as a bare column or Stata-style with ``#`` for the interacted cell:: fe=['time'] # period FE only fe=['unit', 'state#year'] # unit FE + state-by-year FE fe=[] # no fixed effects at all Levels are factorized over the whole panel, so a cell seen only among treated rows still gets a column and is imputed rather than silently absorbing the reference level. | |
| vce | No | Standard-error mode for the overall ATT. ``'analytic'`` is the exact Borusyak--Jaravel--Spiess variance: the estimator is linear in the outcome, so its weights are computed rather than approximated, and the result reproduces Stata ``did_imputation`` and R ``didimputation`` to ~5e-8. Before v1.23.0 this path used an approximation that was materially anti-conservative (18-36% too small on the harness fixtures); see MIGRATION.md. Measured, not asserted. On a homogeneous-effect design with a never-treated half, 400-800 replications per cell: =========== ========== ==================== clusters coverage mean SE / sd(est) =========== ========== ==================== 30 0.925 0.985 60 0.938 0.936 120 0.948 0.978 240 0.935 1.007 480 **0.950** 0.983 =========== ========== ==================== Two things to read off it. The ratio sits at 1 throughout, so the variance formula carries no systematic bias -- the approximation it replaced ran 18-36% low. And coverage reaches nominal by 480 clusters, so the shortfall at 60 is a small-cluster effect rather than a missing term: there the standard error is simply *noisy* (dispersion 9.2% against 3.2% at 480), which fattens the studentised distribution (sd(t) = 1.04 against 1.007). A ``t(G-1)`` critical value barely helps (0.938 to 0.941 at G = 60) because the problem is the variability of the standard error, not the degrees of freedom. ``vce='bootstrap'`` is the remedy in small designs, and ``se_method='auto'`` selects it below 30 clusters. ``'bootstrap'`` resamples whole clusters and re-runs the full imputation estimator. Point estimates are identical either way; per-horizon event-study SEs are unaffected. | analytic |
| time | Yes | Time period column. | |
| alpha | No | Significance level for confidence intervals. | |
| group | Yes | Unit identifier column. | |
| hetby | No | Stata ``did_imputation, hetby(varname)``: report heterogeneous overall ATTs by the levels of a **time-invariant** unit-level variable. Results land in ``model_info['hetby']`` (one row per | |
| min_n | No | Stata ``did_imputation, minn(#)``: drop event-study horizons with fewer than ``min_n`` treated observations (they are noisy and dominated by a single cohort). Dropped horizons are listed in a warning and excluded from the pre-trend test. | |
| detail | No | Payload depth: 'minimal' (~150 tokens) for sub-step calls where only the point estimate is needed; 'standard' (~1K tokens) for diagnostics + coefficient table; 'agent' (~2K tokens, default) adds violations / next_steps / suggested_functions so the LLM can plan its next call without another round-trip. | agent |
| n_boot | No | Number of cluster-bootstrap replications when ``vce='bootstrap'``. | |
| cluster | No | Variable for cluster-robust standard errors. Defaults to ``group`` (unit-level clustering). | |
| horizon | No | Relative time periods for event study estimates, e.g. ``list(range(-5, 6))``. If ``None``, reports only the overall ATT (no event study disaggregation). | |
| project | No | project parameter (Optional[List[str]]). | |
| balanced | No | Stata ``did_imputation, hbalance``: keep only eventually-treated units observed at *every* requested horizon, so the event-study composition is stable across ``k`` (no cohort churn). Never-treated units are always kept. Requires ``horizon`` (or ``pretrends``). Warns with the number of units dropped. | |
| controls | No | Continuous time-varying controls, entering the Y(0) model additively. Stata ``did_imputation``'s ``controls()``. | |
| as_handle | No | If true, cache the fitted result on the server and return result_id + result_uri alongside the JSON payload so a subsequent tools/call can chain without re-running. | |
| boot_seed | No | Seed for the cluster bootstrap (deterministic results). | |
| data_path | Yes | Absolute path or URL to a data file. Supported: .csv / .tsv / .txt (delimited), .parquet / .pq, .feather / .arrow, .xlsx / .xls, .dta (Stata), .json / .jsonl. Schemes: file://, s3://, gs://, https://. | |
| pretrends | No | Stata ``did_imputation, pretrends(k)``: estimate the ``k`` pre-treatment placebo coefficients (horizons ``-k .. -1``, added to ``horizon`` if not already requested) and report their joint Wald test in ``model_info['pretrend_test']``. Under the default ``pretrend_method='bjs'`` the test uses the full cluster-robust covariance of the auxiliary lead regression; under the other conventions it assumes the lead estimates are uncorrelated (valid but conservative), and :func:`statspai.bjs_pretrend_joint` gives the covariance-aware cluster-bootstrap version. | |
| result_id | No | Optional handle to a previously-fitted result (returned by an earlier call when as_handle=true). Tools that operate on a fitted object accept this in place of re-supplying data_path + columns. | |
| se_method | No | Shared DiD spelling for ``vce=`` -- ``'analytic'``, ``'bootstrap'`` (also ``'cluster'``, ``'pairs'``) or ``'auto'``. Passing both raises. ``'auto'`` picks the cluster bootstrap when the design has at most 30 clusters (Cameron, Gelbach & Miller 2008) and the analytic BJS variance otherwise: with few clusters the cluster-score sum behind any sandwich is itself noisy, which is a small-cluster problem rather than a defect in the formula. | |
| first_treat | Yes | Column indicating the period of first treatment. Use ``np.inf``, ``np.nan``, or ``0`` for never-treated units. | |
| data_columns | No | Optional column projection. Parquet/Feather/Stata loaders honour this for fast partial reads. | |
| save_weights | No | Stata ``did_imputation, saveweights()``: store the exact estimation weights ``w`` such that ``ATT = w'y`` in ``model_info['estimation_weights']`` (aligned with the rows of ``data``). Treated rows get ``1/N1``; untreated rows get the (negative) imputation weights implied by the FE projection -- useful for diagnosing which comparisons drive the estimate. | |
| data_sample_n | No | Optional uniform random subsample size (seed=0, deterministic) — useful on huge panels. | |
| save_residuals | No | Stata ``did_imputation, saveresid()``: store the untreated-fit residuals ``y - y0`` in ``model_info['residuals']`` (aligned with the rows of ``data``; ``NaN`` on treated rows, whose ``y - y0`` is the treatment effect, not a residual). | |
| pretrend_method | No | How the **pre-treatment** event-study coefficients are built. The post-treatment coefficients are imputation residuals either way; only the leads differ, and the difference is visible in the plot rather than in the ATT. - ``'bjs'`` -- the convention of Stata ``did_imputation, pretrends(k)``: an auxiliary dynamic TWFE regression on the untreated observations, with all relative times earlier than the requested leads pooled into the omitted category. Reproduces Stata's coefficients and standard errors. Not available with ``fe=``, ``unit_covariates=`` or ``time_covariates=``. - ``'in-sample'`` -- average the imputation residuals at pre-treatment relative times, as ``fect`` and ``did2s`` do. These are in-sample prediction errors: in a non-staggered design they equal the symmetric benchmark times the untreated unit share ``N0/N``, so they understate pre-trends, severely when most units are treated. This was StatsPAI's behaviour before v1.23.0. - ``'symmetric'`` -- Roth's (2026) repair, which uses the average of the pre-treatment periods as the reference for both halves of the path, so the plot matches a dynamic TWFE event study up to a common vertical shift and the usual visual heuristics apply. Non-staggered balanced designs without covariates only; raises otherwise rather than applying an unverified factor. The chosen convention and its caveat are recorded in ``model_info['pretrend_method']`` and ``model_info['event_study_convention']``. | bjs |
| time_covariates | No | Controls interacted with the **period** fixed effects, i.e. one coefficient per period. Stata's ``timecontrols()``. Typically a time-invariant unit characteristic whose effect is allowed to move over calendar time. | |
| unit_covariates | No | Controls interacted with the **unit** fixed effects, i.e. one slope per unit. Stata's ``unitcontrols()``. The canonical use is ``unit_covariates=[time_col]``, which makes Y(0) carry unit-specific linear trends. Identification bites hard here: a unit-specific slope needs at least two untreated periods for that unit, so early-treated cohorts can lose their imputation entirely. |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||