class: center, middle, inverse, title-slide .title[ # 8: Difference-in-Differences Designs ] .subtitle[ ## Quantitative Causal Inference ] .author[ ###
J. Seawright
] .institute[ ###
Northwestern Political Science
] .date[ ### May 22, 2025 ] --- class: center, middle <style type="text/css"> pre { max-height: 400px; overflow-y: auto; } pre[class] { max-height: 200px; } </style> --- ### Before and After Do we need over-time comparisons to achieve causal inference? Do they help? How? ### Before and After Suppose we have a set of confounders that: 1. Varies across cases but is fixed over time, and/or 2. Is completely shared across cases but varies over time. --- ### Difference-In-Differences `\(Y_{0,i,t}\)` and `\(Y_{1,i,t}\)` --- ### Difference-In-Differences `\([Y_{1,i,t+k} - Y_{0,j,t+k}] - [Y_{0,i,t} - Y_{0,j,t}]\)` --- <img src="images/miamiboatlift.JPG" width="90%" style="display: block; margin: auto;" /> --- <img src="images/mariel1.JPG" width="90%" style="display: block; margin: auto;" /> --- <img src="images/mariel2.JPG" width="90%" style="display: block; margin: auto;" /> --- <img src="images/marielcomparison.JPG" width="90%" style="display: block; margin: auto;" /> --- <img src="images/marielcomparison2.JPG" width="90%" style="display: block; margin: auto;" /> --- <img src="images/marielconclusions.JPG" width="90%" style="display: block; margin: auto;" /> --- ### Analysis Starting simple, if we think *all* the confounding variables are constant either in time or space, then the model we want may be something like: `$$y_{i,t} = \alpha_{i} + \gamma_{t} + \beta_{1}Treatment_{i,t} + \epsilon_{i,t}$$` --- ``` r library(causaldata) ``` ``` ## Warning: package 'causaldata' was built under R version 4.4.3 ``` ``` r od <- causaldata::organ_donations library(fixest) ``` ``` ## Warning: package 'fixest' was built under R version 4.4.3 ``` --- ``` r # Treatment variable od <- od %>% mutate(Treated = State == 'California' & Quarter %in% c('Q32011','Q42011','Q12012')) clfe <- feols(Rate ~ Treated | State + Quarter, data = od) ``` --- ``` r summary(clfe) ``` ``` ## OLS estimation, Dep. Var.: Rate ## Observations: 162 ## Fixed-effects: State: 27, Quarter: 6 ## Standard-errors: Clustered (State) ## Estimate Std. Error t value Pr(>|t|) ## TreatedTRUE -0.022459 0.006131 -3.66304 0.0011185 ** ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## RMSE: 0.021982 Adj. R2: 0.974196 ## Within R2: 0.009221 ``` --- ### Parallel Trends The analysis here assumes that the treatment and control cases have *parallel time trends* on the dependent variable, conditional on any control variables and other adjustments in the model. --- <img src="images/parallel1.png" width="80%" style="display: block; margin: auto;" /> --- <img src="images/parallel2.png" width="80%" style="display: block; margin: auto;" /> --- ### Parallel Trends One useful test is to statistically test whether the treatment and control group had equal time trends during the period before the treatment. If we fit a model that estimates separate time slopes for the two groups, and then we test for equality between those two slopes, that is one way of testing the parallel trends assumption. --- ``` r od$calind <- 1*(od$State=='California') odparalleltrends.lm <- lm(Rate~Quarter_Num+Quarter_Num:calind, data=od[od$Quarter_Num<4,]) summary(odparalleltrends.lm) ``` ``` ## ## Call: ## lm(formula = Rate ~ Quarter_Num + Quarter_Num:calind, data = od[od$Quarter_Num < ## 4, ]) ## ## Residuals: ## Min 1Q Median 3Q Max ## -0.31778 -0.11098 0.00612 0.11568 0.32600 ## ## Coefficients: ## Estimate Std. Error t value Pr(>|t|) ## (Intercept) 0.433635 0.046060 9.415 1.7e-14 *** ## Quarter_Num 0.005181 0.021380 0.242 0.8092 ## Quarter_Num:calind -0.074189 0.042673 -1.739 0.0861 . ## --- ## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1 ## ## Residual standard error: 0.1567 on 78 degrees of freedom ## Multiple R-squared: 0.03746, Adjusted R-squared: 0.01278 ## F-statistic: 1.518 on 2 and 78 DF, p-value: 0.2256 ``` --- ``` r library(interactions) ``` ``` ## Warning: package 'interactions' was built under R version 4.4.3 ``` --- ``` r interact_plot(odparalleltrends.lm, pred="Quarter_Num", modx="calind", interval=TRUE) ``` <img src="8diffindiff_files/figure-html/unnamed-chunk-15-1.png" width="70%" style="display: block; margin: auto;" /> --- ### Parallel Trends If the parallel trends assumption fails, a difference-in-differences design may not be salvageable. It might be possible to control for time trends, but this can subtract out part of the causal effect and produce bias. At this point, a great deal of care and thought is needed, as well as substantial skill with time-series methods. --- <img src="images/ImaiKimdifndif.png" width="90%" style="display: block; margin: auto;" /> --- ### Negative Weights Negative weights can emerge in TWFE for difference-in-differences because treated observations of early treatment adopters end up serving as controls for late treatment adopters (see de Chaisemartin and d'Haultfoeuille 2020 and Goodman-Bacon 2021). --- ### Panel Matching Imai and coauthors propose panel-based matching as a way of implementing difference-in-differences designs. --- <img src="images/Shiraef1.png" width="90%" style="display: block; margin: auto;" /> --- <img src="images/Shiraef2.png" width="90%" style="display: block; margin: auto;" /> --- <img src="images/Shiraef3.png" width="90%" style="display: block; margin: auto;" /> --- <img src="images/Liu1.png" width="90%" style="display: block; margin: auto;" /> --- ### Functional Form and Parallel Trends `$$Y_{0,i,t}=f(X_{i,t})+h(U_{i,t})+\epsilon_{i,t}$$` in which `\(f(\cdot)\)` and `\(h(\cdot)\)` are known, parametric functions. --- ### Strict Exogeneity `$$\epsilon_{i,t} \perp\!\!\!\perp \{D_{j,s},X_{j,s},U_{j,s}\}$$` for all `\(i,j \in \{1,2,...,N\}\)` and `\(s,t \in \{1,2,...,T\}\)`. --- ### Low-dimensional Decomposition There exists a low-dimensional decomposition of `$$h(U_{i,t}) : h(U_{i,t}) = L_{i,t}$$` and `\(rank(L_{N×T}) << min\{N,T\}\)`. --- ### Estimation 1. Using only untreated observations, model the outcome, based on `\(X\)` and whatever strategy is needed to capture `\(U\)`. 2. Using the model from step 1, form fitted values `\(\hat{Y}_{0,i,t}\)` for each *treated* observation. 3. Estimate the treatment effect for each treated observation by: `\(\hat{\delta}_{i,t} = Y_{i,t} - \hat{Y}_{0,i,t}\)`. 4. Average the `\(\hat{\delta}\)` values to estimate whatever causal quantity is of interest. --- ### Specific Estimators FEct: estimate `\(\hat{Y}_{0,i,t}\)` using TWFE. IFEct: estimate `\(\hat{Y}_{0,i,t}\)` using a version of TWFE augmented with a factor-analysis model to allow for existence of limited categories of time-varying unobserved covariates. Matrix completion: conceptually similar to factor analysis; somewhat different performance properties. --- <img src="images/Liu2.png" width="90%" style="display: block; margin: auto;" /> --- ``` r library(fect) ``` ``` ## Warning: package 'fect' was built under R version 4.4.3 ``` ``` ## Registered S3 method overwritten by 'GGally': ## method from ## +.gg ggplot2 ``` --- ``` r vdemeu <- read.csv("data/vdemeu.csv") vdemeu <- vdemeu[vdemeu$year<2013,] eufect <- fect(v2x_libdem ~ accession, index = c("country_name", "year"), force = "two-way", data = vdemeu, method="fe",min.T0=1, se=1) ``` ``` ## For identification purposes, units whose number of untreated periods <1 are dropped automatically. ``` ``` ## Parallel computing ... ``` ``` ## Bootstrapping for uncertainties ... ``` ``` ## 76 runs ``` ``` ## ``` ``` ## The estimated covariance matrix is irreversible. ``` ``` ## ``` ``` r euife <- fect(v2x_libdem ~ accession, index = c("country_name", "year"), force = "two-way", data = vdemeu, method="ife",min.T0=1, se=1) ``` ``` ## For identification purposes, units whose number of untreated periods <1 are dropped automatically. ``` ``` ## Parallel computing ... ``` ``` ## Bootstrapping for uncertainties ... ``` ``` ## 66 runs ``` ``` ## ``` ``` ## The estimated covariance matrix is irreversible. ``` ``` ## ``` --- ``` r eufect ``` ``` ## Call: ## fect.formula(formula = v2x_libdem ~ accession, data = vdemeu, ## index = c("country_name", "year"), force = "two-way", method = "fe", ## se = 1, min.T0 = 1) ## ## ATT: ## ATT S.E. CI.lower CI.upper p.value ## Tr obs equally weighted -0.2067 0.06695 -0.3379 -0.07549 2.018e-03 ## Tr units equally weighted -0.2188 0.05398 -0.3246 -0.11300 5.044e-05 ``` --- ``` r euife ``` ``` ## Call: ## fect.formula(formula = v2x_libdem ~ accession, data = vdemeu, ## index = c("country_name", "year"), force = "two-way", method = "ife", ## se = 1, min.T0 = 1) ## ## ATT: ## ATT S.E. CI.lower CI.upper p.value ## Tr obs equally weighted -0.2067 0.05658 -0.3176 -0.09583 2.584e-04 ## Tr units equally weighted -0.2188 0.04693 -0.3108 -0.12681 3.131e-06 ``` --- [An overview of recent directions.](https://doi.org/10.1016/j.jeconom.2023.03.008)