--- title: "Comparison Estimators: ETWFE and BETWFE" author: "Gregory Faletto" date: "`r Sys.Date()`" output: rmarkdown::html_vignette: toc: true math_method: "mathml" output_file: "FETWFE_Comparison_Estimators_Vignette.html" vignette: > %\VignetteIndexEntry{Comparison Estimators: ETWFE and BETWFE} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(fetwfe) ``` # Introduction The `{fetwfe}` package implements `fetwfe()` as its recommended estimator for difference-in-differences with staggered adoptions. It also exports two related estimators that serve as comparison baselines: - `etwfe()` — Wooldridge-style extended two-way fixed effects. - `betwfe()` — bridge-penalized ETWFE; like `fetwfe()` but without the fusion transformation. This vignette demonstrates both, on simulated data so we can compare each estimator against the known true treatment effects. For background on staggered-adoption DiD and a real-data application of the recommended `fetwfe()` estimator, see [the main vignette](https://CRAN.R-project.org/package=fetwfe). For the underlying simulation pipeline used here, see [the simulation vignette](https://CRAN.R-project.org/package=fetwfe). For methodological details, see [Faletto (2025)](https://arxiv.org/abs/2312.05985). All three estimators (`fetwfe()`, `etwfe()`, `betwfe()`) accept the same call signature, so a user familiar with `fetwfe()` can drop in `etwfe()` or `betwfe()` simply by changing the function name. Below we use the `*WithSimulatedData()` wrappers to keep the simulation flow concise. # Setup: simulating panel data We use the `genCoefs()` + `simulateData()` pipeline (the same approach as the simulation vignette). The parameters below are chosen so that both `etwfe()` and `betwfe()` are well-conditioned: enough cohorts and units that `etwfe()` doesn't run into rank-deficiency, and enough density in the true coefficient vector that `betwfe()`'s bridge regularization shrinks toward zero without zeroing everything out. ```{r} sim_coefs <- genCoefs( R = 3, T = 6, d = 2, density = 0.5, eff_size = 2, seed = 20260510 ) sim_data <- simulateData( sim_coefs, N = 120, sig_eps_sq = 1, sig_eps_c_sq = 1 ) # True treatment effects (we'll compare estimator output to these): true_tes <- getTes(sim_coefs) cat("True overall ATT:", true_tes$att_true, "\n") print(true_tes$actual_cohort_tes) ``` # `etwfe()`: extended TWFE without penalty `etwfe()` implements the Wooldridge-style extended two-way fixed effects estimator: cohort-time dummy interactions estimated by OLS, with no regularization. Under the model's assumptions it produces unbiased point estimates and asymptotically exact standard errors. The trade-off compared to `fetwfe()` is variance: with no fusion penalty, the estimator can be high-variance in over-parameterized regimes, and it errors out entirely when cohorts are small relative to the number of covariates (`(d + 1)` units per cohort is the floor). ```{r} res_etwfe <- etwfeWithSimulatedData(sim_data) summary(res_etwfe) ``` We can compare the estimated overall ATT to the truth: ```{r} cat("True ATT: ", true_tes$att_true, "\n") cat("Estimated ATT:", res_etwfe$att_hat, "\n") cat("Squared error:", (res_etwfe$att_hat - true_tes$att_true)^2, "\n") ``` # `betwfe()`: bridge-penalized ETWFE `betwfe()` extends `etwfe()` by adding a bridge (`L_q`, `0 < q < 1`) regularization penalty on the cohort-time effects. Compared to `etwfe()`, this trades a small amount of bias for lower variance — the same idea as `fetwfe()`, but without the fusion transformation that `fetwfe()` applies. So `betwfe()` is essentially "fetwfe minus the fusion." ```{r} res_betwfe <- betwfeWithSimulatedData(sim_data) summary(res_betwfe) ``` Comparing against the truth and against `etwfe()`: ```{r} cat("True ATT: ", true_tes$att_true, "\n") cat("etwfe() ATT: ", res_etwfe$att_hat, "\n") cat("betwfe() ATT: ", res_betwfe$att_hat, "\n") cat("etwfe sq. error: ", (res_etwfe$att_hat - true_tes$att_true)^2, "\n") cat("betwfe sq. error:", (res_betwfe$att_hat - true_tes$att_true)^2, "\n") ``` The bridge penalty in `betwfe()` shrinks the estimate toward zero relative to `etwfe()`. On this regime, that produces a noticeable bias — the textbook bias-variance trade-off in action. In other regimes (sparser true effects, or noisier data), the bias from regularization is more than offset by reduced variance, and `betwfe()` outperforms `etwfe()`. # No-covariate setting The examples above use a panel with `d = 2` time-invariant covariates. The package equally supports the no-covariate case by passing `covs = c()` to any estimator (or by generating data with `genCoefs(d = 0, ...)`). This section runs the same simulated regime with no covariates, side-by-side, so a user can see what `etwfe()` and `fetwfe()` look like in the simpler setting. ```{r} sim_coefs_d0 <- genCoefs( R = 3, T = 6, d = 0, density = 0.5, eff_size = 2, seed = 20260510 ) sim_data_d0 <- simulateData( sim_coefs_d0, N = 120, sig_eps_sq = 1, sig_eps_c_sq = 1 ) true_tes_d0 <- getTes(sim_coefs_d0) cat("True overall ATT (no covariates):", true_tes_d0$att_true, "\n") ``` `etwfe()` in the no-covariate setting: ```{r} res_etwfe_d0 <- etwfeWithSimulatedData(sim_data_d0) summary(res_etwfe_d0) ``` `fetwfe()` in the no-covariate setting: ```{r} res_fetwfe_d0 <- fetwfeWithSimulatedData(sim_data_d0) summary(res_fetwfe_d0) ``` Side-by-side overall ATT estimates against the truth: ```{r} cat("True ATT: ", true_tes_d0$att_true, "\n") cat("etwfe() ATT: ", res_etwfe_d0$att_hat, "\n") cat("fetwfe() ATT: ", res_fetwfe_d0$att_hat, "\n") cat("etwfe sq. error: ", (res_etwfe_d0$att_hat - true_tes_d0$att_true)^2, "\n") cat("fetwfe sq. error:", (res_fetwfe_d0$att_hat - true_tes_d0$att_true)^2, "\n") ``` Two qualitative differences to note in the no-covariate regime: - **Smaller design matrix.** Without covariates and their cohort/time/treatment interactions, the underlying regression has many fewer columns, so both estimators converge faster and `etwfe()` becomes rank-stable at smaller cohort sizes (the `(d + 1)`-units-per-cohort floor that `etwfe()` enforces drops to just one unit per cohort). - **Standard errors typically widen.** Covariates that explain residual variance are absent, so the idiosyncratic-noise variance `sig_eps_sq` enters the SEs without that explanatory cushion. The signal-to-noise ratio on the treatment-effect coefficients drops, which is the trade-off for the simpler model. The package handles `covs = c()` end-to-end without any special-casing on the user's side: the data-prep pipeline (`prep_for_etwfe_core` in `R/core_funcs.R`) dispatches on `d == 0` and skips the covariate-interaction columns automatically. The same applies to `betwfe()` and `twfeCovs()`. # Visualizing event-time effects Each of the three estimator outputs supports `plot()` (which dispatches to a method) and a companion `eventStudy()` helper that returns a tidy data frame of pooled-event-time treatment-effect estimates. The plot is an event study: x-axis is event time `e = t - r` (calendar time minus the cohort's first-treated time), y-axis is the cohort-weighted average of cell-level treatment-effect estimates at each event time, with confidence intervals. Pooling weights are sample-cohort-size weights (matching `did::aggte(type = "dynamic")` convention). The variance combines a regression-coefficient term and a cohort-probability term, mirroring the package's existing overall-ATT SE machinery. Event-time estimates from `etwfe()`: ```{r} eventStudy(res_etwfe) ``` ```{r, eval = requireNamespace("ggplot2", quietly = TRUE)} plot(res_etwfe) ``` Event-time estimates from `betwfe()` on the same simulated panel: ```{r} eventStudy(res_betwfe) ``` ```{r, eval = requireNamespace("ggplot2", quietly = TRUE)} plot(res_betwfe) ``` `eventStudy()` returns the underlying data; `plot()` returns a ggplot2 object you can further customize. `ggplot2` is in `Suggests:`, so it must be installed to use the `plot()` methods; the estimators themselves work without it. # When to use which `fetwfe()` is the recommended estimator for production use; `etwfe()` and `betwfe()` are useful as comparisons or as building blocks for understanding what `fetwfe()` is doing. - **`fetwfe()`** — default choice. Combines bridge regularization with the fusion transformation for both bias and variance reduction. See the main `fetwfe()` vignette for a real-data application and the simulation vignette for the simulation workflow. - **`betwfe()`** — alternative when you want regularization but not the fusion transformation. Useful for inspecting the effect of fusion alone — compare `betwfe()` vs. `fetwfe()` on the same data and the difference is what fusion adds. - **`etwfe()`** — useful as a baseline. On well-conditioned data it produces unbiased point estimates with valid standard errors. On small or over-parameterized data it can fail with rank-deficient cohort errors, which `fetwfe()`'s regularization avoids. For a real-data application of the recommended estimator, see the main `fetwfe()` vignette. # References - Faletto, G. (2025). Fused Extended Two-Way Fixed Effects for Difference-in-Differences with Staggered Adoptions. [arXiv preprint arXiv:2312.05985](https://arxiv.org/abs/2312.05985). - Wooldridge, J. M. (2021). Two-Way Fixed Effects, the Two-Way Mundlak Regression, and Difference-in-Differences Estimators. *SSRN Working Paper No. 3906345*.