Skip to content

Commit 2993d71

Browse files
authored
Merge pull request #183 from igerber/docs/continuous-did-api-reference
Add ContinuousDiD to ReadTheDocs documentation
2 parents feaf199 + 37d2f96 commit 2993d71

File tree

3 files changed

+208
-0
lines changed

3 files changed

+208
-0
lines changed

docs/api/continuous_did.rst

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
Continuous Difference-in-Differences
2+
=====================================
3+
4+
Continuous DiD estimator for dose-response curves with continuous treatment intensity.
5+
6+
This module implements the methodology from Callaway, Goodman-Bacon & Sant'Anna (2024),
7+
"Difference-in-Differences with a Continuous Treatment" (NBER WP 32117), which:
8+
9+
1. **Estimates dose-response curves**: ATT(d) and ACRT(d) as functions of dose
10+
2. **Computes summary parameters**: Overall ATT (binarized) and ACRT aggregated across doses
11+
3. **Uses B-spline smoothing**: Flexible nonparametric estimation of dose-response functions
12+
4. **Supports multiplier bootstrap**: Valid inference with proper standard errors and CIs
13+
14+
.. note::
15+
16+
**Identification assumptions.** The dose-response curves ATT(d) and ACRT(d),
17+
as well as ATT\ :sup:`glob` and ACRT\ :sup:`glob`, require the **Strong Parallel
18+
Trends (SPT)** assumption — that there is no selection into dose groups based on
19+
treatment effects. Under the weaker standard Parallel Trends (PT) assumption,
20+
only the binarized ATT\ :sup:`loc` (``overall_att``) is identified; it equals
21+
ATT\ :sup:`glob` only when SPT holds. See Callaway, Goodman-Bacon & Sant'Anna
22+
(2024), Assumptions 1–2.
23+
24+
**When to use Continuous DiD:**
25+
26+
- Treatment varies in **intensity or dose** across units (not just binary on/off)
27+
- You want to estimate how effects change with treatment dose
28+
- Staggered adoption with heterogeneous dose levels
29+
- You need the full dose-response curve, not just a single average effect
30+
31+
**Data requirements:**
32+
33+
- An **untreated group** (D = 0) must be present in the data
34+
- A **balanced panel** is required (all units observed in all time periods)
35+
- **Dose must be time-invariant** — each unit's dose is fixed across periods
36+
37+
**Reference:** Callaway, B., Goodman-Bacon, A., & Sant'Anna, P. H. C. (2024).
38+
Difference-in-Differences with a Continuous Treatment. *NBER Working Paper* 32117.
39+
`<https://www.nber.org/papers/w32117>`_
40+
41+
.. module:: diff_diff.continuous_did
42+
43+
ContinuousDiD
44+
--------------
45+
46+
Main estimator class for Continuous Difference-in-Differences.
47+
48+
.. autoclass:: diff_diff.ContinuousDiD
49+
:members:
50+
:undoc-members:
51+
:show-inheritance:
52+
:inherited-members:
53+
54+
.. rubric:: Methods
55+
56+
.. autosummary::
57+
58+
~ContinuousDiD.fit
59+
~ContinuousDiD.get_params
60+
~ContinuousDiD.set_params
61+
62+
ContinuousDiDResults
63+
--------------------
64+
65+
Results container for Continuous DiD estimation.
66+
67+
.. autoclass:: diff_diff.continuous_did_results.ContinuousDiDResults
68+
:members:
69+
:undoc-members:
70+
:show-inheritance:
71+
72+
.. rubric:: Methods
73+
74+
.. autosummary::
75+
76+
~ContinuousDiDResults.summary
77+
~ContinuousDiDResults.print_summary
78+
~ContinuousDiDResults.to_dataframe
79+
80+
DoseResponseCurve
81+
-----------------
82+
83+
Dose-response curve container for ATT(d) or ACRT(d).
84+
85+
.. autoclass:: diff_diff.continuous_did_results.DoseResponseCurve
86+
:members:
87+
:undoc-members:
88+
:show-inheritance:
89+
90+
.. rubric:: Methods
91+
92+
.. autosummary::
93+
94+
~DoseResponseCurve.to_dataframe
95+
96+
Example Usage
97+
-------------
98+
99+
Basic usage::
100+
101+
from diff_diff import ContinuousDiD, generate_continuous_did_data
102+
103+
data = generate_continuous_did_data(n_units=200, seed=42)
104+
105+
est = ContinuousDiD(n_bootstrap=199, seed=42)
106+
results = est.fit(data, outcome='outcome', unit='unit',
107+
time='period', first_treat='first_treat',
108+
dose='dose', aggregate='dose')
109+
results.print_summary()
110+
111+
Accessing dose-response curves::
112+
113+
# ATT(d) dose-response curve as DataFrame
114+
att_df = results.dose_response_att.to_dataframe()
115+
print(att_df[['dose', 'effect', 'se', 'p_value']])
116+
117+
# ACRT(d) dose-response curve
118+
acrt_df = results.dose_response_acrt.to_dataframe()
119+
120+
# Overall summary parameters
121+
print(f"Overall ATT: {results.overall_att:.3f} (SE: {results.overall_att_se:.3f})")
122+
print(f"Overall ACRT: {results.overall_acrt:.3f} (SE: {results.overall_acrt_se:.3f})")
123+
124+
Event study aggregation::
125+
126+
# Dynamic effects (binarized ATT by relative period)
127+
results_es = est.fit(data, outcome='outcome', unit='unit',
128+
time='period', first_treat='first_treat',
129+
dose='dose', aggregate='eventstudy')
130+
es_df = results_es.to_dataframe(level='event_study')
131+
132+
Comparison with CallawaySantAnna
133+
--------------------------------
134+
135+
.. list-table::
136+
:header-rows: 1
137+
:widths: 20 40 40
138+
139+
* - Feature
140+
- ContinuousDiD
141+
- CallawaySantAnna
142+
* - Treatment type
143+
- Continuous dose / intensity
144+
- Binary (treated / not treated)
145+
* - Target parameter
146+
- ATT\ :sup:`loc` (PT); ATT(d), ACRT(d), ATT\ :sup:`glob`, ACRT\ :sup:`glob` (SPT)
147+
- ATT(g,t), aggregated ATT
148+
* - Smoothing
149+
- B-spline basis for dose-response
150+
- None (nonparametric group-time)
151+
* - Dose-response curve
152+
- Yes (full curve with CIs)
153+
- No
154+
* - Bootstrap
155+
- Multiplier bootstrap (optional)
156+
- Multiplier bootstrap (optional)
157+
* - Control group
158+
- never_treated / not_yet_treated
159+
- never_treated / not_yet_treated

docs/api/index.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Core estimator classes for DiD analysis:
2222
diff_diff.StackedDiD
2323
diff_diff.TripleDifference
2424
diff_diff.TROP
25+
diff_diff.ContinuousDiD
2526

2627
Results Classes
2728
---------------
@@ -46,6 +47,8 @@ Result containers returned by estimators:
4647
diff_diff.TripleDifferenceResults
4748
diff_diff.StackedDiDResults
4849
diff_diff.trop.TROPResults
50+
diff_diff.ContinuousDiDResults
51+
diff_diff.DoseResponseCurve
4952

5053
Visualization
5154
-------------
@@ -166,6 +169,7 @@ Utilities for preparing DiD data:
166169
:nosignatures:
167170

168171
diff_diff.generate_did_data
172+
diff_diff.generate_continuous_did_data
169173
diff_diff.make_treatment_indicator
170174
diff_diff.make_post_indicator
171175
diff_diff.wide_to_long
@@ -190,6 +194,7 @@ Detailed documentation by module:
190194
stacked_did
191195
triple_diff
192196
trop
197+
continuous_did
193198
results
194199
visualization
195200
diagnostics

docs/choosing_estimator.rst

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ Decision Flowchart
88

99
Start here and follow the questions:
1010

11+
0. **Is treatment continuous?** (Units receive different doses or intensities)
12+
13+
- **No** → Go to question 1
14+
- **Yes** → Use :class:`~diff_diff.ContinuousDiD`
15+
1116
1. **Is treatment staggered?** (Different units treated at different times)
1217

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

6271
Detailed Guidance
6372
-----------------
@@ -173,6 +182,38 @@ Use :class:`~diff_diff.SyntheticDiD` when:
173182
# View the unit weights
174183
print(results.unit_weights)
175184
185+
Continuous Treatment
186+
~~~~~~~~~~~~~~~~~~~~
187+
188+
Use :class:`~diff_diff.ContinuousDiD` when:
189+
190+
- Treatment varies in **intensity or dose** (e.g., subsidy amount, hours of training)
191+
- You want to estimate how effects change with treatment dose
192+
- You need the full dose-response curve, not just a single average effect
193+
- Staggered adoption where units receive different treatment levels
194+
195+
.. note::
196+
197+
Dose-response curves ATT(d) and ACRT(d) require **Strong Parallel Trends (SPT)**.
198+
Under standard PT only the binarized ATT\ :sup:`loc` is identified.
199+
Data must include an untreated group (D = 0), a balanced panel, and
200+
time-invariant dose (each unit's dose is fixed across periods).
201+
202+
.. code-block:: python
203+
204+
from diff_diff import ContinuousDiD, generate_continuous_did_data
205+
206+
data = generate_continuous_did_data(n_units=200, seed=42)
207+
208+
est = ContinuousDiD(n_bootstrap=199, seed=42)
209+
results = est.fit(data, outcome='outcome', unit='unit',
210+
time='period', first_treat='first_treat',
211+
dose='dose', aggregate='dose')
212+
213+
# Overall effect and dose-response curve
214+
print(f"Overall ATT: {results.overall_att:.3f}")
215+
att_curve = results.dose_response_att.to_dataframe()
216+
176217
Common Pitfalls
177218
---------------
178219

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

238282
**Recommendations by sample size:**
239283

0 commit comments

Comments
 (0)