Continuous Difference-in-Differences#

Continuous DiD estimator for dose-response curves with continuous treatment intensity.

This module implements the methodology from Callaway, Goodman-Bacon & Sant’Anna (2024), “Difference-in-Differences with a Continuous Treatment” (NBER WP 32117), which:

  1. Estimates dose-response curves: ATT(d) and ACRT(d) as functions of dose

  2. Computes summary parameters: Overall ATT (binarized) and ACRT aggregated across doses

  3. Uses B-spline smoothing: Flexible nonparametric estimation of dose-response functions

  4. Supports multiplier bootstrap: Valid inference with proper standard errors and CIs

Note

Identification assumptions. The dose-response curves ATT(d) and ACRT(d), as well as ATTglob and ACRTglob, require the Strong Parallel Trends (SPT) assumption — that there is no selection into dose groups based on treatment effects. Under the weaker standard Parallel Trends (PT) assumption, only the binarized ATTloc (overall_att) is identified; it equals ATTglob only when SPT holds. See Callaway, Goodman-Bacon & Sant’Anna (2024), Assumptions 1–2.

When to use Continuous DiD:

  • Treatment varies in intensity or dose across units (not just binary on/off)

  • You want to estimate how effects change with treatment dose

  • Staggered adoption with heterogeneous dose levels

  • You need the full dose-response curve, not just a single average effect

Data requirements:

  • An untreated group (D = 0) must be present in the data

  • A balanced panel is required (all units observed in all time periods)

  • Dose must be time-invariant — each unit’s dose is fixed across periods

Reference: Callaway, B., Goodman-Bacon, A., & Sant’Anna, P. H. C. (2024). Difference-in-Differences with a Continuous Treatment. NBER Working Paper 32117. https://www.nber.org/papers/w32117

ContinuousDiD#

Main estimator class for Continuous Difference-in-Differences.

class diff_diff.ContinuousDiD[source]

Bases: object

Continuous Difference-in-Differences estimator.

Implements the methodology from Callaway, Goodman-Bacon & Sant’Anna (2024) for estimating dose-response curves when treatment has a continuous intensity.

Parameters:
  • degree (int, default=3) – B-spline degree (3 = cubic).

  • num_knots (int, default=0) – Number of interior knots for the B-spline basis.

  • dvals (array-like, optional) – Custom dose evaluation grid. If None, uses quantile-based default.

  • control_group (str, default="never_treated") – "never_treated" or "not_yet_treated".

  • anticipation (int, default=0) – Number of periods of treatment anticipation.

  • base_period (str, default="varying") – "varying" or "universal".

  • alpha (float, default=0.05) – Significance level for confidence intervals.

  • n_bootstrap (int, default=0) – Number of multiplier bootstrap iterations. 0 for analytical SEs only.

  • bootstrap_weights (str, default="rademacher") – Bootstrap weight type: "rademacher", "mammen", or "webb".

  • seed (int, optional) – Random seed for reproducibility.

  • rank_deficient_action (str, default="warn") – Action for rank-deficient B-spline OLS: "warn", "error", or "silent".

Examples

>>> from diff_diff import ContinuousDiD, generate_continuous_did_data
>>> data = generate_continuous_did_data(n_units=200, seed=42)
>>> est = ContinuousDiD(n_bootstrap=199, seed=42)
>>> results = est.fit(data, outcome="outcome", unit="unit",
...                   time="period", first_treat="first_treat",
...                   dose="dose", aggregate="dose")
>>> results.overall_att

Methods

fit(data, outcome, unit, time, first_treat, dose)

Fit the continuous DiD estimator.

get_params()

Return estimator parameters as a dictionary.

set_params(**params)

Set estimator parameters and return self.

__init__(degree=3, num_knots=0, dvals=None, control_group='never_treated', anticipation=0, base_period='varying', alpha=0.05, n_bootstrap=0, bootstrap_weights='rademacher', seed=None, rank_deficient_action='warn')[source]
Parameters:
  • degree (int)

  • num_knots (int)

  • dvals (ndarray | None)

  • control_group (str)

  • anticipation (int)

  • base_period (str)

  • alpha (float)

  • n_bootstrap (int)

  • bootstrap_weights (str)

  • seed (int | None)

  • rank_deficient_action (str)

get_params()[source]

Return estimator parameters as a dictionary.

Return type:

Dict[str, Any]

set_params(**params)[source]

Set estimator parameters and return self.

Return type:

ContinuousDiD

fit(data, outcome, unit, time, first_treat, dose, aggregate=None, survey_design=None)[source]

Fit the continuous DiD estimator.

Parameters:
  • data (pd.DataFrame) – Panel data.

  • outcome (str) – Outcome column name.

  • unit (str) – Unit identifier column.

  • time (str) – Time period column.

  • first_treat (str) – First treatment period column (0 or inf for never-treated).

  • dose (str) – Continuous dose column.

  • aggregate (str, optional) – "dose" for dose-response aggregation, "eventstudy" for binarized event study.

  • survey_design (SurveyDesign, optional) – Survey design specification for design-based inference. Supports weighted estimation and Taylor series linearization variance with strata, PSU, and FPC.

Return type:

ContinuousDiDResults

ContinuousDiDResults#

Results container for Continuous DiD estimation.

class diff_diff.continuous_did_results.ContinuousDiDResults[source]

Bases: object

Results from Continuous Difference-in-Differences estimation.

Implements Callaway, Goodman-Bacon & Sant’Anna (2024).

dose_response_att

ATT(d) dose-response curve.

Type:

DoseResponseCurve

dose_response_acrt

ACRT(d) dose-response curve.

Type:

DoseResponseCurve

overall_att

Binarized overall ATT (ATT^{loc} under PT, equals ATT^{glob} under SPT).

Type:

float

overall_acrt

Plug-in overall ACRT^{glob}.

Type:

float

group_time_effects

Per (g,t) cell results.

Type:

dict

base_period

Base period strategy ("varying" or "universal").

Type:

str

anticipation

Number of anticipation periods.

Type:

int

n_bootstrap

Number of bootstrap iterations used.

Type:

int

bootstrap_weights

Bootstrap weight type ("rademacher", "mammen", or "webb").

Type:

str

seed

Random seed used for bootstrap.

Type:

int or None

rank_deficient_action

How rank deficiency is handled ("warn", "error", "silent").

Type:

str

Methods

summary([alpha])

Generate formatted summary.

print_summary([alpha])

Print summary to stdout.

to_dataframe([level])

Convert results to DataFrame.

dose_response_att: DoseResponseCurve
dose_response_acrt: DoseResponseCurve
overall_att: float
overall_att_se: float
overall_att_t_stat: float
overall_att_p_value: float
overall_att_conf_int: Tuple[float, float]
overall_acrt: float
overall_acrt_se: float
overall_acrt_t_stat: float
overall_acrt_p_value: float
overall_acrt_conf_int: Tuple[float, float]
group_time_effects: Dict[Tuple[Any, Any], Dict[str, Any]]
dose_grid: ndarray
groups: List[Any]
time_periods: List[Any]
n_obs: int
n_treated_units: int
n_control_units: int
alpha: float = 0.05
control_group: str = 'never_treated'
degree: int = 3
num_knots: int = 0
base_period: str = 'varying'
anticipation: int = 0
n_bootstrap: int = 0
bootstrap_weights: str = 'rademacher'
seed: int | None = None
rank_deficient_action: str = 'warn'
event_study_effects: Dict[int, Dict[str, Any]] | None = None
survey_metadata: Any | None = None
property att: float
property se: float
property conf_int: Tuple[float, float]
property p_value: float
property t_stat: float
property overall_se: float
property overall_conf_int: Tuple[float, float]
property overall_p_value: float
property overall_t_stat: float
property coef_var: float

SE / abs(overall ATT). NaN when ATT is 0 or SE non-finite.

Type:

Coefficient of variation

summary(alpha=None)[source]

Generate formatted summary.

Parameters:

alpha (float | None)

Return type:

str

print_summary(alpha=None)[source]

Print summary to stdout.

Parameters:

alpha (float | None)

Return type:

None

to_dataframe(level='dose_response')[source]

Convert results to DataFrame.

Parameters:

level (str, default="dose_response") – "dose_response", "group_time", or "event_study".

Return type:

DataFrame

property is_significant: bool

Check if overall ATT is significant.

property significance_stars: str

Significance stars for overall ATT.

__init__(dose_response_att, dose_response_acrt, overall_att, overall_att_se, overall_att_t_stat, overall_att_p_value, overall_att_conf_int, overall_acrt, overall_acrt_se, overall_acrt_t_stat, overall_acrt_p_value, overall_acrt_conf_int, group_time_effects, dose_grid, groups, time_periods, n_obs, n_treated_units, n_control_units, alpha=0.05, control_group='never_treated', degree=3, num_knots=0, base_period='varying', anticipation=0, n_bootstrap=0, bootstrap_weights='rademacher', seed=None, rank_deficient_action='warn', event_study_effects=None, survey_metadata=None)
Parameters:
Return type:

None

DoseResponseCurve#

Dose-response curve container for ATT(d) or ACRT(d).

class diff_diff.continuous_did_results.DoseResponseCurve[source]

Bases: object

Dose-response curve from continuous DiD estimation.

dose_grid

Evaluation points, shape (n_grid,).

Type:

np.ndarray

effects

ATT(d) or ACRT(d) values, shape (n_grid,).

Type:

np.ndarray

se

Standard errors, shape (n_grid,).

Type:

np.ndarray

conf_int_lower

Lower CI bounds, shape (n_grid,).

Type:

np.ndarray

conf_int_upper

Upper CI bounds, shape (n_grid,).

Type:

np.ndarray

target

"att" or "acrt".

Type:

str

Methods

to_dataframe()

Convert to DataFrame with dose, effect, se, CI, t_stat, p_value.

dose_grid: ndarray
effects: ndarray
se: ndarray
conf_int_lower: ndarray
conf_int_upper: ndarray
target: str
p_value: ndarray | None = None
n_bootstrap: int = 0
df_survey: int | None = None
to_dataframe()[source]

Convert to DataFrame with dose, effect, se, CI, t_stat, p_value.

Return type:

DataFrame

__init__(dose_grid, effects, se, conf_int_lower, conf_int_upper, target, p_value=None, n_bootstrap=0, df_survey=None)
Parameters:
Return type:

None

Example Usage#

Basic usage:

from diff_diff import ContinuousDiD, generate_continuous_did_data

data = generate_continuous_did_data(n_units=200, seed=42)

est = ContinuousDiD(n_bootstrap=199, seed=42)
results = est.fit(data, outcome='outcome', unit='unit',
                  time='period', first_treat='first_treat',
                  dose='dose', aggregate='dose')
results.print_summary()

Accessing dose-response curves:

# ATT(d) dose-response curve as DataFrame
att_df = results.dose_response_att.to_dataframe()
print(att_df[['dose', 'effect', 'se', 'p_value']])

# ACRT(d) dose-response curve
acrt_df = results.dose_response_acrt.to_dataframe()

# Overall summary parameters
print(f"Overall ATT: {results.overall_att:.3f} (SE: {results.overall_att_se:.3f})")
print(f"Overall ACRT: {results.overall_acrt:.3f} (SE: {results.overall_acrt_se:.3f})")

Event study aggregation:

# Dynamic effects (binarized ATT by relative period)
results_es = est.fit(data, outcome='outcome', unit='unit',
                     time='period', first_treat='first_treat',
                     dose='dose', aggregate='eventstudy')
es_df = results_es.to_dataframe(level='event_study')

Comparison with CallawaySantAnna#

Feature

ContinuousDiD

CallawaySantAnna

Treatment type

Continuous dose / intensity

Binary (treated / not treated)

Target parameter

ATTloc (PT); ATT(d), ACRT(d), ATTglob, ACRTglob (SPT)

ATT(g,t), aggregated ATT

Smoothing

B-spline basis for dose-response

None (nonparametric group-time)

Dose-response curve

Yes (full curve with CIs)

No

Bootstrap

Multiplier bootstrap (optional)

Multiplier bootstrap (optional)

Control group

never_treated / not_yet_treated

never_treated / not_yet_treated