Examples

The examples below use the merit.csv dataset included in the package's test suite (test/data/merit.csv). It contains individual-level survey data on college attendance (coll), state identifiers (state), survey year (year), and demographic covariates (male, asian, black). The dataset covers U.S. states over the period 1989–2000 and is used to study the effect of merit scholarships on college attendance.

using DiDInt, CSV, DataFrames

merit = CSV.read(joinpath(@__DIR__, "../../test/data/merit.csv"), DataFrame);
first(merit, 5)
5×16 DataFrame
Rowcollmeritmaleblackasianyearstatechststyretaalphastchstchidindstnchidstid
Float64Float64Float64Float64Float64Int64Int64Float64Float64Float64Float64Float64Float64?Float64Float64?Float64
10.00.01.00.00.01993110.012993.0-0.3837480.03374650.0missing52.011.01.0
21.00.01.00.00.01997110.012997.00.6272880.03374650.0missing52.011.01.0
31.00.00.00.00.01992110.012992.00.5470320.03374650.0missing52.011.01.0
40.00.01.00.00.01989110.012989.0-0.3630450.03374650.0missing52.011.01.0
50.00.01.00.00.01991110.012991.0-0.3721180.03374650.0missing52.011.01.0

Staggered Adoption

This example illustrates a staggered adoption setting in which ten states adopted merit-aid programmes at different points between 1991 and 2000.

Running didint()

# Note that treated_states & state column in the merit data could alternatively
# be string vectors, here they just happen to be numeric codes instead of state names
treated_states = [34, 57, 58, 59, 61, 64, 71, 72, 85, 88];
treated_times  = [2000, 1998, 1993, 1997, 1999, 1996, 1991, 1998, 1997, 2000];

result = DiDInt.didint(
    "coll", "state", "year", merit;
    treatment_times = treated_times,
    treated_states  = treated_states,
    seed       = 1234,
    ccc        = "state",
    covariates = [:male, :asian, :black],
    agg        = "cohort",
    nperm      = 399
);
7×18 DataFrame
Rowtreatment_timeatt_cohortagg_attse_agg_attpval_agg_attjknifese_agg_attjknifepval_agg_attri_pval_agg_attnpermse_att_cohortpval_att_cohortjknifese_att_cohortjknifepval_att_cohortri_pval_att_cohortweightsperiodstart_dateend_date
DateFloat64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64?Float64StringStringString
11991-01-010.05410820.04581720.007627220.0009585930.01245650.0005744880.100251399.00.02153040.0123425missingmissing0.4385960.2017961 year19892000
21993-01-010.0501653missingmissingmissingmissingmissingmissingmissing0.01778720.00508455missingmissing0.5037590.1915351 year19892000
31996-01-010.0377853missingmissingmissingmissingmissingmissingmissing0.02363150.111352missingmissing0.6065160.07567341 year19892000
41997-01-010.060813missingmissingmissingmissingmissingmissingmissing0.02209190.006552220.03281130.06972730.2706770.3210771 year19892000
51998-01-010.0393736missingmissingmissingmissingmissingmissingmissing0.03865380.3103190.08661080.6513620.5614040.1085931 year19892000
61999-01-010.00217049missingmissingmissingmissingmissingmissingmissing0.01252150.862811missingmissing0.9924810.03548531 year19892000
72000-01-01-0.0219891missingmissingmissingmissingmissingmissingmissing0.03195590.4952610.09526930.8184050.8220550.06584011 year19892000

Inspect the aggregate-level ATT and inference columns:

result[:, 3:9]
7×7 DataFrame
Rowagg_attse_agg_attpval_agg_attjknifese_agg_attjknifepval_agg_attri_pval_agg_attnperm
Float64?Float64?Float64?Float64?Float64?Float64?Float64?
10.04581720.007627220.0009585930.01245650.0005744880.100251399.0
2missingmissingmissingmissingmissingmissingmissing
3missingmissingmissingmissingmissingmissingmissing
4missingmissingmissingmissingmissingmissingmissing
5missingmissingmissingmissingmissingmissingmissing
6missingmissingmissingmissingmissingmissingmissing
7missingmissingmissingmissingmissingmissingmissing

And the cohort-level ATT and inference columns:

result[:, vcat([1, 2], 10:ncol(result))]
7×11 DataFrame
Rowtreatment_timeatt_cohortse_att_cohortpval_att_cohortjknifese_att_cohortjknifepval_att_cohortri_pval_att_cohortweightsperiodstart_dateend_date
DateFloat64?Float64?Float64?Float64?Float64?Float64?Float64StringStringString
11991-01-010.05410820.02153040.0123425missingmissing0.4385960.2017961 year19892000
21993-01-010.05016530.01778720.00508455missingmissing0.5037590.1915351 year19892000
31996-01-010.03778530.02363150.111352missingmissing0.6065160.07567341 year19892000
41997-01-010.0608130.02209190.006552220.03281130.06972730.2706770.3210771 year19892000
51998-01-010.03937360.03865380.3103190.08661080.6513620.5614040.1085931 year19892000
61999-01-010.002170490.01252150.862811missingmissing0.9924810.03548531 year19892000
72000-01-01-0.02198910.03195590.4952610.09526930.8184050.8220550.06584011 year19892000

Running didint_plot()

didint_plot() can produce data for two types of plots depending on the event argument.

By default, the outputted dataset will include the parallel trends (or event study) data for each ccc option ("hom", "time", "state", "add", "int") as well as the data estimated without covariates ("none"). You can specify which ccc options you want to plot by inputting a vector of strings to the ccc argument.

Setting event = false returns a long-format DataFrame of residualised outcome means by state and period, suitable for plotting parallel trends across CCC specifications.

plot_parallel = DiDInt.didint_plot(
    "coll", "state", "year", merit;
    treatment_times = treated_times,
    treated_states  = treated_states,
    event      = false,
    covariates = ["asian", "black", "male"]
);

plot_parallel[1:4, :]
4×8 DataFrame
Rowstatetimelambdacccperiodstart_datetreat_periodperiod_length
String?String?Float64?String?Int64?String?Int64?String
11119890.467895hom01989missing1 year
21119900.316912hom11989missing1 year
31119910.445936hom21989missing1 year
41119920.579997hom31989missing1 year

Event study plot data

Setting event = true returns a DataFrame of treated-state means by period relative to treatment, along with standard errors and confidence interval bounds, suitable for plotting an event study.

plot_event = DiDInt.didint_plot(
    "coll", "state", "year", merit;
    treatment_times = treated_times,
    treated_states  = treated_states,
    event      = true,
    covariates = ["asian", "black", "male"]
);

plot_event[1:4, :]
4×8 DataFrame
Rowccctime_since_treatmentyseci_lowerci_upperngroupperiod_length
StringInt64Float64Float64?Float64?Float64?Int64String
1hom-110.5282920.0964386-0.6970771.7536621 year
2hom-100.4476750.05548660.2089360.68641531 year
3hom-90.4708060.02235960.4087250.53288651 year
4hom-80.4529720.02489070.3920670.51387871 year

The lambda column in the parallel trends output contains the residualised outcome for each state-period cell. The ccc column indicates which model was used ("hom", "time", "state", "add", or "int"). In the event study output, time_since_treatment = 0 marks the treatment period; negative values are pre-treatment and positive values are post-treatment.


Common Adoption

This example illustrates a common adoption setting: a single treated state (state "71", treated in 1991) compared against a single untreated control state (state "73"). The data are filtered to just these two states before calling didint().

merit_common = filter(row -> row.state ∈ [71, 73], merit);

result_common = DiDInt.didint(
    "coll", "state", "year", merit_common;
    treated_states  = [71],
    treatment_times = [1991],
    seed       = 1234,
    ccc        = "hom",
    covariates = [:male, :asian, :black],
    agg        = "cohort",
    edgecase = true
)
# Note here that I am setting `edgecase = true` in order to recover the standard error
# For more information, see the section "Computation of Edge Case Standard Errors" on the
# "Details" page of the docoumentation site
1×7 DataFrame
Rowagg_attse_agg_attpval_agg_attjknifese_agg_attjknifepval_agg_attri_pval_agg_attnperm
Float64?Float64?Float64?Float64?Float64?Float64?Float64?
1-0.01405350.0721721missingmissingmissing1.01.0

Trying a different CCC assumption

The ccc argument controls the assumption made about how covariate effects vary across states and time. Swapping ccc = "hom" for any of "time", "state", "add", or "int" changes whether covariate effects are allowed to vary by time, by state, additively by both, or interactively by both, respectively. For example:

result_common_int = DiDInt.didint(
    "coll", "state", "year", merit_common;
    treated_states  = [71],
    treatment_times = [1991],
    seed       = 1234,
    ccc        = "int",
    covariates = [:male, :asian, :black],
    agg        = "cohort",
    edgecase = true
)
1×7 DataFrame
Rowagg_attse_agg_attpval_agg_attjknifese_agg_attjknifepval_agg_attri_pval_agg_attnperm
Float64?Float64?Float64?Float64?Float64?Float64?Float64?
10.02733220.113821missingmissingmissing0.01.0

See the Functions for function syntax and the Under the Hood page for more details.