diff-diff 3.4.0__tar.gz → 3.4.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. {diff_diff-3.4.0 → diff_diff-3.4.2}/PKG-INFO +2 -2
  2. {diff_diff-3.4.0 → diff_diff-3.4.2}/README.md +1 -1
  3. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/__init__.py +1 -1
  4. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/chaisemartin_dhaultfoeuille.py +11 -6
  5. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/chaisemartin_dhaultfoeuille_results.py +8 -3
  6. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/conley.py +70 -19
  7. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/estimators.py +7 -2
  8. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/guides/llms-full.txt +26 -4
  9. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/guides/llms.txt +1 -5
  10. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/had.py +63 -7
  11. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/had_pretests.py +68 -0
  12. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/honest_did.py +115 -52
  13. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/imputation.py +127 -1
  14. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/imputation_bootstrap.py +75 -0
  15. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/imputation_results.py +90 -0
  16. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/linalg.py +531 -130
  17. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/results.py +19 -1
  18. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/spillover.py +708 -74
  19. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/stacked_did.py +276 -22
  20. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/stacked_did_results.py +29 -0
  21. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/staggered.py +424 -7
  22. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/staggered_bootstrap.py +47 -0
  23. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/staggered_results.py +49 -0
  24. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/sun_abraham.py +452 -45
  25. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/survey.py +195 -4
  26. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/triple_diff.py +181 -28
  27. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/twfe.py +179 -88
  28. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/two_stage.py +1182 -99
  29. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/utils.py +113 -50
  30. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/wooldridge.py +806 -106
  31. diff_diff-3.4.2/diff_diff/wooldridge_results.py +850 -0
  32. {diff_diff-3.4.0 → diff_diff-3.4.2}/pyproject.toml +1 -1
  33. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/Cargo.lock +9 -9
  34. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/Cargo.toml +1 -1
  35. diff_diff-3.4.0/diff_diff/wooldridge_results.py +0 -373
  36. {diff_diff-3.4.0 → diff_diff-3.4.2}/LICENSE +0 -0
  37. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/_backend.py +0 -0
  38. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/_guides_api.py +0 -0
  39. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/_nprobust_port.py +0 -0
  40. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/_reporting_helpers.py +0 -0
  41. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/agent_workflow.py +0 -0
  42. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/bacon.py +0 -0
  43. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/bootstrap_utils.py +0 -0
  44. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/business_report.py +0 -0
  45. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
  46. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/continuous_did.py +0 -0
  47. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/continuous_did_bspline.py +0 -0
  48. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/continuous_did_results.py +0 -0
  49. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/datasets.py +0 -0
  50. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/diagnostic_report.py +0 -0
  51. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/diagnostics.py +0 -0
  52. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/efficient_did.py +0 -0
  53. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/efficient_did_bootstrap.py +0 -0
  54. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/efficient_did_covariates.py +0 -0
  55. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/efficient_did_results.py +0 -0
  56. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/efficient_did_weights.py +0 -0
  57. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/guides/__init__.py +0 -0
  58. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/guides/llms-autonomous.txt +0 -0
  59. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/guides/llms-practitioner.txt +0 -0
  60. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/local_linear.py +0 -0
  61. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/power.py +0 -0
  62. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/practitioner.py +0 -0
  63. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/prep.py +0 -0
  64. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/prep_dgp.py +0 -0
  65. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/pretrends.py +0 -0
  66. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/profile.py +0 -0
  67. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/staggered_aggregation.py +0 -0
  68. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/staggered_triple_diff.py +0 -0
  69. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/staggered_triple_diff_results.py +0 -0
  70. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/synthetic_did.py +0 -0
  71. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/trop.py +0 -0
  72. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/trop_global.py +0 -0
  73. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/trop_local.py +0 -0
  74. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/trop_results.py +0 -0
  75. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/two_stage_bootstrap.py +0 -0
  76. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/two_stage_results.py +0 -0
  77. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/__init__.py +0 -0
  78. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_common.py +0 -0
  79. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_continuous.py +0 -0
  80. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_diagnostic.py +0 -0
  81. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_event_study.py +0 -0
  82. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_power.py +0 -0
  83. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_staggered.py +0 -0
  84. {diff_diff-3.4.0 → diff_diff-3.4.2}/diff_diff/visualization/_synthetic.py +0 -0
  85. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/build.rs +0 -0
  86. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/src/bootstrap.rs +0 -0
  87. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/src/lib.rs +0 -0
  88. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/src/linalg.rs +0 -0
  89. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/src/trop.rs +0 -0
  90. {diff_diff-3.4.0 → diff_diff-3.4.2}/rust/src/weights.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 3.4.0
3
+ Version: 3.4.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -159,7 +159,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
159
159
  - [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
160
160
  - [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html) - Borusyak, Jaravel & Spiess (2024) imputation estimator, most efficient under homogeneous effects
161
161
  - [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
162
- - [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing
162
+ - [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing; supports survey-design variance under `survey_design=` for HC1 / CR1 (Wave E.1 Binder TSL) and Conley (Wave E.2 panel-aware stratified-Conley sandwich on per-period PSU totals; extended in Wave E.2 follow-up to `conley_lag_cutoff > 0` via panel-block composition with within-PSU serial Bartlett HAC — `lag>0` requires an effective PSU via explicit `survey_design.psu` or injected `cluster=<col>`); `SurveyDesign.subpopulation()` preserves full-design `n_psu` / `df_survey` via zero-padded scores (Wave E.3, R `svyrecvar(subset())` form)
163
163
  - [SyntheticDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - Synthetic DiD combining standard DiD and synthetic control for few treated units
164
164
  - [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
165
165
  - [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html) - Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
@@ -106,7 +106,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
106
106
  - [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
107
107
  - [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html) - Borusyak, Jaravel & Spiess (2024) imputation estimator, most efficient under homogeneous effects
108
108
  - [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
109
- - [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing
109
+ - [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html) - Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover on near-control units; handles non-staggered and staggered timing; supports survey-design variance under `survey_design=` for HC1 / CR1 (Wave E.1 Binder TSL) and Conley (Wave E.2 panel-aware stratified-Conley sandwich on per-period PSU totals; extended in Wave E.2 follow-up to `conley_lag_cutoff > 0` via panel-block composition with within-PSU serial Bartlett HAC — `lag>0` requires an effective PSU via explicit `survey_design.psu` or injected `cluster=<col>`); `SurveyDesign.subpopulation()` preserves full-design `n_psu` / `df_survey` via zero-padded scores (Wave E.3, R `svyrecvar(subset())` form)
110
110
  - [SyntheticDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - Synthetic DiD combining standard DiD and synthetic control for few treated units
111
111
  - [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html) - triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
112
112
  - [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html) - Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
@@ -293,7 +293,7 @@ ETWFE = WooldridgeDiD
293
293
  DCDH = ChaisemartinDHaultfoeuille
294
294
  HAD = HeterogeneousAdoptionDiD
295
295
 
296
- __version__ = "3.4.0"
296
+ __version__ = "3.4.2"
297
297
  __all__ = [
298
298
  # Estimators
299
299
  "DifferenceInDifferences",
@@ -19,7 +19,7 @@ References
19
19
  - de Chaisemartin, C. & D'Haultfoeuille, X. (2020). Two-Way Fixed Effects
20
20
  Estimators with Heterogeneous Treatment Effects. *American Economic
21
21
  Review*, 110(9), 2964-2996.
22
- - de Chaisemartin, C. & D'Haultfoeuille, X. (2022, revised 2023).
22
+ - de Chaisemartin, C. & D'Haultfoeuille, X. (2022, revised July 2023).
23
23
  Difference-in-Differences Estimators of Intertemporal Treatment Effects.
24
24
  NBER Working Paper 29873. Web Appendix Section 3.7.3 contains the
25
25
  cohort-recentered plug-in variance formula implemented here.
@@ -4506,11 +4506,15 @@ def _compute_per_period_dids(
4506
4506
  leaver_mask = (d_prev == 1) & (d_curr == 0) & present
4507
4507
  stable1_mask = (d_prev == 1) & (d_curr == 1) & present
4508
4508
 
4509
- # AER 2020 Theorem 3 N_{a,b,t} weights are CELL counts, not
4510
- # within-cell observation sums. Each (g, t) cell contributes once
4509
+ # Library equal-cell weighting: N_{a,b,t} here counts (g, t)
4510
+ # cells in each transition state. Each cell contributes once
4511
4511
  # regardless of how many original observations fed into the
4512
- # y_gt cell mean. See REGISTRY.md ChaisemartinDHaultfoeuille
4513
- # estimator equations.
4512
+ # y_gt cell mean. This is a documented library deviation from
4513
+ # AER 2020 Equation 3 (which defines N_{d,d',t} = sum N_{g,t},
4514
+ # i.e., observation-sum weighting) and from R DIDmultiplegtDYN
4515
+ # (individual-row weighting on unbalanced inputs). See
4516
+ # REGISTRY.md ## ChaisemartinDHaultfoeuille L517 "Note (deviation
4517
+ # from R DIDmultiplegtDYN ...)" for the full text.
4514
4518
  n_10 = int(joiner_mask.sum())
4515
4519
  n_00 = int(stable0_mask.sum())
4516
4520
  n_01 = int(leaver_mask.sum())
@@ -4635,7 +4639,8 @@ def _compute_placebo(
4635
4639
  leaver_mask = (d_pre_prev == 1) & (d_prev == 1) & (d_curr == 0) & present
4636
4640
  stable1_mask = (d_pre_prev == 1) & (d_prev == 1) & (d_curr == 1) & present
4637
4641
 
4638
- # Placebo weights are CELL counts (matching Theorem 3 convention)
4642
+ # Placebo weights: library equal-cell counts (same documented
4643
+ # deviation as the main DID_M path; see REGISTRY.md L517).
4639
4644
  n_10 = int(joiner_mask.sum())
4640
4645
  n_00 = int(stable0_mask.sum())
4641
4646
  n_01 = int(leaver_mask.sum())
@@ -13,7 +13,7 @@ References
13
13
  - de Chaisemartin, C. & D'Haultfoeuille, X. (2020). Two-Way Fixed Effects
14
14
  Estimators with Heterogeneous Treatment Effects. *American Economic
15
15
  Review*, 110(9), 2964-2996.
16
- - de Chaisemartin, C. & D'Haultfoeuille, X. (2022, revised 2023).
16
+ - de Chaisemartin, C. & D'Haultfoeuille, X. (2022, revised July 2023).
17
17
  Difference-in-Differences Estimators of Intertemporal Treatment Effects.
18
18
  NBER Working Paper 29873.
19
19
  """
@@ -335,8 +335,13 @@ class ChaisemartinDHaultfoeuilleResults:
335
335
  ``L_max >= 1`` it was repurposed to hold the per-group count
336
336
  that matches the ``DID_1`` estimand. Originally equals
337
337
  once regardless of how many original observations fed into it.
338
- This is the ``N_S`` denominator of ``DID_M`` per AER 2020
339
- Theorem 3 cell counts, not within-cell observation counts.
338
+ This is the ``N_S`` denominator of ``DID_M`` under the library's
339
+ equal-cell weighting convention (cell counts, not within-cell
340
+ observation sums). The AER 2020 paper's Equation 3 defines
341
+ ``N_{d,d',t} = sum_g N_{g,t}`` (observation sums); the
342
+ library's choice is a documented deviation - see
343
+ ``docs/methodology/REGISTRY.md`` ``## ChaisemartinDHaultfoeuille``
344
+ L517 for the full Note.
340
345
  n_cohorts : int
341
346
  Distinct cohorts ``(D_{g,1}, F_g, S_g)`` after filtering.
342
347
  n_groups_dropped_crossers : int
@@ -365,6 +365,61 @@ def _uniform_kernel(u: np.ndarray) -> np.ndarray:
365
365
  return (np.abs(u) <= 1.0).astype(np.float64)
366
366
 
367
367
 
368
+ def _serial_bartlett_kernel_matrix(t_codes: np.ndarray, L: int) -> np.ndarray:
369
+ """Within-unit Newey-West (1987) Bartlett HAC kernel matrix for serial
370
+ correlation in panel data, indexed by panel-wide dense time codes.
371
+
372
+ Returns the K matrix with ``K[i, j] = 1 - |t_i - t_j| / (L + 1)`` for
373
+ ``0 < |t_i - t_j| <= L``, else 0. The lag-0 diagonal is excluded so
374
+ callers can add this to a spatial within-period meat without
375
+ double-counting the diagonal.
376
+
377
+ Uses the 1-D radial pairwise form (matches conleyreg::time_dist), NOT
378
+ Conley 1999 Eq 3.14's 2-D separable product window — see the methodology
379
+ lock at :func:`_compute_conley_meat` for context.
380
+ """
381
+ t = t_codes.astype(np.float64, copy=False)
382
+ lag_mat = np.abs(t[:, None] - t[None, :])
383
+ return ((lag_mat <= L) & (lag_mat != 0)).astype(np.float64) * (1.0 - lag_mat / (L + 1.0))
384
+
385
+
386
+ def _validate_meat_psd(
387
+ M: np.ndarray,
388
+ *,
389
+ error_msg: str,
390
+ warning_template: str,
391
+ stacklevel: int = 3,
392
+ ) -> None:
393
+ """Finite + PSD guard for sandwich meat matrices. Raises ``ValueError``
394
+ on non-finite entries; warns ``UserWarning`` when ``min(eigvalsh(M)) <
395
+ -1e-12``.
396
+
397
+ Parameters
398
+ ----------
399
+ error_msg
400
+ Message passed to ``ValueError`` on non-finite entries.
401
+ warning_template
402
+ Format string for the negative-eigenvalue warning. May contain an
403
+ ``{eigval}`` placeholder; the caller embeds ``{eigval:.2e}`` directly
404
+ in the template so the helper formats the minimum eigenvalue with
405
+ scientific notation.
406
+ stacklevel
407
+ Frame count from inside ``_validate_meat_psd``: ``stacklevel=N``
408
+ attributes the warning to the Nth caller above the helper itself.
409
+ Default 3 covers a single intermediate frame (the helper's direct
410
+ caller's caller); pass an explicit value matching call-site depth.
411
+ """
412
+ if not np.all(np.isfinite(M)):
413
+ raise ValueError(error_msg)
414
+ eigvals = np.linalg.eigvalsh(M)
415
+ if eigvals.size and eigvals.min() < -1e-12:
416
+ warnings.warn(
417
+ warning_template.format(eigval=eigvals.min()),
418
+ UserWarning,
419
+ stacklevel=stacklevel,
420
+ )
421
+
422
+
368
423
  def _compute_spatial_bartlett_meat_sparse(
369
424
  S: np.ndarray,
370
425
  coords: np.ndarray,
@@ -957,37 +1012,33 @@ def _compute_conley_meat(
957
1012
  mask_u = unit_arr == u_val
958
1013
  scores_u = scores[mask_u]
959
1014
  # Use dense panel-period codes (NOT raw labels) for lag math.
960
- t_u = time_codes[mask_u].astype(np.float64)
961
- lag_mat = np.abs(t_u[:, None] - t_u[None, :])
962
- K_u = ((lag_mat <= L) & (lag_mat != 0)).astype(np.float64) * (
963
- 1.0 - lag_mat / (L + 1.0)
964
- )
1015
+ K_u = _serial_bartlett_kernel_matrix(time_codes[mask_u], L)
965
1016
  meat += scores_u.T @ K_u @ scores_u
966
- if not np.all(np.isfinite(meat)):
967
- raise ValueError(
968
- "Conley meat contains non-finite values; check residuals and "
969
- "score matrix for NaN/Inf."
970
- )
971
-
972
1017
  # PSD guard. Neither the uniform kernel (Conley 1999 fn 11) nor the
973
1018
  # radial 1-D Bartlett specialization is formally PSD-guaranteed —
974
1019
  # Conley's explicit PSD Bartlett formula (Eq 3.14) is the 2-D separable
975
1020
  # product window, not the 1-D radial pairwise form that R `conleyreg`,
976
1021
  # Stata `acreg`, and this implementation use. Check both kernels.
977
- eigvals = np.linalg.eigvalsh(meat)
978
- if eigvals.size and eigvals.min() < -1e-12:
979
- warnings.warn(
1022
+ # ``{eigval:.2e}`` is a literal placeholder for ``_validate_meat_psd``;
1023
+ # only ``{kernel!r}`` is interpolated by the f-string here.
1024
+ _validate_meat_psd(
1025
+ meat,
1026
+ error_msg=(
1027
+ "Conley meat contains non-finite values; check residuals and "
1028
+ "score matrix for NaN/Inf."
1029
+ ),
1030
+ warning_template=(
980
1031
  f"Conley meat with conley_kernel={kernel!r} has a materially "
981
- f"negative eigenvalue ({eigvals.min():.2e}); the variance "
1032
+ "negative eigenvalue ({eigval:.2e}); the variance "
982
1033
  "estimator is not guaranteed PSD on this design. Both "
983
1034
  "supported kernels (radial bartlett and uniform) are "
984
1035
  "practitioner specializations of Conley 1999 and are not "
985
1036
  "formally PSD-guaranteed; consider varying conley_cutoff_km "
986
1037
  "or reviewing the design for collinearity / degenerate "
987
- "residual structure.",
988
- UserWarning,
989
- stacklevel=3,
990
- )
1038
+ "residual structure."
1039
+ ),
1040
+ stacklevel=4,
1041
+ )
991
1042
 
992
1043
  return meat
993
1044
 
@@ -1891,8 +1891,13 @@ class MultiPeriodDiD(DifferenceInDifferences):
1891
1891
  else:
1892
1892
  # Cluster-aware CR2 BM Satterthwaite DOF for per-coefficient
1893
1893
  # AND post-period-average compound contrast (Gate 6 lift).
1894
- # Weighted CR2-BM is a separate gate; survey paths never
1895
- # reach this block (outer `not _use_survey_vcov` guard).
1894
+ # This branch is guarded above by `not _use_survey_vcov`,
1895
+ # so when reached, `survey_weights` is None (survey designs
1896
+ # always route through the TSL path). The clubSandwich
1897
+ # WLS-CR2 port lifted `_compute_cr2_bm_contrast_dof` to
1898
+ # accept `weights=`, but no MPD entry point currently
1899
+ # passes non-None weights here — `weights=None` is the
1900
+ # de facto contract on this code path today.
1896
1901
  _dof_all = _compute_cr2_bm_contrast_dof(
1897
1902
  X_kept,
1898
1903
  effective_cluster_ids,
@@ -2,7 +2,7 @@
2
2
 
3
3
  > A Python library for Difference-in-Differences causal inference analysis. Provides sklearn-like estimators with statsmodels-style output for econometric analysis.
4
4
 
5
- - Version: 3.4.0
5
+ - Version: 3.4.2
6
6
  - Repository: https://github.com/igerber/diff-diff
7
7
  - License: MIT
8
8
  - Dependencies: numpy, pandas, scipy (no statsmodels dependency)
@@ -188,7 +188,7 @@ CallawaySantAnna(
188
188
  anticipation: int = 0, # Anticipation periods
189
189
  estimation_method: str = "dr", # "dr", "ipw", or "reg"
190
190
  alpha: float = 0.05,
191
- cluster: str | None = None, # Defaults to unit-level clustering
191
+ cluster: str | None = None, # Cluster col; activates CR1 on the IF via synthesized SurveyDesign(psu=col). None → per-unit IF.
192
192
  n_bootstrap: int = 0, # 0 = analytical SEs, 999+ recommended
193
193
  bootstrap_weights: str | None = None, # "rademacher", "mammen", or "webb"
194
194
  seed: int | None = None,
@@ -196,6 +196,7 @@ CallawaySantAnna(
196
196
  base_period: str = "varying", # "varying" or "universal"
197
197
  cband: bool = True, # Simultaneous confidence bands
198
198
  pscore_trim: float = 0.01, # Propensity score trimming bound
199
+ vcov_type: str = "hc1", # {"hc1"} only — IF-based variance per Callaway & Sant'Anna (2021). Analytical-sandwich {classical, hc2, hc2_bm} and conley REJECTED at __init__ (see REGISTRY.md IF-vs-sandwich subsection).
199
200
  )
200
201
  ```
201
202
 
@@ -335,10 +336,11 @@ SunAbraham(
335
336
  control_group: str = "never_treated", # "never_treated" or "not_yet_treated"
336
337
  anticipation: int = 0,
337
338
  alpha: float = 0.05,
338
- cluster: str | None = None, # Defaults to unit-level clustering
339
+ cluster: str | None = None, # Defaults to unit-level clustering (dropped on explicit vcov_type='hc2' / 'classical')
339
340
  n_bootstrap: int = 0, # 0 = analytical cluster-robust SEs
340
341
  seed: int | None = None,
341
342
  rank_deficient_action: str = "warn",
343
+ vcov_type: str = "hc1", # {"classical","hc1","hc2","hc2_bm"}; classical/hc2/hc2_bm route through full-dummy saturated design. survey_design=... is rejected for classical/hc2/hc2_bm (use hc1 default for surveys)
342
344
  )
343
345
  ```
344
346
 
@@ -377,12 +379,14 @@ ImputationDiD(
377
379
  anticipation: int = 0,
378
380
  alpha: float = 0.05,
379
381
  cluster: str | None = None, # Defaults to unit-level clustering
382
+ vcov_type: str = "hc1", # {"hc1"} only — IF-based variance per Borusyak et al. (2024) Theorem 3
380
383
  n_bootstrap: int = 0, # 0 = analytical (Theorem 3 variance)
381
384
  bootstrap_weights: str = "rademacher", # "rademacher", "mammen", or "webb"
382
385
  seed: int | None = None,
383
386
  rank_deficient_action: str = "warn",
384
387
  horizon_max: int | None = None, # Max event-study horizon
385
388
  aux_partition: str = "cohort_horizon", # "cohort_horizon", "cohort", or "horizon"
389
+ pretrends: bool = False, # Include pre-treatment horizons in event study
386
390
  )
387
391
  ```
388
392
 
@@ -400,6 +404,7 @@ imp.fit(
400
404
  covariates: list[str] = None,
401
405
  aggregate: str = None, # None, "simple", "event_study", "group", or "all"
402
406
  balance_e: int = None,
407
+ survey_design: SurveyDesign = None, # Optional design-based inference (pweight + analytical strata/PSU/FPC or replicate BRR/Fay/JK1/JKn/SDR)
403
408
  ) -> ImputationDiDResults
404
409
  ```
405
410
 
@@ -575,6 +580,7 @@ TripleDifference(
575
580
  estimation_method: str = "dr", # "dr", "reg", or "ipw"
576
581
  robust: bool = True,
577
582
  cluster: str | None = None,
583
+ vcov_type: str = "hc1", # {"hc1"} only — IF-based variance per Ortiz-Villavicencio & Sant'Anna (2025). Analytical-sandwich {classical, hc2, hc2_bm} and conley REJECTED at __init__ (see REGISTRY.md IF-vs-sandwich subsection).
578
584
  alpha: float = 0.05,
579
585
  pscore_trim: float = 0.01,
580
586
  rank_deficient_action: str = "warn",
@@ -742,6 +748,7 @@ StackedDiD(
742
748
  alpha: float = 0.05,
743
749
  anticipation: int = 0,
744
750
  rank_deficient_action: str = "warn",
751
+ vcov_type: str = "hc1", # {"hc1","hc2_bm"}; classical/hc2 rejected (intrinsically clustered), conley deferred. survey_design=... requires hc1
745
752
  )
746
753
  ```
747
754
 
@@ -965,6 +972,13 @@ WooldridgeDiD(
965
972
  bootstrap_weights: str = "rademacher",
966
973
  seed: int | None = None,
967
974
  rank_deficient_action: str = "warn",
975
+ vcov_type: str = "hc1", # {"classical","hc1","hc2","hc2_bm"}; OLS path only.
976
+ # hc1 (default) preserves prior bit-equal within-transform CR1.
977
+ # hc2_bm auto-routes to full-dummy + clubSandwich WLS-CR2 algebra.
978
+ # classical/hc2 auto-drop the unit auto-cluster (one-way only);
979
+ # explicit cluster="X" + one-way raises at the linalg validator.
980
+ # conley deferred. method != "ols" requires hc1 (QMLE CR2-BM TBD).
981
+ # survey_design= requires hc1 (survey TSL overrides analytical).
968
982
  )
969
983
  ```
970
984
 
@@ -1267,7 +1281,15 @@ ImputationDiDResults, TwoStageDiDResults, StackedDiDResults, and EfficientDiDRes
1267
1281
 
1268
1282
  Each event study effect dict contains: `effect`, `se`, `t_stat`, `p_value`, `conf_int`, `n_obs` (or `n_groups`).
1269
1283
 
1270
- **Methods:** `summary()`, `print_summary()`, `to_dataframe()`
1284
+ **Variance metadata** (`ImputationDiDResults` carries these; other staggered Results may surface a subset):
1285
+
1286
+ | Attribute | Type | Description |
1287
+ |-----------|------|-------------|
1288
+ | `vcov_type` | `str` | Variance estimator family (`"hc1"` for IF-based estimators; permanently narrow on `ImputationDiD` / `CallawaySantAnna` / `TripleDifference` per IF-vs-sandwich taxonomy) |
1289
+ | `cluster_name` | `str | None` | Effective cluster column name (e.g. `"unit"` for default `cluster=None`); `None` under survey designs (the survey block already names PSU/strata) |
1290
+ | `n_clusters` | `int | None` | Number of effective clusters; `None` under survey designs |
1291
+
1292
+ **Methods:** `summary()`, `print_summary()`, `to_dataframe()`, `to_dict()` (flat dict of headline aliases + `vcov_type` + conditional `cluster_name`/`n_clusters`/`n_bootstrap`/`inference_method`)
1271
1293
 
1272
1294
  ### ContinuousDiDResults
1273
1295
 
@@ -10,10 +10,6 @@ diff-diff offers 17 estimators covering basic 2x2 DiD, modern staggered adoption
10
10
  - Source: https://github.com/igerber/diff-diff
11
11
  - Docs: https://diff-diff.readthedocs.io/en/stable/
12
12
 
13
- ## Agent Quickstart
14
-
15
- LLM agents: call `diff_diff.agent_workflow(df, unit=..., time=..., treatment=..., outcome=...)` first. It prints the recommended 5-step workflow (`profile_panel` → `get_llm_guide` → `<Estimator>().fit` → `practitioner_next_steps` → `BusinessReport`) with your column names wired in.
16
-
17
13
  ## Practitioner Workflow (based on Baker et al. 2025)
18
14
 
19
15
  IMPORTANT: For rigorous DiD analysis, follow these 8 steps. Skipping
@@ -62,7 +58,7 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")`
62
58
  - [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html): Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
63
59
  - [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html): Borusyak, Jaravel & Spiess (2024) imputation estimator — most efficient under homogeneous effects
64
60
  - [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html): Gardner (2022) two-stage estimator with GMM sandwich variance
65
- - [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html): Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover-on-control; reuses `conley_coords` for ring construction; handles non-staggered and staggered timing
61
+ - [SpilloverDiD](https://diff-diff.readthedocs.io/en/stable/api/spillover.html): Butts (2021) ring-indicator spillover-aware DiD identifying direct effect on treated + per-ring spillover-on-control; reuses `conley_coords` for ring construction; handles non-staggered and staggered timing; supports `SurveyDesign(weights, strata, psu, fpc)` under `vcov_type="hc1"` with optional `cluster=<col>` for CR1 via Gerber (2026) Binder TSL (Wave E.1) and under `vcov_type="conley"` via a panel-aware stratified-Conley sandwich on per-period PSU totals (Wave E.2 cross-sectional `conley_lag_cutoff=0`) extended in Wave E.2 follow-up to `conley_lag_cutoff > 0` via panel-block composition with within-PSU serial Bartlett HAC (Newey-West 1987 separable form; `lag>0` requires an effective PSU via explicit `survey_design.psu` or injected `cluster=<col>`), both composed with the Wave D Gardner GMM correction; `SurveyDesign.subpopulation()` preserves full-design `n_psu` / `df_survey` via zero-padded scores at the meat-helper boundary (Wave E.3, R `svyrecvar(subset())` form) (replicate weights queued as follow-up)
66
62
  - [SyntheticDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html): Synthetic DiD combining standard DiD and synthetic control methods for few treated units
67
63
  - [TripleDifference](https://diff-diff.readthedocs.io/en/stable/api/triple_diff.html): Triple difference (DDD) estimator for designs requiring two criteria for treatment eligibility
68
64
  - [ContinuousDiD](https://diff-diff.readthedocs.io/en/stable/api/continuous_did.html): Callaway, Goodman-Bacon & Sant'Anna (2024) continuous treatment DiD with dose-response curves
@@ -2595,6 +2595,50 @@ class HeterogeneousAdoptionDiD:
2595
2595
 
2596
2596
  Notes
2597
2597
  -----
2598
+ **Non-testable assumptions (paper Section 3.1.2).** Point identification
2599
+ of ``WAS_{d_lower}`` on the Design 1 family
2600
+ (``continuous_near_d_lower`` and ``mass_point``) requires Assumption 6
2601
+ in addition to parallel trends; sign identification requires
2602
+ Assumption 5. Neither is testable via pre-trends:
2603
+
2604
+ - Assumption 5 (sign identification): the boundary slope-ratio
2605
+ ``lim_{d down d_lower} E(TE_2 | D_2 <= d) / WAS < E(D_2) / d_lower``
2606
+ relates the conditional expectation near the boundary to the
2607
+ overall WAS; it cannot be inferred from pre-period outcome
2608
+ trajectories alone.
2609
+ - Assumption 6 (point identification): the counterfactual-mean
2610
+ alignment ``lim_{d down d_lower} E[Y_2(d_lower) - Y_2(0) | D_2 <= d]
2611
+ = E[Y_2(d_lower) - Y_2(0)]`` is a statement about an unobserved
2612
+ counterfactual at the support infimum.
2613
+
2614
+ The fit() method emits a ``UserWarning`` whenever ``resolved_design``
2615
+ is on the Design 1 family (``continuous_near_d_lower`` or
2616
+ ``mass_point``) so users are not silently led to interpret point
2617
+ estimates as full point identification. The available pre-tests
2618
+ verify ADJACENT identifying conditions:
2619
+
2620
+ - :func:`diff_diff.qug_test`: Theorem 4 / Design 1' support-infimum
2621
+ null ``d_lower = 0`` (adjacent evidence on the ``d_lower = 0``
2622
+ clause of Assumption 4 only, NOT a test of the full Assumption 4
2623
+ statement which also covers boundary-density positivity,
2624
+ conditional-mean smoothness, conditional-variance regularity, and
2625
+ bandwidth conditions).
2626
+ - :func:`diff_diff.stute_test` / :func:`diff_diff.yatchew_hr_test`:
2627
+ Assumption 8 linearity of ``E[ΔY | D_2]`` in ``D_2`` (residuals
2628
+ from ``dy ~ 1 + d``).
2629
+ - :func:`diff_diff.joint_pretrends_test`: Assumption 7
2630
+ mean-independence pre-trends across multi-period placebos
2631
+ (intercept-only residual form via ``null_form="mean_independence"``;
2632
+ the raw ``stute_test`` / ``yatchew_hr_test`` helpers do NOT cover
2633
+ Assumption 7 on their own).
2634
+
2635
+ None of these test Assumptions 5 or 6 directly. The Assumption 5/6
2636
+ non-testability caveat is surfaced by the Design 1 fit-time
2637
+ ``UserWarning`` and by T21 (HAD pretest workflow tutorial) prose,
2638
+ NOT by the composite workflow verdict string (which only flags the
2639
+ Assumption 7 step-2 gap on the two-period ``aggregate="overall"``
2640
+ path).
2641
+
2598
2642
  **Diagnostics coverage.** ``HeterogeneousAdoptionDiDResults.bandwidth_diagnostics``
2599
2643
  and ``.bias_corrected_fit`` are populated only on the continuous
2600
2644
  paths; both are ``None`` on the mass-point path (which is parametric
@@ -2831,13 +2875,22 @@ class HeterogeneousAdoptionDiD:
2831
2875
  first_treat_col : str or None
2832
2876
  Optional first-treatment column (the period at which each
2833
2877
  unit first receives treatment; ``0`` for never-treated).
2834
- Required on the event-study path when the panel has more
2835
- than two distinct first-treat values (staggered timing):
2836
- the estimator auto-filters to the last-treatment cohort
2837
- with a ``UserWarning`` per paper Appendix B.2 prescription.
2838
2878
  For common-adoption panels the column is optional; when
2839
2879
  omitted, the event-study path infers the first-treatment
2840
- period ``F`` from the dose invariant.
2880
+ period ``F`` from the dose invariant. **Staggered-timing
2881
+ contract (HAD Appendix B.2):**
2882
+
2883
+ - **`first_treat_col` supplied + multiple cohorts detected**:
2884
+ auto-filter to the last-treatment cohort + never-treated
2885
+ units with a ``UserWarning`` naming kept / dropped counts.
2886
+ - **`first_treat_col` omitted + multiple distinct first-
2887
+ positive-dose cohorts inferred from the dose path**: the
2888
+ estimator FAIL-CLOSES with ``ValueError`` directing the
2889
+ user to either pass ``first_treat_col`` (activates the
2890
+ auto-filter) or use :class:`ChaisemartinDHaultfoeuille`
2891
+ (``did_multiplegt_dyn``) for full staggered support. See
2892
+ REGISTRY § "Library extension: Staggered-timing fail-
2893
+ closed" for the rationale on raising vs. warning.
2841
2894
  aggregate : {"overall", "event_study"}
2842
2895
  ``"overall"`` (default): returns a single-period
2843
2896
  :class:`HeterogeneousAdoptionDiDResults` (Phase 2a). Requires
@@ -2847,8 +2900,11 @@ class HeterogeneousAdoptionDiD:
2847
2900
  event-time WAS estimates on the multi-period panel (paper
2848
2901
  Appendix B.2). Requires more than two time periods. Pointwise
2849
2902
  CIs per horizon; joint cross-horizon covariance is deferred
2850
- to a follow-up PR. Staggered-timing panels are auto-filtered
2851
- to the last-treatment cohort with a ``UserWarning``.
2903
+ to a follow-up PR. Staggered-timing panels: see the
2904
+ ``first_treat_col`` contract above (auto-filter to last
2905
+ cohort + never-treated with ``UserWarning`` when supplied;
2906
+ fail-closed ``ValueError`` when omitted on a staggered
2907
+ panel).
2852
2908
  survey_design : SurveyDesign or None, keyword-only
2853
2909
  Survey design (sampling weights + optional strata / PSU / FPC)
2854
2910
  for design-based inference. Supported on ALL design × aggregate
@@ -1349,6 +1349,22 @@ def qug_test(
1349
1349
 
1350
1350
  Notes
1351
1351
  -----
1352
+ **Scope (what this test does NOT cover).** ``qug_test`` tests the
1353
+ Theorem 4 / Design 1' support-infimum null ``H_0: d_lower = 0``. It
1354
+ does not validate the full Assumption 4 (Assumption 4 also requires
1355
+ positive boundary density, twice-differentiable conditional-mean,
1356
+ bounded continuous conditional-variance, and bandwidth regularity —
1357
+ QUG is adjacent evidence on the ``d_lower = 0`` clause only). It
1358
+ does NOT and CANNOT test Assumptions 5 and 6 from the same paper
1359
+ (Section 3.1.2), which are required for sign identification (A5) and
1360
+ point identification (A6) of ``WAS_{d_lower}`` on the Design 1 family
1361
+ (``d_lower > 0``). Assumptions 5 and 6 are statements about
1362
+ conditional expectations near the support boundary and about
1363
+ counterfactual-mean alignment respectively; they are non-testable via
1364
+ pre-trends. See :class:`HeterogeneousAdoptionDiD` class docstring
1365
+ Notes for the full statement and T21 (HAD pretest workflow tutorial)
1366
+ for the verdict-language convention that surfaces this gap.
1367
+
1352
1368
  Tie-break: when ``D_{(1)} == D_{(2)}`` the statistic is undefined.
1353
1369
  The test returns ``t_stat=NaN, p_value=NaN, reject=False`` with a
1354
1370
  ``UserWarning`` rather than raising.
@@ -1636,6 +1652,23 @@ def stute_test(
1636
1652
 
1637
1653
  Notes
1638
1654
  -----
1655
+ **Scope (what this test does NOT cover).** ``stute_test`` targets
1656
+ paper Assumption 8 (linearity of ``E[ΔY | D_2]`` in ``D_2``) — the
1657
+ raw helper always fits ``dy ~ 1 + d`` and tests the linearity null;
1658
+ it does NOT target Assumption 7 mean-independence pre-trends on its
1659
+ own. For Assumption 7 mean-independence (residuals from intercept-
1660
+ only ``dy ~ 1``), use :func:`joint_pretrends_test` (which routes
1661
+ ``null_form="mean_independence"`` into the joint CvM core). It does
1662
+ NOT and CANNOT test Assumptions 5 and 6 from de Chaisemartin et al.
1663
+ (2026) Section 3.1.2, which are required for sign / point
1664
+ identification of ``WAS_{d_lower}`` on the Design 1 family
1665
+ (``d_lower > 0``). Assumptions 5/6 are non-testable via pre-trends
1666
+ (boundary-conditional expectations and counterfactual-mean alignment
1667
+ statements); they are surfaced by the Design 1 fit-time
1668
+ ``UserWarning`` and by T21 tutorial prose, NOT by the workflow
1669
+ verdict string. See :class:`HeterogeneousAdoptionDiD` class
1670
+ docstring Notes for the full statement.
1671
+
1639
1672
  Sample-size gate: below ``G = 10`` the CvM statistic is not
1640
1673
  well-calibrated. In that case the function emits ``UserWarning`` and
1641
1674
  returns all-NaN inference rather than raising.
@@ -2112,6 +2145,20 @@ def yatchew_hr_test(
2112
2145
 
2113
2146
  Notes
2114
2147
  -----
2148
+ **Scope (what this test does NOT cover).** ``yatchew_hr_test`` targets
2149
+ paper Assumption 8 (linearity of ``E[ΔY | D_2]`` in ``D_2``) under
2150
+ ``null="linearity"`` (default); ``null="mean_independence"`` swaps
2151
+ the residual definition to intercept-only ``dy ~ 1`` for R parity
2152
+ with ``YatchewTest::yatchew_test(order=0)`` on pre-trend placebos.
2153
+ It does NOT and CANNOT test Assumptions 5 and 6 from de
2154
+ Chaisemartin et al. (2026) Section 3.1.2, which are required for
2155
+ sign / point identification of ``WAS_{d_lower}`` on the Design 1
2156
+ family (``d_lower > 0``). Assumptions 5/6 are non-testable via
2157
+ pre-trends; they are surfaced by the Design 1 fit-time
2158
+ ``UserWarning`` and by T21 tutorial prose, NOT by the workflow
2159
+ verdict string. See :class:`HeterogeneousAdoptionDiD` class
2160
+ docstring Notes for the full statement.
2161
+
2115
2162
  Sample-size gate: below ``G = 3`` the difference-variance estimator
2116
2163
  is undefined; the function emits ``UserWarning`` and returns NaN
2117
2164
  rather than raising.
@@ -4548,6 +4595,27 @@ def did_had_pretest_workflow(
4548
4595
 
4549
4596
  Notes
4550
4597
  -----
4598
+ **Scope (what this composite workflow does NOT cover).** The
4599
+ component pretests target the Theorem 4 / Design 1' support-infimum
4600
+ null (QUG: ``d_lower = 0``, adjacent evidence on the ``d_lower = 0``
4601
+ clause of Assumption 4 only — does not validate boundary density,
4602
+ conditional-mean smoothness, or variance regularity), Assumption 7
4603
+ (joint Stute pre-trends: mean-independence of placebo first-
4604
+ differences from dose), and Assumption 8 (Yatchew / joint
4605
+ homogeneity: linearity of treatment effects in dose). The workflow
4606
+ does NOT and CANNOT test Assumptions 5 and 6
4607
+ from de Chaisemartin et al. (2026) Section 3.1.2, which are required
4608
+ for sign / point identification of ``WAS_{d_lower}`` on the Design 1
4609
+ family (``d_lower > 0``). Assumptions 5/6 are non-testable via
4610
+ pre-trends. The composite verdict string does NOT mention
4611
+ Assumptions 5 or 6 — it only flags the Assumption 7 step-2 gap on
4612
+ the two-period ``aggregate="overall"`` path. The Assumption 5/6
4613
+ caveat is surfaced separately by (a) the
4614
+ ``HeterogeneousAdoptionDiD.fit()`` fit-time ``UserWarning`` (which
4615
+ fires whenever the resolved design is Design 1 family —
4616
+ ``continuous_near_d_lower`` or ``mass_point``) and (b) T21 (HAD
4617
+ pretest workflow tutorial) tutorial prose.
4618
+
4551
4619
  Survey/weighted data (Phase 4.5 C): under ``survey=`` or ``weights=``,
4552
4620
  the workflow:
4553
4621