Working with results

Everything csdid computes is available programmatically, as tidy data for tables, as matrices for your own calculations, and as plot-ready data (so that the graph stays yours).

The data

Let’s build the sample once and estimate once (we re-use that run throughout the page), since everything below reads the same set of results.

import delimited using ///
    "https://raw.githubusercontent.com/pedrohcgs/JEL-DiD/50f4f18/data/county_mortality_data.csv", ///
    clear varnames(1) bindquote(strict) stringcols(_all)
destring deaths population_20_64 year yaca county_code stfips unemp_rate poverty_rate, ///
    replace force
generate double mrate = 100000 * deaths / population_20_64
drop if missing(mrate) | population_20_64 <= 0
generate int gvar = yaca
replace gvar = 0 if missing(gvar) | gvar > 2016
bysort county_code: generate byte nyears = _N
keep if nyears == 11
save "jel_results.dta", replace

A run to work with

use "jel_results.dta", clear
csdid mrate, ivar(county_code) time(year) gvar(gvar) cluster(stfips) rseed(20250101)

Results as a dataset

Add saving() to any estat subcommand and it writes what it computed to a dataset instead of only printing it. saving() is the same option that margins, simulate and graph take, so there is no separate export command to learn. We use it for anything that has to end up in a paper, on the grounds that a dataset is easier to check, and easier to re-run, than a log file.

estat attgt, saving() gives one row per ATT(g,t) cell. Each row carries the estimate, its standard error, the test statistic, the p-value, and both the reported and the pointwise confidence limits (everything the printed table shows, and a little more besides):

estat attgt, saving("attgt_cells.dta") replace
preserve
use "attgt_cells.dta", clear
describe
list in 1/5
restore
capture erase "attgt_cells.dta"

Every aggregation exports itself in the same way. The file then holds that aggregation rather than the underlying cells:

estat event, saving("eventstudy.dta") replace
preserve
use "eventstudy.dta", clear
list in 1/5
restore
capture erase "eventstudy.dta"
estat simple, saving("overall.dta") replace
preserve
use "overall.dta", clear
list
restore
capture erase "overall.dta"

Because saving() sits on the subcommand, the options that shape a result go there too. The file then matches the table you have just read, so that estat event, window(-3 3) saving(w.dta) saves the windowed event study and not the full one.

Stored results

The full set is in e():

use "jel_results.dta", clear
quietly csdid mrate, ivar(county_code) time(year) gvar(gvar) cluster(stfips) analytical
display "cells        : " e(N_attgt)
display "units        : " e(N_units)
display "cohorts      : " e(N_groups)
display "periods      : " e(N_time)
display "method       : " e(method)
display "comparison group: " e(control_group)
display "base period  : " e(base_period)
display "panel mode   : " e(panel_mode)
display "clusters     : " e(N_clusters)
display "pre-test W   : " e(wald_stat) "  p = " e(wald_pvalue)

e(attgt) is the estimate matrix, holding cohort, period, the estimate and its standard error (in that order):

matrix A = e(attgt)
matrix list A

Influence functions

The influence function has one column per ATT(g,t) cell and one row per unit. The standard errors, the uniform bands and the clustered inference are all built from it. It stays internal by default because it is a large object, so you ask for it with storeall when you want to do your own inference:

use "jel_results.dta", clear
quietly csdid mrate, ivar(county_code) time(year) gvar(gvar) analytical storeall
matrix IF = e(inffunc)
display "influence function: " rowsof(IF) " units x " colsof(IF) " cells"
mata: printf("columns are mean-zero to %g\n", max(abs(mean(st_matrix("IF")))))

For a version that survives the session, and that feeds csdid_stats using for later aggregation, write it to a dataset instead with saverif().

Each column is mean-zero by construction, which is what the printed maximum above confirms. Note that the option changes what is stored and leaves what is estimated alone, so the numbers you report are the same either way. With the influence function in hand you can compute standard errors for aggregations that csdid does not provide, or feed a sensitivity analysis of your own (we do exactly this when a referee asks for a weighting we have not implemented).

Plot-ready data

csdid_plot exports what a graph needs instead of drawing one, so the styling stays under your control.

use "jel_results.dta", clear
quietly csdid mrate, ivar(county_code) time(year) gvar(gvar) rseed(20250101)
csdid_plot, saving("eventdata.dta") replace

preserve
use "eventdata.dta", clear
list in 1/5
twoway (rcap ci_high ci_low x) (scatter estimate x), ///
    yline(0) xtitle("Years since expansion") ///
    ytitle("Effect on mortality per 100,000") ///
    title("Event study") name(es, replace)
restore
capture erase "eventdata.dta"

The exported columns are x (the value on the horizontal axis), estimate, the bounds ci_low and ci_high, plus group, time, event_time, series (Pre/Post), x_label and significant.

Note that the estimate column is estimate rather than att, since csdid_plot renames it on export. Because the bounds come from the same run as the estimates, a simultaneous band stays simultaneous. Build the interval yourself out of a standard error and it silently becomes a pointwise one. See Inference.

Replaying without re-estimating

Aggregations are computed from the stored influence function, so asking for a different one does not re-estimate anything:

use "jel_results.dta", clear
quietly csdid mrate, ivar(county_code) time(year) gvar(gvar) rseed(20250101)
estat event
estat group
estat calendar
estat simple

Nothing is re-estimated. That is four aggregations from one estimation, and the bootstrap ran once.

capture erase "jel_results.dta"