---
title: "Panel Data Estimators for Quantile Treatment Effects"
author: "Brantly Callaway"
date: today
format: html
bibliography: refs.bib
knitr:
  opts_chunk:
    collapse: true
    comment: "#>"
    fig.width: 6
    fig.height: 4
vignette: >
  %\VignetteIndexEntry{Panel Data Estimators for Quantile Treatment Effects}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

## Overview

The `qte` package provides six DiD-based estimators for the QTT. All share
the same `yname`/`gname`/`tname`/`idname` interface and support staggered
treatment adoption natively. They differ in their identification assumptions
and the restrictions those assumptions impose on the data-generating process.

This vignette describes each estimator conceptually and shows the minimal
call. For a full applied workflow with aggregations and event-study plots, see
`vignette("staggered-adoption")`.

```{r}
#| label: setup
#| message: false
library(qte)
library(ptetools)
set.seed(42)
data(mpdta, package = "did")
```

```{r}
#| label: compute-results
#| include: false
# Results are precomputed and cached so CRAN's vignette rebuild doesn't have
# to re-run the bootstrap on every check. Delete
# precomputed/panel-estimators-results.rds and re-render to regenerate after
# a code change (see CLAUDE.md).
cache_file <- "precomputed/panel-estimators-results.rds"
use_cache <- file.exists(cache_file)

if (use_cache) {
  cached   <- readRDS(cache_file)
  res_cic  <- cached$res_cic
  res_qdid <- cached$res_qdid
  res_pqtt <- cached$res_pqtt
  res_ddid <- cached$res_ddid
  res_mdid <- cached$res_mdid
  res_lou  <- cached$res_lou
} else {
  res_cic <- cic(
    yname  = "lemp", gname = "first.treat", tname = "year",
    idname = "countyreal", data = mpdta,
    gt_type = "att", biters = 100
  )
  res_qdid <- qdid(
    yname  = "lemp", gname = "first.treat", tname = "year",
    idname = "countyreal", data = mpdta,
    gt_type = "att", biters = 100
  )
  res_pqtt <- panel_qtt(
    yname  = "lemp", gname = "first.treat", tname = "year",
    idname = "countyreal", data = mpdta,
    gt_type = "qtt", probs = seq(0.1, 0.9, 0.1), biters = 100
  )
  res_ddid <- ddid(
    yname  = "lemp", gname = "first.treat", tname = "year",
    idname = "countyreal", data = mpdta,
    gt_type = "att", biters = 100
  )
  res_mdid <- mdid(
    yname  = "lemp", gname = "first.treat", tname = "year",
    idname = "countyreal", data = mpdta,
    gt_type = "att", biters = 100
  )
  res_lou <- lou_qtt(
    yname  = "lemp", gname = "first.treat", tname = "year",
    idname = "countyreal", data = mpdta,
    gt_type = "att", biters = 100
  )
  saveRDS(
    list(res_cic = res_cic, res_qdid = res_qdid, res_pqtt = res_pqtt,
      res_ddid = res_ddid, res_mdid = res_mdid, res_lou = res_lou),
    cache_file
  )
}
```

We use `mpdta` throughout: a balanced panel of 500 US counties observed
annually from 2003–2007, with `lemp` (log employment) as the outcome and
`first.treat` as the cohort variable (year of first minimum wage increase; 0
= never treated).

The common call structure for all six estimators is:

```r
estimator(
  yname  = "lemp",        # outcome
  gname  = "first.treat", # first treatment period (0 = never treated)
  tname  = "year",        # calendar time
  idname = "countyreal",  # unit id
  data   = mpdta,
  gt_type = "att",        # "att" or "qtt"
  biters  = 100
)
```

---

## Change in Changes — `cic()`

**Reference:** @athey-imbens-2006

**Identifying assumption:** The distributional time-change is the same for both groups:
$$Q_{Y_t(0)|D=1}\!\left(F_{Y_{t-1}(0)|D=1}(y)\right) = Q_{Y_t(0)|D=0}\!\left(F_{Y_{t-1}(0)|D=0}(y)\right) \quad \text{for all } y,$$
along with a support condition: $\text{supp}(Y_{t-1}(0)|D=1) \subseteq \text{supp}(Y_{t-1}(0)|D=0)$.

**Intuition:** The expression $Q_{Y_t(0)|D=g}(F_{Y_{t-1}(0)|D=g}(y))$ takes a pre-period value $y$, finds its quantile rank in group $g$'s pre-period distribution, and returns the post-period value at that same rank within group $g$'s distribution. The assumption requires this operation to give the same result for both groups at every $y$. It can be thought of as a nonlinear version of parallel trends: where parallel trends requires the *mean* of $Y(0)$ to shift equally across groups, CiC requires the *entire distributional time-change* to be the same.

**Identification:** Under this assumption the counterfactual quantile function is
$$Q_{Y_t(0)|D=1}(\tau) = Q_{Y_t|D=0}\!\left(F_{Y_{t-1}|D=0}\!\left(Q_{Y_{t-1}|D=1}(\tau)\right)\right).$$

**When to use:** Works with panel or repeated cross sections.

```{r}
#| label: cic-att
#| eval: false
res_cic <- cic(
  yname  = "lemp", gname = "first.treat", tname = "year",
  idname = "countyreal", data = mpdta,
  gt_type = "att", biters = 100
)
summary(res_cic)
```

```{r}
#| label: cic-att-output
#| echo: false
summary(res_cic)
```

---

## Quantile Difference-in-Differences — `qdid()`

**References:** @athey-imbens-2006; @meyer-viscusi-durbin-1995

**Identifying assumption:** The change in quantiles over time is the same for both groups:
$$Q_{Y_t(0)|D=1}(\tau) - Q_{Y_{t-1}(0)|D=1}(\tau) = Q_{Y_t(0)|D=0}(\tau) - Q_{Y_{t-1}(0)|D=0}(\tau) \quad \text{for all } \tau \in (0,1).$$

**Intuition:** This requires parallel trends to hold at every quantile level $\tau$, not just in the mean. It directly identifies the counterfactual:
$$Q_{Y_t(0)|D=1}(\tau) = Q_{Y_{t-1}|D=1}(\tau) + Q_{Y_t|D=0}(\tau) - Q_{Y_{t-1}|D=0}(\tau).$$

**When to use:** Works with panel or repeated cross sections.

```{r}
#| label: qdid-att
#| eval: false
res_qdid <- qdid(
  yname  = "lemp", gname = "first.treat", tname = "year",
  idname = "countyreal", data = mpdta,
  gt_type = "att", biters = 100
)
summary(res_qdid)
```

```{r}
#| label: qdid-att-output
#| echo: false
summary(res_qdid)
```

---

## Panel QTT — `panel_qtt()`

**Reference:** @callaway-li-2019

**Identifying assumptions:** The first assumption is $\Delta Y_t(0) \perp D$ — the distribution of the change in untreated potential outcomes is the same for both groups:
$$F_{\Delta Y_t(0)|D=1} = F_{\Delta Y_t(0)|D=0}.$$
This identifies the marginal distribution of $\Delta Y_t(0)$ for the treated group. As shown in @fan-yu-2012, this is not sufficient on its own to identify the QTT. Identification is completed by a copula stability assumption: the copula linking $\Delta Y_t(0)$ to the lagged outcome $Y_{t-1}(0)$ is stable over time within the treated group,
$$C_{\Delta Y_t(0),\, Y_{t-1}(0)|D=1} = C_{\Delta Y_{t-1}(0),\, Y_{t-2}(0)|D=1}.$$

**Intuition:** For treated units, $Y_{t-1}$ and $Y_{t-2}$ are observed pre-treatment, so the right-hand copula can be estimated directly from the data. Copula stability says this same dependence structure governs $(\Delta Y_t(0), Y_{t-1}(0))$ in the post-period. Combined with the identified marginals, this pins down the conditional distribution of $\Delta Y_t(0)$ given $Y_{t-1}(0) = y$ for treated units. Since $Y_t(0) = Y_{t-1} + \Delta Y_t(0)$ and $Y_{t-1}$ is observed, the QTT is identified. Requires panel data with at least two pre-treatment periods per cohort.

```{r}
#| label: panel-qtt
#| eval: false
res_pqtt <- panel_qtt(
  yname  = "lemp", gname = "first.treat", tname = "year",
  idname = "countyreal", data = mpdta,
  gt_type = "qtt", probs = seq(0.1, 0.9, 0.1), biters = 100
)
summary(res_pqtt)
```

```{r}
#| label: panel-qtt-output
#| echo: false
summary(res_pqtt)
```

---

## Distributional DiD — `ddid()`

**Reference:** @callaway-li-2019

**Identifying assumptions:** The first is $\Delta Y_t(0) \perp D$ — the same as in `panel_qtt()`. The copula assumption differs: rather than stability over time, `ddid()` assumes the copula of $(\Delta Y_t(0), Y_{t-1}(0))$ is the same for treated and untreated groups,
$$C_{\Delta Y_t(0),\, Y_{t-1}(0)|D=1} = C_{\Delta Y_t(0),\, Y_{t-1}(0)|D=0}.$$

**Intuition:** For the untreated group, $\Delta Y_t(0) = \Delta Y_t$ is observed, so the right-hand copula can be estimated directly. The assumption transfers it to the treated group. Combined with the identified marginals, this pins down the joint distribution of $(\Delta Y_t(0), Y_{t-1}(0))$ for treated units and identifies the QTT — in exactly the same way as `panel_qtt()`. The key contrast is the source of the copula: `ddid()` borrows it from the untreated group at the same point in time, while `panel_qtt()` borrows it from the treated group's own pre-treatment history.

```{r}
#| label: ddid-att
#| eval: false
res_ddid <- ddid(
  yname  = "lemp", gname = "first.treat", tname = "year",
  idname = "countyreal", data = mpdta,
  gt_type = "att", biters = 100
)
summary(res_ddid)
```

```{r}
#| label: ddid-att-output
#| echo: false
summary(res_ddid)
```

---

## Mean Difference-in-Differences — `mdid()`

**Reference:** @thuysbaert-2007

**Identifying assumption:** The counterfactual quantile for the treated group in the post-period is the pre-period quantile shifted by the mean change in outcomes for the untreated group:
$$Q_{Y_t(0)|D=1}(\tau) = Q_{Y_{t-1}|D=1}(\tau) + E[Y_t|D=0] - E[Y_{t-1}|D=0].$$

**Intuition:** The assumption says the shape of the treated group's outcome distribution is unchanged in the counterfactual — the entire distribution simply shifts by the amount the untreated group's mean changed. This implies mean parallel trends (so the ATT is identified), but is stronger: it additionally requires that only the location of the distribution shifts, not its shape. Works with panel or repeated cross sections.

```{r}
#| label: mdid-att
#| eval: false
res_mdid <- mdid(
  yname  = "lemp", gname = "first.treat", tname = "year",
  idname = "countyreal", data = mpdta,
  gt_type = "att", biters = 100
)
summary(res_mdid)
```

```{r}
#| label: mdid-att-output
#| echo: false
summary(res_mdid)
```

---

## Lagged-Outcome Unconfoundedness — `lou_qtt()`

**Identification assumption:** Unconfoundedness conditional on the lagged
outcome — treatment is as good as randomly assigned after conditioning on the
pre-treatment value of the outcome.

**When to use:** When selection into treatment may depend on lagged outcomes
but not on unobserved heterogeneity — i.e., once you condition on the
pre-treatment outcome, treatment assignment is as good as random. Requires
panel data.

```{r}
#| label: lou-att
#| eval: false
res_lou <- lou_qtt(
  yname  = "lemp", gname = "first.treat", tname = "year",
  idname = "countyreal", data = mpdta,
  gt_type = "att", biters = 100
)
summary(res_lou)
```

```{r}
#| label: lou-att-output
#| echo: false
summary(res_lou)
```

---

## Summary

| Function | Assumption | Panel required | Notes |
|---|---|---|---|
| `cic()` | Rank invariance + support | Optional | Nonparametric; strongest assumption |
| `qdid()` | Distributional parallel trends | Optional | DiD analog for distributions |
| `panel_qtt()` | Copula stability | Yes | Requires ≥ 2 pre-periods per cohort |
| `ddid()` | Conditional distributional PT | Yes | Adds covariate adjustment to QDiD |
| `mdid()` | Mean parallel trends | Optional | Weakest DiD assumption |
| `lou_qtt()` | Lagged-outcome unconfoundedness | Yes | Not a parallel trends assumption |

All estimators return either a `pte_results` object (`gt_type = "att"`) or a
`pte_qtt` object (`gt_type = "qtt"`), and support `autoplot()` for plotting
overall curves and event-study summaries. See `vignette("staggered-adoption")`
for the full applied workflow.

## References
