diff-diff 3.6.0__tar.gz → 3.6.1__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 (96) hide show
  1. {diff_diff-3.6.0 → diff_diff-3.6.1}/PKG-INFO +4 -4
  2. {diff_diff-3.6.0 → diff_diff-3.6.1}/README.md +3 -3
  3. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/__init__.py +1 -1
  4. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/business_report.py +13 -4
  5. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/chaisemartin_dhaultfoeuille.py +13 -6
  6. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/chaisemartin_dhaultfoeuille_results.py +5 -3
  7. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/estimators.py +31 -46
  8. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/guides/llms-autonomous.txt +15 -6
  9. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/guides/llms-full.txt +9 -5
  10. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/guides/llms-practitioner.txt +6 -3
  11. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/guides/llms.txt +3 -3
  12. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/lpdid.py +461 -59
  13. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/lpdid_results.py +43 -2
  14. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/staggered.py +185 -33
  15. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/staggered_aggregation.py +16 -7
  16. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/staggered_results.py +9 -0
  17. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/trop.py +153 -22
  18. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/trop_global.py +17 -1
  19. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/trop_local.py +179 -19
  20. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/trop_results.py +33 -2
  21. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/utils.py +195 -89
  22. {diff_diff-3.6.0 → diff_diff-3.6.1}/pyproject.toml +1 -1
  23. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/Cargo.lock +1 -1
  24. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/Cargo.toml +1 -1
  25. {diff_diff-3.6.0 → diff_diff-3.6.1}/LICENSE +0 -0
  26. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/_backend.py +0 -0
  27. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/_guides_api.py +0 -0
  28. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/_nprobust_port.py +0 -0
  29. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/_reporting_helpers.py +0 -0
  30. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/agent_workflow.py +0 -0
  31. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/bacon.py +0 -0
  32. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/balancing.py +0 -0
  33. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/bootstrap_chunking.py +0 -0
  34. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/bootstrap_utils.py +0 -0
  35. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
  36. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/conformal.py +0 -0
  37. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/conley.py +0 -0
  38. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/continuous_did.py +0 -0
  39. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/continuous_did_bspline.py +0 -0
  40. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/continuous_did_results.py +0 -0
  41. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/datasets.py +0 -0
  42. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/diagnostic_report.py +0 -0
  43. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/diagnostics.py +0 -0
  44. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/efficient_did.py +0 -0
  45. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/efficient_did_bootstrap.py +0 -0
  46. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/efficient_did_covariates.py +0 -0
  47. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/efficient_did_results.py +0 -0
  48. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/efficient_did_weights.py +0 -0
  49. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/guides/__init__.py +0 -0
  50. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/had.py +0 -0
  51. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/had_pretests.py +0 -0
  52. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/honest_did.py +0 -0
  53. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/imputation.py +0 -0
  54. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/imputation_bootstrap.py +0 -0
  55. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/imputation_results.py +0 -0
  56. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/linalg.py +0 -0
  57. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/local_linear.py +0 -0
  58. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/power.py +0 -0
  59. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/practitioner.py +0 -0
  60. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/prep.py +0 -0
  61. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/prep_dgp.py +0 -0
  62. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/pretrends.py +0 -0
  63. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/profile.py +0 -0
  64. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/results.py +0 -0
  65. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/spillover.py +0 -0
  66. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/stacked_did.py +0 -0
  67. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/stacked_did_results.py +0 -0
  68. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/staggered_bootstrap.py +0 -0
  69. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/staggered_triple_diff.py +0 -0
  70. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/staggered_triple_diff_results.py +0 -0
  71. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/sun_abraham.py +0 -0
  72. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/survey.py +0 -0
  73. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/synthetic_control.py +0 -0
  74. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/synthetic_control_results.py +0 -0
  75. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/synthetic_did.py +0 -0
  76. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/triple_diff.py +0 -0
  77. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/twfe.py +0 -0
  78. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/two_stage.py +0 -0
  79. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/two_stage_bootstrap.py +0 -0
  80. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/two_stage_results.py +0 -0
  81. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/__init__.py +0 -0
  82. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_common.py +0 -0
  83. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_continuous.py +0 -0
  84. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_diagnostic.py +0 -0
  85. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_event_study.py +0 -0
  86. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_power.py +0 -0
  87. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_staggered.py +0 -0
  88. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/visualization/_synthetic.py +0 -0
  89. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/wooldridge.py +0 -0
  90. {diff_diff-3.6.0 → diff_diff-3.6.1}/diff_diff/wooldridge_results.py +0 -0
  91. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/build.rs +0 -0
  92. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/src/bootstrap.rs +0 -0
  93. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/src/lib.rs +0 -0
  94. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/src/linalg.rs +0 -0
  95. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/src/trop.rs +0 -0
  96. {diff_diff-3.6.0 → diff_diff-3.6.1}/rust/src/weights.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: diff-diff
3
- Version: 3.6.0
3
+ Version: 3.6.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -155,7 +155,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
155
155
  - [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - panel data DiD with unit and time fixed effects via within-transformation or dummies
156
156
  - [MultiPeriodDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - event study design with period-specific treatment effects for dynamic analysis
157
157
  - [CallawaySantAnna](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Callaway & Sant'Anna (2021) group-time ATT estimator for staggered adoption
158
- - [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The only library option for treatments that switch on AND off. Alias `DCDH`.
158
+ - [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The most general option for treatments that switch on AND off (see also `LPDiD`/`TROP` `non_absorbing`). Alias `DCDH`.
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
@@ -170,7 +170,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
170
170
  - [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
171
171
  - [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
172
172
  - [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html) - Wooldridge (2023, 2025) ETWFE: saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias `ETWFE`.
173
- - [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing treatment
173
+ - [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing or non-absorbing (reversible) treatment
174
174
  - [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
175
175
 
176
176
  ## Diagnostics & Sensitivity
@@ -198,7 +198,7 @@ No other Python or R DiD package offers design-based variance estimation for mod
198
198
  - Python 3.9 - 3.14
199
199
  - numpy >= 1.20
200
200
  - pandas >= 1.3
201
- - scipy >= 1.7
201
+ - scipy >= 1.10
202
202
 
203
203
  ## Development
204
204
 
@@ -102,7 +102,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
102
102
  - [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - panel data DiD with unit and time fixed effects via within-transformation or dummies
103
103
  - [MultiPeriodDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - event study design with period-specific treatment effects for dynamic analysis
104
104
  - [CallawaySantAnna](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Callaway & Sant'Anna (2021) group-time ATT estimator for staggered adoption
105
- - [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The only library option for treatments that switch on AND off. Alias `DCDH`.
105
+ - [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The most general option for treatments that switch on AND off (see also `LPDiD`/`TROP` `non_absorbing`). Alias `DCDH`.
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
@@ -117,7 +117,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
117
117
  - [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
118
118
  - [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
119
119
  - [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html) - Wooldridge (2023, 2025) ETWFE: saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias `ETWFE`.
120
- - [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing treatment
120
+ - [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing or non-absorbing (reversible) treatment
121
121
  - [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
122
122
 
123
123
  ## Diagnostics & Sensitivity
@@ -145,7 +145,7 @@ No other Python or R DiD package offers design-based variance estimation for mod
145
145
  - Python 3.9 - 3.14
146
146
  - numpy >= 1.20
147
147
  - pandas >= 1.3
148
- - scipy >= 1.7
148
+ - scipy >= 1.10
149
149
 
150
150
  ## Development
151
151
 
@@ -301,7 +301,7 @@ ETWFE = WooldridgeDiD
301
301
  DCDH = ChaisemartinDHaultfoeuille
302
302
  HAD = HeterogeneousAdoptionDiD
303
303
 
304
- __version__ = "3.6.0"
304
+ __version__ = "3.6.1"
305
305
  __all__ = [
306
306
  # Estimators
307
307
  "DifferenceInDifferences",
@@ -353,12 +353,21 @@ class BusinessReport:
353
353
  """Return a structured multi-section markdown report."""
354
354
  base = _render_full_report(self.to_dict())
355
355
  if self._include_appendix:
356
+ appendix_text = None
356
357
  try:
357
358
  appendix = self._results.summary()
358
- except Exception: # noqa: BLE001
359
- appendix = None
360
- if appendix:
361
- base = base + "\n\n## Technical Appendix\n\n```\n" + str(appendix) + "\n```\n"
359
+ if appendix:
360
+ appendix_text = str(appendix)
361
+ except Exception as exc: # noqa: BLE001
362
+ appendix_error = type(exc).__name__ or "Exception"
363
+ base = (
364
+ base
365
+ + "\n\n## Technical Appendix\n\n"
366
+ + "Technical appendix unavailable: estimator summary rendering failed "
367
+ + f"({appendix_error}).\n"
368
+ )
369
+ if appendix_text:
370
+ base = base + "\n\n## Technical Appendix\n\n```\n" + appendix_text + "\n```\n"
362
371
  return base
363
372
 
364
373
  def export_markdown(self) -> str:
@@ -1,9 +1,14 @@
1
1
  """
2
2
  de Chaisemartin-D'Haultfoeuille (dCDH) estimator for reversible-treatment DiD.
3
3
 
4
- The dCDH estimator is the only modern DiD estimator in the diff-diff library
5
- that handles **non-absorbing (reversible) treatments** — treatment can switch
6
- on AND off over time. All other staggered estimators in the library
4
+ The dCDH estimator is the most general DiD estimator in the diff-diff library
5
+ for **non-absorbing (reversible) treatments** — treatment can switch on AND off
6
+ over time, switcher vs non-switcher comparisons are its primitive object, and it
7
+ allows dynamic (carryover) effects with explicit joiner/leaver (``DID_+`` /
8
+ ``DID_-``) decomposition. ``LPDiD`` (``non_absorbing="first_entry"`` /
9
+ ``"effect_stabilization"``) and ``TROP`` (``non_absorbing=True``, under a
10
+ no-dynamic-effects assumption) also accept non-absorbing treatment under stronger
11
+ assumptions. The remaining staggered estimators in the library
7
12
  (``CallawaySantAnna``, ``SunAbraham``, ``ImputationDiD``, ``TwoStageDiD``,
8
13
  ``EfficientDiD``, ``WooldridgeDiD``) assume treatment is absorbing.
9
14
 
@@ -354,9 +359,11 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin):
354
359
  """
355
360
  de Chaisemartin-D'Haultfoeuille (dCDH) estimator.
356
361
 
357
- The only modern DiD estimator in the library that handles **reversible
358
- (non-absorbing) treatments** - treatment may switch on AND off over
359
- time. Computes the contemporaneous-switch DiD ``DID_M`` from the
362
+ The most general library estimator for **reversible (non-absorbing)
363
+ treatments** - treatment may switch on AND off over time, with explicit
364
+ joiner/leaver (``DID_+`` / ``DID_-``) decomposition (``LPDiD`` and ``TROP``
365
+ also support non-absorbing treatment under stronger assumptions; see their
366
+ ``non_absorbing`` parameters). Computes the contemporaneous-switch DiD ``DID_M`` from the
360
367
  AER 2020 paper (equivalently ``DID_1`` at horizon ``l = 1`` of the
361
368
  dynamic companion paper, NBER WP 29873) plus the full multi-horizon
362
369
  event study ``DID_l`` for ``l = 1..L_max`` via the ``L_max`` parameter
@@ -4,9 +4,11 @@ Result containers for the de Chaisemartin-D'Haultfoeuille (dCDH) estimator.
4
4
  This module contains ``ChaisemartinDHaultfoeuilleResults`` and
5
5
  ``DCDHBootstrapResults`` dataclasses produced by the
6
6
  ``ChaisemartinDHaultfoeuille`` (alias ``DCDH``) estimator. The dCDH
7
- estimator is the only modern DiD estimator in the library that handles
8
- non-absorbing (reversible) treatments. Phase 1 ships the contemporaneous-
9
- switch case ``DID_M`` (= ``DID_1`` of the dynamic companion paper).
7
+ estimator is the most general library estimator for non-absorbing
8
+ (reversible) treatments (``LPDiD`` and ``TROP`` also support non-absorbing
9
+ treatment under stronger assumptions; see their ``non_absorbing`` parameters).
10
+ Phase 1 ships the contemporaneous-switch case ``DID_M`` (= ``DID_1`` of the
11
+ dynamic companion paper).
10
12
 
11
13
  References
12
14
  ----------
@@ -28,7 +28,7 @@ from diff_diff.linalg import (
28
28
  from diff_diff.results import DiDResults, MultiPeriodDiDResults, PeriodEffect
29
29
  from diff_diff.utils import (
30
30
  WildBootstrapResults,
31
- demean_by_group,
31
+ demean_by_groups,
32
32
  fe_dummy_names,
33
33
  safe_inference,
34
34
  validate_binary,
@@ -414,17 +414,9 @@ class DifferenceInDifferences:
414
414
  absorbed_vars = []
415
415
  n_absorbed_effects = 0
416
416
 
417
- # Reject multi-absorb with survey weights (single-pass demeaning is
418
- # not the correct weighted FWL projection for N > 1 dimensions). Only
419
- # fires when absorb is still set i.e., the auto-route above didn't
420
- # consume it.
421
- if absorb and len(absorb) > 1 and survey_weights is not None:
422
- raise ValueError(
423
- f"Multiple absorbed fixed effects (absorb={absorb}) with survey "
424
- "weights is not supported. Single-pass sequential demeaning is not "
425
- "the correct weighted FWL projection for multiple absorbed dimensions. "
426
- "Use absorb with a single variable, or use fixed_effects= instead."
427
- )
417
+ # Weighted multiple absorbed FE is supported: the absorb path below uses
418
+ # iterative alternating projections (demean_by_groups), the exact weighted
419
+ # FWL projection for N > 1 dimensions on both balanced and unbalanced panels.
428
420
 
429
421
  # Validate vcov_type="conley" wire-up. DiD.fit() accepts `unit`
430
422
  # as a fit-time arg (NOT on __init__) because cluster/unit
@@ -462,16 +454,18 @@ class DifferenceInDifferences:
462
454
  float
463
455
  ) * working_data[time].values.astype(float)
464
456
  vars_to_demean = [outcome, treatment, time, "_treat_time"] + (covariates or [])
465
- for ab_var in absorb:
466
- working_data, n_fe = demean_by_group(
467
- working_data,
468
- vars_to_demean,
469
- ab_var,
470
- inplace=True,
471
- weights=survey_weights,
472
- )
473
- n_absorbed_effects += n_fe
474
- absorbed_vars.append(ab_var)
457
+ # Method of alternating projections: for N > 1 absorbed dimensions a
458
+ # single sequential sweep is only exact on balanced (orthogonal-FE)
459
+ # panels; demean_by_groups iterates to the exact (W)LS-FWL residual.
460
+ working_data, n_fe = demean_by_groups(
461
+ working_data,
462
+ vars_to_demean,
463
+ list(absorb),
464
+ inplace=True,
465
+ weights=survey_weights,
466
+ )
467
+ n_absorbed_effects += n_fe
468
+ absorbed_vars = list(absorb)
475
469
 
476
470
  # Extract variables (may be demeaned if absorb was used)
477
471
  y = working_data[outcome].values.astype(float)
@@ -644,8 +638,7 @@ class DifferenceInDifferences:
644
638
  float
645
639
  )
646
640
  vars_dm = [outcome, treatment, time, "_treat_time"] + (covariates or [])
647
- for ab_var in _absorb_list:
648
- wd, _ = demean_by_group(wd, vars_dm, ab_var, inplace=True, weights=w_nz)
641
+ wd, _ = demean_by_groups(wd, vars_dm, _absorb_list, inplace=True, weights=w_nz)
649
642
  y_r = wd[outcome].values.astype(float)
650
643
  d_r = wd[treatment].values.astype(float)
651
644
  t_r = wd[time].values.astype(float)
@@ -1572,17 +1565,9 @@ class MultiPeriodDiD(DifferenceInDifferences):
1572
1565
  absorb = None
1573
1566
  n_absorbed_effects = 0
1574
1567
 
1575
- # Reject multi-absorb with survey weights (single-pass demeaning is
1576
- # not the correct weighted FWL projection for N > 1 dimensions).
1577
- # Only fires when absorb is still set i.e., the auto-route above
1578
- # didn't consume it.
1579
- if absorb and len(absorb) > 1 and survey_weights is not None:
1580
- raise ValueError(
1581
- f"Multiple absorbed fixed effects (absorb={absorb}) with survey "
1582
- "weights is not supported. Single-pass sequential demeaning is not "
1583
- "the correct weighted FWL projection for multiple absorbed dimensions. "
1584
- "Use absorb with a single variable, or use fixed_effects= instead."
1585
- )
1568
+ # Weighted multiple absorbed FE is supported: the absorb path below uses
1569
+ # iterative alternating projections (demean_by_groups), the exact weighted
1570
+ # FWL projection for N > 1 dimensions on both balanced and unbalanced panels.
1586
1571
 
1587
1572
  # MultiPeriodDiD is intrinsically a multi-period panel estimator;
1588
1573
  # Phase 2 panel block-decomposed Conley (matches R conleyreg) needs
@@ -1622,15 +1607,16 @@ class MultiPeriodDiD(DifferenceInDifferences):
1622
1607
  + [f"_did_interact_{p}" for p in non_ref_periods]
1623
1608
  + (covariates or [])
1624
1609
  )
1625
- for ab_var in absorb:
1626
- working_data, n_fe = demean_by_group(
1627
- working_data,
1628
- vars_to_demean,
1629
- ab_var,
1630
- inplace=True,
1631
- weights=survey_weights,
1632
- )
1633
- n_absorbed_effects += n_fe
1610
+ # Method of alternating projections (exact for unbalanced panels; a
1611
+ # single sequential sweep is exact only on balanced orthogonal-FE panels).
1612
+ working_data, n_fe = demean_by_groups(
1613
+ working_data,
1614
+ vars_to_demean,
1615
+ list(absorb),
1616
+ inplace=True,
1617
+ weights=survey_weights,
1618
+ )
1619
+ n_absorbed_effects += n_fe
1634
1620
 
1635
1621
  # Extract outcome and treatment (may be demeaned if absorb was used)
1636
1622
  y = working_data[outcome].values.astype(float)
@@ -1854,8 +1840,7 @@ class MultiPeriodDiD(DifferenceInDifferences):
1854
1840
  + [f"_did_interact_{p}" for p in non_ref_periods]
1855
1841
  + (covariates or [])
1856
1842
  )
1857
- for ab_var_ in _absorb_list_mp:
1858
- wd, _ = demean_by_group(wd, vars_dm_, ab_var_, inplace=True, weights=w_nz)
1843
+ wd, _ = demean_by_groups(wd, vars_dm_, _absorb_list_mp, inplace=True, weights=w_nz)
1859
1844
  y_r = wd[outcome].values.astype(float)
1860
1845
  d_r = wd["_did_treatment"].values.astype(float)
1861
1846
  X_r = np.column_stack([np.ones(len(y_r)), d_r])
@@ -531,12 +531,21 @@ When `has_never_treated == False`:
531
531
 
532
532
  When `treatment_type == "binary_non_absorbing"`:
533
533
 
534
- - `ChaisemartinDHaultfoeuille` is the only estimator in the library
535
- that treats this natively. Switcher / non-switcher comparisons are
536
- its primitive object.
537
- - Other estimators assume absorbing treatment and will produce
538
- estimates whose interpretation is unclear. Do not use them without
539
- a well-argued reason.
534
+ - `ChaisemartinDHaultfoeuille` is the most general / default choice and
535
+ treats this natively. Switcher / non-switcher comparisons are its
536
+ primitive object; it allows dynamic (carryover) effects and reports
537
+ joiner/leaver (`DID_+` / `DID_-`) views. Prefer it when effects may
538
+ persist after treatment turns off.
539
+ - `LPDiD(non_absorbing="first_entry")` or `"effect_stabilization"`
540
+ (entry-effect estimands) and `TROP(non_absorbing=True, method="local")`
541
+ (valid under a no-dynamic-effects / no-carryover assumption) also handle
542
+ non-absorbing treatment, under stronger assumptions. Use TROP's option
543
+ only when effects are contemporaneous (no carryover).
544
+ - The remaining estimators (`CallawaySantAnna`, `SunAbraham`,
545
+ `ImputationDiD`, `TwoStageDiD`, `EfficientDiD`, `WooldridgeDiD`) assume
546
+ absorbing treatment and will produce estimates whose interpretation is
547
+ unclear on non-absorbing data. Do not use them without a well-argued
548
+ reason.
540
549
 
541
550
  ### §4.6 Triple-difference design (DDD)
542
551
 
@@ -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.6.0
5
+ - Version: 3.6.1
6
6
  - Repository: https://github.com/igerber/diff-diff
7
7
  - License: MIT
8
8
  - Dependencies: numpy, pandas, scipy (no statsmodels dependency)
@@ -231,7 +231,7 @@ plot_event_study(results)
231
231
 
232
232
  ### ChaisemartinDHaultfoeuille
233
233
 
234
- de Chaisemartin & D'Haultfœuille (2020/2022) estimator for **non-absorbing (reversible) treatments**. The only library estimator that handles treatments which can switch on AND off over time. Ships `DID_M` (= `DID_1` at horizon `l = 1`) plus the full multi-horizon event study `DID_l` for `l = 1..L_max` from the dynamic companion paper (NBER WP 29873). Includes normalized estimator `DID^n_l`, cost-benefit aggregate `delta`, dynamic placebos `DID^{pl}_l`, and sup-t simultaneous confidence bands.
234
+ de Chaisemartin & D'Haultfœuille (2020/2022) estimator for **non-absorbing (reversible) treatments**. The most general library estimator for treatments that switch on AND off over time (allows dynamic/carryover effects + joiner/leaver decomposition); `LPDiD` (`non_absorbing="first_entry"`/`"effect_stabilization"`) and `TROP` (`non_absorbing=True`, no-dynamic-effects) also handle non-absorbing treatment under stronger assumptions. Ships `DID_M` (= `DID_1` at horizon `l = 1`) plus the full multi-horizon event study `DID_l` for `l = 1..L_max` from the dynamic companion paper (NBER WP 29873). Includes normalized estimator `DID^n_l`, cost-benefit aggregate `delta`, dynamic placebos `DID^{pl}_l`, and sup-t simultaneous confidence bands.
235
235
 
236
236
  ```python
237
237
  ChaisemartinDHaultfoeuille(
@@ -897,7 +897,7 @@ results.print_summary()
897
897
 
898
898
  ### LPDiD
899
899
 
900
- Local Projections DiD (Dube, Girardi, Jorda & Taylor 2025). Estimates a separate OLS at each event-time horizon of a long difference (`y_{i,t+h} - y_{i,t-1}`) on the treatment-switch indicator plus calendar-time fixed effects (no unit FE), restricted to a flexible "clean control" sample of newly-treated and not-yet-treated units. Excluding already-treated units from the control group removes the negative-weighting bias of naive TWFE, so the default (variance-weighted) estimand has strictly non-negative weights. `reweight=True` yields the equally-weighted ATT (numerically equivalent to Callaway-Sant'Anna); covariates then enter via regression adjustment. Standard errors on the default/weighted path are cluster-robust at the unit level (the paper specifies no SE; matches Stata `lpdid` `vce(cluster unit)`); the regression-adjustment covariate path (`reweight=True`) instead reports an influence-function cluster variance (ImputationDiD/BJS family). Scope: binary, absorbing treatment (rejects panels where treatment turns off).
900
+ Local Projections DiD (Dube, Girardi, Jorda & Taylor 2025). Estimates a separate OLS at each event-time horizon of a long difference (`y_{i,t+h} - y_{i,t-1}`) on the treatment-switch indicator plus calendar-time fixed effects (no unit FE), restricted to a flexible "clean control" sample of newly-treated and not-yet-treated units. Excluding already-treated units from the control group removes the negative-weighting bias of naive TWFE, so the default (variance-weighted) estimand has strictly non-negative weights. `reweight=True` yields the equally-weighted ATT (numerically equivalent to Callaway-Sant'Anna); covariates then enter via regression adjustment. Standard errors on the default/weighted path are cluster-robust at the unit level (the paper specifies no SE; matches Stata `lpdid` `vce(cluster unit)`); the regression-adjustment covariate path (`reweight=True`) instead reports an influence-function cluster variance (ImputationDiD/BJS family). Scope: binary treatment; absorbing by default (rejects panels where treatment turns off), with non-absorbing (reversible) treatment available via `non_absorbing` - `"first_entry"` (Dube et al. Eq. 12, the effect of entering for the first time and staying treated) or `"effect_stabilization"` (Eq. 13, requires `stabilization_window=L`; lets units whose treatment has been stable for at least `L` periods act as clean controls, so estimation is feasible with few/no never-treated units). Non-absorbing modes require a gap-free panel within each unit's observed span. Complex-survey designs are supported on the variance-weighted default path via the `survey_design=` argument to `fit()` (probability weights enter the WLS point estimate; the SE is the stratified-PSU Taylor-linearization sandwich with `df = n_PSU - n_strata`, with optional FPC and lonely-PSU handling) — rejected with `reweight=True`, replicate weights, or non-pweight types.
901
901
 
902
902
  ```python
903
903
  LPDiD(
@@ -910,6 +910,8 @@ LPDiD(
910
910
  alpha: float = 0.05,
911
911
  cluster: str | None = None, # Cluster column for cluster-robust SEs; defaults to the unit identifier
912
912
  rank_deficient_action: str = "warn", # "warn", "error", or "silent"
913
+ non_absorbing: str | None = None, # None=absorbing; "first_entry" (Eq. 12); "effect_stabilization" (Eq. 13)
914
+ stabilization_window: int | None = None, # The paper's L; required when non_absorbing="effect_stabilization"
913
915
  )
914
916
  ```
915
917
 
@@ -921,7 +923,7 @@ lpdid.fit(
921
923
  outcome: str,
922
924
  unit: str,
923
925
  time: str,
924
- treatment: str, # Binary, absorbing treatment indicator (0/1)
926
+ treatment: str, # Binary treatment indicator (0/1); absorbing unless non_absorbing is set
925
927
  covariates: list[str] = None, # Direct inclusion (reweight=False) or regression adjustment (reweight=True)
926
928
  ylags: int = 0, # Lagged-outcome controls
927
929
  dylags: int = 0, # Lagged first-difference controls
@@ -930,6 +932,7 @@ lpdid.fit(
930
932
  pre_pooled: int | tuple = None, # Pooled pre-window horizons (int or (start, end))
931
933
  only_event: bool = False, # Compute only the event-study table
932
934
  only_pooled: bool = False, # Compute only the pooled pre/post table
935
+ survey_design: SurveyDesign = None, # Complex-survey design (pweight + optional strata/PSU/FPC); variance-weighted default path only (rejected with reweight=True)
933
936
  ) -> LPDiDResults
934
937
  ```
935
938
 
@@ -960,6 +963,7 @@ TROP(
960
963
  alpha: float = 0.05,
961
964
  n_bootstrap: int = 200,
962
965
  seed: int | None = None,
966
+ non_absorbing: bool = False, # False: require absorbing D (reject non-monotonic). True: allow on/off treatment (Eq. 12/Alg. 2), method='local' only; emits a caveat warning (Thm 5.1 is block-only).
963
967
  )
964
968
  ```
965
969
 
@@ -969,7 +973,7 @@ TROP(
969
973
  trop.fit(
970
974
  data: pd.DataFrame,
971
975
  outcome: str,
972
- treatment: str, # Absorbing-state treatment indicator (0/1). Must be 0 for all pre-treatment periods and 1 for treatment and post-treatment periods.
976
+ treatment: str, # Treatment indicator (0/1). Default (non_absorbing=False): absorbing state -- 0 for all pre-treatment periods, 1 for treatment and post-treatment; non-monotonic D raises ValueError. With non_absorbing=True: any on/off pattern (general assignment).
973
977
  unit: str,
974
978
  time: str,
975
979
  ) -> TROPResults
@@ -213,9 +213,12 @@ Is treatment adoption staggered (multiple cohorts, different timing)?
213
213
  |
214
214
  |-- Treatment switches ON and OFF (reversible / non-absorbing)?
215
215
  | \-- ChaisemartinDHaultfoeuille (dCDH / alias `DCDH`)
216
- | -- Only library estimator for non-absorbing treatments; supports
217
- | L_max multi-horizon, dynamic placebos, cost-benefit delta,
218
- | HonestDiD, and `survey_design=` (pweight + strata/PSU/FPC via TSL)
216
+ | -- Most general option for non-absorbing treatments (allows dynamic
217
+ | effects + joiner/leaver views); supports L_max multi-horizon,
218
+ | dynamic placebos, cost-benefit delta, HonestDiD, and
219
+ | `survey_design=` (pweight + strata/PSU/FPC via TSL)
220
+ | -- Also: LPDiD(non_absorbing="first_entry"/"effect_stabilization")
221
+ | and TROP(non_absorbing=True, no-dynamic-effects) under stronger assumptions
219
222
  |
220
223
  |-- Few treated units (< 20)?
221
224
  | \-- SyntheticDiD (SDiD) -- synthetic control + DiD hybrid
@@ -54,7 +54,7 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")`
54
54
  - [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html): Panel data DiD with unit and time fixed effects via within-transformation or dummies
55
55
  - [MultiPeriodDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html): Event study design with period-specific treatment effects for dynamic analysis
56
56
  - [CallawaySantAnna](https://diff-diff.readthedocs.io/en/stable/api/staggered.html): Callaway & Sant'Anna (2021) group-time ATT estimator for staggered adoption with aggregation
57
- - [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html): de Chaisemartin & D'Haultfœuille (2020/2022) estimator for **reversible (non-absorbing) treatments** with multi-horizon event study (`L_max`), normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The only library option for treatments that switch on AND off. Alias `DCDH`.
57
+ - [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html): de Chaisemartin & D'Haultfœuille (2020/2022) estimator for **reversible (non-absorbing) treatments** with multi-horizon event study (`L_max`), normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The most general option for treatments that switch on AND off (LPDiD/TROP `non_absorbing` also handle non-absorbing treatment under stronger assumptions). Alias `DCDH`.
58
58
  - [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html): Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
59
59
  - [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html): Borusyak, Jaravel & Spiess (2024) imputation estimator — most efficient under homogeneous effects
60
60
  - [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html): Gardner (2022) two-stage estimator with GMM sandwich variance
@@ -66,10 +66,10 @@ Full practitioner guide: call `diff_diff.get_llm_guide("practitioner")`
66
66
  - [HeterogeneousAdoptionDiD](https://diff-diff.readthedocs.io/en/stable/api/had.html): de Chaisemartin, Ciccia, D'Haultfœuille & Knau (2026) for designs where **no unit remains untreated**; local-linear estimator at the dose support boundary returning Weighted Average Slope (WAS) on Design 1' (`d̲=0` / QUG) or `WAS_{d̲}` on Design 1 (`d̲>0`, continuous-near-d̲ or mass-point), with multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release (repeated cross-sections rejected by the validator). Alias `HAD`.
67
67
  - [StackedDiD](https://diff-diff.readthedocs.io/en/stable/api/stacked_did.html): Wing, Freedman & Hollingsworth (2024) stacked DiD with Q-weights and sub-experiments; optional covariate balancing (`balance="entropy"`, Ustyuzhanin 2026)
68
68
  - [EfficientDiD](https://diff-diff.readthedocs.io/en/stable/api/efficient_did.html): Chen, Sant'Anna & Xie (2025) efficient DiD with optimal weighting for tighter SEs
69
- - [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html): Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
69
+ - [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html): Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment (absorbing by default; `non_absorbing=True` for on/off treatment, method='local')
70
70
  - [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference): Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
71
71
  - [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html): Wooldridge (2023, 2025) ETWFE — saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias: ETWFE
72
- - [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html): Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting); variance- or equally-weighted ATT, premean differencing, pooled pre/post, fast. Absorbing treatment.
72
+ - [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html): Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting); variance- or equally-weighted ATT, premean differencing, pooled pre/post, fast. Absorbing by default; non-absorbing (reversible) treatment via `non_absorbing="first_entry"` (Eq. 12) or `"effect_stabilization"` (Eq. 13, window `L`). Complex-survey designs (pweight + stratified-PSU TSL SEs) on the default path via `fit(survey_design=...)`.
73
73
  - [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html): Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
74
74
 
75
75
  ## Diagnostics and Sensitivity Analysis