Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
729939c
#42 bd infrastructure to support both
zhizhongpu Aug 8, 2025
ffb1b95
Revert "#42 bd infrastructure to support both"
zhizhongpu Aug 15, 2025
8430109
#42 bd start development
zhizhongpu Aug 15, 2025
3ff46cf
#42 bd feols for ols
zhizhongpu Aug 21, 2025
681fe78
#42 bd initialize new plotting script
zhizhongpu Aug 21, 2025
d00d853
#42 cl rename function arguments to align with input
zhizhongpu Aug 21, 2025
33caaef
#42 cl simplify useless code
zhizhongpu Aug 21, 2025
92d7d6f
#42 bd adapt AddSuptBand() to fixest
zhizhongpu Aug 21, 2025
d72bd18
#42 bd adapt TestLinear to fixest
zhizhongpu Aug 21, 2025
de4d7bf
#42 cl
zhizhongpu Aug 21, 2025
360cdcd
#42 cl rename objects
zhizhongpu Aug 21, 2025
ca4255b
#42 adapt AddZerosCovar to fixest
zhizhongpu Aug 21, 2025
8d9afa0
#42 fx bug
zhizhongpu Oct 3, 2025
709e9f4
#42 bd
zhizhongpu Oct 3, 2025
ac6aa6d
#42 cl drop legacy EventStudyPlotFixest.R
zhizhongpu Oct 3, 2025
3a1d318
#42 bd migrate feols functions to package from testing.r
zhizhongpu Oct 3, 2025
909175c
#42 fx bug
zhizhongpu Oct 3, 2025
3553ef1
#42 bd feols tests
zhizhongpu Oct 3, 2025
baa5e85
#42 bd fhs first pass
zhizhongpu Oct 3, 2025
f059cae
#42 bd test fhs
zhizhongpu Oct 3, 2025
d0d237d
#42 fx bug in existing code
zhizhongpu Oct 3, 2025
e430297
#42 bd loop for tests
zhizhongpu Oct 3, 2025
3fae492
#42 bd rename
zhizhongpu Oct 3, 2025
a016600
#42 revise test
zhizhongpu Oct 3, 2025
bb15d79
#57 fx bug in AddSuptBand.R
zhizhongpu Oct 15, 2025
f8c4421
#42 bd explicitly specify clustered vcov
zhizhongpu Oct 21, 2025
2355c6d
#42 fx small sample correction to have SE line up w/ STATA
zhizhongpu Oct 21, 2025
482c1a4
#42 cl style
zhizhongpu Oct 21, 2025
7f02fca
#42 fx bug
zhizhongpu Oct 22, 2025
a0f32c5
#42 bd add kernel argument
zhizhongpu Oct 22, 2025
d48d480
#42 bd add explicit argument kernel="estimatr"
zhizhongpu Oct 22, 2025
b497226
#42 bd iterated if statements for kernel
zhizhongpu Oct 22, 2025
13a448c
#42 cl relocate fhs
zhizhongpu Oct 22, 2025
2f99219
#42 bd futurewarning
zhizhongpu Oct 22, 2025
d57c469
#42 bd unify PrepareModelFormula
zhizhongpu Jan 8, 2026
044cc2f
#42 fx tidy for EventStudyPlot.R
zhizhongpu Jan 8, 2026
f46c3bb
#42 fx EventStudyFEOLS_FHS wrong se adjustment
zhizhongpu Jan 8, 2026
a6ab57f
#42 switch to relative tolerance
zhizhongpu Jan 8, 2026
fb71686
#42 reorg relocate test
zhizhongpu Jan 9, 2026
ccfd73c
#42 switch to relative tolerance for FHS
zhizhongpu Jan 9, 2026
b7f23d3
#42 fx iid -> HC1 for unclustered case
zhizhongpu Jan 9, 2026
c3352cb
#42 cl anes
zhizhongpu Jan 9, 2026
6961e56
#42 cl
zhizhongpu Jan 9, 2026
7038619
#42 cl
zhizhongpu Jan 9, 2026
ddec6eb
#42 cl combine tests
zhizhongpu Jan 9, 2026
7db8a91
#42 cl drop redundant test
zhizhongpu Jan 9, 2026
209e7e1
#42 cl combine tests
zhizhongpu Jan 9, 2026
1c4a5c9
#42 cl drop useless tests
zhizhongpu Jan 9, 2026
551bb3e
#42 cl consolidate tests fhs
zhizhongpu Jan 9, 2026
990166c
#42 cl drop redundant test
zhizhongpu Jan 9, 2026
b823f76
#42 bd add roxygen parameters
zhizhongpu Jan 9, 2026
9e4b01a
#42 bd roxygen
zhizhongpu Jan 9, 2026
2ac8106
#42 fx correct vcov / coef citation
zhizhongpu Jan 9, 2026
7553dac
#42 fx namespace
zhizhongpu Jan 9, 2026
cd55cdf
#42 fx undefined coefs object
zhizhongpu Jan 9, 2026
3194553
#42 bd add `fixest` to mwe
zhizhongpu Mar 19, 2026
b54da49
#42 simplify feols-FHS implementation
zhizhongpu Mar 19, 2026
fa8cd25
#42 cl replace broom:tidy
zhizhongpu Mar 19, 2026
d36ff17
#42 fx coef_table rownames
zhizhongpu Mar 19, 2026
fba4b3b
#42 cl simplify vignette
zhizhongpu Mar 19, 2026
c489fa2
#42 cl housekeeping
zhizhongpu Mar 19, 2026
fda9a78
#42 fx restore fix for Actions to pass
zhizhongpu Mar 19, 2026
82a5f06
#42 bd update metadata
zhizhongpu Mar 19, 2026
57d80b5
#42 refresh
zhizhongpu Mar 19, 2026
ff1a216
increase version number #42
santiagohermo Mar 19, 2026
7f05a31
#42 doc remove crossreferences to unexported functions
zhizhongpu Mar 23, 2026
a8e0e79
#42 drop issue/
zhizhongpu Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: eventstudyr
Title: Estimation and Visualization of Linear Panel Event Studies
Version: 1.1.5
Version: 1.2.0
Authors@R:
c(person(given = "Simon",
family = "Freyaldenhoven",
Expand Down Expand Up @@ -43,6 +43,7 @@ Imports:
data.table,
dplyr,
estimatr,
fixest,
ggplot2,
MASS,
rlang,
Expand All @@ -53,7 +54,7 @@ VignetteBuilder:
knitr
LazyData: true
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
RoxygenNote: 7.3.3
Suggests:
rmarkdown,
knitr,
Expand Down
8 changes: 8 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ importFrom(data.table,setnames)
importFrom(data.table,setorder)
importFrom(data.table,setorderv)
importFrom(data.table,shift)
importFrom(dplyr,rename)
importFrom(dplyr,select)
importFrom(fixest,coeftable)
importFrom(fixest,feols)
importFrom(fixest,fitstat)
importFrom(fixest,ssc)
importFrom(pracma,inv)
importFrom(pracma,pinv)
importFrom(rlang,.data)
importFrom(stats,as.formula)
importFrom(stats,coef)
importFrom(stats,pnorm)
importFrom(stats,qchisq)
importFrom(stats,qnorm)
importFrom(stats,reformulate)
importFrom(stats,setNames)
importFrom(stats,vcov)
22 changes: 16 additions & 6 deletions R/AddSuptBand.R
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
#' for each event-study coefficient.
#' @import estimatr
#' @importFrom MASS mvrnorm
#' @importFrom fixest coeftable
#' @importFrom dplyr rename
#' @keywords internal
#' @noRd
#'
Expand Down Expand Up @@ -47,16 +49,18 @@
#' eventstudy_coefficients = eventstudy_estimates$arguments$eventstudy_coefficients
#')

AddSuptBand <- function(estimates, num_sim = 1000, conf_level = .95, eventstudy_coefficients) {
AddSuptBand <- function(model_estimates, num_sim = 1000, conf_level = .95, eventstudy_coefficients) {

if (! class(estimates) %in% c("lm_robust", "iv_robust")) {
if (! class(model_estimates) %in% c("lm_robust", "iv_robust", "fixest")) {
stop("estimates is not a data frame with coefficient estimates and standard errors")
}
if (! is.numeric(num_sim) | num_sim %% 1 != 0 | num_sim <= 0) {stop("num_sim should be a natural number.")}
if (! is.numeric(conf_level) | conf_level < 0 | conf_level > 1) {stop("conf_level should be a real number between 0 and 1, inclusive.")}
if (! is.character(eventstudy_coefficients)) {stop("eventstudy_coefficients should be a character.")}

vcov_matrix_all <- estimates$vcov
fixest = class(model_estimates) == "fixest"

vcov_matrix_all <- if(fixest){vcov(model_estimates)} else {model_estimates$vcov}
v_terms_to_keep <- colnames(vcov_matrix_all) %in% eventstudy_coefficients
vcov_matrix <- vcov_matrix_all[v_terms_to_keep, v_terms_to_keep]

Expand All @@ -75,12 +79,18 @@ AddSuptBand <- function(estimates, num_sim = 1000, conf_level = .95, eventstudy_
critical_value = t[floor(conf_level_num_sim) + 1]
}

df_estimates_tidy <- estimatr::tidy(estimates)
df_estimates_tidy <- if(fixest){
coef_table <- model_estimates |>
fixest::coeftable() |>
as.data.frame()
coef_table$term <- rownames(coef_table)
coef_table |>
dplyr::rename(estimate = Estimate, std.error = `Std. Error`) |>
dplyr::select(term, estimate, std.error)
} else {estimatr::tidy(model_estimates)}

df_estimates_tidy["suptband_lower"] <- df_estimates_tidy$estimate - (critical_value * df_estimates_tidy$std.error)
df_estimates_tidy["suptband_upper"] <- df_estimates_tidy$estimate + (critical_value * df_estimates_tidy$std.error)


return(df_estimates_tidy)

}
52 changes: 35 additions & 17 deletions R/EventStudy.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#' when there are anticipation effects. If set to FALSE, does not make the switch. Defaults to TRUE.
#' @param allow_duplicate_id If TRUE, the function estimates a regression where duplicated ID-time rows are weighted by their duplication count. If FALSE, the function raises an error if duplicate unit-time keys exist in the input data. Default is FALSE.
#' @param avoid_internal_copy If TRUE, the function avoids making an internal deep copy of the input data, and instead directly modifies the input data.table. Default is FALSE.
#' @param kernel Accepts one of "estimatr" or "fixest". If "estimatr" is specified, uses the estimatr package for estimation. If "fixest" is specified, uses the fixest package for estimation. Defaults to "estimatr" (deprecated - will change to "fixest" in a future release).
#'
#' @return A list that contains, under "output", the estimation output as an lm_robust object, and under "arguments", the arguments passed to the function.
#' @import dplyr
Expand All @@ -54,7 +55,8 @@
#' idvar = "id",
#' timevar = "t",
#' pre = 0, post = 3,
#' normalize = -1
#' normalize = -1,
#' kernel = "fixest"
#' )
#'
#' ### Access estimated model
Expand All @@ -63,7 +65,8 @@
#' summary(eventstudy_model$output)
#'
#' ### data.frame of estimates
#' estimatr::tidy(eventstudy_model$output)
#' fixest::coeftable(eventstudy_model$output) # for kernel='fixest'
#' # estimatr::tidy(eventstudy_model$output) # for kernel='estimatr'
#'
#' ### Access arguments
#' eventstudy_model$arguments
Expand All @@ -83,6 +86,7 @@
#' pre = 2, overidpre = 4,
#' normalize = - 3,
#' cluster = TRUE,
#' kernel = "fixest",
#' anticipation_effects_normalization = TRUE
#' )
#'
Expand All @@ -100,7 +104,8 @@
#' FE = TRUE, TFE = TRUE,
#' post = 0, overidpost = 0,
#' pre = 0, overidpre = 0,
#' cluster = TRUE
#' cluster = TRUE,
#' kernel = "fixest"
#' )
#'
#' summary(eventstudy_model_static$output)
Expand All @@ -117,7 +122,8 @@
#' idvar = "id",
#' timevar = "t",
#' pre = 0, post = 3,
#' normalize = -1
#' normalize = -1,
#' kernel = "fixest"
#' )
#'
#' summary(eventstudy_model_unbal$output)
Expand All @@ -136,7 +142,8 @@
#' post = 2, overidpost = 1,
#' pre = 0, overidpre = 3,
#' normalize = -1,
#' cluster = TRUE
#' cluster = TRUE,
#' kernel = "fixest"
#' )
#'
#' summary(eventstudy_model_iv$output)
Expand All @@ -145,10 +152,13 @@
EventStudy <- function(estimator, data, outcomevar, policyvar, idvar, timevar, controls = NULL,
proxy = NULL, proxyIV = NULL, FE = TRUE, TFE = TRUE, post, overidpost = 1, pre, overidpre = post + pre,
normalize = -1 * (pre + 1), cluster = TRUE, anticipation_effects_normalization = TRUE,
allow_duplicate_id = FALSE, avoid_internal_copy = FALSE) {
allow_duplicate_id = FALSE, avoid_internal_copy = FALSE, kernel = "estimatr") {

# Check for errors in arguments
if (! estimator %in% c("OLS", "FHS")) {stop("estimator should be either 'OLS' or 'FHS'.")}
if (! kernel %in% c("estimatr", "fixest")) {stop("kernel should be either 'estimatr' or 'fixest'.")}
if (missing(kernel)) {warning("Argument 'kernel' was not specified; using 'estimatr' as default; we strongly recommend explicitly specifying a kernel because the default is scheduled to change.")}
if (kernel == "estimatr") {warning("'estimatr' selected as kernel. We no longer maintain it and will depreciate it in a future release. We recommend using 'fixest' instead.")}
if (! is.data.frame(data)) {stop("data should be a data frame.")}
for (var in c(idvar, timevar, outcomevar, policyvar)) {
if ((! is.character(var))) {
Expand Down Expand Up @@ -336,14 +346,17 @@ EventStudy <- function(estimator, data, outcomevar, policyvar, idvar, timevar, c
}

if (estimator == "OLS") {
event_study_formula <- PrepareModelFormula(estimator, outcomevar, str_policy_vars,
static, controls, proxy, proxyIV)

output <- EventStudyOLS(event_study_formula, data, idvar, timevar, FE, TFE, cluster)
formula <- PrepareModelFormula(estimator, outcomevar, str_policy_vars,
static, controls, proxy, proxyIV,
kernel, idvar, timevar, FE, TFE)

if (kernel == "estimatr") {
output <- EventStudyOLS(formula, data, idvar, timevar, FE, TFE, cluster)
} else if (kernel == "fixest") {
output <- EventStudyFEOLS(formula, data, idvar, timevar, FE, TFE, cluster)
}
coefficients <- str_policy_vars
}
if (estimator == "FHS") {

} else if (estimator == "FHS") {
if (is.null(proxyIV)) {
Fstart <- 0
str_fd_leads <- str_policy_vars[grepl("^z_fd_lead", str_policy_vars)]
Expand All @@ -360,10 +373,15 @@ EventStudy <- function(estimator, data, outcomevar, policyvar, idvar, timevar, c
". To specify a different proxyIV use the proxyIV argument."))
}

event_study_formula <- PrepareModelFormula(estimator, outcomevar, str_policy_vars,
static, controls, proxy, proxyIV)

output <- EventStudyFHS(event_study_formula, data, idvar, timevar, FE, TFE, cluster)
formula <- PrepareModelFormula(estimator, outcomevar, str_policy_vars,
static, controls, proxy, proxyIV,
kernel, idvar, timevar, FE, TFE)

if (kernel == "estimatr") {
output <- EventStudyFHS(formula, data, idvar, timevar, FE, TFE, cluster)
} else if (kernel == "fixest") {
output <- EventStudyFEOLS_FHS(formula, data, idvar, timevar, FE, TFE, cluster)
}
coefficients <- dplyr::setdiff(str_policy_vars, proxyIV)
}

Expand Down
51 changes: 50 additions & 1 deletion R/EventStudyFHS.R
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
#'
#' @return A data.frame that contains the estimates for the event study coefficients.
#' @import estimatr
#' @importFrom stats qnorm pnorm
#' @importFrom fixest feols
#' @importFrom stats qnorm pnorm coef
#' @keywords internal
#' @noRd
#'
Expand Down Expand Up @@ -127,3 +128,51 @@ EventStudyFHS <- function(prepared_model_formula, prepared_data,
return(fhs_output)
}


EventStudyFEOLS_FHS <- function(formula, prepared_data,
idvar, timevar, FE, TFE, cluster) {

if (! inherits(formula, "formula")) {stop("formula should be a formula")}
if (! is.data.frame(prepared_data)) {stop("data should be a data frame.")}
if (! is.character(idvar)) {stop("idvar should be a character.")}
if (! is.character(timevar)) {stop("timevar should be a character.")}
if (! is.logical(FE)) {stop("FE should be either TRUE or FALSE.")}
if (! is.logical(TFE)) {stop("TFE should be either TRUE or FALSE.")}
if (! is.logical(cluster)) {stop("cluster should be either TRUE or FALSE.")}
if (FE & !cluster) {stop("cluster=TRUE required when FE=TRUE.")}

if (cluster) {
vcov_arg <- as.formula(paste0("~", idvar))
} else {
vcov_arg <- "HC1"
}

fhs_output <- fixest::feols(
fml = formula,
data = prepared_data,
vcov = vcov_arg
)

if (FE & cluster) {
coefs <- coef(fhs_output)
N <- fhs_output$nobs
n <- length(unique(prepared_data[[idvar]]))

if (TFE) {
K <- fhs_output$fixef_sizes[[timevar]] + length(coefs)
} else {
K <- 1 + length(coefs)
}

adjustment_factor <- (N - K) / (N - n - K + 1)
fhs_output$se <- fhs_output$se / sqrt(adjustment_factor)
fhs_output$cov.scaled <- fhs_output$cov.scaled / adjustment_factor

fhs_output$tstat <- coefs / fhs_output$se
fhs_output$pvalue <- 2 * stats::pnorm(abs(fhs_output$tstat), lower.tail = FALSE)
fhs_output$conf.low <- coefs - stats::qnorm(0.975) * fhs_output$se
fhs_output$conf.high <- coefs + stats::qnorm(0.975) * fhs_output$se
}

return(fhs_output)
}
23 changes: 22 additions & 1 deletion R/EventStudyOLS.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#' Runs Ordinary Least Squares (OLS) with optional fixed effects and clustering
#'
#' @param prepared_model_formula A formula object created in [PrepareModelFormula()] that is passed to [EventStudy()].
#' @param prepared_model_formula A formula object created in `PrepareModelFormula()` that is passed to [EventStudy()].
#' @param prepared_data Data frame containing all of the parameters required for [EventStudy()] plus leads and
#' lags of the first differenced policy variable and leads and lags of the policy variable.
#' @param idvar Character indicating column of units.
Expand All @@ -12,6 +12,7 @@
#'
#' @return A data.frame that contains the estimates for the event study coefficients.
#' @import estimatr
#' @importFrom fixest feols ssc
#' @keywords internal
#' @noRd
#'
Expand Down Expand Up @@ -123,3 +124,23 @@ EventStudyOLS <- function(prepared_model_formula, prepared_data,

return(ols_output)
}

EventStudyFEOLS <- function(formula, prepared_data,
idvar, timevar, FE, TFE, cluster) {

if (cluster) {
vcov_fixest <- as.formula(paste0("~", idvar))
small_sample_correction <- fixest::ssc(K.fixef = "full")
} else {
vcov_fixest <- "iid"
small_sample_correction <- fixest::ssc()
}

ols_output <- fixest::feols(
fml = formula,
data = prepared_data,
vcov = vcov_fixest,
ssc = small_sample_correction
)
return(ols_output)
}
Loading
Loading