--- title: "Quantile Treatment Effects in R" author: "Brantly Callaway" date: today format: html bibliography: refs.bib knitr: opts_chunk: collapse: true comment: "#>" fig.width: 6 fig.height: 4 vignette: > %\VignetteIndexEntry{Quantile Treatment Effects in R} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8} --- ## What are Quantile Treatment Effects? The Average Treatment Effect (ATE) or Average Treatment Effect on the Treated (ATT) summarizes the causal effect of a treatment by a single number: the mean difference in outcomes between treated and untreated units. This is often a natural quantity, but it can miss important heterogeneity. If a job training program substantially raises earnings at the bottom of the distribution while having little effect at the top, the ATE obscures this. If a minimum wage increase compresses the wage distribution, an average effect conceals the compression. The Quantile Treatment Effect at level $\tau$ is $QTE(\tau) = Q_{Y(1)}(\tau) - Q_{Y(0)}(\tau)$, where $Q_{Y(d)}(\tau)$ is the $\tau$-th quantile of the potential outcome distribution under treatment status $d$. Mapping $QTE(\tau)$ across $\tau \in (0,1)$ traces how the treatment shifts the entire outcome distribution. The Quantile Treatment Effect on the Treated ($QTT$) conditions on the treated group. This vignette demonstrates `unc_qte()`, the `qte` package's estimator for cross-sectional settings (no panel data required). For DiD-based estimators and staggered treatment adoption, see `vignette("panel-estimators")` and `vignette("staggered-adoption")`. ```{r} #| label: setup #| message: false library(qte) library(ggplot2) set.seed(42) data(lalonde) xf <- ~ age + I(age^2) + education + black + hispanic + married + nodegree ``` ```{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/intro-results.rds # and re-render to regenerate after a code change (see CLAUDE.md). cache_file <- "precomputed/intro-results.rds" use_cache <- file.exists(cache_file) if (use_cache) { cached <- readRDS(cache_file) res_exp <- cached$res_exp res_psid <- cached$res_psid res_qtt <- cached$res_qtt } else { res_exp <- unc_qte( yname = "re78", dname = "treat", data = lalonde.exp, target = "qte", probs = seq(0.1, 0.9, 0.1), biters = 100 ) res_psid <- unc_qte( yname = "re78", dname = "treat", data = lalonde.psid, xformla = xf, est_method = "aipw", target = "qte", probs = seq(0.1, 0.9, 0.1), biters = 100 ) res_qtt <- unc_qte( yname = "re78", dname = "treat", data = lalonde.psid, xformla = xf, est_method = "aipw", target = "qtt", probs = seq(0.1, 0.9, 0.1), biters = 100 ) # pscore.reg (fitted glm) carries a full copy of the training data and is # never read by summary()/autoplot(); dropping it keeps the cache small. save_psid <- res_psid; save_psid$pscore.reg <- NULL save_qtt <- res_qtt; save_qtt$pscore.reg <- NULL saveRDS(list(res_exp = res_exp, res_psid = save_psid, res_qtt = save_qtt), cache_file) } ``` ## Random assignment Under random assignment, no covariate adjustment is needed: the unconditional quantiles of the treated and control outcome distributions identify the QTE directly. `unc_qte()` implements the semiparametrically efficient estimator of @firpo-2007. We use the experimental Lalonde data (`lalonde.exp`), where treatment was randomly assigned. ```{r} #| label: random-qte #| eval: false res_exp <- unc_qte( yname = "re78", dname = "treat", data = lalonde.exp, target = "qte", probs = seq(0.1, 0.9, 0.1), biters = 100 ) summary(res_exp) ``` ```{r} #| label: random-qte-output #| echo: false summary(res_exp) ``` ```{r} #| label: random-qte-plot #| fig-alt: "QTE curve under random assignment" autoplot(res_exp, ylab = "QTE (earnings, 1978)") ``` The confidence band indicates that the QTE is positive across much of the distribution, with the effect somewhat larger in the lower quantiles. ## Unconfoundedness When treatment is not randomly assigned, the unconfoundedness assumption (conditional independence of potential outcomes given covariates) provides identification [@firpo-2007]. We use the observational Lalonde data (`lalonde.psid`), which combines the treated group from the experiment with a comparison group from the PSID. `unc_qte()` supports three estimation methods: - `"ipw"` — inverse probability weighting (propensity score re-weighting) - `"or"` — outcome regression (conditional quantile regression) - `"aipw"` — augmented IPW (doubly robust; recommended) ```{r} #| label: obs-qte #| eval: false xf <- ~ age + I(age^2) + education + black + hispanic + married + nodegree res_psid <- unc_qte( yname = "re78", dname = "treat", data = lalonde.psid, xformla = xf, est_method = "aipw", target = "qte", probs = seq(0.1, 0.9, 0.1), biters = 100 ) summary(res_psid) ``` ```{r} #| label: obs-qte-output #| echo: false summary(res_psid) ``` ```{r} #| label: obs-qte-plot #| fig-alt: "QTE curve under unconfoundedness" autoplot(res_psid, ylab = "QTE (earnings, 1978)") ``` ## QTT under unconfoundedness Setting `target = "qtt"` estimates the Quantile Treatment Effect on the Treated — the distributional effect for the subpopulation that actually received treatment. ```{r} #| label: qtt #| eval: false res_qtt <- unc_qte( yname = "re78", dname = "treat", data = lalonde.psid, xformla = xf, est_method = "aipw", target = "qtt", probs = seq(0.1, 0.9, 0.1), biters = 100 ) summary(res_qtt) ``` ```{r} #| label: qtt-output #| echo: false summary(res_qtt) ``` ```{r} #| label: qtt-plot #| fig-alt: "QTT curve under unconfoundedness" autoplot(res_qtt, ylab = "QTT (earnings, 1978)") ``` ## Reading the output `summary()` prints the overall ATT, the QTE/QTT at each quantile, and standard errors with confidence intervals. `autoplot()` returns a `ggplot` object so you can add layers: ```{r} #| label: custom-plot #| eval: false autoplot(res_qtt) + ggplot2::labs(title = "QTT — Lalonde (observational)", subtitle = "AIPW with pre-treatment covariates") ``` By default `autoplot()` shows a uniform confidence band (`cband = TRUE`), which provides simultaneous coverage over all quantile levels — a stronger guarantee than pointwise intervals. Pass `cband = FALSE` for pointwise intervals instead. Standard errors are computed via the empirical bootstrap. The `biters` argument controls the number of iterations (default 100). Parallel computation is available via the `cl` argument. ## References