| Title: | GMM Estimation of Treatment Effects Under Staggered Adoption |
|---|---|
| Description: | Estimates cohort-by-time average treatment effects under staggered treatment adoption by the generalized method of moments. Three weighting schemes are provided, corresponding to a pooled stationary covariance, a cohort-specific stationary covariance, and an unrestricted within-cohort covariance. Optional adjustment for baseline covariates by outcome regression, and a serial-correlation robust over-identification test of parallel trends and no anticipation, are also supported. The methods are described in Arora and Bijani (2026) <doi:10.2139/ssrn.6558759>. |
| Authors: | Rishabh Bijani [aut, cre, cph] (ORCID: <https://orcid.org/0009-0003-9636-1774>), Parush Arora [aut, cph] (ORCID: <https://orcid.org/0009-0004-6571-3850>) |
| Maintainer: | Rishabh Bijani <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.0 |
| Built: | 2026-10-16 09:06:59 UTC |
| Source: | https://github.com/rishabhbijani/staggeredgmm |
A panel of 49 US states over 1976–2006, used to study intrastate bank branching deregulation and the distribution of income.
beck_banksbeck_banks
A data frame with 1519 rows and 8 variables:
Two-letter state abbreviation.
State name.
State FIPS code.
Calendar year, 1976–2006.
Income Gini coefficient.
Year the state deregulated intrastate bank
branching. 1960 marks states already deregulated before the sample
begins.
Natural log of gini.
Treatment indicator.
This dataset is a worked example of a
coding pitfall as much as of a design. Thirteen of the 49 states
deregulated at or before 1976, the first observed year: AK, AZ, CA, DC,
ID, MD, ME, NC, NV, NY, RI, SC and VT, ten of them carrying
branch_reform == 1960. These units have no pre-treatment period, so
none of their effects is identified. No state is coded 0, so the
dataset also contains no never-treated group.
Passed to gmm_staggered() as it stands, the always-treated states
raise an error naming them. That is deliberate: silently including them
would return a plausible-looking aggregate contaminated by cells that
are not estimated at all. Restrict to states adopting inside the
window before estimating:
dat <- beck_banks[beck_banks$branch_reform > 1976, ]
fit <- gmm_staggered(dat, yname = "ln_gini", tname = "wrkyr",
idname = "state", gname = "branch_reform")
This leaves 36 states across cohorts 1977–1999, identified from not-yet-treated controls alone.
Beck, T., Levine, R. and Levkov, A. (2010). "Big Bad Banks? The Winners and Losers from Bank Deregulation in the United States." The Journal of Finance 65(5), 1637–1667. doi:10.1111/j.1540-6261.2010.01589.x
Replication data deposited by the authors at DataverseNL, doi:10.34894/B1K9OU, and made available under the Creative Commons Attribution 4.0 International licence, https://creativecommons.org/licenses/by/4.0/.
Modified from the deposited data: a subset of columns was selected and
the remainder discarded. No values were altered, recoded or derived. The
full licence note is installed with the package; see
system.file("LICENSE.note", package = "staggeredGMM").
Extract cohort-by-time effect estimates
## S3 method for class 'staggered_gmm' coef(object, ...)## S3 method for class 'staggered_gmm' coef(object, ...)
object |
A |
... |
Ignored. |
A named numeric vector of cohort-by-time effects, one per cell,
named "g<cohort>:t<period>". Cells with no clean comparison are NA.
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") head(coef(fit))fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") head(coef(fit))
Pointwise normal-approximation intervals. They are not adjusted for multiplicity, so reading a whole event-study path off them overstates confidence.
## S3 method for class 'staggered_gmm' confint(object, parm, level = 0.95, ...)## S3 method for class 'staggered_gmm' confint(object, parm, level = 0.95, ...)
object |
A |
parm |
Optional character vector of coefficient names. Defaults to all of them. |
level |
Confidence level, strictly between 0 and 1. |
... |
Ignored. |
A two-column matrix of lower and upper limits, with one row per
requested coefficient. Unidentified cells are NA.
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") head(confint(fit))fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") head(confint(fit))
A serial-correlation robust joint test of the identifying assumptions, following Section 4.4 of Arora and Bijani (2026).
gmm_j_test(object, type = c("local", "full"), window = 3L)gmm_j_test(object, type = c("local", "full"), window = 3L)
object |
A fitted |
type |
Character. |
window |
Integer. Width of the local pre-window, in periods. Ignored
when |
The test is built on the never-treated event-study basis of Eq. (27) of
the paper. Fixing a base pre-period , each treated cohort
and period defines
Those with identify the cohort-by-time effects. Those with
are placebo restrictions whose expectation is zero only under
parallel trends and no anticipation. Collecting a set of placebo
moments into with covariance implied by
the fitted serial-correlation model,
under the null. Because the placebo moments do not involve the treatment effects, no degrees of freedom are subtracted for estimated parameters.
The base period is , the last period before any
cohort adopts, so that every moment has an untreated reference.
A large number of restrictions degrades the chi-squared approximation in
small panels. type = "local" therefore uses only the placebo moments
within window periods of each cohort's adoption, which holds its
nominal size across sample sizes at essentially no cost in power, since a
differential trend or an anticipation effect is already visible just
before treatment. type = "full" uses every placebo moment and is
well-sized only when the number of units is comfortably large.
An object of class staggered_gmm_jtest, a list with components
statistic, df, p_value, type, window, base_period,
n_moments and moments (the placebo moment table).
Arora, P. and Bijani, R. (2026). "Estimating Treatment Effects under Staggered Timing and Non-Spherical Errors." doi:10.2139/ssrn.6558759
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") gmm_j_test(fit)fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") gmm_j_test(fit)
Estimates cohort-by-time average treatment effects (CATTs) under staggered treatment adoption by the generalized method of moments, following Arora and Bijani (2026).
gmm_staggered( data, yname, tname, idname, gname, weighting = c("pooled_toeplitz", "cohort_toeplitz", "unrestricted"), never_treated = NULL, covar = NULL, max_iter = 100L, tol = 1e-06 )gmm_staggered( data, yname, tname, idname, gname, weighting = c("pooled_toeplitz", "cohort_toeplitz", "unrestricted"), never_treated = NULL, covar = NULL, max_iter = 100L, tol = 1e-06 )
data |
A data frame with one row per unit-period. |
yname |
Character. Name of the outcome column. Must be numeric; factor, character and logical outcomes are rejected rather than coerced. |
tname |
Character. Name of the calendar-time column. Must be
integer-valued, and the set of periods present in |
idname |
Character. Name of the unit identifier column. |
gname |
Character. Name of the cohort column, giving each unit's
first treated period, with |
weighting |
Character. The covariance model used to form the optimal
GMM weight. One of |
never_treated |
Logical or |
covar |
Optional character vector of baseline (pre-treatment,
time-invariant) covariate columns. When supplied, the outcome-regression
adjustment of Section 4.5 of the paper is applied, so that parallel
trends need hold only conditional on |
max_iter |
Integer. Maximum number of iterated-GMM reweighting steps. |
tol |
Numeric. Convergence tolerance on the largest absolute change in any CATT estimate between successive iterations. |
An object of class staggered_gmm: a list with components
call, weighting, coefficients, vcov, catt, aggregate,
weights, convergence, design, controls and validation. See
summary.staggered_gmm() and vignette("staggeredGMM").
The following conditions are checked before any estimation, and each raises an error naming the offending rows, units, periods or cohorts:
data is a data frame and every named column exists.
The outcome is numeric; time and cohort are integer-valued.
No missing unit id, period or cohort. Missing outcomes are permitted.
Each unit-period combination appears exactly once.
The cohort is constant within unit.
No period is absent from the whole panel. Individual units may be missing individual periods.
No unit is treated at or before the first observed period. Such units have no pre-treatment period, so none of their effects is identified, and silently including them contaminates the aggregate.
At least one cohort adopts strictly inside the observed window.
Units whose cohort falls after the last observed period are never treated within the window and are therefore pooled with the never-treated group as clean controls, rather than discarded.
Missing outcomes are supported. Group-period means are taken over the units observed at that period; autocovariances are divided by the number of residual pairs actually observed at each lag; and the variance of a group-period mean accounts for the overlap between the unit sets contributing at each pair of periods. When the panel is balanced all three reduce exactly to the complete-data formulas. Validity requires that missingness be independent of the outcome given group and period; selection on outcomes is not addressed by any weighting scheme.
A cohort-by-time effect with no clean comparison is not estimable. Such
cells are reported with estimate = NA and identified = FALSE. Where an
unidentified cell carries positive
aggregation weight the corresponding aggregate is NA, and an
identified-subset aggregate renormalised over the estimable cells is
reported alongside it, with the share of weight it covers.
Three states are reported separately. converged is TRUE only when the
largest change in any CATT fell below tol. solve_ok records the weaker
fact that at least one weighted solve succeeded. termination gives the
reason the iteration stopped.
When solve_ok is FALSE, no weighted step was ever completed and the
returned estimates are the identity-weighted seed rather than a GMM
estimate under the requested weighting. That always raises a warning.
When solve_ok is TRUE but converged is FALSE, the iteration ran
and did not settle; the estimates are the last valid iterate. That warns
for the two Toeplitz weightings but not for "unrestricted", where slow
convergence is routine.
Arora, P. and Bijani, R. (2026). "Estimating Treatment Effects under Staggered Timing and Non-Spherical Errors." doi:10.2139/ssrn.6558759
gmm_j_test() for the over-identification test of parallel
trends and no anticipation.
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") fitfit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") fit
Print a fitted staggered GMM model
## S3 method for class 'staggered_gmm' print(x, ...)## S3 method for class 'staggered_gmm' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly. Called for the side effect of printing.
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") print(fit)fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") print(fit)
Print a specification test result
## S3 method for class 'staggered_gmm_jtest' print(x, ...)## S3 method for class 'staggered_gmm_jtest' print(x, ...)
x |
A |
... |
Ignored. |
x, invisibly. Called for the side effect of printing.
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") print(gmm_j_test(fit))fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") print(gmm_j_test(fit))
A balanced synthetic panel used in the examples, the vignette and the
tests: 60 units observed over 33 periods, with five treatment cohorts
(first treated in periods 10, 13, 16, 19 or 22; 10 units each) and a
never-treated group of 10 units. True effects are deterministic given
(cohort, period), so both aggregate targets are known exactly:
ATT_CW = -16.79375 under treated-observation weighting and
ATT_EW = -15.82020 under cohort-equal weighting. The two differ because
earlier cohorts are observed for more post-treatment periods; see
gmm_staggered() for the definitions.
sim_panelsim_panel
A data frame with 1980 rows and 6 variables:
Unit identifier, integer 1–60.
Period, integer 1–33.
0 = never treated; 10, 13, 16, 19 or 22 = first treated period.
Observed outcome.
Continuous baseline covariate.
Binary baseline covariate.
The data-generating process, for unit in cohort
at period :
: unit fixed effect.
: period fixed effect.
for
and 0 otherwise, with
and
for cohorts
.
: AR(1) within unit, .
, : baseline covariates with
cohort-correlated means and linear-in-time loadings
(, ), so an unconditional
difference-in-differences is genuinely confounded by them.
set.seed(312844) throughout. See data-raw/sim_panel.R for the exact
implementation.
This is the design of Appendix E of the paper, so the specification test has 70 full-set and 14 local-window restrictions on it.
Simulated for Arora, P. and Bijani, R. (2026). "Estimating Treatment Effects under Staggered Timing and Non-Spherical Errors." doi:10.2139/ssrn.6558759
Prints everything print.staggered_gmm() shows, followed by the full
table of cohort-by-time effects and the covariate-adjustment status.
## S3 method for class 'staggered_gmm' summary(object, ...)## S3 method for class 'staggered_gmm' summary(object, ...)
object |
A |
... |
Ignored. |
object, invisibly. Called for the side effect of printing.
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") summary(fit)fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") summary(fit)
Covariance matrix of the estimated cohort-by-time effects
## S3 method for class 'staggered_gmm' vcov(object, ...)## S3 method for class 'staggered_gmm' vcov(object, ...)
object |
A |
... |
Ignored. |
A square numeric matrix covering the identified cells only, with
row and column names matching the corresponding entries of coef().
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") dim(vcov(fit))fit <- gmm_staggered(sim_panel, yname = "y", tname = "year", idname = "unit_id", gname = "cohort") dim(vcov(fit))