Skip to content

Commit 6119fa9

Browse files
igerberclaude
andcommitted
docs: HAD ecosystem completion (RTD audit Batch A)
Closes the gaps left after PR igerber#372 added HeterogeneousAdoptionDiD to the canonical surfaces. The narrative pages did not yet mention HAD, and the 12-symbol HAD pretest suite shipped in `had_pretests.py` was absent from the API page. Also refreshes the inference-contract block to use the `survey_design=` canonical kwarg consolidated in PR igerber#376. - `docs/api/had.rst`: new HAD Pretests section covering all 12 public symbols (4 single-period tests + 4 result classes + 3 joint tests + 1 joint result), split into `aggregate="overall"` and `aggregate="event_study"` subsections matching the workflow's dispatch. Refreshes the existing inference-contract block to reference `survey_design=make_pweight_design(weights)` (pweight shortcut) and `survey_design=SurveyDesign(...)` (full TSL); notes `survey=` / `weights=` are deprecated aliases. - `docs/choosing_estimator.rst`: HAD entries in all 3 tables (Quick Reference, Standard Error Methods, Survey Design Support) plus a new "Universal Rollout / No Untreated Control" subsection in Detailed Guidance. SE Methods row uses `survey_design=` canonical naming. - `docs/r_comparison.rst`: HAD row in Feature Comparison Table, new "No-Untreated Designs (no R parallel)" subsection, Migration Tips bullet. - `docs/troubleshooting.rst`: new HAD Issues section with 4 subsections (estimand resolution / mass-point fallback / classical SE under survey_design / panel-only event-study). - `docs/practitioner_decision_tree.rst`: Start Here option 7, At a Glance row, new "Universal Rollout" section with `_section-no-untreated` anchor. - `docs/doc-deps.yaml`: extend had_pretests.py entry with llms.txt user-guide dep; add new top-level local_linear.py entry. Verification: all 12 HAD pretest symbols importable; `make_pweight_design` + `SurveyDesign` importable; sphinx build succeeds with 0 new warnings (71 pre-existing unaffected); HTML render contains expected HAD content (276 hits in had.html, 4-8 in narrative pages); 0 em dashes; `_section-no-untreated` anchor resolves. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 81b9430 commit 6119fa9

7 files changed

Lines changed: 343 additions & 9 deletions

File tree

docs/api/had.rst

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,20 +46,27 @@ Unit Remains Untreated" (arXiv:2405.04465v6), which:
4646
- **Unweighted** - continuous paths use the CCT-2014 weighted-robust SE
4747
from the in-house ``lprobust`` port; the mass-point path uses a
4848
structural-residual 2SLS sandwich. No cross-horizon covariance.
49-
- **``weights=`` shortcut** - continuous paths reuse the CCT-2014 SE;
50-
the mass-point path uses an analytical weighted 2SLS sandwich
51-
(``classical`` / ``hc1`` only - ``hc2`` / ``hc2_bm`` raise
52-
``NotImplementedError`` pending a 2SLS-specific leverage derivation).
53-
- **``survey=``** - both paths compose Binder (1983) Taylor-series
54-
linearization with ``df_survey`` threaded into ``safe_inference``.
49+
- **``survey_design=make_pweight_design(weights)``** (pweight-only
50+
shortcut) - continuous paths reuse the CCT-2014 SE; the mass-point
51+
path uses an analytical weighted 2SLS sandwich (``classical`` /
52+
``hc1`` only - ``hc2`` / ``hc2_bm`` raise ``NotImplementedError``
53+
pending a 2SLS-specific leverage derivation).
54+
- **``survey_design=SurveyDesign(...)``** (full TSL with strata / PSU
55+
/ FPC) - both paths compose Binder (1983) Taylor-series linearization
56+
with ``df_survey`` threaded into ``safe_inference``.
57+
58+
The deprecated ``survey=`` and ``weights=`` aliases still resolve to
59+
the same paths with a ``DeprecationWarning`` (removal queued for the
60+
next minor release).
5561

5662
A simultaneous confidence band (sup-t) is available only on the
5763
**weighted event-study path** via ``cband=True``. Joint cross-horizon
5864
analytical covariance is not computed in this release; tracked in
5965
``TODO.md``.
6066

6167
**Mass-point ``vcov_type="classical"`` deviation.** The mass-point
62-
``survey=`` paths (static and event-study) and the ``weights=`` +
68+
``survey_design=SurveyDesign(...)`` paths (static and event-study) and
69+
the ``survey_design=make_pweight_design(weights)`` +
6370
``aggregate="event_study"`` + ``cband=True`` path reject
6471
``vcov_type="classical"`` with ``NotImplementedError``. The per-unit
6572
2SLS influence function returned by the mass-point fit is HC1-scaled
@@ -97,3 +104,59 @@ Multi-period event-study results container for the Appendix B.2 extension.
97104
:members:
98105
:undoc-members:
99106
:show-inheritance:
107+
108+
HAD Pretests
109+
------------
110+
111+
Diagnostic pretests for the HAD identification assumptions from de Chaisemartin
112+
et al. (2026). The composite orchestrator
113+
:func:`~diff_diff.did_had_pretest_workflow` dispatches to two shapes based on
114+
panel structure: the **overall** path (two-period first-differenced sample)
115+
runs single-period tests; the **event-study** path (three or more periods)
116+
runs joint multi-period tests. Both paths return a unified
117+
:class:`~diff_diff.HADPretestReport`.
118+
119+
.. autofunction:: diff_diff.did_had_pretest_workflow
120+
121+
.. autoclass:: diff_diff.HADPretestReport
122+
:members:
123+
:undoc-members:
124+
:show-inheritance:
125+
126+
Single-period tests (``aggregate="overall"``)
127+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
128+
129+
.. autofunction:: diff_diff.qug_test
130+
131+
.. autofunction:: diff_diff.stute_test
132+
133+
.. autofunction:: diff_diff.yatchew_hr_test
134+
135+
.. autoclass:: diff_diff.QUGTestResults
136+
:members:
137+
:undoc-members:
138+
:show-inheritance:
139+
140+
.. autoclass:: diff_diff.StuteTestResults
141+
:members:
142+
:undoc-members:
143+
:show-inheritance:
144+
145+
.. autoclass:: diff_diff.YatchewTestResults
146+
:members:
147+
:undoc-members:
148+
:show-inheritance:
149+
150+
Joint multi-period tests (``aggregate="event_study"``)
151+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
152+
153+
.. autofunction:: diff_diff.stute_joint_pretest
154+
155+
.. autofunction:: diff_diff.joint_pretrends_test
156+
157+
.. autofunction:: diff_diff.joint_homogeneity_test
158+
159+
.. autoclass:: diff_diff.StuteJointResult
160+
:members:
161+
:undoc-members:
162+
:show-inheritance:

docs/choosing_estimator.rst

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ Quick Reference
9393
- Continuous dose / treatment intensity
9494
- Strong Parallel Trends (SPT) for dose-response; PT for binarized ATT
9595
- ATT\ :sup:`loc` (PT); ATT(d), ACRT(d) (SPT)
96+
* - ``HeterogeneousAdoptionDiD``
97+
- Universal rollout, dose varies, no untreated unit
98+
- dCDH 2026 Assumptions (Design 1' QUG case or Design 1 with A6/A5)
99+
- WAS or WAS\ :sub:`d_lower` per resolved estimand; event-study Appendix B.2
96100
* - ``SunAbraham``
97101
- Staggered adoption, interaction-weighted
98102
- Conditional parallel trends
@@ -357,6 +361,49 @@ Use :class:`~diff_diff.ContinuousDiD` when:
357361
print(f"Overall ATT: {results.overall_att:.3f}")
358362
att_curve = results.dose_response_att.to_dataframe()
359363
364+
Universal Rollout / No Untreated Control
365+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
366+
367+
Use :class:`~diff_diff.HeterogeneousAdoptionDiD` when:
368+
369+
- **Every unit is treated at the post period** (universal-rollout policy,
370+
industry-wide tariff change, simultaneous launch into all markets)
371+
- Treatment **intensity (dose) varies across units**, but no genuinely
372+
untreated control group exists to anchor a standard DiD contrast
373+
- :class:`~diff_diff.ContinuousDiD` is unavailable because its untreated-group
374+
requirement (``D = 0``) is violated
375+
376+
The estimator implements de Chaisemartin, Ciccia, D'Haultfoeuille and Knau
377+
(2026, arXiv:2405.04465v6) and resolves to one of two estimands depending on
378+
the dose support:
379+
380+
- **Design 1' (QUG case, ``d_lower = 0``)** identifies the **Weighted Average
381+
Slope (WAS)** under the Quasi-Untreated-Group assumption (units with the
382+
smallest dose serve as the comparison anchor). The shipped result class
383+
exposes ``target_parameter == "WAS"``.
384+
- **Design 1 (no QUG, ``d_lower > 0``)** identifies ``WAS_{d_lower}`` under
385+
Assumption 6, or sign identification only under Assumption 5; neither
386+
additional assumption is testable via pre-trends. Result class exposes
387+
``target_parameter == "WAS_d_lower"``.
388+
389+
The dose-distribution path is auto-detected. Run
390+
:func:`~diff_diff.did_had_pretest_workflow` to vet the identifying assumptions
391+
before estimation; see :doc:`api/had` for the full API and SE-regime contract.
392+
393+
.. code-block:: python
394+
395+
from diff_diff import HeterogeneousAdoptionDiD, did_had_pretest_workflow
396+
397+
pretests = did_had_pretest_workflow(data, outcome='y', unit='unit',
398+
time='period', dose='dose')
399+
400+
est = HeterogeneousAdoptionDiD()
401+
results = est.fit(data, outcome='y', unit='unit',
402+
time='period', dose='dose')
403+
404+
print(f"Resolved estimand: {results.target_parameter}")
405+
print(f"Estimate: {results.coef:.3f}")
406+
360407
Efficient DiD
361408
~~~~~~~~~~~~~
362409

@@ -615,6 +662,9 @@ differences helps interpret results and choose appropriate inference.
615662
* - ``ContinuousDiD``
616663
- Analytical (influence function)
617664
- Uses influence-function-based SEs by default. Use ``n_bootstrap=199`` (or higher) for multiplier bootstrap inference with proper CIs.
665+
* - ``HeterogeneousAdoptionDiD``
666+
- Path-dependent (CCT-2014 / 2SLS / Binder TSL)
667+
- Three SE regimes per :doc:`api/had`. **Unweighted**: continuous-dose paths use the CCT-2014 weighted-robust SE from the in-house ``lprobust`` port; mass-point uses a 2SLS sandwich. **``survey_design=make_pweight_design(weights)``** (pweight shortcut): continuous reuses CCT-2014; mass-point uses analytical weighted 2SLS (``classical`` / ``hc1`` only). **``survey_design=SurveyDesign(...)``** (full TSL): both paths compose Binder (1983) Taylor-series linearization. Per-horizon CIs are pointwise; sup-t bands available only on the weighted event-study path via ``cband=True``. The deprecated ``survey=`` / ``weights=`` aliases still resolve with a DeprecationWarning.
618668
* - ``SunAbraham``
619669
- Cluster-robust (unit level)
620670
- Clusters at unit level by default. Specify ``cluster`` to override. Use ``n_bootstrap`` for pairs bootstrap inference.
@@ -777,6 +827,11 @@ estimation. The depth of support varies by estimator:
777827
- Full
778828
- Full (analytical)
779829
- Multiplier at PSU
830+
* - ``HeterogeneousAdoptionDiD``
831+
- pweight only
832+
- Full (Binder TSL)
833+
- --
834+
- Multiplier (event-study, ``cband=True`` only)
780835
* - ``EfficientDiD``
781836
- Full
782837
- Full

docs/doc-deps.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,19 @@ sources:
385385

386386
diff_diff/had_pretests.py:
387387
drift_risk: medium
388+
docs:
389+
- path: docs/methodology/REGISTRY.md
390+
section: "HeterogeneousAdoptionDiD"
391+
type: methodology
392+
- path: docs/api/had.rst
393+
section: "HAD Pretests"
394+
type: api_reference
395+
- path: diff_diff/guides/llms.txt
396+
section: "Estimators"
397+
type: user_guide
398+
399+
diff_diff/local_linear.py:
400+
drift_risk: low
388401
docs:
389402
- path: docs/methodology/REGISTRY.md
390403
section: "HeterogeneousAdoptionDiD"

docs/practitioner_decision_tree.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ Which of these best describes your situation?
4444
Your outcome comes from a survey with complex sampling. Go to
4545
:ref:`section-survey`.
4646

47+
7. **All my markets received the campaign at the same time, but spend levels varied** (no untreated control market exists)
48+
49+
Universal rollout with dose-only variation. Go to
50+
:ref:`section-no-untreated`.
51+
4752
.. tip::
4853

4954
In academic literature, "rolling out in waves" is called *staggered adoption*,
@@ -258,6 +263,51 @@ appropriate identification assumptions in place.
258263
require Strong Parallel Trends (see warning above).
259264

260265

266+
.. _section-no-untreated:
267+
268+
Universal Rollout (No Untreated Markets)
269+
----------------------------------------
270+
271+
**Your situation:** Every market got the campaign at the same time - there is no
272+
holdout group - but spending levels varied across markets. ``ContinuousDiD`` cannot
273+
help here because it requires an untreated comparison group; standard DiD has no
274+
control to anchor the contrast.
275+
276+
**Recommended method:** :class:`~diff_diff.HeterogeneousAdoptionDiD`
277+
278+
This estimator implements de Chaisemartin, Ciccia, D'Haultfoeuille and Knau (2026)
279+
and resolves to one of two estimands depending on whether the smallest-dose
280+
markets can serve as a quasi-untreated anchor (Design 1') or whether the
281+
identification rests on stronger structural assumptions (Design 1).
282+
283+
.. code-block:: python
284+
285+
from diff_diff import HeterogeneousAdoptionDiD, did_had_pretest_workflow
286+
287+
# Run the pretest workflow first - it adjudicates which design path
288+
# your data supports and surfaces assumption violations
289+
pretests = did_had_pretest_workflow(
290+
data, outcome="y", unit="unit", time="period", dose="dose",
291+
)
292+
print(pretests)
293+
294+
est = HeterogeneousAdoptionDiD()
295+
results = est.fit(
296+
data, outcome="y", unit="unit", time="period", dose="dose",
297+
)
298+
print(f"Resolved estimand: {results.target_parameter}")
299+
print(f"Average lift per unit of dose: {results.coef:.2f}")
300+
301+
.. note::
302+
303+
**Academic term:** The estimator targets the *Weighted Average Slope (WAS)* under
304+
the QUG / Design 1' case, or *WAS_{d_lower}* under Design 1. Neither identifying
305+
assumption is testable via pre-trends alone - run
306+
:func:`~diff_diff.did_had_pretest_workflow` for the recommended battery. See
307+
:doc:`api/had` for the inference contract (three SE regimes; pointwise CIs;
308+
sup-t bands only on the weighted event-study path).
309+
310+
261311
.. _section-few-markets:
262312

263313
Few Test Markets
@@ -377,6 +427,9 @@ At a Glance
377427
* - Varied spending levels
378428
- ``ContinuousDiD``
379429
- Dose-response curve
430+
* - Universal rollout, no untreated markets
431+
- ``HeterogeneousAdoptionDiD``
432+
- Targets WAS / WAS_{d_lower} when no holdout exists
380433
* - Only a few test markets
381434
- ``SyntheticDiD``
382435
- Optimal with few treated units

docs/r_comparison.rst

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,30 @@ The synthdid package implements Arkhangelsky et al. (2021):
213213
post_periods=post_periods
214214
)
215215
216+
No-Untreated Designs (no R parallel)
217+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
218+
219+
When every unit is treated at the post period (universal-rollout policies,
220+
industry-wide regime changes) but treatment intensity varies across units,
221+
the standard R DiD ecosystem has no direct entry point - ``did``, ``fixest``,
222+
``synthdid``, and ``DIDmultiplegtDYN`` all assume an untreated comparison
223+
group exists. ``diff-diff`` ships
224+
:class:`~diff_diff.HeterogeneousAdoptionDiD`, which implements
225+
de Chaisemartin, Ciccia, D'Haultfoeuille and Knau (2026, arXiv:2405.04465v6).
226+
The estimator targets the Weighted Average Slope (WAS) when the smallest
227+
dose serves as a quasi-untreated anchor (Design 1') or ``WAS_{d_lower}``
228+
otherwise (Design 1, requiring Assumption 6 or sign-only Assumption 5).
229+
The dCDH 2026 paper has not yet been packaged in R, so this is a
230+
methodology niche covered in Python first.
231+
232+
.. code-block:: python
233+
234+
from diff_diff import HeterogeneousAdoptionDiD
235+
236+
est = HeterogeneousAdoptionDiD()
237+
results = est.fit(data, outcome='y', unit='unit',
238+
time='period', dose='dose')
239+
216240
Key Differences
217241
---------------
218242

@@ -372,6 +396,11 @@ Feature Comparison Table
372396
- ❌
373397
- ❌
374398
- ❌
399+
* - Heterogeneous adoption / no-untreated designs
400+
- ✅
401+
- ❌
402+
- ❌
403+
- ❌
375404

376405
.. note::
377406

@@ -382,7 +411,8 @@ Feature Comparison Table
382411
Stacked DiD requires manual implementation or the ``stackedev`` package;
383412
Continuous DiD is available via the ``did`` package continuous extension;
384413
Triple Difference requires manual implementation in R.
385-
TROP and Efficient DiD have no direct R equivalents.
414+
TROP, Efficient DiD, and HeterogeneousAdoptionDiD (dCDH 2026, the
415+
no-untreated-control design) have no direct R equivalents.
386416

387417
Migration Tips
388418
--------------
@@ -399,3 +429,9 @@ Migration Tips
399429

400430
5. **Missing data**: diff-diff requires complete data; use ``balance_panel()``
401431
or ``dropna()`` first
432+
433+
6. **No-untreated designs**: If your R workflow stalls because every unit was
434+
treated at the post period (universal rollout, dose-only variation), reach
435+
for :class:`~diff_diff.HeterogeneousAdoptionDiD`. See the
436+
`No-Untreated Designs (no R parallel)`_ section above for the migration
437+
pattern.

docs/references.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ Survey-Design Inference (Taylor-Series Linearization)
6666

6767
- **Binder, D. A. (1983).** "On the Variances of Asymptotically Normal Estimators from Complex Surveys." *International Statistical Review*, 51(3), 279-292. https://doi.org/10.2307/1402588
6868

69-
Foundational TSL (Taylor-Series Linearization) variance derivation used across diff-diff's survey-aware estimators (``compute_survey_if_variance`` and the per-estimator influence-function compositions, including the dCDH and HeterogeneousAdoptionDiD ``survey=`` paths).
69+
Foundational TSL (Taylor-Series Linearization) variance derivation used across diff-diff's survey-aware estimators (``compute_survey_if_variance`` and the per-estimator influence-function compositions, including the dCDH and HeterogeneousAdoptionDiD ``survey_design=`` paths).
7070

7171
Placebo Tests and DiD Diagnostics
7272
---------------------------------

0 commit comments

Comments
 (0)