diff-diff 3.4.2__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 (91) hide show
  1. {diff_diff-3.4.2 → diff_diff-3.5.0}/PKG-INFO +2 -1
  2. {diff_diff-3.4.2 → diff_diff-3.5.0}/README.md +1 -0
  3. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/__init__.py +9 -1
  4. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/_reporting_helpers.py +17 -0
  5. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/business_report.py +62 -14
  6. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/conley.py +9 -5
  7. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/diagnostic_report.py +225 -23
  8. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/efficient_did.py +105 -8
  9. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/efficient_did_bootstrap.py +92 -2
  10. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/efficient_did_results.py +93 -7
  11. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/estimators.py +4 -0
  12. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/guides/llms-full.txt +111 -11
  13. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/guides/llms.txt +2 -1
  14. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/linalg.py +304 -59
  15. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/power.py +133 -44
  16. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/practitioner.py +70 -1
  17. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/stacked_did.py +16 -9
  18. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/staggered.py +62 -25
  19. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/staggered_aggregation.py +81 -15
  20. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/staggered_bootstrap.py +57 -5
  21. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/staggered_triple_diff.py +125 -34
  22. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/staggered_triple_diff_results.py +47 -0
  23. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/sun_abraham.py +119 -18
  24. diff_diff-3.5.0/diff_diff/synthetic_control.py +1312 -0
  25. diff_diff-3.5.0/diff_diff/synthetic_control_results.py +729 -0
  26. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/triple_diff.py +53 -30
  27. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/two_stage.py +112 -1
  28. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/two_stage_bootstrap.py +102 -6
  29. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/two_stage_results.py +91 -0
  30. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/wooldridge.py +191 -24
  31. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/wooldridge_results.py +22 -1
  32. {diff_diff-3.4.2 → diff_diff-3.5.0}/pyproject.toml +1 -1
  33. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/Cargo.lock +15 -15
  34. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/Cargo.toml +1 -1
  35. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/src/linalg.rs +37 -5
  36. {diff_diff-3.4.2 → diff_diff-3.5.0}/LICENSE +0 -0
  37. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/_backend.py +0 -0
  38. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/_guides_api.py +0 -0
  39. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/_nprobust_port.py +0 -0
  40. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/agent_workflow.py +0 -0
  41. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/bacon.py +0 -0
  42. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/bootstrap_utils.py +0 -0
  43. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/chaisemartin_dhaultfoeuille.py +0 -0
  44. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
  45. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
  46. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/continuous_did.py +0 -0
  47. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/continuous_did_bspline.py +0 -0
  48. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/continuous_did_results.py +0 -0
  49. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/datasets.py +0 -0
  50. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/diagnostics.py +0 -0
  51. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/efficient_did_covariates.py +0 -0
  52. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/efficient_did_weights.py +0 -0
  53. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/guides/__init__.py +0 -0
  54. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/guides/llms-autonomous.txt +0 -0
  55. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/guides/llms-practitioner.txt +0 -0
  56. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/had.py +0 -0
  57. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/had_pretests.py +0 -0
  58. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/honest_did.py +0 -0
  59. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/imputation.py +0 -0
  60. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/imputation_bootstrap.py +0 -0
  61. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/imputation_results.py +0 -0
  62. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/local_linear.py +0 -0
  63. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/prep.py +0 -0
  64. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/prep_dgp.py +0 -0
  65. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/pretrends.py +0 -0
  66. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/profile.py +0 -0
  67. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/results.py +0 -0
  68. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/spillover.py +0 -0
  69. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/stacked_did_results.py +0 -0
  70. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/staggered_results.py +0 -0
  71. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/survey.py +0 -0
  72. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/synthetic_did.py +0 -0
  73. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/trop.py +0 -0
  74. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/trop_global.py +0 -0
  75. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/trop_local.py +0 -0
  76. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/trop_results.py +0 -0
  77. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/twfe.py +0 -0
  78. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/utils.py +0 -0
  79. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/__init__.py +0 -0
  80. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_common.py +0 -0
  81. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_continuous.py +0 -0
  82. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_diagnostic.py +0 -0
  83. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_event_study.py +0 -0
  84. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_power.py +0 -0
  85. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_staggered.py +0 -0
  86. {diff_diff-3.4.2 → diff_diff-3.5.0}/diff_diff/visualization/_synthetic.py +0 -0
  87. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/build.rs +0 -0
  88. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/src/bootstrap.rs +0 -0
  89. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/src/lib.rs +0 -0
  90. {diff_diff-3.4.2 → diff_diff-3.5.0}/rust/src/trop.rs +0 -0
  91. {diff_diff-3.4.2 → 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.2
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
@@ -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/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`.
@@ -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/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.2"
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
@@ -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(