*! didintjl 0.7.8 June 14th 2026 /*------------------------------------*/ /*didintjl*/ /*written by Eric Jamieson */ /*version 0.7.8 2026-14-07 */ /*------------------------------------*/ cap program drop didintjl program define didintjl, rclass version 16 syntax, outcome(varname) state(varname) time(varname) /// [gvar(varname) /// treated_states(string) treatment_times(string) date_format(string) /// covariates(string) ccc(string) agg(string) weighting(string) ref_column(string) ref_group(string) /// freq(string) freq_multiplier(int 1) start_date(string) end_date(string) /// nperm(int 999) seed(int 0) use_pre_controls(int 0) notyet(int -1) hc(int 1) truejack(int 0) edgecase(int 0) process(int 1)] // PART ONE: BASIC SETUP qui cap which jl if _rc { di as error "The 'julia' package is required but not installed or not found in the system path. See https://github.com/droodman/julia.ado for more details." exit 3 } // Check process if `process' != 0 & `process' != 1 { di as error "process must be either 1 (true) or 0 (false)." exit 42 } // Check seed value if `seed' == 0 { qui jl: seed = abs(round(randn(1)[1]*10000)) } else { qui jl: seed = `seed' } // Check truejack if `truejack' == 0 { qui jl: truejack = false } else if `truejack' == 1 { qui jl: truejack = true } else { di as error "truejack must be either 1 (true) or 0 (false)." } // Check edgecase if `edgecase' == 0 { qui jl: edgecase = false } else if `edgecase' == 1 { qui jl: edgecase = true } else { di as error "edgecase must be either 1 (true) or 0 (false)." } // notyet arg if `notyet' >= 0 { if `notyet' != 0 & `notyet' != 1 { di as error "'notyet' must be either 1 (true) or 0 (false)." } `use_pre_controls' = `notyet' } // Check use_pre_controls arg if `use_pre_controls' == 1 { qui jl: use_pre_controls = true } else if `use_pre_controls' == 0 { qui jl: use_pre_controls = false } else { di as error "use_pre_controls must be 0 (False) or 1 (True) (Default)" exit 43 } // Pass hc arg to julia qui jl: hc = `hc' // Check date_format if "`date_format'" == "" { qui jl: date_format = nothing } else { qui jl: date_format = "`date_format'" } // Check that DiDInt.jl v0.9.6 or later is installed tempname DiDIntOK qui jl: using Pkg qui jl: _didint_pkgs = filter(p -> p.second.name == "DiDInt", Pkg.dependencies()) qui jl: _didint_ok = !isempty(_didint_pkgs) && first(values(_didint_pkgs)).version >= v"0.9.6" qui jl: SF_scal_save("`DiDIntOK'", _didint_ok ? 1.0 : 0.0) if `DiDIntOK' != 1 { di as error "DiDInt.jl v0.9.6 or later is required but not found." di as error "Please install or update DiDInt.jl by running: jl AddPkg DiDInt" exit 44 } qui jl: using DiDInt // This section is to deal with the invalid warnings and to ensure proper conversion of categorical variables local allvars `outcome' `state' `time' if "`covariates'" != "" { local allvars `allvars' `covariates' } if "`gvar'" != "" { local allvars `allvars' `gvar' } preserve keep `allvars' if `process' == 1 { local outlabel : value label `outcome' if "`outlabel'" != "" { quietly label values `outcome' . di as text "Warning: `outcome' has a value label. Label stripped to ensure numeric outcome. Set 'process(0)' to skip this conversion." } foreach v of local covariates { local vallabel : value label `v' if "`vallabel'" != "" { quietly decode `v', gen(`v'_decoded) quietly destring `v'_decoded, gen(`v'_test) ignore(",") quietly count if missing(`v'_test) & !missing(`v') if r(N) == 0 { local converted 1 // Truly numeric - just strip label drop `v'_decoded `v'_test quietly label values `v' . di as text "Warning: `v' has a value label but contains numeric data. Value label stripped, variable passed as numeric. Set 'process(0)' to skip this conversion." } else { // Real categorical - replace with decoded string drop `v' `v'_test rename `v'_decoded `v' di as text "Warning: `v' has a value label and contains non-numeric data. Variable converted from numeric to string. Set 'process(0)' to skip this conversion." } } } qui label drop _all qui notes drop _all } qui jl save df restore // Allow some variables to be passed to Julia qui jl: outcome = Symbol("`outcome'") qui jl: state = Symbol("`state'") qui jl: time = Symbol("`time'") qui jl: nperm = `nperm' qui jl: freq_multiplier = `freq_multiplier' if "`gvar'" != "" { qui jl: gvar = Symbol("`gvar'") } else { qui jl: gvar = nothing } if "`freq'" == "" { qui jl: freq = nothing } else { qui jl: freq = "`freq'" } if "`start_date'" == "" { qui jl: start_date = nothing } else { qui jl: start_date = "`start_date'" } if "`end_date'" == "" { qui jl: end_date = nothing } else { qui jl: end_date = "`end_date'" } // Parse treated_states and treatment_times if "`treated_states'" != "" { qui jl: treated_states = String[] qui jl: treated_times = String[] qui tokenize "`treated_states'" while "`1'" != "" { local token = trim("`1'") if ("`token'" != "") { qui jl: temp = "`token'" qui jl: push!(treated_states, temp) } macro shift } } else { qui jl: treated_states = nothing } if "`treatment_times'" != "" { qui tokenize "`treatment_times'" while "`1'" != "" { local token = trim("`1'") if ("`token'" != "") { qui jl: temp = "`token'" qui jl: push!(treated_times, temp) } macro shift } } else { qui jl: treated_times = nothing } // Parse covariates if necessary if "`covariates'" == ""{ qui jl: covariates = nothing } else { qui jl: covariates = String[] tokenize "`covariates'" while "`1'" != "" { local token = trim("`1'") if ("`token'" != "") { qui jl: temp = "`token'" qui jl: push!(covariates, temp) } macro shift } } if "`ccc'" == "" { local ccc "int" qui jl: ccc = "int" } else { qui jl: ccc = "`ccc'" } if "`agg'" == "" { qui jl: agg = "cohort" } else { qui jl: agg = "`agg'" } if "`weighting'" == "" { local weighting "both" qui jl: weighting = "both" } else { qui jl: weighting = "`weighting'" } // Parse ref_column tokens with trimming if "`ref_column'" != "" { qui jl: ref_keys = String[] tokenize "`ref_column'" while "`1'" != "" { local token = trim("`1'") if ("`token'" != "") { qui jl: temp = "`token'" qui jl: push!(ref_keys, temp) } macro shift } if "`ref_group'" == "" { di as error "If ref_column is specified, then ref_group must be specified as well!" exit 6 } } // Parse ref_group tokens with trimming if "`ref_group'" != "" { qui jl: ref_values = String[] tokenize "`ref_group'" while "`1'" != "" { local token = trim("`1'") if ("`token'" != "") { qui jl: temp = "`token'" qui jl: push!(ref_values, temp) } macro shift } if "`ref_column'" == "" { di as error "If ref_group is specified, then ref_column must be specified as well!" exit 7 } qui jl: ref = Dict(zip(ref_keys, ref_values)) } if "`ref_column'" == "" & "`ref_group'" == "" { qui jl: ref = nothing } // PART TWO: RUN DiDInt.jl and convert some columns to strings jl: results = DiDInt.didint(outcome, state, time, df, gvar = gvar, treated_states = treated_states, treatment_times = treated_times, date_format = date_format, covariates = covariates, ccc = ccc, agg = agg, weighting = weighting, ref = ref, freq = freq, freq_multiplier = freq_multiplier, start_date = start_date, end_date = end_date, nperm = nperm, seed = seed, use_pre_controls = use_pre_controls, hc = hc, truejack = truejack, edgecase = edgecase); qui jl: if "att_cohort" in DataFrames.names(results) /// results.labels = string.(results.treatment_time); /// elseif "att_s" in DataFrames.names(results) /// results.labels = string.(results.state); /// elseif "att_gt" in DataFrames.names(results) /// results.labels = string.(results.gvar, ";", results.time); /// elseif "att_sgt" in DataFrames.names(results) /// results.labels = string.(results.state, ";", results.gvar, ";", results.t); /// elseif "att_t" in DataFrames.names(results) /// results.labels = string.(results.periods_post_treat); /// end // PART THREE: PASS RESULTS TO STATA tempname result_frame qui cap frame drop `result_frame' qui frame create `result_frame' qui frame change `result_frame' qui jl use results qui cap confirm variable labels if !_rc { qui jl: st_local("rowlabels", join(string.(results.labels), " ")) qui tostring labels, replace local counter = 1 foreach rowlabel in `rowlabels' { qui replace labels = "`rowlabel'" in `counter' local counter = `counter' + 1 } } qui ds qui local result_vars `r(varlist)' foreach var in `result_vars' { tempvar tmp_`var' qui gen `tmp_`var'' = `var' qui drop `var' } local condition_met 0 qui capture confirm variable `tmp_att_s' if _rc == 0 & `condition_met' == 0 { local condition_met 1 di as text "-----------------------------------------------------------------------------------------------------" di as text " DiDInt.jl Sub-Aggregate Results " di as text "-----------------------------------------------------------------------------------------------------" di as text "State | " as text "ATT | SE | p-val | JKNIFE SE | JKNIFE p-val | RI p-val" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Initialize a temporary matrix to store the numeric results tempname table_matrix local num_rows = _N local num_cols = 7 matrix `table_matrix' = J(`num_rows', `num_cols', .) local state_names "" forvalues i = 1/`=_N' { di as text %-25s "`=`tmp_labels'[`i']'" as text " |" as result %-16.7f `tmp_att_s'[`i'] as text " | " as result %-7.3f `tmp_se_att_s'[`i'] as text "| " as result %-7.3f `tmp_pval_att_s'[`i'] as text "| " as result %-11.3f `tmp_jknifese_att_s'[`i'] as text "| " as result %-13.3f `tmp_jknifepval_att_s'[`i'] as text "|" as result %-9.3f `tmp_ri_pval_att_s'[`i'] as text "|" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Store the state name local state_name = `tmp_labels'[`i'] local state_names `state_names' `state_name' // Fill the matrix with numeric values matrix `table_matrix'[`i', 1] = `tmp_att_s'[`i'] matrix `table_matrix'[`i', 2] = `tmp_se_att_s'[`i'] matrix `table_matrix'[`i', 3] = `tmp_pval_att_s'[`i'] matrix `table_matrix'[`i', 4] = `tmp_jknifese_att_s'[`i'] matrix `table_matrix'[`i', 5] = `tmp_jknifepval_att_s'[`i'] matrix `table_matrix'[`i', 6] = `tmp_ri_pval_att_s'[`i'] matrix `table_matrix'[`i', 7] = `tmp_weights'[`i'] } // Set column names for the matrix matrix colnames `table_matrix' = ATT SE pval JKNIFE_SE JKNIFE_pval RI_pval W // Set row names for the matrix using the state names matrix rownames `table_matrix' = `state_names' // Store the matrix in r() return matrix didint = `table_matrix' local linesize = c(linesize) if `linesize' < 103 { di as text "Results table may be squished, try expanding Stata results window." } di as text _n "Aggregation Method: " as result "State" } qui capture confirm variable `tmp_att_cohort' if _rc == 0 & `condition_met' == 0 { local condition_met 1 di as text "-----------------------------------------------------------------------------------------------------" di as text " DiDInt.jl Sub-Aggregate Results " di as text "-----------------------------------------------------------------------------------------------------" di as text "Cohort | " as text "ATT | SE | p-val | JKNIFE SE | JKNIFE p-val | RI p-val" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Initialize a temporary matrix to store the numeric results tempname table_matrix local num_rows = _N local num_cols = 7 matrix `table_matrix' = J(`num_rows', `num_cols', .) local cohort_names "" forvalues i = 1/`=_N' { di as text %-25s "`=`tmp_labels'[`i']'" as text " |" as result %-16.7f `tmp_att_cohort'[`i'] as text " | " as result %-7.3f `tmp_se_att_cohort'[`i'] as text "| " as result %-7.3f `tmp_pval_att_cohort'[`i'] as text "| " as result %-11.3f `tmp_jknifese_att_cohort'[`i'] as text "| " as result %-13.3f `tmp_jknifepval_att_cohort'[`i'] as text "|" as result %-9.3f `tmp_ri_pval_att_cohort'[`i'] as text "|" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Store the cohort name local cohort_name = `tmp_labels'[`i'] local cohort_names `cohort_names' `cohort_name' // Fill the matrix with numeric values matrix `table_matrix'[`i', 1] = `tmp_att_cohort'[`i'] matrix `table_matrix'[`i', 2] = `tmp_se_att_cohort'[`i'] matrix `table_matrix'[`i', 3] = `tmp_pval_att_cohort'[`i'] matrix `table_matrix'[`i', 4] = `tmp_jknifese_att_cohort'[`i'] matrix `table_matrix'[`i', 5] = `tmp_jknifepval_att_cohort'[`i'] matrix `table_matrix'[`i', 6] = `tmp_ri_pval_att_cohort'[`i'] matrix `table_matrix'[`i', 7] = `tmp_weights'[`i'] } // Set column names for the matrix matrix colnames `table_matrix' = ATT SE pval JKNIFE_SE JKNIFE_pval RI_pval W // Set row names for the matrix using the state names matrix rownames `table_matrix' = `cohort_names' // Store the matrix in r() return matrix didint = `table_matrix' local linesize = c(linesize) if `linesize' < 103 { di as text "Results table may be squished, try expanding Stata results window." } di as text _n "Aggregation Method: " as result "Cohort" } qui capture confirm variable `tmp_att_sgt' if _rc == 0 & `condition_met' == 0 { local condition_met 1 di as text "-----------------------------------------------------------------------------------------------------" di as text " DiDInt.jl Sub-Aggregate Results " di as text "-----------------------------------------------------------------------------------------------------" di as text "s;g;t | " as text "ATT | SE | p-val | JKNIFE SE | JKNIFE p-val | RI p-val" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Initialize a temporary matrix to store the numeric results tempname table_matrix local num_rows = _N local num_cols = 7 matrix `table_matrix' = J(`num_rows', `num_cols', .) local state_names "" // Create the gt varialbe in julia forvalues i = 1/`=_N' { di as text %-25s "`=`tmp_labels'[`i']'" as text " |" as result %-16.7f `tmp_att_sgt'[`i'] as text " | " as result %-7.3f `tmp_se_att_sgt'[`i'] as text "| " as result %-7.3f `tmp_pval_att_sgt'[`i'] as text "| " as result %-11.3f `tmp_jknifese_att_sgt'[`i'] as text "| " as result %-13.3f `tmp_jknifepval_att_sgt'[`i'] as text "|" as result %-9.3f `tmp_ri_pval_att_sgt'[`i'] as text "|" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Store the gt local sgt_name = `tmp_labels'[`i'] local sgt_names `sgt_names' `sgt_name' // Fill the matrix with numeric values matrix `table_matrix'[`i', 1] = `tmp_att_sgt'[`i'] matrix `table_matrix'[`i', 2] = `tmp_se_att_sgt'[`i'] matrix `table_matrix'[`i', 3] = `tmp_pval_att_sgt'[`i'] matrix `table_matrix'[`i', 4] = `tmp_jknifese_att_sgt'[`i'] matrix `table_matrix'[`i', 5] = `tmp_jknifepval_att_sgt'[`i'] matrix `table_matrix'[`i', 6] = `tmp_ri_pval_att_sgt'[`i'] matrix `table_matrix'[`i', 7] = `tmp_weights'[`i'] } // Set column names for the matrix matrix colnames `table_matrix' = ATT SE pval JKNIFE_SE JKNIFE_pval RI_pval W // Set row names for the matrix using the state names matrix rownames `table_matrix' = `sgt_names' // Store the matrix in r() return matrix didint = `table_matrix' local linesize = c(linesize) if `linesize' < 103 { di as text "Results table may be squished, try expanding Stata results window." } di as text _n "Aggregation Method: " as result "sgt" } qui capture confirm variable `tmp_att_gt' if _rc == 0 & `condition_met' == 0 { local condition_met 1 di as text "-----------------------------------------------------------------------------------------------------" di as text " DiDInt.jl Sub-Aggregate Results " di as text "-----------------------------------------------------------------------------------------------------" di as text "g;t | " as text "ATT | SE | p-val | JKNIFE SE | JKNIFE p-val | RI p-val" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Initialize a temporary matrix to store the numeric results tempname table_matrix local num_rows = _N local num_cols = 7 matrix `table_matrix' = J(`num_rows', `num_cols', .) // Create the gt varialbe in julia forvalues i = 1/`=_N' { di as text %-25s "`=`tmp_labels'[`i']'" as text " |" as result %-16.7f `tmp_att_gt'[`i'] as text " | " as result %-7.3f `tmp_se_att_gt'[`i'] as text "| " as result %-7.3f `tmp_pval_att_gt'[`i'] as text "| " as result %-11.3f `tmp_jknifese_att_gt'[`i'] as text "| " as result %-13.3f `tmp_jknifepval_att_gt'[`i'] as text "|" as result %-9.3f `tmp_ri_pval_att_gt'[`i'] as text "|" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Store the gt local gt_name = `tmp_labels'[`i'] local gt_names `gt_names' `gt_name' // Fill the matrix with numeric values matrix `table_matrix'[`i', 1] = `tmp_att_gt'[`i'] matrix `table_matrix'[`i', 2] = `tmp_se_att_gt'[`i'] matrix `table_matrix'[`i', 3] = `tmp_pval_att_gt'[`i'] matrix `table_matrix'[`i', 4] = `tmp_jknifese_att_gt'[`i'] matrix `table_matrix'[`i', 5] = `tmp_jknifepval_att_gt'[`i'] matrix `table_matrix'[`i', 6] = `tmp_ri_pval_att_gt'[`i'] matrix `table_matrix'[`i', 7] = `tmp_weights'[`i'] } // Set column names for the matrix matrix colnames `table_matrix' = ATT SE pval JKNIFE_SE JKNIFE_pval RI_pval W // Set row names for the matrix using the state names matrix rownames `table_matrix' = `gt_names' // Store the matrix in r() return matrix didint = `table_matrix' local linesize = c(linesize) if `linesize' < 103 { di as text "Results table may be squished, try expanding Stata results window." } di as text _n "Aggregation Method: " as result "Simple" } qui capture confirm variable `tmp_att_t' if _rc == 0 & `condition_met' == 0 { local condition_met 1 di as text "-----------------------------------------------------------------------------------------------------" di as text " DiDInt.jl Sub-Aggregate Results " di as text "-----------------------------------------------------------------------------------------------------" di as text "Periods Since Treatment | " as text "ATT | SE | p-val | JKNIFE SE | JKNIFE p-val | RI p-val" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Initialize a temporary matrix to store the numeric results tempname table_matrix local num_rows = _N local num_cols = 7 matrix `table_matrix' = J(`num_rows', `num_cols', .) local time_names "" forvalues i = 1/`=_N' { di as text %-25s "`=`tmp_labels'[`i']'" as text " |" as result %-16.7f `tmp_att_t'[`i'] as text " | " as result %-7.3f `tmp_se_att_t'[`i'] as text "| " as result %-7.3f `tmp_pval_att_t'[`i'] as text "| " as result %-11.3f `tmp_jknifese_att_t'[`i'] as text "| " as result %-13.3f `tmp_jknifepval_att_t'[`i'] as text "|" as result %-9.3f `tmp_ri_pval_att_t'[`i'] as text "|" di as text "--------------------------|-----------------|--------|--------|------------|--------------|---------|" // Store the state name local time_name = `tmp_labels'[`i'] local time_names `time_names' `time_name' // Fill the matrix with numeric values matrix `table_matrix'[`i', 1] = `tmp_att_t'[`i'] matrix `table_matrix'[`i', 2] = `tmp_se_att_t'[`i'] matrix `table_matrix'[`i', 3] = `tmp_pval_att_t'[`i'] matrix `table_matrix'[`i', 4] = `tmp_jknifese_att_t'[`i'] matrix `table_matrix'[`i', 5] = `tmp_jknifepval_att_t'[`i'] matrix `table_matrix'[`i', 6] = `tmp_ri_pval_att_t'[`i'] matrix `table_matrix'[`i', 7] = `tmp_weights'[`i'] } // Set column names for the matrix matrix colnames `table_matrix' = ATT SE pval JKNIFE_SE JKNIFE_pval RI_pval W // Set row names for the matrix using the state names matrix rownames `table_matrix' = `state_names' // Store the matrix in r() return matrix didint = `table_matrix' local linesize = c(linesize) if `linesize' < 103 { di as text "Results table may be squished, try expanding Stata results window." } di as text _n "Aggregation Method: " as result "Periods Since Treatment" } if "`ccc'" == "int" { local model_spec "Two-way DID-INT" } else if "`ccc'" == "state" { local model_spec "State-varying DID-INT" } else if "`ccc'" == "time" { local model_spec "Time-varying DID-INT" } else if "`ccc'" == "hom" { local model_spec "Homogeneous DID-INT" } else if "`ccc'" == "add" { local model_spec "Two one-way DID-INT" } di as text "Model Specification: " as result "`model_spec'" di as text "Weighting: " as result "`weighting'" // Display aggregate results di as text _n "---------------------------------" di as text " DiDInt.jl: Aggregate Results " di as text "---------------------------------" di as text "Aggregate ATT: " as result `tmp_agg_att'[1] di as text "Standard error: " as result `tmp_se_agg_att'[1] di as text "p-value: " as result `tmp_pval_agg_att'[1] di as text "Jackknife SE: " as result `tmp_jknifese_agg_att'[1] di as text "Jackknife p-value: " as result `tmp_jknifepval_agg_att'[1] di as text "RI p-value: " as result `tmp_ri_pval_agg_att'[1] di as text "Random permutations: " as result `tmp_nperm'[1] // Store aggregate results in r() return scalar att = `tmp_agg_att'[1] return scalar se = `tmp_se_agg_att'[1] return scalar p = `tmp_pval_agg_att'[1] return scalar jkse = `tmp_jknifese_agg_att'[1] return scalar jkp = `tmp_jknifepval_agg_att'[1] return scalar rip = `tmp_ri_pval_agg_att'[1] return scalar nperm = `tmp_nperm'[1] qui drop _all qui frame change default qui frame drop `result_frame' qui jl: results = nothing; GC.gc() end /*--------------------------------------*/ /* Change Log */ /*--------------------------------------*/ *0.7.8 - suggest user have DiDInt.jl version v0.9.6 or later (as opposed to only 0.9.5) *0.7.7 - No longer automatically download DiDInt, instead suggest user download explicitly via 'jl AddPkg DiDInt' *0.7.6 - Added arg for edgecase, made sure value labels are dropped for outcome *0.7.5 - Added better processing for labelled variables *0.7.4 - better error messaging *0.7.3 - added arg for truejack *0.7.2 - added hc arg and changed nperm to 999 *0.7.1 - run didint() from Julia with ; ending, shows error messages, but suppresses other displays. Clear results from Julia memory after running *0.7.0 - updated output display, changed return matrix name from restab to didint *0.6.1 - forgot a qui smh *0.6.0 - changed syntax to accept varnames, added gvar option, overall more in line with csdid and Stata norms *0.5.3 - changed the way that the results row labels are passed to Stata from Julia to try and work around a Stata-Julia interface bug *0.5.2 - fixed assignment issue with start_date / end_date *0.5.1 - changed use_pre_controls default to false *0.5.0 - added start_date and end_date args and removed autoadjust to conincide with new version of DiDInt.jl package *0.4.1 - added weighting arg *0.4.0 - added sgt agg option, RI_pvals for sub-aggregate level, and seed arg *0.3.0 - changed to rclass and added displays for outputs *0.2.1 - removed 'stata_debug' arg, hopefully not needed anymore *0.2.0 - fixed 'freq' arg - function actually works now for common + staggered adoption *0.1.2 - added 'stata_debug' arg and trim whitespce for tokenized args *0.1.1 - added 'agg' arg *0.1.0 - created function