From 9f9577a4c8ab0ff4aa996a814c8da4eb3133ca36 Mon Sep 17 00:00:00 2001 From: Kevin Michael Frick Date: Fri, 13 Feb 2026 20:20:36 +0100 Subject: [PATCH] Fix iplot parsing for IV sunab terms --- R/coefplot.R | 21 ++++++++++++++++++--- tests/fixest_tests.R | 34 +++++++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/R/coefplot.R b/R/coefplot.R index 830eb8db..2d3e0cda 100644 --- a/R/coefplot.R +++ b/R/coefplot.R @@ -2089,7 +2089,24 @@ coefplot_prms = function(all_models, vcov = NULL, se, ci_low, ci_high, x, x.shif # avoids bug with IVs => problem if user names the variables that way is_IV = FALSE if(isTRUE(object$iv) && identical(object$iv_stage, 2)){ - all_vars = gsub("^fit_", "", all_vars) + iv_fit_names = object[["iv_endo_names_fit"]] + if(is.character(iv_fit_names) && length(iv_fit_names) > 0){ + # Restrict renaming to fitted endogenous terms only. + # This avoids altering user variables that simply start with "fit_". + is_fit = all_vars %in% iv_fit_names + if(any(is_fit)){ + all_vars_fit = all_vars[is_fit] + # fit_x:var -> var (single ':'), but keep names like fit_user::2 unchanged here. + all_vars_fit = sub("^fit_[^:]+:(?!:)", "", all_vars_fit, perl = TRUE) + # For fitted terms still starting with fit_ (e.g. fit_x2::2), remove the prefix. + all_vars_fit = sub("^fit_", "", all_vars_fit) + all_vars[is_fit] = all_vars_fit + } + } else { + # Legacy fallback for old objects without iv_endo_names_fit. + all_vars = sub("^fit_[^:]+:(?!:)", "", all_vars, perl = TRUE) + all_vars = sub("^fit_", "", all_vars) + } names(estimate) = all_vars } @@ -2878,8 +2895,6 @@ getFixest_coefplot = function(){ - - diff --git a/tests/fixest_tests.R b/tests/fixest_tests.R index fc875f95..04f632b2 100644 --- a/tests/fixest_tests.R +++ b/tests/fixest_tests.R @@ -2000,6 +2000,35 @@ res_sunab = feols(y ~ x1 + sunab(year_treated, year, bin.rel = "bin::2"), base_s iplot(res_sunab) test(length(coef(res_sunab)), 12) +# IV + sunab + interacted fitted regressor +set.seed(20260213) +n_id = 200L +never_cohort = 100L +dt_sunab_iv = expand.grid(id = 1:n_id, rel_time = -2:2) +dt_sunab_iv$high_growth = as.integer(dt_sunab_iv$id <= n_id / 2L) +dt_sunab_iv$cohort = ifelse(dt_sunab_iv$high_growth == 1L, 0L, never_cohort) +dt_sunab_iv$x = rnorm(nrow(dt_sunab_iv)) +dt_sunab_iv$id_fe = rnorm(n_id)[dt_sunab_iv$id] +dt_sunab_iv$y = 0.08 * dt_sunab_iv$x * (dt_sunab_iv$rel_time >= 0L) * dt_sunab_iv$high_growth + dt_sunab_iv$id_fe + rnorm(nrow(dt_sunab_iv), sd = 0.25) + +res_sunab_iv = feols(y ~ 1 | x:sunab(cohort, rel_time, ref.p = -1, ref.c = never_cohort, no_agg = TRUE) ~ sunab(cohort, rel_time, ref.p = -1, ref.c = never_cohort, no_agg = TRUE), dt_sunab_iv) +iv_sunab_iplot_ok = tryCatch({ + iplot(res_sunab_iv) + TRUE +}, error = function(e) FALSE) +test(iv_sunab_iplot_ok, TRUE) + +# IV with an exogenous i() variable whose name starts with "fit_" +set.seed(1) +base$fit_user = sample(1:3, nrow(base), TRUE) +res_iv_fit_user = feols(y ~ i(fit_user) | x2 ~ x3, base) +iv_fit_user_iplot_ok = tryCatch({ + iplot(res_iv_fit_user) + TRUE +}, error = function(e) FALSE) +test(iv_fit_user_iplot_ok, TRUE) +base$fit_user = NULL + #### #### bin #### @@ -3182,8 +3211,3 @@ test(nrow(fixest_data(est_mult, "esti")), 45) - - - - -