diff-diff 3.4.2__tar.gz → 3.5.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 (91) hide show
  1. {diff_diff-3.4.2 → diff_diff-3.5.1}/PKG-INFO +2 -1
  2. {diff_diff-3.4.2 → diff_diff-3.5.1}/README.md +1 -0
  3. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/__init__.py +9 -1
  4. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/_reporting_helpers.py +17 -0
  5. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/business_report.py +65 -14
  6. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/conley.py +9 -5
  7. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/diagnostic_report.py +356 -23
  8. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/efficient_did.py +201 -40
  9. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/efficient_did_bootstrap.py +92 -2
  10. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/efficient_did_covariates.py +238 -62
  11. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/efficient_did_results.py +98 -9
  12. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/estimators.py +74 -3
  13. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/guides/llms-full.txt +115 -12
  14. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/guides/llms.txt +2 -1
  15. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/had.py +77 -0
  16. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/linalg.py +304 -59
  17. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/power.py +133 -44
  18. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/practitioner.py +106 -1
  19. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/stacked_did.py +16 -9
  20. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/staggered.py +62 -25
  21. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/staggered_aggregation.py +81 -15
  22. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/staggered_bootstrap.py +57 -5
  23. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/staggered_triple_diff.py +125 -34
  24. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/staggered_triple_diff_results.py +47 -0
  25. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/sun_abraham.py +119 -18
  26. diff_diff-3.5.1/diff_diff/synthetic_control.py +2004 -0
  27. diff_diff-3.5.1/diff_diff/synthetic_control_results.py +1403 -0
  28. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/triple_diff.py +53 -30
  29. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/twfe.py +39 -1
  30. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/two_stage.py +112 -1
  31. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/two_stage_bootstrap.py +102 -6
  32. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/two_stage_results.py +91 -0
  33. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/utils.py +151 -1
  34. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/wooldridge.py +191 -24
  35. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/wooldridge_results.py +22 -1
  36. {diff_diff-3.4.2 → diff_diff-3.5.1}/pyproject.toml +1 -1
  37. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/Cargo.lock +19 -19
  38. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/Cargo.toml +1 -1
  39. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/src/linalg.rs +37 -5
  40. {diff_diff-3.4.2 → diff_diff-3.5.1}/LICENSE +0 -0
  41. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/_backend.py +0 -0
  42. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/_guides_api.py +0 -0
  43. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/_nprobust_port.py +0 -0
  44. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/agent_workflow.py +0 -0
  45. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/bacon.py +0 -0
  46. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/bootstrap_utils.py +0 -0
  47. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/chaisemartin_dhaultfoeuille.py +0 -0
  48. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
  49. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
  50. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/continuous_did.py +0 -0
  51. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/continuous_did_bspline.py +0 -0
  52. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/continuous_did_results.py +0 -0
  53. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/datasets.py +0 -0
  54. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/diagnostics.py +0 -0
  55. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/efficient_did_weights.py +0 -0
  56. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/guides/__init__.py +0 -0
  57. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/guides/llms-autonomous.txt +0 -0
  58. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/guides/llms-practitioner.txt +0 -0
  59. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/had_pretests.py +0 -0
  60. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/honest_did.py +0 -0
  61. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/imputation.py +0 -0
  62. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/imputation_bootstrap.py +0 -0
  63. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/imputation_results.py +0 -0
  64. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/local_linear.py +0 -0
  65. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/prep.py +0 -0
  66. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/prep_dgp.py +0 -0
  67. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/pretrends.py +0 -0
  68. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/profile.py +0 -0
  69. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/results.py +0 -0
  70. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/spillover.py +0 -0
  71. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/stacked_did_results.py +0 -0
  72. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/staggered_results.py +0 -0
  73. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/survey.py +0 -0
  74. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/synthetic_did.py +0 -0
  75. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/trop.py +0 -0
  76. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/trop_global.py +0 -0
  77. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/trop_local.py +0 -0
  78. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/trop_results.py +0 -0
  79. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/__init__.py +0 -0
  80. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_common.py +0 -0
  81. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_continuous.py +0 -0
  82. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_diagnostic.py +0 -0
  83. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_event_study.py +0 -0
  84. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_power.py +0 -0
  85. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_staggered.py +0 -0
  86. {diff_diff-3.4.2 → diff_diff-3.5.1}/diff_diff/visualization/_synthetic.py +0 -0
  87. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/build.rs +0 -0
  88. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/src/bootstrap.rs +0 -0
  89. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/src/lib.rs +0 -0
  90. {diff_diff-3.4.2 → diff_diff-3.5.1}/rust/src/trop.rs +0 -0
  91. {diff_diff-3.4.2 → diff_diff-3.5.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.4.2
3
+ Version: 3.5.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -161,6 +161,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
161
161
  - [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
162
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/cv/inverse-variance/custom V; in-space placebo permutation inference via `in_space_placebo()`, plus ADH-2015 `leave_one_out()` + `in_time_placebo()` robustness)
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`.
@@ -108,6 +108,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
108
108
  - [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
109
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/cv/inverse-variance/custom V; in-space placebo permutation inference via `in_space_placebo()`, plus ADH-2015 `leave_one_out()` + `in_time_placebo()` robustness)
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.2"
301
+ __version__ = "3.5.1"
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,28 @@ 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")
1022
+ # ADH-2015 robustness diagnostics (opt-in; "not_run" stub until run).
1023
+ native_block["leave_one_out"] = native.get("leave_one_out")
1024
+ native_block["in_time_placebo"] = native.get("in_time_placebo")
1005
1025
  return {
1006
1026
  "bacon": {
1007
1027
  "status": bacon.get("status"),
1008
1028
  "forbidden_weight": bacon.get("forbidden_weight"),
1009
1029
  "verdict": bacon.get("verdict"),
1010
1030
  },
1011
- "estimator_native": {
1012
- "status": native.get("status"),
1013
- "pre_treatment_fit": native.get("pre_treatment_fit"),
1014
- },
1031
+ "estimator_native": native_block,
1015
1032
  }
1016
1033
 
1017
1034
 
@@ -1153,6 +1170,20 @@ def _describe_assumption(estimator_name: str, results: Any = None) -> Dict[str,
1153
1170
  "captured through latent factor loadings."
1154
1171
  ),
1155
1172
  }
1173
+ if estimator_name in {"SyntheticControlResults"}:
1174
+ return {
1175
+ # Distinct from SDiD's "synthetic_fit" weighted-PT analogue: classic
1176
+ # SCM is a donor-weighted level match (matches the DR "scm_fit" method).
1177
+ "parallel_trends_variant": "scm_fit",
1178
+ "no_anticipation": True,
1179
+ "description": (
1180
+ "Classic synthetic control identifies the single treated unit's "
1181
+ "counterfactual via a donor-weighted match to its pre-treatment "
1182
+ "trajectory (a design-enforced fit, not a parallel-trends test); "
1183
+ "significance comes from in-space placebo permutation inference "
1184
+ "rather than an analytical standard error."
1185
+ ),
1186
+ }
1156
1187
  if estimator_name == "ContinuousDiDResults":
1157
1188
  # Callaway, Goodman-Bacon & Sant'Anna (2024), two-level PT:
1158
1189
  # REGISTRY.md §ContinuousDiD > Identification.
@@ -1780,6 +1811,8 @@ def _pt_method_subject(method: Optional[str]) -> str:
1780
1811
  return "Pre-treatment event-study coefficients"
1781
1812
  if method == "synthetic_fit":
1782
1813
  return "The synthetic-control pre-treatment fit"
1814
+ if method == "scm_fit":
1815
+ return "The synthetic-control donor-weighted pre-treatment fit"
1783
1816
  if method == "factor":
1784
1817
  return "The factor-model pre-treatment fit"
1785
1818
  return "Pre-treatment data"
@@ -1806,7 +1839,9 @@ def _pt_method_stat_label(method: Optional[str]) -> Optional[str]:
1806
1839
  return "joint p"
1807
1840
  if method in {"slope_difference", "hausman"}:
1808
1841
  return "p"
1809
- if method in {"synthetic_fit", "factor"}:
1842
+ if method in {"synthetic_fit", "scm_fit", "factor"}:
1843
+ # Design-enforced fit-based paths have no p-value label (SCM's significance
1844
+ # is the in-space placebo, not a PT joint test).
1810
1845
  return None
1811
1846
  return "joint p"
1812
1847
 
@@ -1846,6 +1881,13 @@ def _references_for(estimator_name: str) -> List[Dict[str, str]]:
1846
1881
  "& Wager, S. (2021). Synthetic Difference in Differences."
1847
1882
  ),
1848
1883
  },
1884
+ "SyntheticControlResults": {
1885
+ "role": "estimator",
1886
+ "citation": (
1887
+ "Abadie, A., Diamond, A., & Hainmueller, J. (2010). Synthetic "
1888
+ "Control Methods for Comparative Case Studies. JASA, 105(490)."
1889
+ ),
1890
+ },
1849
1891
  "SunAbrahamResults": {
1850
1892
  "role": "estimator",
1851
1893
  "citation": (
@@ -2181,11 +2223,20 @@ def _render_summary(schema: Dict[str, Any]) -> str:
2181
2223
  "assumption." + sens_tail_see_reliable
2182
2224
  )
2183
2225
  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
- )
2226
+ if method == "scm_fit":
2227
+ sentences.append(
2228
+ "The synthetic control is designed to reproduce the treated "
2229
+ "unit's pre-period trajectory via donor weights (classic SCM's "
2230
+ "design-enforced analogue of parallel trends); significance "
2231
+ "comes from in-space placebo permutation inference, not a "
2232
+ "parallel-trends test."
2233
+ )
2234
+ else:
2235
+ sentences.append(
2236
+ "The synthetic control is designed to match the treated "
2237
+ "group's pre-period trajectory (SDiD's weighted-parallel-"
2238
+ "trends analogue)."
2239
+ )
2189
2240
  elif verdict == "inconclusive":
2190
2241
  # Round-35 P1 CI review on PR #318: a ``verdict=="inconclusive"``
2191
2242
  # state means one or more pre-period coefficients had
@@ -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(