---
title: "Getting started with staggeredGMM"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Getting started with staggeredGMM}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r setup}
library(staggeredGMM)
```

## Overview

`staggeredGMM` estimates cohort-by-time average treatment effects (CATTs)
under staggered treatment adoption by the generalized method of moments, as
proposed in

> Arora, P. and Bijani, R. (2026). "Estimating Treatment Effects under
> Staggered Timing and Non-Spherical Errors." Available at SSRN:
> <https://doi.org/10.2139/ssrn.6558759>

Only *clean* two-by-two difference-in-differences comparisons enter the
moment system: a treated cohort against a never-treated group, or against a
cohort not yet treated at the relevant period.

This vignette covers how to call the estimator and what it returns. For the
methodology, the choice among the three weighting schemes, and the
simulation and empirical evidence, see the paper.

## Data coding conventions

`gmm_staggered()` has the following input requirements:

* one row per unit-period, with no duplicated unit-period pair;
* a numeric outcome (factor, character and logical outcomes are errors);
* integer-valued time and cohort columns;
* the cohort constant within unit, with `0` for never-treated units;
* if never-treated units are coded `0`, period `0` not after the first
  observed period (otherwise `0` would also mean "first treated in period 0"; code
  never-treated units with any value after the last period instead);
* no period absent from the whole panel;
* no unit treated at or before the first observed period.

A missing outcome is fine, and is what an unbalanced panel looks like. A
missing unit id, period or cohort is an error, since the row cannot be placed
in the panel at all.

Two conventions are worth stating explicitly. First, a unit whose cohort
falls *after* the last observed period is untreated throughout the window,
so it is pooled with the never-treated group as a clean control rather than
discarded. Second, a unit treated at or before the first observed period has
no pre-treatment period at all, so none of its effects is identified; rather
than return zeros for those cells and contaminate the aggregate, the
estimator stops and names the offending cohort.

## A first estimate

`sim_panel` is a balanced synthetic panel: 60 units over 33 periods, five
treated cohorts and a never-treated group.

```{r basic}
fit <- gmm_staggered(sim_panel, yname = "y", tname = "year",
                     idname = "unit_id", gname = "cohort")
fit
```

The never-treated group was detected automatically. `never_treated = TRUE`
requires one and errors if none exists; `never_treated = FALSE` excludes
them.

## What is returned

```{r catt}
head(fit$catt)
```

`catt` gives one row per cohort-by-time cell, with the event time, the
estimate and its standard error, whether the cell is identified, and how
many clean comparisons anchor it. `coef()`, `vcov()` and `confint()` work as
usual:

```{r methods}
head(coef(fit))
head(confint(fit))
```

## The two aggregates

Two scalar summaries are reported, and they answer different questions.

* **`CW`, treated-observation weighting.** Every treated unit-period gets
  equal weight, so a cohort observed for longer contributes more total
  weight. This is the aggregation used throughout the paper, and it
  coincides with the "simple" aggregation of Callaway and Sant'Anna (2021).
* **`EW`, cohort-equal weighting.** Each treated cohort gets total weight
  `1/K`, spread equally over its own post-treatment window, so exposure
  length does not affect a cohort's influence.

```{r aggregates}
c(CW = fit$aggregate$CW$estimate, EW = fit$aggregate$EW$estimate)
```

On this design the true values are `-16.79375` and `-15.82020`
respectively; they differ because earlier cohorts are observed for more
post-treatment periods. Neither is more correct than the other. For any
other target, weight `fit$catt$estimate` directly.

## Choosing a weighting

```{r weighting}
fit_ht <- gmm_staggered(sim_panel, yname = "y", tname = "year",
                        idname = "unit_id", gname = "cohort",
                        weighting = "cohort_toeplitz")
c(pooled = fit$aggregate$CW$estimate, cohort = fit_ht$aggregate$CW$estimate)
```

All three weightings target the same CATT vector using the same moment
conditions. They do not generally return the same numbers: the system is
over-identified, so a different weighting matrix gives a different estimate.
Which to prefer is a methodological question the paper addresses.

## Convergence

```{r convergence}
unlist(fit$convergence[c("converged", "solve_ok", "termination", "n_iter")])
```

`converged` is `TRUE` only when the largest change in any CATT fell below
`tol`. It is deliberately distinct from `solve_ok`, which records merely that
a weighted solve succeeded; a run can solve successfully at every iteration
and still exhaust `max_iter` without converging. `termination` says why the
iteration stopped.

`max_iter` defaults to 100 because the iteration can converge linearly at a
slow rate.

On this design the unrestricted weighting cannot complete a weighted step at
all:

```{r unrestricted}
fit_u <- gmm_staggered(sim_panel, yname = "y", tname = "year",
                       idname = "unit_id", gname = "cohort",
                       weighting = "unrestricted")
```

The estimate returned in that case is the identity-weighted seed, not an
unrestricted-weighted GMM estimator, which is why it always warns. Failing to
*converge* after a weighted step did run is a different matter, and is common
enough under this weighting that it does not warn.

## Unbalanced panels

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. All three reduce exactly to the
complete-data formulas when nothing is missing, so there is one code path
rather than two.

```{r unbalanced}
gappy <- sim_panel
set.seed(1)
gappy$y[sample(nrow(gappy), 100)] <- NA
fit_gap <- gmm_staggered(gappy, yname = "y", tname = "year",
                         idname = "unit_id", gname = "cohort")
c(balanced = fit$aggregate$CW$estimate,
  unbalanced = fit_gap$aggregate$CW$estimate)
```

This is valid when missingness is independent of the outcome given group and
period. Selection on outcomes is not addressed by any weighting scheme.

A comparison referencing a group-period at which *no* unit is observed
cannot be formed and is dropped, with a warning giving the count.

## Partial identification

A cohort-by-time effect with no clean comparison is not estimable. Such
cells are reported as `NA` with `identified = FALSE`, never as an estimated
zero, and are excluded from the parameter vector so that one unestimable
cell does not disable the efficient weighting everywhere else.

Where an unidentified cell carries positive aggregation weight, the
aggregate itself is `NA`, and an identified-subset aggregate renormalised
over the estimable cells is reported alongside it:

```{r partial}
names(fit$aggregate$CW)
```

## Baseline covariates

`covar` takes a character vector of baseline, time-invariant covariate
columns and applies the outcome-regression adjustment of Section 4.5 of the
paper, so that parallel trends need hold only conditional on them.

```{r covar}
fit_cov <- gmm_staggered(sim_panel, yname = "y", tname = "year",
                         idname = "unit_id", gname = "cohort",
                         covar = c("x1", "x2"))
fit_cov$aggregate$CW$estimate
```

If a supplied covariate is not in fact constant within unit, each affected
unit's value at its **earliest observed period** is used and a warning names
the units. Earliest by time, not first by row: the answer does not depend on
how the data were sorted.

Where the adjustment cannot be identified for some control cohort and pair
of periods -- too few complete-case control units, or a rank-deficient fit
-- those comparisons alone revert to the unconditional form, and the count
is reported in `fit$controls$cov_n_fallback`.

If a covariate has no observed value for any unit of a treated cohort, that
cohort's covariate mean is undefined and the estimator stops, naming the
covariate and the cohort.

**The specification test below does not adjust for covariates.** It tests
unconditional parallel trends, not the covariate-conditional parallel trends
under which `fit_cov` is estimated; see the next section.

## Testing the identifying assumptions

The over-identifying restrictions are the pre-treatment placebo moments,
whose expectation is zero only under parallel trends and no anticipation.
`gmm_j_test()` tests them jointly, weighting them by the estimated
serial-correlation model.

```{r jtest}
gmm_j_test(fit)
```

**The test is unconditional.** Its placebo moments are unadjusted group
means, whether or not the fitted model adjusts for covariates. After a fit
with `covar`, it therefore tests unconditional parallel trends and no
anticipation, not the covariate-conditional parallel trends assumption under
which the effects were estimated. If covariates are distributed differently
across cohorts, the test can reject although conditional parallel trends
hold, and a non-rejection does not validate the conditional assumption. The
printed result says so whenever the fit used covariates:

```{r jtest-cov}
gmm_j_test(fit_cov)
```

By default the test uses a local pre-window of three periods before each
cohort's adoption. Using every placebo restriction is also available, but a
large number of restrictions degrades the chi-squared approximation when the
number of units is small:

```{r jtest-full}
gmm_j_test(fit, type = "full")
```

The test requires a never-treated group, since the event-study basis it uses
differences every cohort against a common never-treated reference.

## Two coding pitfalls

A unit treated at or before the first observed period has no pre-treatment
period. Recoding the first cohort of `sim_panel` to adopt in period 1 shows
what happens:

```{r pitfall-always, error = TRUE}
bad <- sim_panel
bad$cohort[bad$cohort == 10] <- 1
gmm_staggered(bad, yname = "y", tname = "year",
              idname = "unit_id", gname = "cohort")
```

Silently including such units would return a plausible-looking aggregate
contaminated by cells that were never estimated, so the estimator stops and
names the cohort. Drop those units, or restrict to cohorts adopting inside the
window, before estimating.

Without a never-treated group, identification rests entirely on
not-yet-treated controls. Dropping the never-treated units of `sim_panel`:

```{r pitfall-never}
fit_nyt <- gmm_staggered(sim_panel[sim_panel$cohort != 0, ], yname = "y",
                         tname = "year", idname = "unit_id",
                         gname = "cohort")
table(fit_nyt$catt$identified)
fit_nyt$aggregate$CW$identified_weight_share
```

The last cohort to adopt has no later cohort to serve as its control, so none
of its effects is identified, and neither is any cell at or after its
adoption period: only 30 of the 90 cells are estimable. Because unidentified
cells carry positive weight, the aggregate itself is `NA` and the
identified-subset figure is reported with the share of weight it covers. An
aggregate over a set of cells two thirds of which were never estimated would
not mean what its name suggests.

## Citation

```{r citation, eval = FALSE}
citation("staggeredGMM")
```
