*! version 1.2.3 20261030 github.com/mdroste/stata-ctools program define civreghdfe, eclass version 14.1 * Check observation limit (Stata plugin API limitation) if _N > 2147483647 { di as error "ctools does not support datasets exceeding 2^31 (2.147 billion) observations" di as error "This is a limitation of Stata's plugin API" exit 920 } * Start wall clock timer immediately quietly _ctools_clock __ct_lap97 * e(cmdline) is the command as typed: syntax below reuses `0' for the * variable specification alone local cmdline `"civreghdfe `0'"' * Parse syntax * Basic syntax: civreghdfe depvar (endogvars = instruments) [exogvars], absorb() [options] * absorb() is optional - without it, runs as regular IV (no FE absorption) syntax anything(name=0 equalok) [if] [in] [aw fw pw/], /// [Absorb(string) /// vce(string) /// Robust /// CLuster(varlist) /// TOLerance(real 1e-8) /// MAXiter(integer 500) /// Verbose /// FIRST /// FFIRst /// RF /// SMALL /// LIML /// FULLER(real 0) /// Kclass(real 0) /// GMM2s /// CUE /// COVIV /// BW(integer 0) /// KERNEL(string) /// DKRAAY(integer 0) /// KIEFER /// CENTer /// B0(string) /// RESiduals(name) /// RESiduals2 /// NOConstant /// Level(cilevel) /// noHEADer /// noFOOTer /// noOUTput /// TItle(string) /// SUBtitle(string) /// DEPname(string) /// noid /// ORThog(varlist fv ts) /// ENDOGtest(varlist fv ts) /// REDundant(varlist fv ts) /// PARTial(varlist fv ts) /// FWL(varlist fv ts) /// DOFminus(integer 0) /// SDOFminus(integer 0) /// NOPARTIALsmall /// EForm(string) /// PLUS /// NOOMITted /// OMITted /// VSQuish /// NOEMPtycells /// BASElevels /// ALLBASElevels /// SAVEFirst /// SAVEFPrefix(string) /// SAVERF /// SAVERFPrefix(string) /// THReads(integer 0) /// NORETURN] * Drop whatever an earlier run left behind, so no stale state is read _ctools_cleanup civreghdfe * Plugin statistics are copied into temporary scalars before they are read tempname temp_F temp_r2 temp_underid temp_underid_df temp_cd_f temp_kp_f temp_kp_f2 tempname temp_sargan temp_sargan_df temp_endog_chi2 temp_endog_f temp_endog_df tempname temp_cstat temp_cstat_df temp_endogtest_stat temp_endogtest_df tempname temp_redund_stat temp_redund_df * Handle standalone robust/cluster as aliases for vce() if "`robust'" != "" & `"`vce'"' == "" { local vce "robust" } if "`cluster'" != "" & `"`vce'"' == "" { local vce "cluster `cluster'" } if "`robust'" != "" & "`cluster'" != "" { di as error "cannot specify both robust and cluster()" exit 198 } if "`robust'" != "" & `"`vce'"' != "" & `"`vce'"' != "robust" { di as error "cannot specify both robust and vce()" exit 198 } if "`cluster'" != "" & `"`vce'"' != "" & !regexm(`"`vce'"', "^cluster") { di as error "cannot specify both cluster() and vce()" exit 198 } * Handle fwl() as alias for partial() if "`fwl'" != "" & "`partial'" == "" { local partial "`fwl'" } else if "`fwl'" != "" & "`partial'" != "" { di as error "cannot specify both fwl() and partial()" exit 198 } if "`center'" != "" { di as error "civreghdfe: center is not supported" exit 198 } * Handle residuals2 - auto-named residuals if "`residuals2'" != "" { local residuals "_civreghdfe_resid" } if "`residuals'" != "" & "`residuals2'" == "" _ctools_newvars `residuals' * Parse the varlist with parentheses for endogenous vars and instruments * Format: depvar [exog_before] (endog1 endog2 = inst1 inst2 inst3) [exog_after] local 0 `"`0'"' * First get everything before the opening parenthesis gettoken before_paren rest : 0, parse("(") local before_paren = strtrim("`before_paren'") * First token of before_paren is depvar, rest is exogenous vars before parentheses gettoken depvar exog_before : before_paren local depvar = strtrim("`depvar'") local exog_before = strtrim("`exog_before'") * Check for parentheses containing endogenous = instruments * If no parentheses, treat remaining vars as exogenous and run as OLS (no IV) local is_iv_model = 0 if strpos("`rest'", "(") == 0 { * No IV specification - treat rest as exogenous vars, run as OLS local exog_after = strtrim("`rest'") local paren_content "" local endogvars "" local instruments "" } else { local is_iv_model = 1 * Extract content within parentheses local rest = strtrim("`rest'") if substr("`rest'", 1, 1) == "(" { local rest = substr("`rest'", 2, .) } local paren_end = 0 local paren_depth = 1 forvalues pos = 1/`=strlen("`rest'")' { local char = substr("`rest'", `pos', 1) if "`char'" == "(" local ++paren_depth if "`char'" == ")" local --paren_depth if `paren_depth' == 0 { local paren_end = `pos' continue, break } } if `paren_end' == 0 { di as error "Unmatched parenthesis in variable specification" exit 198 } local paren_content = substr("`rest'", 1, `paren_end' - 1) local exog_after = strtrim(substr("`rest'", `paren_end' + 1, .)) * Parse endogenous = instruments local eq_pos = strpos("`paren_content'", "=") if `eq_pos' == 0 { di as error "Must specify instruments after '=' in parentheses" exit 198 } local endogvars = strtrim(substr("`paren_content'", 1, `eq_pos' - 1)) local instruments = strtrim(substr("`paren_content'", `eq_pos' + 1, .)) } * Combine exogenous variables from before and after parentheses local exogvars `exog_before' `exog_after' local exogvars = strtrim("`exogvars'") * Validate dependent variable exists and is numeric (error 109 for string vars) foreach v of local depvar { capture confirm string variable `v' if _rc == 0 { di as error "type mismatch" exit 109 } confirm numeric variable `v' } * Mark sample early for fvrevar marksample touse markout `touse' `depvar' * One numeric variable per absorbed effect for the plugin: string * variables and interactions are numbered first (the plugin renumbers * every effect on the estimation sample). absorb_names holds the effects * as reghdfe names them, for e(absorb), the output and the auxiliary * creghdfe regressions. local absorb_names if `"`absorb'"' != "" { if strpos(`"`absorb'"', ",") { di as error "civreghdfe: absorb() takes no suboptions" exit 198 } _ctools_hdfe_absorb `absorb', touse(`touse') cmd(civreghdfe) local n_absorb = r(n) forvalues g = 1/`n_absorb' { local absorb_name_`g' `r(name_`g')' local absorb_vars_`g' `r(vars_`g')' local absorb_group_`g' = r(group_`g') } local absorb forvalues g = 1/`n_absorb' { local absorb_names `absorb_names' `absorb_name_`g'' if `absorb_group_`g'' { tempvar absorb_id_`g' quietly _ctools_group `absorb_vars_`g'' if `touse', generate(`absorb_id_`g'') local absorb `absorb' `absorb_id_`g'' } else local absorb `absorb' `absorb_vars_`g'' } } * Parse VCE. Its cluster variables are marked out before the factor * variables are expanded, so that their levels come from the estimation * sample. cluster_var and cluster_var2 hold the dimensions as reghdfe * names them (an interaction as var1#var2). local vce_type = 0 local cluster_var "" local cluster_var2 "" if `"`vce'"' != "" { _ctools_hdfe_vce, spec(`"`vce'"') touse(`touse') cmd(civreghdfe) local vce_kind `r(kind)' if "`vce_kind'" == "robust" { local vce_type = 1 } else if "`vce_kind'" == "cluster" { * One-way (2) or two-way (3) clustering if !inlist(r(ndims), 1, 2) { di as error "vce(cluster) supports 1 or 2 cluster variables" exit 198 } local vce_type = 1 + r(ndims) local cluster_var `r(name_1)' if r(ndims) == 2 local cluster_var2 `r(name_2)' } else if inlist("`vce_kind'", "unadjusted", "ols") { local vce_type = 0 } else { di as error "vce(`vce') not supported. Use vce(robust), vce(cluster varname), or vce(cluster var1 var2)" exit 198 } } * The first-stage and reduced-form regressions run through creghdfe, * which clusters in one dimension only: refuse before estimating rather * than fail after the main results are shown if `vce_type' == 3 & "`first'`rf'`savefirst'`saverf'" != "" { di as error "civreghdfe: first, rf, savefirst and saverf are not available with two-way clustering" exit 198 } * Expand factor variables — single fvrevar, base pre-filter, then _rmcoll * fvexpand (cheap) gets names; fvrevar (expensive) creates temp vars * Pre-filtering base/omitted levels reduces _rmcoll input size local endogvars_names_all "" local __n_endog_total = 0 if "`endogvars'" != "" { fvexpand `endogvars' if `touse' local endogvars_names_all "`r(varlist)'" local __n_endog_total : word count `endogvars_names_all' } local exogvars_names_all "" local __n_exog_total = 0 if "`exogvars'" != "" { fvexpand `exogvars' if `touse' local exogvars_names_all "`r(varlist)'" local __n_exog_total : word count `exogvars_names_all' } local instruments_names_all "" local __n_inst_total = 0 if "`instruments'" != "" { fvexpand `instruments' if `touse' local instruments_names_all "`r(varlist)'" local __n_inst_total : word count `instruments_names_all' } * Single fvrevar call for all variable groups local endogvars_expanded "" local endogvars_coef "" local exogvars_expanded "" local exogvars_coef "" local instruments_expanded "" local instruments_coef "" local __all_fv `endogvars' `exogvars' `instruments' if `"`__all_fv'"' != "" { fvrevar `__all_fv' if `touse' local __all_exp "`r(varlist)'" * Base/omitted placeholders are not sent to the plugin, which also * detects collinearity. The fvrevar variables follow the three name * lists in order. foreach __grp in endogvars exogvars instruments { local __grp_vars "" forvalues __i = 1/`: word count ``__grp'_names_all'' { gettoken __v __all_exp : __all_exp local __grp_vars `__grp_vars' `__v' } _ctools_fvsplit, names(``__grp'_names_all') vars(`__grp_vars') local `__grp'_expanded "`r(vars)'" local `__grp'_coef "`r(names)'" } } * Check that no endogenous variable is also an excluded instrument if `is_iv_model' { foreach evar of local endogvars_coef { foreach ivar of local instruments_coef { if "`evar'" == "`ivar'" { di as error "`evar' is both endogenous and an instrument" exit 481 } } } } * Load the platform-appropriate ctools plugin if not already loaded _ctools_load * Stata scopes plugin registrations to the calling ado program. capture program ctools_plugin, plugin using("`__ctools_plugin'") if _rc != 0 & _rc != 110 exit 601 capture confirm number 0 * When no absorb specified and noconstant not requested, add explicit constant * (With absorb, the constant is implicitly absorbed by the FE) local has_cons = 0 local G : word count `absorb' if `G' == 0 & "`noconstant'" == "" { tempvar _cons_var gen byte `_cons_var' = 1 local exogvars_expanded `exogvars_expanded' `_cons_var' local exogvars_coef `exogvars_coef' _cons local exogvars_names_all `exogvars_names_all' _cons local has_cons = 1 } * Count variables (using expanded counts) local K_endog : word count `endogvars_expanded' local K_exog : word count `exogvars_expanded' local K_inst_excl : word count `instruments_expanded' * Build full instrument list (exogenous + excluded instruments) - use expanded local all_instruments `exogvars_expanded' `instruments_expanded' local K_iv : word count `all_instruments' * Check identification if `K_iv' < `K_endog' + `K_exog' { di as error "Model is underidentified" di as error " Endogenous variables: `K_endog'" di as error " Exogenous variables: `K_exog'" di as error " Total instruments: `K_iv'" exit 481 } * Handle weights local has_weights = 0 local weight_type = 0 local weight_var "" if "`weight'" != "" { local has_weights = 1 if "`weight'" == "aweight" local weight_type = 1 else if "`weight'" == "fweight" local weight_type = 2 else if "`weight'" == "pweight" local weight_type = 3 else { di as error "Weight type `weight' not supported" exit 198 } * Evaluate the weight expression; marks out and validates the weights tempvar evaluated_weight local weight_expr = trim(`"`exp'"') if substr(`"`weight_expr'"', 1, 1) == "=" local weight_expr = substr(`"`weight_expr'"', 2, .) _ctools_weight, generate(`evaluated_weight') touse(`touse') type(`weight') expression(`"= `weight_expr'"') local weight_var "`evaluated_weight'" } * Kernel-based VCEs, as in ivreg2: bw() alone gives the AC estimator * (autocorrelation-consistent, conditionally homoskedastic errors), bw() * with robust HAC, bw() with cluster(