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.
- {diff_diff-3.9.0 → diff_diff-3.9.1}/PKG-INFO +3 -2
- {diff_diff-3.9.0 → diff_diff-3.9.1}/README.md +1 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/__init__.py +10 -1
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_reporting_helpers.py +9 -6
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_staggered_triple_diff_engine.py +9 -4
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/aggregation.py +91 -6
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/business_report.py +23 -5
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did.py +18 -3
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/diagnostic_report.py +567 -113
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did.py +24 -8
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_results.py +58 -4
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms-autonomous.txt +5 -3
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms-full.txt +151 -34
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms-practitioner.txt +29 -4
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/llms.txt +3 -2
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation.py +20 -3
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation_results.py +55 -5
- diff_diff-3.9.1/diff_diff/mmm.py +1600 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/practitioner.py +9 -5
- diff_diff-3.9.1/diff_diff/rddensity.py +1417 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/spillover.py +13 -16
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/stacked_did.py +17 -3
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered.py +48 -14
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_results.py +149 -12
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_triple_diff.py +3 -1
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/sun_abraham.py +9 -1
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/triple_diff.py +7 -5
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage.py +10 -4
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage_results.py +55 -7
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/utils.py +17 -5
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/wooldridge.py +14 -6
- {diff_diff-3.9.0 → diff_diff-3.9.1}/pyproject.toml +2 -2
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/Cargo.lock +15 -15
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/Cargo.toml +1 -1
- diff_diff-3.9.0/diff_diff/mmm.py +0 -653
- {diff_diff-3.9.0 → diff_diff-3.9.1}/LICENSE +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_backend.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_base.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_deprecation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_guides_api.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_nprobust_port.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/_rdrobust_port.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/agent_workflow.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/bacon.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/balancing.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/bootstrap_chunking.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/bootstrap_utils.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/chaisemartin_dhaultfoeuille.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/changes_in_changes.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/changes_in_changes_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/conformal.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/conley.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/datasets.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/estimators.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/guides/__init__.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/had.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/had_pretests.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/honest_did.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/linalg.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/local_linear.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/lpdid.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/lpdid_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/power.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/prep.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/profile.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/rdd.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/rdplot.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/results_base.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/survey.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/synthetic_control.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/synthetic_control_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop_global.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop_local.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/trop_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/twfe.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_event_study.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/build.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/alloc_profile.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/batched_solve.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/bootstrap.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/demean.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/lib.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/linalg.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.9.1}/rust/src/trop.rs +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
62
|
-
(``
|
|
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
|
|
141
|
-
"
|
|
142
|
-
"``
|
|
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
|
-
|
|
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
|
|
458
|
-
|
|
459
|
-
``
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
355
|
-
"
|
|
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:
|