diff-diff 3.4.1__tar.gz → 3.5.0__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 (93) hide show
  1. {diff_diff-3.4.1 → diff_diff-3.5.0}/PKG-INFO +3 -2
  2. {diff_diff-3.4.1 → diff_diff-3.5.0}/README.md +2 -1
  3. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/__init__.py +9 -1
  4. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/_reporting_helpers.py +17 -0
  5. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/business_report.py +62 -14
  6. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/chaisemartin_dhaultfoeuille.py +11 -6
  7. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/chaisemartin_dhaultfoeuille_results.py +8 -3
  8. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/conley.py +79 -24
  9. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/diagnostic_report.py +225 -23
  10. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/efficient_did.py +105 -8
  11. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/efficient_did_bootstrap.py +92 -2
  12. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/efficient_did_results.py +93 -7
  13. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/estimators.py +4 -0
  14. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/guides/llms-full.txt +128 -7
  15. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/guides/llms.txt +3 -2
  16. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/honest_did.py +115 -52
  17. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/imputation.py +127 -1
  18. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/imputation_bootstrap.py +75 -0
  19. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/imputation_results.py +90 -0
  20. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/linalg.py +304 -59
  21. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/power.py +133 -44
  22. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/practitioner.py +70 -1
  23. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/results.py +10 -1
  24. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/spillover.py +299 -89
  25. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/stacked_did.py +283 -22
  26. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/stacked_did_results.py +29 -0
  27. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/staggered.py +486 -32
  28. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/staggered_aggregation.py +81 -15
  29. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/staggered_bootstrap.py +101 -2
  30. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/staggered_results.py +49 -0
  31. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/staggered_triple_diff.py +125 -34
  32. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/staggered_triple_diff_results.py +47 -0
  33. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/sun_abraham.py +119 -18
  34. diff_diff-3.5.0/diff_diff/synthetic_control.py +1312 -0
  35. diff_diff-3.5.0/diff_diff/synthetic_control_results.py +729 -0
  36. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/triple_diff.py +234 -58
  37. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/two_stage.py +425 -88
  38. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/two_stage_bootstrap.py +102 -6
  39. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/two_stage_results.py +91 -0
  40. diff_diff-3.5.0/diff_diff/wooldridge.py +2172 -0
  41. diff_diff-3.5.0/diff_diff/wooldridge_results.py +871 -0
  42. {diff_diff-3.4.1 → diff_diff-3.5.0}/pyproject.toml +1 -1
  43. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/Cargo.lock +21 -21
  44. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/Cargo.toml +1 -1
  45. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/src/linalg.rs +37 -5
  46. diff_diff-3.4.1/diff_diff/wooldridge.py +0 -1305
  47. diff_diff-3.4.1/diff_diff/wooldridge_results.py +0 -373
  48. {diff_diff-3.4.1 → diff_diff-3.5.0}/LICENSE +0 -0
  49. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/_backend.py +0 -0
  50. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/_guides_api.py +0 -0
  51. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/_nprobust_port.py +0 -0
  52. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/agent_workflow.py +0 -0
  53. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/bacon.py +0 -0
  54. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/bootstrap_utils.py +0 -0
  55. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
  56. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/continuous_did.py +0 -0
  57. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/continuous_did_bspline.py +0 -0
  58. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/continuous_did_results.py +0 -0
  59. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/datasets.py +0 -0
  60. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/diagnostics.py +0 -0
  61. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/efficient_did_covariates.py +0 -0
  62. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/efficient_did_weights.py +0 -0
  63. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/guides/__init__.py +0 -0
  64. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/guides/llms-autonomous.txt +0 -0
  65. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/guides/llms-practitioner.txt +0 -0
  66. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/had.py +0 -0
  67. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/had_pretests.py +0 -0
  68. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/local_linear.py +0 -0
  69. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/prep.py +0 -0
  70. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/prep_dgp.py +0 -0
  71. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/pretrends.py +0 -0
  72. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/profile.py +0 -0
  73. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/survey.py +0 -0
  74. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/synthetic_did.py +0 -0
  75. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/trop.py +0 -0
  76. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/trop_global.py +0 -0
  77. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/trop_local.py +0 -0
  78. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/trop_results.py +0 -0
  79. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/twfe.py +0 -0
  80. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/utils.py +0 -0
  81. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/__init__.py +0 -0
  82. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_common.py +0 -0
  83. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_continuous.py +0 -0
  84. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_diagnostic.py +0 -0
  85. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_event_study.py +0 -0
  86. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_power.py +0 -0
  87. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_staggered.py +0 -0
  88. {diff_diff-3.4.1 → diff_diff-3.5.0}/diff_diff/visualization/_synthetic.py +0 -0
  89. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/build.rs +0 -0
  90. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/src/bootstrap.rs +0 -0
  91. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/src/lib.rs +0 -0
  92. {diff_diff-3.4.1 → diff_diff-3.5.0}/rust/src/trop.rs +0 -0
  93. {diff_diff-3.4.1 → diff_diff-3.5.0}/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.1
3
+ Version: 3.5.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -159,8 +159,9 @@ 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; 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>`)
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
+ - [SyntheticControl](https://diff-diff.readthedocs.io/en/stable/api/synthetic_control.html) - Abadie, Diamond & Hainmueller (2010) classic synthetic control for a single treated unit (donor-weight counterfactual, nested/custom V; in-space placebo permutation inference via `in_space_placebo()`)
164
165
  - [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
166
  - [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
166
167
  - [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 a multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release - repeated cross-sections rejected by the validator. Alias `HAD`.
@@ -106,8 +106,9 @@ 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; 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>`)
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
+ - [SyntheticControl](https://diff-diff.readthedocs.io/en/stable/api/synthetic_control.html) - Abadie, Diamond & Hainmueller (2010) classic synthetic control for a single treated unit (donor-weight counterfactual, nested/custom V; in-space placebo permutation inference via `in_space_placebo()`)
111
112
  - [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
113
  - [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
113
114
  - [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 a multi-period event-study extension (last-treatment cohort, pointwise CIs). **Panel-only** in this release - repeated cross-sections rejected by the validator. Alias `HAD`.
@@ -222,6 +222,11 @@ from diff_diff.trop import (
222
222
  TROPResults,
223
223
  trop,
224
224
  )
225
+ from diff_diff.synthetic_control import (
226
+ SyntheticControl,
227
+ synthetic_control,
228
+ )
229
+ from diff_diff.synthetic_control_results import SyntheticControlResults
225
230
  from diff_diff.wooldridge import WooldridgeDiD
226
231
  from diff_diff.wooldridge_results import WooldridgeDiDResults
227
232
  from diff_diff.utils import (
@@ -293,7 +298,7 @@ ETWFE = WooldridgeDiD
293
298
  DCDH = ChaisemartinDHaultfoeuille
294
299
  HAD = HeterogeneousAdoptionDiD
295
300
 
296
- __version__ = "3.4.1"
301
+ __version__ = "3.5.0"
297
302
  __all__ = [
298
303
  # Estimators
299
304
  "DifferenceInDifferences",
@@ -309,6 +314,7 @@ __all__ = [
309
314
  "SpilloverDiD",
310
315
  "TripleDifference",
311
316
  "TROP",
317
+ "SyntheticControl",
312
318
  "StackedDiD",
313
319
  # Estimator aliases (short names)
314
320
  "DiD",
@@ -355,6 +361,8 @@ __all__ = [
355
361
  "StaggeredTripleDiffResults",
356
362
  "TROPResults",
357
363
  "trop",
364
+ "SyntheticControlResults",
365
+ "synthetic_control",
358
366
  "StackedDiDResults",
359
367
  "stacked_did",
360
368
  # EfficientDiD
@@ -618,6 +618,23 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
618
618
  "reference": "REGISTRY.md Sec. TROP",
619
619
  }
620
620
 
621
+ if name == "SyntheticControlResults":
622
+ return {
623
+ "name": "SCM ATT (mean post-treatment gap for the single treated unit)",
624
+ "definition": (
625
+ "The average over the post-treatment periods of the gap "
626
+ "``alpha_hat_{1t} = Y_{1t} - sum_j w_j Y_{jt}`` between the single "
627
+ "treated unit and its donor-weighted synthetic control (Abadie, "
628
+ "Diamond & Hainmueller 2010). There is no population-averaging or "
629
+ "sampling estimand — it is the effect on the one treated unit; "
630
+ "significance is assessed by in-space placebo permutation inference "
631
+ "(no analytical standard error)."
632
+ ),
633
+ "aggregation": "single_unit_gap",
634
+ "headline_attribute": "att",
635
+ "reference": "REGISTRY.md Sec. SyntheticControl",
636
+ }
637
+
621
638
  # Default: unrecognized result class. Fall through with a neutral
622
639
  # block — agents / downstream consumers can still dispatch on
623
640
  # ``aggregation="unknown"`` and fall back to generic ATT narration.
@@ -203,6 +203,7 @@ class BusinessReport:
203
203
  if honest_did_results is not None and type(results).__name__ in {
204
204
  "SyntheticDiDResults",
205
205
  "TROPResults",
206
+ "SyntheticControlResults",
206
207
  }:
207
208
  raise ValueError(
208
209
  f"{type(results).__name__} routes robustness to "
@@ -213,8 +214,9 @@ class BusinessReport:
213
214
  "object's native diagnostics "
214
215
  "(SDiD: ``in_time_placebo()``, ``sensitivity_to_zeta_omega()``, "
215
216
  "``pre_treatment_fit``; TROP: ``effective_rank``, "
216
- "``loocv_score``) — BusinessReport surfaces these "
217
- "automatically under ``estimator_native_diagnostics``."
217
+ "``loocv_score``; SyntheticControl: ``in_space_placebo()``, "
218
+ "``pre_rmspe``, ``get_placebo_df()``) — BusinessReport surfaces "
219
+ "these automatically under ``estimator_native_diagnostics``."
218
220
  )
219
221
 
220
222
  # Round-44 P1 CI review on PR #318: mirror the SDiD/TROP
@@ -646,10 +648,13 @@ class BusinessReport:
646
648
  if att is None or not np.isfinite(att):
647
649
  sign = "undefined"
648
650
  ci_level = int(round((1.0 - display_alpha) * 100))
649
- is_significant = (
651
+ # bool(...) coerces away numpy bool_ — when ``p`` is a numpy NaN (e.g.
652
+ # SyntheticControl, whose analytical p_value is always NaN), ``np.isfinite``
653
+ # yields a numpy bool that is NOT JSON-serializable in the schema.
654
+ is_significant = bool(
650
655
  p is not None and np.isfinite(p) and p < phrasing_alpha if p is not None else False
651
656
  )
652
- near_threshold = (
657
+ near_threshold = bool(
653
658
  p is not None
654
659
  and np.isfinite(p)
655
660
  and (phrasing_alpha - 0.01) < p < (phrasing_alpha + 0.001)
@@ -1002,16 +1007,25 @@ def _lift_robustness(dr: Optional[Dict[str, Any]]) -> Dict[str, Any]:
1002
1007
  return {"status": "skipped", "reason": "auto_diagnostics=False"}
1003
1008
  bacon = dr.get("bacon") or {}
1004
1009
  native = dr.get("estimator_native_diagnostics") or {}
1010
+ native_block = {
1011
+ "status": native.get("status"),
1012
+ "estimator": native.get("estimator"),
1013
+ "pre_treatment_fit": native.get("pre_treatment_fit"),
1014
+ }
1015
+ # Classic SCM exposes pre_rmspe + donor-weight concentration + the (opt-in)
1016
+ # in-space placebo rather than SDiD's pre_treatment_fit; surface those so the
1017
+ # top-level robustness block is not empty for SyntheticControl.
1018
+ if native.get("estimator") == "SyntheticControl":
1019
+ native_block["pre_rmspe"] = native.get("pre_rmspe")
1020
+ native_block["weight_concentration"] = native.get("weight_concentration")
1021
+ native_block["in_space_placebo"] = native.get("in_space_placebo")
1005
1022
  return {
1006
1023
  "bacon": {
1007
1024
  "status": bacon.get("status"),
1008
1025
  "forbidden_weight": bacon.get("forbidden_weight"),
1009
1026
  "verdict": bacon.get("verdict"),
1010
1027
  },
1011
- "estimator_native": {
1012
- "status": native.get("status"),
1013
- "pre_treatment_fit": native.get("pre_treatment_fit"),
1014
- },
1028
+ "estimator_native": native_block,
1015
1029
  }
1016
1030
 
1017
1031
 
@@ -1153,6 +1167,20 @@ def _describe_assumption(estimator_name: str, results: Any = None) -> Dict[str,
1153
1167
  "captured through latent factor loadings."
1154
1168
  ),
1155
1169
  }
1170
+ if estimator_name in {"SyntheticControlResults"}:
1171
+ return {
1172
+ # Distinct from SDiD's "synthetic_fit" weighted-PT analogue: classic
1173
+ # SCM is a donor-weighted level match (matches the DR "scm_fit" method).
1174
+ "parallel_trends_variant": "scm_fit",
1175
+ "no_anticipation": True,
1176
+ "description": (
1177
+ "Classic synthetic control identifies the single treated unit's "
1178
+ "counterfactual via a donor-weighted match to its pre-treatment "
1179
+ "trajectory (a design-enforced fit, not a parallel-trends test); "
1180
+ "significance comes from in-space placebo permutation inference "
1181
+ "rather than an analytical standard error."
1182
+ ),
1183
+ }
1156
1184
  if estimator_name == "ContinuousDiDResults":
1157
1185
  # Callaway, Goodman-Bacon & Sant'Anna (2024), two-level PT:
1158
1186
  # REGISTRY.md §ContinuousDiD > Identification.
@@ -1780,6 +1808,8 @@ def _pt_method_subject(method: Optional[str]) -> str:
1780
1808
  return "Pre-treatment event-study coefficients"
1781
1809
  if method == "synthetic_fit":
1782
1810
  return "The synthetic-control pre-treatment fit"
1811
+ if method == "scm_fit":
1812
+ return "The synthetic-control donor-weighted pre-treatment fit"
1783
1813
  if method == "factor":
1784
1814
  return "The factor-model pre-treatment fit"
1785
1815
  return "Pre-treatment data"
@@ -1806,7 +1836,9 @@ def _pt_method_stat_label(method: Optional[str]) -> Optional[str]:
1806
1836
  return "joint p"
1807
1837
  if method in {"slope_difference", "hausman"}:
1808
1838
  return "p"
1809
- if method in {"synthetic_fit", "factor"}:
1839
+ if method in {"synthetic_fit", "scm_fit", "factor"}:
1840
+ # Design-enforced fit-based paths have no p-value label (SCM's significance
1841
+ # is the in-space placebo, not a PT joint test).
1810
1842
  return None
1811
1843
  return "joint p"
1812
1844
 
@@ -1846,6 +1878,13 @@ def _references_for(estimator_name: str) -> List[Dict[str, str]]:
1846
1878
  "& Wager, S. (2021). Synthetic Difference in Differences."
1847
1879
  ),
1848
1880
  },
1881
+ "SyntheticControlResults": {
1882
+ "role": "estimator",
1883
+ "citation": (
1884
+ "Abadie, A., Diamond, A., & Hainmueller, J. (2010). Synthetic "
1885
+ "Control Methods for Comparative Case Studies. JASA, 105(490)."
1886
+ ),
1887
+ },
1849
1888
  "SunAbrahamResults": {
1850
1889
  "role": "estimator",
1851
1890
  "citation": (
@@ -2181,11 +2220,20 @@ def _render_summary(schema: Dict[str, Any]) -> str:
2181
2220
  "assumption." + sens_tail_see_reliable
2182
2221
  )
2183
2222
  elif verdict == "design_enforced_pt":
2184
- sentences.append(
2185
- "The synthetic control is designed to match the treated "
2186
- "group's pre-period trajectory (SDiD's weighted-parallel-"
2187
- "trends analogue)."
2188
- )
2223
+ if method == "scm_fit":
2224
+ sentences.append(
2225
+ "The synthetic control is designed to reproduce the treated "
2226
+ "unit's pre-period trajectory via donor weights (classic SCM's "
2227
+ "design-enforced analogue of parallel trends); significance "
2228
+ "comes from in-space placebo permutation inference, not a "
2229
+ "parallel-trends test."
2230
+ )
2231
+ else:
2232
+ sentences.append(
2233
+ "The synthetic control is designed to match the treated "
2234
+ "group's pre-period trajectory (SDiD's weighted-parallel-"
2235
+ "trends analogue)."
2236
+ )
2189
2237
  elif verdict == "inconclusive":
2190
2238
  # Round-35 P1 CI review on PR #318: a ``verdict=="inconclusive"``
2191
2239
  # state means one or more pre-period coefficients had
@@ -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
@@ -212,7 +212,7 @@ def _validate_conley_estimator_inputs(
212
212
  ) -> None:
213
213
  """Shared front-door validation for ``vcov_type='conley'`` on the
214
214
  estimator entry points (``DifferenceInDifferences``, ``MultiPeriodDiD``,
215
- ``TwoWayFixedEffects``).
215
+ ``TwoWayFixedEffects``, ``SunAbraham``, and ``WooldridgeDiD``-OLS).
216
216
 
217
217
  Each estimator's ``fit()`` calls this BEFORE building Conley arrays or
218
218
  threading them into the variance computation. The eight checks below
@@ -296,10 +296,14 @@ def _validate_conley_estimator_inputs(
296
296
  raise ValueError(f"Cluster column '{cluster}' not found in data")
297
297
  if survey_design is not None:
298
298
  raise NotImplementedError(
299
- f"{estimator_name}(vcov_type='conley') + survey_design is a "
300
- "follow-up (Bertanha-Imbens 2014 weighted-Conley). Drop "
301
- "survey_design for cross-sectional Conley, or use vcov_type='hc1' "
302
- "for survey-aware cluster-robust without spatial HAC."
299
+ f"{estimator_name}(vcov_type='conley') + survey_design is "
300
+ "deferred weighted spatial-HAC under probability sampling "
301
+ "is an open methodological question; no canonical extension "
302
+ "of Conley (1999) exists for the combination. Drop "
303
+ "survey_design for unweighted Conley (cross-sectional or panel "
304
+ "block-decomposed via conley_lag_cutoff > 0), or use "
305
+ "vcov_type='hc1' for survey-aware cluster-robust without "
306
+ "spatial HAC."
303
307
  )
304
308
  if inference == "wild_bootstrap":
305
309
  raise NotImplementedError(
@@ -365,6 +369,61 @@ def _uniform_kernel(u: np.ndarray) -> np.ndarray:
365
369
  return (np.abs(u) <= 1.0).astype(np.float64)
366
370
 
367
371
 
372
+ def _serial_bartlett_kernel_matrix(t_codes: np.ndarray, L: int) -> np.ndarray:
373
+ """Within-unit Newey-West (1987) Bartlett HAC kernel matrix for serial
374
+ correlation in panel data, indexed by panel-wide dense time codes.
375
+
376
+ Returns the K matrix with ``K[i, j] = 1 - |t_i - t_j| / (L + 1)`` for
377
+ ``0 < |t_i - t_j| <= L``, else 0. The lag-0 diagonal is excluded so
378
+ callers can add this to a spatial within-period meat without
379
+ double-counting the diagonal.
380
+
381
+ Uses the 1-D radial pairwise form (matches conleyreg::time_dist), NOT
382
+ Conley 1999 Eq 3.14's 2-D separable product window — see the methodology
383
+ lock at :func:`_compute_conley_meat` for context.
384
+ """
385
+ t = t_codes.astype(np.float64, copy=False)
386
+ lag_mat = np.abs(t[:, None] - t[None, :])
387
+ return ((lag_mat <= L) & (lag_mat != 0)).astype(np.float64) * (1.0 - lag_mat / (L + 1.0))
388
+
389
+
390
+ def _validate_meat_psd(
391
+ M: np.ndarray,
392
+ *,
393
+ error_msg: str,
394
+ warning_template: str,
395
+ stacklevel: int = 3,
396
+ ) -> None:
397
+ """Finite + PSD guard for sandwich meat matrices. Raises ``ValueError``
398
+ on non-finite entries; warns ``UserWarning`` when ``min(eigvalsh(M)) <
399
+ -1e-12``.
400
+
401
+ Parameters
402
+ ----------
403
+ error_msg
404
+ Message passed to ``ValueError`` on non-finite entries.
405
+ warning_template
406
+ Format string for the negative-eigenvalue warning. May contain an
407
+ ``{eigval}`` placeholder; the caller embeds ``{eigval:.2e}`` directly
408
+ in the template so the helper formats the minimum eigenvalue with
409
+ scientific notation.
410
+ stacklevel
411
+ Frame count from inside ``_validate_meat_psd``: ``stacklevel=N``
412
+ attributes the warning to the Nth caller above the helper itself.
413
+ Default 3 covers a single intermediate frame (the helper's direct
414
+ caller's caller); pass an explicit value matching call-site depth.
415
+ """
416
+ if not np.all(np.isfinite(M)):
417
+ raise ValueError(error_msg)
418
+ eigvals = np.linalg.eigvalsh(M)
419
+ if eigvals.size and eigvals.min() < -1e-12:
420
+ warnings.warn(
421
+ warning_template.format(eigval=eigvals.min()),
422
+ UserWarning,
423
+ stacklevel=stacklevel,
424
+ )
425
+
426
+
368
427
  def _compute_spatial_bartlett_meat_sparse(
369
428
  S: np.ndarray,
370
429
  coords: np.ndarray,
@@ -957,37 +1016,33 @@ def _compute_conley_meat(
957
1016
  mask_u = unit_arr == u_val
958
1017
  scores_u = scores[mask_u]
959
1018
  # 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
- )
1019
+ K_u = _serial_bartlett_kernel_matrix(time_codes[mask_u], L)
965
1020
  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
1021
  # PSD guard. Neither the uniform kernel (Conley 1999 fn 11) nor the
973
1022
  # radial 1-D Bartlett specialization is formally PSD-guaranteed —
974
1023
  # Conley's explicit PSD Bartlett formula (Eq 3.14) is the 2-D separable
975
1024
  # product window, not the 1-D radial pairwise form that R `conleyreg`,
976
1025
  # 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(
1026
+ # ``{eigval:.2e}`` is a literal placeholder for ``_validate_meat_psd``;
1027
+ # only ``{kernel!r}`` is interpolated by the f-string here.
1028
+ _validate_meat_psd(
1029
+ meat,
1030
+ error_msg=(
1031
+ "Conley meat contains non-finite values; check residuals and "
1032
+ "score matrix for NaN/Inf."
1033
+ ),
1034
+ warning_template=(
980
1035
  f"Conley meat with conley_kernel={kernel!r} has a materially "
981
- f"negative eigenvalue ({eigvals.min():.2e}); the variance "
1036
+ "negative eigenvalue ({eigval:.2e}); the variance "
982
1037
  "estimator is not guaranteed PSD on this design. Both "
983
1038
  "supported kernels (radial bartlett and uniform) are "
984
1039
  "practitioner specializations of Conley 1999 and are not "
985
1040
  "formally PSD-guaranteed; consider varying conley_cutoff_km "
986
1041
  "or reviewing the design for collinearity / degenerate "
987
- "residual structure.",
988
- UserWarning,
989
- stacklevel=3,
990
- )
1042
+ "residual structure."
1043
+ ),
1044
+ stacklevel=4,
1045
+ )
991
1046
 
992
1047
  return meat
993
1048