*! version 1.2.1 17feb2025 capture program drop stackdid program define stackdid, rclass version 11 /* SYNTAX */ syntax [anything] /* */ [aw fw pw iw] [if] [in] /* */ [, /* */ TReatment(varname numeric) /* */ GRoup(varname) /* */ Window(string) /* */ nevertreat /* */ poisson /* */ nobuild /* */ noREGress /* */ clear /* */ saving(string) /* */ noLOG /* */ absorb(varlist fv) /* */ sw /* */ * /* estimator-specific options other than absorb() */ ] * Confirm required options, or nobuild, specified if ("`treatment'"=="" | "`group'"=="") & ("`build'"=="") { di as err "treatment() and group() are required unless nobuild is specified" exit 198 } * Confirm dependencies installed if ("`regress'"=="") { if ("`poisson'"!="") { cap which ppmlhdfe if (_rc) { di as err "package ppmlhdfe required: " as smcl "{bf:{stata ssc describe ppmlhdfe}}" exit 199 } else local cmd "ppmlhdfe" } else { cap which reghdfe if (_rc) { di as err "package reghdfe required: " as smcl "{bf:{stata ssc describe reghdfe}}" exit 199 } else local cmd "reghdfe" } } /* BUILD */ if ("`build'"=="") { * Assert data is xtset'ed capture xtset if (_rc) | inlist("","`r(panelvar)'","`r(timevar)'") { di as err "must xtset data with panelvar and timevar" exit 198 } local unit `r(panelvar)' local time `r(timevar)' * Note to self: Is the following check worth it? * Assert treatment takes values {0,1,.} capture assert inlist(`treatment',0,1,.) `if' `in' if (_rc) { di as err "`treatment' is not a 0/1/. variable" exit 450 } * Parse window() if ("`window'"!="") { gettoken pre post: window capture assert `pre'<`post' & inrange(0,`pre',`post'-1) if (_rc) { di as err "option window() specified incorrectly" exit 198 } } * Assert varnames are available foreach vname in _cohort _cohort_time _cohort_group { capture ds `vname' if (!_rc) { di as err "varname `vname' must be available" exit 110 } } tempvar treat_prev treat_event nevertreated tostack treated latest_treat lost_treat gained_treat stacked * Helper macros local ttype: type `time' // `time' datatype local tfmt : format `time' // `time' format if ("`log'"!="") local nolog "quietly" // suppress build log if ("`if'"!="") local ampif = subinstr("`if'","if","&",1) // replace if w/ ampersand * Preserve preserve * Find event times sort `group' `time' qui gen byte `treat_prev' = `treatment'[_n-1] if (`group'[_n-1]==`group' & `time'[_n-1]+1==`time') qui gen byte `treat_event' = (`treatment'==1 & `treat_prev'==0) qui levelsof `time' if (`treat_event'==1), local(cohorts) `nolog' di _n as text "treatment cohorts: " as result "`cohorts'" drop `treat_prev' * Initialize cohort identifier and nevertreated identifier qui gen `ttype' _cohort = . // missing means original, nonmissing means stacked... if ("`nevertreat'"!="") qui egen byte `nevertreated' = min(`treatment'==0), by(`group') * For each cohort... foreach co of local cohorts { * (helper: treated/control ... {1:treatment cohort, 0:control, .:neither}) qui egen byte `treated' = max(cond(`time'==`co',`treat_event'==1,.)), by(`group') qui replace `treated' = 0 if (missing(`treated') & `treatment'==0) // recover controls where cohort year is not observed if ("`nevertreat'"!="") qui replace `treated' = . if (`treated'==0 & `nevertreated'==0) * (1): grab everything within window of event if ("`window'"!="") { qui gen byte `tostack' = inrange(`time',`pre'+`co',`post'+`co'-1) if missing(_cohort) & !missing(`treated') `ampif' `in' } else { qui gen byte `tostack' = (missing(_cohort) & !missing(`treated')) `ampif' `in' } * (2): remove latest treatment and prior qui egen `ttype' `latest_treat' = max(cond(`treatment'==1,`time',.)) if (`tostack'==1 & `time'<`co'), by(`group') qui replace `tostack' = 0 if (`tostack'==1 & `treatment'==1 & `time'<=`latest_treat' & !missing(`latest_treat')) * (3): remove if treated group loses treatment status post-event qui egen `ttype' `lost_treat' = min(cond(`treatment'==0,`time',.)) if (`treated'==1 & `co'<`time'), by(`group') qui replace `tostack' = 0 if (`treated'==1 & `lost_treat'<=`time' & !missing(`lost_treat')) * (4): remove if control group gains treatment status post-event if ("`nevertreat'"=="") { qui egen `ttype' `gained_treat' = min(cond(`treatment'==1,`time',.)) if (`treated'==0 & `co'<=`time'), by(`group') qui replace `tostack' = 0 if (`treated'==0 & `gained_treat'<=`time' & !missing(`gained_treat')) drop `gained_treat' } * (5): create stack using -expand- drop `treated' `latest_treat' `lost_treat' `nolog' di as text "cohort " as result "`co'" as text " stacked " _cont `nolog' expand 2 if (`tostack'==1), gen(`stacked') qui replace _cohort = `co' if (`stacked'==1) drop `tostack' `stacked' } di * Generate fixed effects qui egen _cohort_time = group(_cohort `time') if !missing(_cohort), autotype qui egen _cohort_unit = group(_cohort `unit') if !missing(_cohort), autotype * Generate sample weight (inverse frequency) if ("`clear'"!="") | ("`sw'"!="") { qui bysort `unit' `time': gen _sw = 1/(_N-1) label var _sw "-stackdid- sample weight" } * Label saved (non-temporary) variables format _cohort `tfmt' label var _cohort "-stackdid- treatment cohort" label var _cohort_time "-stackdid- cohort-time fixed effect" label var _cohort_unit "-stackdid- unit-cohort fixed effect" * Clean up & grab N qui count if missing(_cohort) local N_orig = r(N) qui count if !missing(_cohort) local N_stacked = r(N) } /* ESTIMATE */ if ("`regress'"=="") { local abs absorb(`absorb' _cohort_time _cohort_unit) local w = cond(("`sw'"!=""), "[aweight=_sw]", "[`weight'`exp']") `cmd' `anything' `w' `if' `in', `abs' `options' return local regline "`e(cmdline)'" } /* CLEAN UP */ * Apply clear/saving options if ("`build'"=="") { if ("`clear'"!="") { restore, not qui drop if missing(_cohort) if ("`saving'"!="") { di save `saving' } } else if ("`saving'"!="") { qui drop if missing(_cohort) di save `saving' restore } else restore } /* RETURNS + MESSAGES */ if ("`build'"=="") { return local treatment "`treatment'" return local group "`group'" return local window "`window'" return scalar N_orig = `N_orig' return scalar N_stacked = `N_stacked' } return local cmdline "stackdid `0'" end * Note. The author recommends visually decomposing stacked data: * table (`group') (`time') (_cohort), nototal statistic(firstnm `treatment')