Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions docs/api/continuous_did.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
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 ATT\ :sup:`glob` and ACRT\ :sup:`glob`, 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 ATT\ :sup:`loc` (``overall_att``) is identified; it equals
ATT\ :sup:`glob` 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>`_

.. module:: diff_diff.continuous_did

ContinuousDiD
--------------

Main estimator class for Continuous Difference-in-Differences.

.. autoclass:: diff_diff.ContinuousDiD
:members:
:undoc-members:
:show-inheritance:
:inherited-members:

.. rubric:: Methods

.. autosummary::

~ContinuousDiD.fit
~ContinuousDiD.get_params
~ContinuousDiD.set_params

ContinuousDiDResults
--------------------

Results container for Continuous DiD estimation.

.. autoclass:: diff_diff.continuous_did_results.ContinuousDiDResults
:members:
:undoc-members:
:show-inheritance:

.. rubric:: Methods

.. autosummary::

~ContinuousDiDResults.summary
~ContinuousDiDResults.print_summary
~ContinuousDiDResults.to_dataframe

DoseResponseCurve
-----------------

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

.. autoclass:: diff_diff.continuous_did_results.DoseResponseCurve
:members:
:undoc-members:
:show-inheritance:

.. rubric:: Methods

.. autosummary::

~DoseResponseCurve.to_dataframe

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
--------------------------------

.. list-table::
:header-rows: 1
:widths: 20 40 40

* - Feature
- ContinuousDiD
- CallawaySantAnna
* - Treatment type
- Continuous dose / intensity
- Binary (treated / not treated)
* - Target parameter
- ATT\ :sup:`loc` (PT); ATT(d), ACRT(d), ATT\ :sup:`glob`, ACRT\ :sup:`glob` (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
5 changes: 5 additions & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Core estimator classes for DiD analysis:
diff_diff.StackedDiD
diff_diff.TripleDifference
diff_diff.TROP
diff_diff.ContinuousDiD

Results Classes
---------------
Expand All @@ -46,6 +47,8 @@ Result containers returned by estimators:
diff_diff.TripleDifferenceResults
diff_diff.StackedDiDResults
diff_diff.trop.TROPResults
diff_diff.ContinuousDiDResults
diff_diff.DoseResponseCurve

Visualization
-------------
Expand Down Expand Up @@ -166,6 +169,7 @@ Utilities for preparing DiD data:
:nosignatures:

diff_diff.generate_did_data
diff_diff.generate_continuous_did_data
diff_diff.make_treatment_indicator
diff_diff.make_post_indicator
diff_diff.wide_to_long
Expand All @@ -190,6 +194,7 @@ Detailed documentation by module:
stacked_did
triple_diff
trop
continuous_did
results
visualization
diagnostics
Expand Down
44 changes: 44 additions & 0 deletions docs/choosing_estimator.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ Decision Flowchart

Start here and follow the questions:

0. **Is treatment continuous?** (Units receive different doses or intensities)

- **No** → Go to question 1
- **Yes** → Use :class:`~diff_diff.ContinuousDiD`

1. **Is treatment staggered?** (Different units treated at different times)

- **No** → Go to question 2
Expand Down Expand Up @@ -58,6 +63,10 @@ Quick Reference
- Few treated units, many controls
- Synthetic parallel trends
- ATT with unit/time weights
* - ``ContinuousDiD``
- Continuous dose / treatment intensity
- Strong Parallel Trends (SPT) for dose-response; PT for binarized ATT
- ATT\ :sup:`loc` (PT); ATT(d), ACRT(d) (SPT)

Detailed Guidance
-----------------
Expand Down Expand Up @@ -173,6 +182,38 @@ Use :class:`~diff_diff.SyntheticDiD` when:
# View the unit weights
print(results.unit_weights)

Continuous Treatment
~~~~~~~~~~~~~~~~~~~~

Use :class:`~diff_diff.ContinuousDiD` when:

- Treatment varies in **intensity or dose** (e.g., subsidy amount, hours of training)
- You want to estimate how effects change with treatment dose
- You need the full dose-response curve, not just a single average effect
- Staggered adoption where units receive different treatment levels

.. note::

Dose-response curves ATT(d) and ACRT(d) require **Strong Parallel Trends (SPT)**.
Under standard PT only the binarized ATT\ :sup:`loc` is identified.
Data must include an untreated group (D = 0), a balanced panel, and
time-invariant dose (each unit's dose is fixed across periods).

.. code-block:: python

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')

# Overall effect and dose-response curve
print(f"Overall ATT: {results.overall_att:.3f}")
att_curve = results.dose_response_att.to_dataframe()

Common Pitfalls
---------------

Expand Down Expand Up @@ -234,6 +275,9 @@ differences helps interpret results and choose appropriate inference.
* - ``SyntheticDiD``
- Bootstrap or placebo-based
- Default uses bootstrap resampling. Set ``n_bootstrap=0`` for placebo-based inference using pre-treatment residuals.
* - ``ContinuousDiD``
- Analytical (default)
- Uses delta method SEs by default. Use ``n_bootstrap=199`` (or higher) for multiplier bootstrap inference with proper CIs.

**Recommendations by sample size:**

Expand Down