didintjl

didintjl

Estimates the average treatment effect on the treated (ATT) while accounting for covariates that may vary by state, time, or both. That is, estimates ATT while accounting for different violations of the common causal covariates assumption.
Based on Karim & Webb (2025) https://arxiv.org/abs/2412.14447.

Command Description

didintjl is a Stata command that serves as a wrapper for the didint() function from the DiDInt.jl Julia package.
It requires you to specify the names of the outcome, state, and time variables as well as either a gvar variable, or, to specifically list the treated states and their treatment times.

Requirements

didintjl and didintjl_plot are tested and designed for the following versions:

Requirement

Julia version 1.12.0 or later

Stata version 16 or later

julia.ado version 2.0.0 or later (https://github.com/droodman/julia.ado)

DiDInt.jl version 0.9.6 or later (https://github.com/ebjamieson97/DiDInt.jl)

To install julia.ado, run:

. ssc install julia

To install DiDInt.jl, run:

. jl AddPkg DiDInt

Stored results

didintjl stores the following in r():

Scalars

r(att) aggregate ATT estimate

r(se) standard error of aggregate ATT

r(p) p-value from two-sided t-test

r(jkse) jackknife standard error

r(jkp) p-value using jackknife standard error

r(rip) p-value from randomization inference

r(nperm) number of randomizations done

Matrices

r(didint) sub-aggregate ATT results table

Syntax

didintjl
,
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(integer)
start_date(string)
end_date(string)
nperm(integer)
seed(integer)
truejack(integer)
notyet(integer)
use_pre_controls(integer)
hc(integer)
edgecase(integer)
process(integer)]

Parameters

Required
outcome(varname) name of outcome variable

state(varname) name of state identifier variable

time(varname) name of time variable

Treatment Specification
gvar(varname) variable indicating time of first treatment for each state

treated_states(string) list of treated states (use with treatment_times)

treatment_times(string) list of treatment times (use with treated_states)

Date Options
date_format(string) format of time variable, start_date, end_date, and treatment_times

Period Grid Construction (Staggered Adoption Only)
freq(string) time period frequency: "year", "month", "week", or "day"; triggers date matching procedure

freq_multiplier(integer) multiplier for freq (default: 1); e.g., 2 for two-year periods. Has no effect unless used with freq.

start_date(string) starting date for the period grid construction

end_date(string) ending date for the period grid construction

Covariates
covariates(varnames or string) space-separated list of covariate names

ref_column(string) column name for categorical reference variable

ref_group(string) reference group for categorical variable (requires ref_column)

notyet(integer) shorthand for use_pre_controls(); use pre-treatment periods from treated states as controls (0 is false, 1 is true). Overwrites use_pre_controls() if both are specified.

use_pre_controls(integer) use pre-treatment periods from treated states as controls (0 is false, 1 is true, default: 0)

CCC, Aggregation, and Weighting
ccc(string) CCC violation to control for: "hom", "time", "state", "add", or "int" (default: "int")

agg(string) aggregation method

weighting(string) weighting scheme

Inference
nperm(integer) number of permutations for randomization inference (default: 999)

seed(integer) random seed for replication

truejack(integer) jackknife method: 1 = re-estimate from first step, 0 = use diff matrix (default: 0)

hc(integer) heteroskedasticity-consistent standard error type (default: 1)

edgecase(integer) when enabled, computes standard errors for ATTs in the edge case where only two states contribute to a long difference regression, using variance and covariance terms of the relevant means. Computationally expensive; disabled by default (0 = false, 1 = true, default: 0)

Data Processing
process(integer) automatic processing of labeled covariate variables before passing to Julia (0 = off, 1 = on, default: 1).
When enabled, any covariate with a value label attached is decoded and then tested for numeric conversion.
If all decoded values are numeric, the value label is stripped and the variable is passed as numeric.
If the decoded values contain non-numeric characters (e.g. occupation or race category names), the variable is converted from a labeled numeric to a string and passed as a string categorical.
A warning message is displayed for each variable that is converted. Set process(0) to skip this conversion and pass all variables as-is. By default, any labelled variables are passed
from Stata to Julia as categorical variables, however this may not always be desired (e.g. the case of a labelled numeric variable). This argument attempts to rectify this potential issue.

Examples

For more examples, including an example dataset and do-file, please visit the GitHub repository:

https://github.com/ebjamieson97/didintjl

Basic example using Merit scholarship data:

. use "MeritExampleDataDiDIntjl.dta", clear

. didintjl, outcome("coll") state("state") time("year") ///

treated_states("34 57 58 59 61 64 71 72 85 88") ///

treatment_times("2000 1998 1993 1997 1999 1996 1991 1998 1997 2000") ///

date_format("yyyy") covariates("asian male black") ccc("int")

This example estimates the effect of state merit scholarship programs on college enrollment,
accounting for demographic covariates that vary by state and time (two-way intersection).

Alternative syntax using gvar():

It is also possible to generate a gvar column and use syntax similar to csdid.
Note that the variable merit is 1 for treated observations and 0 for non-treated observations:

. gen year_numeric = real(year)

. bysort state (year_numeric): egen gvar = min(cond(merit == 1, year_numeric, .))

. replace gvar = 0 if missing(gvar) // Optional: leave non-treated states as missing

. didintjl, outcome(coll) state(state) time(year_numeric) gvar(gvar) ///

covariates(asian male black) seed(1234)

CCC Violations and Model Specifications

The ccc() option controls how didintjl accounts for potential violations of the
Common Causal Covariates (CCC) assumption. Different violations require different model
specifications. The five options are:

CCC Option

hom Homogeneous specification. Use when the CCC assumption seems plausible,
meaning covariate effects on the outcome are constant across all states and time periods.

state State-varying specification. Use when covariate effects vary by state but
are constant over time.

time Time-varying specification. Use when covariate effects vary over time but
are constant across states.

add Two one-way specification. Models covariate effects separately for each state and each time period.

int Two-way specification (default). Models covariate effects uniquely for each state-time combination.

For guidance on model selection, see Karim and Webb (2025) https://arxiv.org/abs/2412.14447 (perhaps especially sections 8 & 9).
It is also highly recommended to use the didintjl_plot command prior to running didintjl in order to help determine which model specification should be used.

Aggregation Methods

The agg() option determines which sub-aggregate ATTs are calculated and consequently affects the
calculation of the aggregate ATT. All options except "none" compute sub-aggregate ATTs,
which are then combined into an aggregate ATT as a weighted mean. All options are available
under staggered adoption, while only "none" and "state" are available for common adoption.

Aggregation Option

cohort Aggregates ATTs by treatment time (cohort), computing sub-aggregate ATTs
for each group based on when treatment begins.

state Aggregates ATTs by treated state, computing sub-aggregate ATTs for each state.

simple Aggregates ATTs by (g,t) group, where g is the first treatment time and t
is any period from g to the end of the data. Computes sub-aggregate ATTs for each treated cohort
in each post-treatment period.

sgt Aggregates ATTs by (s,g,t) combinations, where simple aggregation is further
divided by state. Computes sub-aggregate ATTs for each state-cohort-period combination.

time Aggregates ATTs by periods since treatment (event time), computing sub-aggregate
ATTs based on time elapsed since treatment began.

none No aggregation. Computes a single aggregate ATT without sub-aggregates.

Computing Sub-Aggregate ATTs

Sub-aggregate ATTs are calculated using a two-step process:

1. Period-to-period contrasts are computed for all states. Each contrast represents the difference
in mean outcomes (or covariate-adjusted mean outcomes) between two time periods.

2. These contrasts are restricted to only those belonging to the sub-aggregate group, then regressed
on an intercept and a binary indicator for whether the contrast came from a treated state. The
resulting coefficient on the binary indicator is the sub-aggregate ATT. This is repeated for each sub-aggregate group.

Example with cohort aggregation: Consider a staggered adoption scenario with treatment
times in 1991 and 1993, where data spans 1989-1994 with yearly periods. The sub-aggregate ATT
for the 1993 cohort uses two contrasts:

- Mean outcome in 1993 minus mean outcome in 1992

- Mean outcome in 1994 minus mean outcome in 1992

This regression includes contrasts from both the 1993 cohort (treated) and all control states
for these same contrasts. The coefficient on the treatment indicator gives the sub-aggregate
ATT for the 1993 cohort.

Example with state aggregation: Using agg("state") in the same scenario performs
a nearly identical calculation, except only contrasts from the specific state being analyzed
(rather than all states in the 1993 cohort) are included in the treated group.

For additional aggregation methods ("simple", "sgt", "time"), the same
principle applies: contrasts are restricted to the relevant sub-aggregate grouping before
computing the ATT estimate. Setting "none" skips the first step, and instead simply runs
the regression of contrasts on an intercept and binary indicator directly, without subsetting the data.

Weighting Options

The weighting() option controls how observations are weighted when computing sub-aggregate
and aggregate ATTs.

Weighting Option

none No weighting. All contrasts receive equal weight when computing
sub-aggregate ATTs, and all sub-aggregate ATTs receive equal weight when computing the
aggregate ATT.

diff Difference weighting. Applies weights when computing sub-aggregate ATTs.
Each contrast is weighted by the total number of observations used to compute it (i.e., all
observations used to calculate both means in the contrast). Contrasts based on more observations
receive greater weight.

att ATT weighting. Applies weights when computing the aggregate ATT from sub-aggregate
ATTs. Each sub-aggregate ATT is weighted by the total number of treated observations included
in its calculation (i.e., all treated observations across all contrasts used for that sub-aggregate).
Sub-aggregates with more treated observations receive greater weight.

both Combined weighting (default). Applies both diff weighting (when computing sub-aggregate
ATTs) and att weighting (when computing the aggregate ATT).

Date Matching Procedure for Staggered Adoption Settings

If freq is not specified in staggered adoption settings, every unique date found in
the data is treated as a distinct period. When freq is specified in staggered adoption settings,
didintjl performs a date matching procedure to align observations and treatment times to period boundaries:

1. Period grid construction: A sequence of period start dates is created from start_date
to end_date using the specified freq and freq_multiplier. By default,
start_date and end_date are set to the minimum and maximum dates in the data
unless explicitly specified by the user. If either start_date or end_date are specified,
then date_format will have to be specified as well.

2. Observation matching: Each observation's time value is matched to the most recently passed
period start date, with special handling for observations near treatment times. If a treatment
occurs between two period boundaries, observations before the treatment are matched to the
earlier period, and observations after are matched to the later period.

3. Treatment time matching: Treatment times are matched to the first period start date that
is greater than or equal to each treatment time. That is, if a treatment occurs between two period
boundaries (as opposed to directly at a period boundary), it is matched forward to the next period boundary.

Edge cases: Observations occurring before the first period boundary are matched to the
first period. Observations occurring at or after the last period boundary are matched to the
last period.

Example: Consider yearly data from 1989-2000 with seven treatment cohorts (1991, 1993,
1996, 1997, 1998, 1999, 2000). Specifying freq("year") and freq_multiplier(2)
creates a 2-year period grid: [1989, 1991, 1993, 1995, 1997, 1999]. Treatment times are
matched forward to period boundaries: 1991→1991, 1993→1993, 1996→1997, 1998→1999,
1999→1999, 2000→1999 (capped at the last period). This consolidates the seven cohorts
into four (1991, 1993, 1997, 1999). For observations, when no treatment occurs between
period boundaries, all observations in that interval are matched to the earlier boundary
(e.g., observations in 1993 and 1994 both match to period 1993). However, the 1996 treatment
splits the [1995, 1997) interval: observations in 1995 match to period 1995, while observations
in 1996 match forward to period 1997 (along with the treatment time at 1996).
Observations in 2000 are capped at the final period 1999.

Note: The date matching procedure is only relevant for staggered adoption designs.
In common adoption settings (where all treated units adopt at the same time), observations
are simply classified as "pre" or "post" treatment, making date matching unnecessary.

Randomization Inference Procedure

didintjl implements a randomization inference procedure following MacKinnon and Webb (2020)
to compute p-values. See the paper here: https://www.sciencedirect.com/science/article/abs/pii/S0304407620301445

The procedure works as follows:

1. Treatment times are randomly reassigned among states nperm times, ensuring each
randomization produces a unique assignment of treatment times to states.

2. For each randomization, sub-aggregate and aggregate ATTs are recalculated according
to the randomized treatment times assignment.

3. The distribution of ATTs calculated from these randomizations is compared against the actual
ATT estimates from the true treatment assignment in order to compute p-values.

Randomization inference p-values are returned in r(rip) for the aggregate ATT and
can be seen at the sub-aggregate ATT level with matrix list r(didint).
Set seed() for reproducible results across runs with the same data.

Date Format Compatibility

didintjl is compatible with a variety of date formats. The time() variable can be
a string in any of the following formats, as long as date_format() is specified appropriately:

Format

"yyyy/mm/dd" Example: 1997/08/25

"yyyy-mm-dd" Example: 1997-08-25

"yyyymmdd" Example: 19970825

"yyyy/dd/mm" Example: 1997/25/08

"yyyy-dd-mm" Example: 1997-25-08

"yyyyddmm" Example: 19972508

"dd/mm/yyyy" Example: 25/08/1997

"dd-mm-yyyy" Example: 25-08-1997

"ddmmyyyy" Example: 25081997

"mm/dd/yyyy" Example: 08/25/1997

"mm-dd-yyyy" Example: 08-25-1997

"mmddyyyy" Example: 08251997

"mm/yyyy" Example: 08/1997

"mm-yyyy" Example: 08-1997

"mmyyyy" Example: 081997

"yyyy" Example: 1997

"ddmonyyyy" Example: 25aug1997

"yyyym00" Example: 1997m8

Ensure that the format specified in date_format() exactly matches how dates appear
in your time() variable. Note that if the time() variable is simply a 4-digit year,
the date_format() argument does not need to be specified.

Important: The time() variable must be of the same type as either the treatment_times()
values or the gvar() variable depending on which argument is specified. This means:

- If using treatment_times(), the time() variable and dates specified in treatment_times()
must both be identically formatted strings matching one of the compatible date formats above

- If using gvar(), both time() and gvar() should either both be numeric variables or both be string variables of the same format

Further Reading

For a deeper dive into the backend implementation, see the DiDInt.jl package documentation:

https://ebjamieson97.github.io/DiDInt.jl/stable/details/

Package Author

Eric Jamieson. Tell me about all the bugs in my code: ericbrucejamieson@gmail.com or at https://github.com/ebjamieson97/didintjl.

Citations

If you use didintjl in your research, please cite:

Sunny Karim and Matthew D. Webb. "Good Controls Gone Bad: Difference-in-Differences with Covariates."
https://arxiv.org/abs/2412.14447

If you reference the randomization inference p-values in your work, please also cite:

MacKinnon, James G., and Matthew D. Webb. "Randomization Inference for Difference-in-Differences
with Few Treated Clusters." https://www.sciencedirect.com/science/article/abs/pii/S0304407620301445