Both endid and lwdidR implement the panel
Difference-in-Differences (DiD) approach proposed by Lee &
Wooldridge (2025). The core idea is to perform unit-specific
pre-treatment transformations (like demeaning or detrending) and then
collapse the panel into a cross-section for estimation.
| Feature | lwdidR |
endid |
|---|---|---|
| Estimation | Ordinary Least Squares (OLS) | Engression (Neural Network) |
| Main Target | Average Treatment Effect (ATT) | ATT and Distributional Effects (QTE) |
| Inference | Analytical SEs, Wild Bootstrap, Permutation | Non-parametric Unit Bootstrap |
| Non-linearity | Linear only | Handles complex non-linearities |
We’ll simulate a panel where the treatment has a location-scale effect: it shifts both the mean and the spread of the outcome distribution. This creates quantile treatment effects (QTEs) that vary across the distribution — small or even negative at lower quantiles, and large at upper quantiles.
The true QTE at quantile \(\tau\) is: \[\text{QTE}(\tau) = \delta + (\sigma_1 - \sigma_0) \cdot \Phi^{-1}(\tau)\] where \(\delta = 0.5\) is the location shift, \(\sigma_0 = 0.5\) and \(\sigma_1 = 1.5\), so \(\text{QTE}(\tau) = 0.5 + \Phi^{-1}(\tau)\). This ranges from about \(-0.78\) at \(\tau = 0.1\) to \(1.78\) at \(\tau = 0.9\).
library(lwdidR)
library(endid)
set.seed(42)
# Generate synthetic panel data with location-scale treatment effect
N <- 200
T_total <- 6
tpost1 <- 4
unit <- rep(1:N, each = T_total)
time <- rep(1:T_total, times = N)
alpha <- rep(rnorm(N, sd = 0.3), each = T_total) # Unit fixed effects
# Group 1: treated, Group 0: control
group <- rep(c(1, 0), each = N/2 * T_total)
post <- as.integer(time >= tpost1)
D <- post * (group == 1)
# Location-scale DGP: treatment shifts mean by 0.5 and increases SD from 0.5 to 1.5
epsilon <- rnorm(N * T_total)
y <- alpha + 0.5 * D + (0.5 + 1.0 * D) * epsilon
df <- data.frame(unit = unit, time = time, y = y,
post_treat = D, group = group)lwdidR estimates the Average Treatment Effect using
OLS.
# Using 'post_treat' as the treatment receipt indicator
fit_lwdid <- lwdid(df, y = "y", ivar = "unit", tvar = "time", post = "post_treat",
vce = "hc3")
summary(fit_lwdid)
#>
#> Lee-Wooldridge DiD (lwdidR)
#> Design: common_timing
#> Transf.: demean
#> VCE: HC3
#> --------------------------------------------------
#> ATT: 0.3559
#> SE: 0.0972
#> t-stat: 3.6598
#> p-value: 0.0003
#> 95% CI: [0.1641, 0.5477]
#> N (firstpost): 200
#>
#>
#> Period-specific effects:
#> tindex att se tstat pvalue
#> 4 0.4487 0.1642 2.733 0.006845
#> 5 0.3278 0.1642 1.997 0.047194
#> 6 0.2912 0.1601 1.819 0.070414endid uses engression to capture the
distributional nature of the effect.
# We use fewer epochs and bootstrap draws for speed in this vignette
# For endid, 'post' is a calendar indicator, 'dvar' is the group indicator
fit_endid <- endid(df, y = "y", ivar = "unit", tvar = "time",
post = "post_treat", dvar = "group",
num_epochs = 500, nboot = 20, silent = TRUE)summary(fit_endid)
#> Engression-Based Distributional DiD
#> Design: common_timing | Transformation: demean
#>
#> --- ATT ---
#> Estimate SE CI_Lower CI_Upper
#> 0.299272 0.1159214 0.1195097 0.4639049
#>
#> --- QTE ---
#> quantile effect se ci_lower ci_upper
#> 0.1 -0.20180307 0.2039926 -0.47052683 0.2049584
#> 0.2 -0.04952917 0.1642889 -0.27667590 0.2777383
#> 0.3 0.07747465 0.1355604 -0.11262283 0.3407710
#> 0.4 0.19329061 0.1166920 0.02117358 0.3999235
#> 0.5 0.30422739 0.1058315 0.14371438 0.4562038
#> 0.6 0.41358909 0.1029745 0.24453744 0.5437221
#> 0.7 0.52691167 0.1087370 0.35075445 0.6763236
#> 0.8 0.64955456 0.1268273 0.45511885 0.8459054
#> 0.9 0.79483563 0.1522518 0.57100394 1.0411651While lwdidR provides a single ATT (around 0.5, the true
location shift), endid reveals the full picture: the
treatment hurts at low quantiles (by increasing downside risk)
and helps substantially at high quantiles (by increasing
upside). The QTE plot should show an upward-sloping curve crossing the
ATT line.
We can overlay the analytical QTE to verify that endid
recovers the true distributional effect.
# True QTE: 0.5 + qnorm(tau)
taus <- seq(0.05, 0.95, by = 0.05)
true_qte <- data.frame(tau = taus, qte = 0.5 + qnorm(taus))
plot(fit_endid) +
geom_line(data = true_qte, aes(x = tau, y = qte),
linetype = "dashed", color = "red", linewidth = 0.8) +
labs(caption = "Dashed red: true QTE")lwdidR when you want a fast,
industry-standard linear DiD estimate with well-understood frequentist
properties and high-performance standard errors (like Wild Cluster
Bootstrap).endid when you suspect the
treatment effect might be non-linear, vary across the distribution, or
if you want to estimate Quantile Treatment Effects (QTE). In this
example, lwdidR correctly estimates the ATT of 0.5, but
completely misses that the treatment increases inequality — a
conclusion only visible through the distributional lens of
endid.