diff-diff 3.9.0__tar.gz → 3.9.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 (116) hide show
  1. {diff_diff-3.9.0 → diff_diff-3.9.1}/PKG-INFO +3 -2
  2. {diff_diff-3.9.0 → diff_diff-3.9.1}/README.md +1 -0
  3. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/__init__.py +10 -1
  4. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_reporting_helpers.py +9 -6
  5. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_staggered_triple_diff_engine.py +9 -4
  6. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/aggregation.py +91 -6
  7. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/business_report.py +23 -5
  8. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did.py +18 -3
  9. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/diagnostic_report.py +567 -113
  10. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did.py +24 -8
  11. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_results.py +58 -4
  12. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms-autonomous.txt +5 -3
  13. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms-full.txt +151 -34
  14. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms-practitioner.txt +29 -4
  15. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms.txt +3 -2
  16. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation.py +20 -3
  17. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation_results.py +55 -5
  18. diff_diff-3.9.1/diff_diff/mmm.py +1600 -0
  19. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/practitioner.py +9 -5
  20. diff_diff-3.9.1/diff_diff/rddensity.py +1417 -0
  21. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/spillover.py +13 -16
  22. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/stacked_did.py +17 -3
  23. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered.py +48 -14
  24. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_results.py +149 -12
  25. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_triple_diff.py +3 -1
  26. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/sun_abraham.py +9 -1
  27. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/triple_diff.py +7 -5
  28. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage.py +10 -4
  29. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage_results.py +55 -7
  30. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/utils.py +17 -5
  31. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/wooldridge.py +14 -6
  32. {diff_diff-3.9.0 → diff_diff-3.9.1}/pyproject.toml +2 -2
  33. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/Cargo.lock +15 -15
  34. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/Cargo.toml +1 -1
  35. diff_diff-3.9.0/diff_diff/mmm.py +0 -653
  36. {diff_diff-3.9.0 → diff_diff-3.9.1}/LICENSE +0 -0
  37. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_backend.py +0 -0
  38. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_base.py +0 -0
  39. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_deprecation.py +0 -0
  40. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_guides_api.py +0 -0
  41. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_nprobust_port.py +0 -0
  42. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_rdrobust_port.py +0 -0
  43. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/agent_workflow.py +0 -0
  44. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/bacon.py +0 -0
  45. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/balancing.py +0 -0
  46. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/bootstrap_chunking.py +0 -0
  47. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/bootstrap_utils.py +0 -0
  48. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/chaisemartin_dhaultfoeuille.py +0 -0
  49. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
  50. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
  51. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/changes_in_changes.py +0 -0
  52. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/changes_in_changes_results.py +0 -0
  53. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/conformal.py +0 -0
  54. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/conley.py +0 -0
  55. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did_aggregation.py +0 -0
  56. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did_bspline.py +0 -0
  57. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did_results.py +0 -0
  58. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/datasets.py +0 -0
  59. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/diagnostics.py +0 -0
  60. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_aggregation.py +0 -0
  61. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_bootstrap.py +0 -0
  62. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_covariates.py +0 -0
  63. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_weights.py +0 -0
  64. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/estimators.py +0 -0
  65. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/__init__.py +0 -0
  66. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/had.py +0 -0
  67. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/had_pretests.py +0 -0
  68. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/honest_did.py +0 -0
  69. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation_aggregation.py +0 -0
  70. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation_bootstrap.py +0 -0
  71. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/linalg.py +0 -0
  72. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/local_linear.py +0 -0
  73. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/lpdid.py +0 -0
  74. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/lpdid_results.py +0 -0
  75. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/power.py +0 -0
  76. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/prep.py +0 -0
  77. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/prep_dgp.py +0 -0
  78. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/pretrends.py +0 -0
  79. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/profile.py +0 -0
  80. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/rdd.py +0 -0
  81. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/rdplot.py +0 -0
  82. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/results.py +0 -0
  83. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/results_base.py +0 -0
  84. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/stacked_did_results.py +0 -0
  85. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_aggregation.py +0 -0
  86. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_bootstrap.py +0 -0
  87. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_triple_diff_results.py +0 -0
  88. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/survey.py +0 -0
  89. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/synthetic_control.py +0 -0
  90. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/synthetic_control_results.py +0 -0
  91. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/synthetic_did.py +0 -0
  92. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop.py +0 -0
  93. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop_global.py +0 -0
  94. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop_local.py +0 -0
  95. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop_results.py +0 -0
  96. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/twfe.py +0 -0
  97. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage_aggregation.py +0 -0
  98. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage_bootstrap.py +0 -0
  99. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/__init__.py +0 -0
  100. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_common.py +0 -0
  101. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_continuous.py +0 -0
  102. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_diagnostic.py +0 -0
  103. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_event_study.py +0 -0
  104. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_power.py +0 -0
  105. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_staggered.py +0 -0
  106. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_synthetic.py +0 -0
  107. {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/wooldridge_results.py +0 -0
  108. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/build.rs +0 -0
  109. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/alloc_profile.rs +0 -0
  110. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/batched_solve.rs +0 -0
  111. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/bootstrap.rs +0 -0
  112. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/demean.rs +0 -0
  113. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/lib.rs +0 -0
  114. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/linalg.rs +0 -0
  115. {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/trop.rs +0 -0
  116. {diff_diff-3.9.0 → diff_diff-3.9.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.9.0
3
+ Version: 3.9.1
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Intended Audience :: Science/Research
6
6
  Classifier: Operating System :: OS Independent
@@ -21,7 +21,7 @@ Requires-Dist: pytest>=7.0 ; extra == 'dev'
21
21
  Requires-Dist: pytest-xdist>=3.0 ; extra == 'dev'
22
22
  Requires-Dist: pytest-cov>=4.0 ; extra == 'dev'
23
23
  Requires-Dist: black==26.5.1 ; extra == 'dev'
24
- Requires-Dist: ruff==0.16.0 ; extra == 'dev'
24
+ Requires-Dist: ruff==0.16.2 ; extra == 'dev'
25
25
  Requires-Dist: mypy==2.3.0 ; extra == 'dev'
26
26
  Requires-Dist: maturin>=1.4,<2.0 ; extra == 'dev'
27
27
  Requires-Dist: matplotlib>=3.5 ; extra == 'dev'
@@ -180,6 +180,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
180
180
  ## Diagnostics & Sensitivity
181
181
 
182
182
  - [RD Plots](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Calonico, Cattaneo & Titiunik (2015) optimal data-driven RD plots (`RDPlot`): all 8 rdrobust `binselect` bin selectors, implied-scale/WIMSE-weight reporting, optional matplotlib rendering
183
+ - [Manipulation Testing](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Cattaneo, Jansson & Ma (2020) density-discontinuity test (`RDDensityTest`): rddensity 3.0 parity, robust bias-corrected inference, unrestricted/restricted models, mass-point adjustment
183
184
  - [Parallel Trends Testing](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - simple and Wasserstein-robust parallel trends tests, equivalence testing (TOST)
184
185
  - [Placebo Tests](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - placebo timing, group, permutation, leave-one-out
185
186
  - [Honest DiD](https://diff-diff.readthedocs.io/en/stable/api/honest_did.html) - Rambachan & Roth (2023) sensitivity analysis: robust CI under PT violations, breakdown values
@@ -125,6 +125,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
125
125
  ## Diagnostics & Sensitivity
126
126
 
127
127
  - [RD Plots](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Calonico, Cattaneo & Titiunik (2015) optimal data-driven RD plots (`RDPlot`): all 8 rdrobust `binselect` bin selectors, implied-scale/WIMSE-weight reporting, optional matplotlib rendering
128
+ - [Manipulation Testing](https://diff-diff.readthedocs.io/en/stable/api/regression_discontinuity.html) - Cattaneo, Jansson & Ma (2020) density-discontinuity test (`RDDensityTest`): rddensity 3.0 parity, robust bias-corrected inference, unrestricted/restricted models, mass-point adjustment
128
129
  - [Parallel Trends Testing](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - simple and Wasserstein-robust parallel trends tests, equivalence testing (TOST)
129
130
  - [Placebo Tests](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - placebo timing, group, permutation, leave-one-out
130
131
  - [Honest DiD](https://diff-diff.readthedocs.io/en/stable/api/honest_did.html) - Rambachan & Roth (2023) sensitivity analysis: robust CI under PT violations, breakdown values
@@ -162,6 +162,7 @@ from diff_diff.lpdid import LPDiD
162
162
  from diff_diff.lpdid_results import LPDiDResults
163
163
  from diff_diff.mmm import (
164
164
  MeridianROIPrior,
165
+ meridian_calibration_mask,
165
166
  to_meridian_roi_prior,
166
167
  to_pymc_marketing_lift_test,
167
168
  )
@@ -223,6 +224,10 @@ from diff_diff.rdd import (
223
224
  RegressionDiscontinuity,
224
225
  RegressionDiscontinuityResults,
225
226
  )
227
+ from diff_diff.rddensity import (
228
+ RDDensityTest,
229
+ RDDensityTestResult,
230
+ )
226
231
  from diff_diff.rdplot import (
227
232
  RDPlot,
228
233
  RDPlotResult,
@@ -361,7 +366,7 @@ def __getattr__(name: str) -> _Any:
361
366
  raise AttributeError(f"module 'diff_diff' has no attribute {name!r}")
362
367
 
363
368
 
364
- __version__ = "3.9.0"
369
+ __version__ = "3.9.1"
365
370
  __all__ = [
366
371
  # Estimators
367
372
  "DifferenceInDifferences",
@@ -570,6 +575,9 @@ __all__ = [
570
575
  # RDPlot (data-driven RD plots, rdplot parity)
571
576
  "RDPlot",
572
577
  "RDPlotResult",
578
+ # RDDensityTest (manipulation testing, rddensity parity)
579
+ "RDDensityTest",
580
+ "RDDensityTestResult",
573
581
  # HeterogeneousAdoptionDiD pre-test diagnostics (Phase 3)
574
582
  "qug_test",
575
583
  "stute_test",
@@ -613,6 +621,7 @@ __all__ = [
613
621
  # MMM calibration export (interop)
614
622
  "to_pymc_marketing_lift_test",
615
623
  "to_meridian_roi_prior",
624
+ "meridian_calibration_mask",
616
625
  "MeridianROIPrior",
617
626
  # LLM guide accessor
618
627
  "get_llm_guide",
@@ -57,9 +57,10 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
57
57
  the horizon / group target.
58
58
  - ``CallawaySantAnna``: ``overall_att`` is cohort-size-weighted
59
59
  across post-treatment ``ATT(g, t)`` cells regardless of the
60
- fit-time ``aggregate`` kwarg. The event-study / group
61
- aggregations live on dedicated fields
62
- (``event_study_effects`` / ``group_effects``).
60
+ fit-time ``aggregate`` kwarg. The event-study / group tables are
61
+ produced post-fit via ``results.aggregate('event_study'/'group')``
62
+ (the deprecated fit-time ``aggregate=`` kwarg populates the legacy
63
+ ``event_study_effects`` / ``group_effects`` fields until 4.0).
63
64
  - ``ContinuousDiD``: the regime (PT vs. SPT) is a user-level
64
65
  assumption, not a library setting. The ``definition`` names
65
66
  both regime readings (``ATT^loc`` under PT,
@@ -137,9 +138,11 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
137
138
  "A cohort-size-weighted average of group-time ATTs "
138
139
  "``ATT(g, t)`` across post-treatment cells (``t >= g``). "
139
140
  "``overall_att`` is the simple-aggregation headline regardless "
140
- "of the fit-time ``aggregate`` kwarg; event-study and group "
141
- "aggregations populate ``event_study_effects`` / "
142
- "``group_effects`` fields when requested."
141
+ "of aggregation choices; event-study and group tables are "
142
+ "produced post-fit via "
143
+ "``results.aggregate('event_study'/'group')`` (the deprecated "
144
+ "fit-time ``aggregate=`` kwarg populates the legacy "
145
+ "``event_study_effects`` / ``group_effects`` fields until 4.0)."
143
146
  ),
144
147
  "aggregation": "simple",
145
148
  "headline_attribute": "overall_att",
@@ -64,10 +64,13 @@ class _StaggeredTripleDiffEngineMixin:
64
64
  supply the constructor attributes and the CS aggregation/bootstrap mixins
65
65
  this core calls. The annotations below exist because mypy type-checks this
66
66
  class independently of its hosts (`attr-defined` is not disabled) - they are
67
- declarations, never assignments.
67
+ declarations, never assignments — with ONE exception: `anticipation` is
68
+ re-assigned by the core's fit-time re-validation (the mutation-defense
69
+ re-check normalizes it to a Python int; see `_fit_staggered_core`).
68
70
  """
69
71
 
70
- # Constructor attributes read from the host class.
72
+ # Constructor attributes read from the host class. (`anticipation` is
73
+ # additionally RE-ASSIGNED at fit — the validate-and-normalize re-check.)
71
74
  estimation_method: str
72
75
  control_group: str
73
76
  alpha: float
@@ -178,8 +181,10 @@ class _StaggeredTripleDiffEngineMixin:
178
181
  # the deprecated StaggeredTripleDifference (whose 3.x API SHAPE is
179
182
  # frozen through removal - that freeze was never a licence to emit
180
183
  # silently-biased numbers) and direct attribute mutation on either
181
- # class, which bypasses __init__ and set_params alike.
182
- validate_anticipation(self.anticipation)
184
+ # class, which bypasses __init__ and set_params alike. The
185
+ # assignment form also normalizes a numpy scalar to a Python int
186
+ # before any `g - 1 - anticipation` arithmetic can overflow.
187
+ self.anticipation = validate_anticipation(self.anticipation)
183
188
  from diff_diff.survey import (
184
189
  _resolve_survey_for_fit,
185
190
  _validate_unit_constant_survey,
@@ -45,6 +45,13 @@ AGGREGATION_VOCABULARY: Tuple[str, ...] = (
45
45
  "event_study",
46
46
  "group",
47
47
  "calendar",
48
+ # "total": the estimator-owned total incremental outcome - an exact relay
49
+ # C x overall (C = the estimator's finite-masked complete-case aggregation
50
+ # support), CONDITIONAL on the realized aggregation mass. Shared meaning
51
+ # across adopters (CallawaySantAnna / ImputationDiD / TwoStageDiD /
52
+ # EfficientDiD); per-estimator support remains a subset, so non-adopters
53
+ # raise the same vocabulary-suffixed error as "calendar".
54
+ "total",
48
55
  )
49
56
 
50
57
  #: Pinned column schema of ``AggregationResult.to_dataframe()`` - identical for
@@ -97,6 +104,77 @@ def resolve_inference_df(results: Any) -> Optional[float]:
97
104
  return None
98
105
 
99
106
 
107
+ def build_total_relay_row(
108
+ *,
109
+ mass: float,
110
+ att: Optional[float],
111
+ se: Optional[float],
112
+ t_stat: Optional[float],
113
+ p_value: Optional[float],
114
+ conf_int: Tuple[float, float],
115
+ df: Optional[float],
116
+ alpha: float,
117
+ estimator: str,
118
+ ) -> "AggregationResult":
119
+ """One ``level='total'`` row: the exact relay ``mass x overall``.
120
+
121
+ The estimand is the total incremental outcome CONDITIONAL ON THE REALIZED
122
+ AGGREGATION MASS: ``att = mass x overall_att``, ``se = mass x overall_se``,
123
+ CI scaled by ``mass``, ``t_stat``/``p_value``/``df`` inherited unchanged
124
+ (every SE branch is homogeneous of degree 1, so the relay is exact under
125
+ analytical AND percentile-bootstrap inference). Inherited NaNs PASS
126
+ THROUGH - a degenerate fit whose CI is (nan, nan) beside a finite att
127
+ keeps its finite ``att``/``n``, mirroring what ``aggregate('simple')``
128
+ publishes on the same fit (the repo's non-estimable-row convention:
129
+ att/n are inputs, never blanked by NaN inference fields).
130
+
131
+ Two cases blank the WHOLE row (att/se/t/p/CI/n/df all NaN):
132
+ - ``mass`` is not finite (no aggregable support exists), or
133
+ - a FINITE overall value became non-finite BY the scaling (true float
134
+ overflow; surfaced per this code family's finiteness convention rather
135
+ than an ``errstate`` wrap).
136
+ """
137
+
138
+ def _f(v: Optional[float]) -> float:
139
+ return float(v) if v is not None else np.nan
140
+
141
+ a, s = _f(att), _f(se)
142
+ t, p = _f(t_stat), _f(p_value)
143
+ lo, hi = _f(conf_int[0]), _f(conf_int[1])
144
+ d = _f(df)
145
+
146
+ s_att, s_se = mass * a, mass * s
147
+ s_lo, s_hi = mass * lo, mass * hi
148
+ n_val = float(mass)
149
+ overflowed = np.isfinite(mass) and (
150
+ (np.isfinite(a) and not np.isfinite(s_att))
151
+ or (np.isfinite(s) and not np.isfinite(s_se))
152
+ or (np.isfinite(lo) and not np.isfinite(s_lo))
153
+ or (np.isfinite(hi) and not np.isfinite(s_hi))
154
+ )
155
+ if not np.isfinite(mass) or overflowed:
156
+ s_att = s_se = s_lo = s_hi = t = p = d = np.nan
157
+ n_val = np.nan
158
+
159
+ return AggregationResult(
160
+ level="total",
161
+ label=np.array(["total"], dtype=object),
162
+ target=np.array(["total"], dtype=object),
163
+ att=np.array([s_att], dtype=float),
164
+ se=np.array([s_se], dtype=float),
165
+ t_stat=np.array([t], dtype=float),
166
+ p_value=np.array([p], dtype=float),
167
+ conf_int_lower=np.array([s_lo], dtype=float),
168
+ conf_int_upper=np.array([s_hi], dtype=float),
169
+ n=np.array([n_val], dtype=float),
170
+ df=d,
171
+ alpha=alpha,
172
+ n_kind="obs",
173
+ weight=np.array([1.0], dtype=float),
174
+ estimator=estimator,
175
+ )
176
+
177
+
100
178
  def _sortable(labels: np.ndarray) -> bool:
101
179
  """Can ``labels`` be ordered without raising?
102
180
 
@@ -128,11 +206,14 @@ class AggregationResult(BaseResults):
128
206
  label : np.ndarray
129
207
  Per-row aggregation key: the cohort for ``"group"``, the calendar
130
208
  period for ``"calendar"``, the dose for ``"dose"``. A single
131
- ``"overall"`` entry for ``"simple"``.
209
+ ``"overall"`` entry for ``"simple"``; a single ``"total"`` entry for
210
+ ``"total"``.
132
211
  target : np.ndarray
133
212
  Per-row estimand discriminator, so one container can carry two
134
213
  aligned estimands over the same labels (ContinuousDiD's ATT(d) and
135
- ACRT(d) become 2N rows). ``"att"`` where an estimator has one.
214
+ ACRT(d) become 2N rows). ``"att"`` where an estimator has one;
215
+ ``"total"`` on a ``"total"`` row - a total incremental outcome over
216
+ the estimator's aggregation mass, not an ATT.
136
217
  att, se, t_stat, p_value : np.ndarray
137
218
  The canonical quintet, per row, carrying WHATEVER inference the fit
138
219
  stored - never recomputed. On a bootstrapped fit that usually means
@@ -157,7 +238,8 @@ class AggregationResult(BaseResults):
157
238
  ``(level, target)`` group. ``None`` where no per-row mass exists -
158
239
  CallawaySantAnna's ``"group"`` aggregation weights ``(g, t)`` cells
159
240
  equally WITHIN each cohort and has no cross-cohort mass, so inventing
160
- one would be a fabricated number.
241
+ one would be a fabricated number. Single-row containers
242
+ (``"simple"``, ``"total"``) carry ``weight=[1.0]``.
161
243
  df : np.ndarray
162
244
  Per-row inference degrees of freedom, NaN where none governed the
163
245
  stored p-value. NaN on percentile-bootstrap rows (no df governs
@@ -454,9 +536,12 @@ class AggregationKit:
454
536
  ----------
455
537
  bookkeeping : dict
456
538
  The aggregation-relevant subset of the estimator's ``precomputed``
457
- mapping. O(n_units) on panel fits; several entries are
458
- observation-length on repeated cross-sections, where
459
- ``all_units = np.arange(n_obs)`` by construction.
539
+ mapping, plus fit-time DERIVED keys stashed by the kit builder
540
+ (CallawaySantAnna's ``agg_gt_cells``/``is_survey_fit``,
541
+ ImputationDiD's ``total_support`` - see each builder). O(n_units) on
542
+ panel fits, except ``agg_gt_cells`` which is O(n_gt) 4-tuples;
543
+ several entries are observation-length on repeated cross-sections,
544
+ where ``all_units = np.arange(n_obs)`` by construction.
460
545
  influence : dict
461
546
  Per-``(g, t)`` influence-function payload. The DOMINANT retained
462
547
  object, roughly O(n_units x n_gt).
@@ -13,9 +13,13 @@ Design principles:
13
13
 
14
14
  - Plain English, not academic jargon. The library ships this in addition to, not
15
15
  in place of, the estimator's existing ``results.summary()`` academic output.
16
- - No estimator fitting and no variance re-derivation. Every effect, SE, p-value,
17
- CI, and sensitivity bound is either read from ``results`` or produced by an
18
- existing diff-diff utility. The report layer does compose a few cross-period
16
+ - No estimator fitting. Every effect, SE, p-value, CI, and sensitivity bound
17
+ is either read from ``results``, derived by the auto-constructed
18
+ ``DiagnosticReport`` from the result's own post-fit
19
+ ``aggregate('event_study')`` surface (a view or retained-kit recompute,
20
+ used only when the raw ``event_study_effects`` field is absent; see the
21
+ ``DiagnosticReport`` module docstring), or produced by an existing
22
+ diff-diff utility. The report layer does compose a few cross-period
19
23
  summaries from per-period inputs already on the result (joint-Wald / Bonferroni
20
24
  pre-trends p-value, MDV-to-ATT ratio, heterogeneity dispersion over
21
25
  post-treatment effects); see ``docs/methodology/REPORTING.md`` for the full
@@ -351,8 +355,10 @@ class BusinessReport:
351
355
  "precomputed= contains keys that are not implemented: "
352
356
  f"{sorted(_br_unsupported)}. Supported keys: "
353
357
  f"{sorted(_br_supported_precomputed)}. ``design_effect``, "
354
- "``heterogeneity``, and ``epv`` are read directly from the "
355
- "fitted result and do not accept precomputed overrides."
358
+ "``heterogeneity``, and ``epv`` are read from the fitted "
359
+ "result (heterogeneity may also derive the post-fit "
360
+ "aggregate('event_study') surface) and do not accept "
361
+ "precomputed overrides."
356
362
  )
357
363
 
358
364
  resolved_alpha = alpha if alpha is not None else getattr(results, "alpha", 0.05)
@@ -941,6 +947,10 @@ def _lift_pre_trends(dr: Optional[Dict[str, Any]]) -> Dict[str, Any]:
941
947
  return {
942
948
  "status": pt.get("status", "not_run"),
943
949
  "reason": pt.get("reason"),
950
+ # DR attaches derived-surface provenance to gate-skipped
951
+ # sections too (a successful-but-empty derivation) — the skip
952
+ # path must not drop it (None on raw routes, same as below).
953
+ "pre_period_source": pt.get("pre_period_source"),
944
954
  }
945
955
  return {
946
956
  "status": "computed",
@@ -956,6 +966,14 @@ def _lift_pre_trends(dr: Optional[Dict[str, Any]]) -> Dict[str, Any]:
956
966
  # ``verdict == "inconclusive"`` per ``_pt_event_study``'s
957
967
  # inconclusive branch (``diagnostic_report.py:999``).
958
968
  "n_dropped_undefined": pt.get("n_dropped_undefined"),
969
+ # Provenance of the pre-period surface: "aggregate_event_study"
970
+ # when DR derived it from the post-fit
971
+ # ``results.aggregate('event_study')`` container. BR always emits
972
+ # the key — ``None`` on raw-field routes (DR itself omits the key
973
+ # there; ``dict.get`` maps that to None). Lifted explicitly for
974
+ # the same reason as ``n_dropped_undefined`` — this function is a
975
+ # field whitelist.
976
+ "pre_period_source": pt.get("pre_period_source"),
959
977
  "reason": pt.get("reason"),
960
978
  # Carry the denominator df through when the survey F-reference
961
979
  # branch was used so BR consumers can flag the finite-sample
@@ -45,7 +45,7 @@ from diff_diff.survey import (
45
45
  build_unit_first_row_index,
46
46
  compute_survey_vcov,
47
47
  )
48
- from diff_diff.utils import safe_inference, validate_n_bootstrap
48
+ from diff_diff.utils import safe_inference, validate_anticipation, validate_n_bootstrap
49
49
 
50
50
  if TYPE_CHECKING:
51
51
  from diff_diff.survey import ResolvedSurveyDesign, SurveyDesign
@@ -182,7 +182,8 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
182
182
  ``P(D=d_L) > 0``) and no never-treated units present. Single-cohort only
183
183
  (multi-cohort and ``covariates=`` raise ``NotImplementedError``).
184
184
  anticipation : int, default=0
185
- Number of periods of treatment anticipation.
185
+ Number of periods of treatment anticipation. Must be a
186
+ non-negative integer; ``bool`` is rejected.
186
187
  base_period : str, default="varying"
187
188
  ``"varying"`` or ``"universal"``.
188
189
  alpha : float, default=0.05
@@ -310,12 +311,18 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
310
311
  self._validate_constrained_params()
311
312
 
312
313
  def _validate_constrained_params(self) -> None:
313
- """Validate control_group, base_period, and estimation_method values."""
314
+ """Validate control_group, base_period, and estimation_method values.
315
+
316
+ Also validates ``anticipation`` and re-assigns it as a normalized
317
+ Python ``int`` — idempotent on an already-normalized value, so a
318
+ re-run never changes fitted config.
319
+ """
314
320
  if self.control_group not in self._VALID_CONTROL_GROUPS:
315
321
  raise ValueError(
316
322
  f"Invalid control_group: '{self.control_group}'. "
317
323
  f"Must be one of {self._VALID_CONTROL_GROUPS}."
318
324
  )
325
+ self.anticipation = validate_anticipation(self.anticipation)
319
326
  if self.base_period not in self._VALID_BASE_PERIODS:
320
327
  raise ValueError(
321
328
  f"Invalid base_period: '{self.base_period}'. "
@@ -442,6 +449,14 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
442
449
  else:
443
450
  aggregate = None
444
451
 
452
+ # Fit-time re-check: __init__ and set_params validate eagerly, so
453
+ # this only catches DIRECT attribute mutation (est.anticipation = ...)
454
+ # — an out-of-domain value silently changes the ESTIMAND. The
455
+ # assignment also re-normalizes a mutated numpy scalar to int. Placed
456
+ # AFTER the deprecation shim so a caller who both mutated and passed
457
+ # a deprecated argument still sees the FutureWarning before the raise.
458
+ self.anticipation = validate_anticipation(self.anticipation)
459
+
445
460
  # 1. Validate & prepare
446
461
  _VALID_AGGREGATES = (None, "dose", "eventstudy")
447
462
  if aggregate not in _VALID_AGGREGATES: