diff-diff 3.9.0__tar.gz → 3.10.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.
- {diff_diff-3.9.0 → diff_diff-3.10.0}/PKG-INFO +4 -2
- {diff_diff-3.9.0 → diff_diff-3.10.0}/README.md +2 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/__init__.py +15 -1
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_reporting_helpers.py +9 -6
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_staggered_triple_diff_engine.py +11 -4
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/aggregation.py +120 -10
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/bootstrap_chunking.py +17 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/bootstrap_utils.py +113 -6
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/business_report.py +23 -5
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/continuous_did.py +18 -3
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/datasets.py +5 -1
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/diagnostic_report.py +573 -113
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/efficient_did.py +78 -57
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/efficient_did_aggregation.py +36 -7
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/efficient_did_bootstrap.py +99 -15
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/efficient_did_results.py +169 -24
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/estimators.py +16 -4
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/guides/llms-autonomous.txt +46 -4
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/guides/llms-full.txt +256 -47
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/guides/llms-practitioner.txt +61 -12
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/guides/llms.txt +4 -2
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/imputation.py +29 -6
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/imputation_results.py +55 -5
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/linalg.py +84 -25
- diff_diff-3.10.0/diff_diff/lwdid.py +4503 -0
- diff_diff-3.10.0/diff_diff/lwdid_randomization.py +508 -0
- diff_diff-3.10.0/diff_diff/lwdid_results.py +826 -0
- diff_diff-3.10.0/diff_diff/lwdid_sensitivity.py +907 -0
- diff_diff-3.10.0/diff_diff/lwdid_staggered.py +817 -0
- diff_diff-3.10.0/diff_diff/lwdid_visualization.py +303 -0
- diff_diff-3.10.0/diff_diff/lwdid_wild_bootstrap.py +360 -0
- diff_diff-3.10.0/diff_diff/mmm.py +1600 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/practitioner.py +32 -28
- diff_diff-3.10.0/diff_diff/rddensity.py +1417 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/results.py +2 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/results_base.py +3 -2
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/spillover.py +45 -16
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/stacked_did.py +17 -3
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/staggered.py +94 -86
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/staggered_bootstrap.py +126 -9
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/staggered_results.py +269 -31
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/staggered_triple_diff.py +3 -1
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/sun_abraham.py +9 -1
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/triple_diff.py +7 -5
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/twfe.py +14 -14
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/two_stage.py +10 -4
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/two_stage_results.py +55 -7
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/utils.py +17 -5
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/wooldridge.py +14 -6
- {diff_diff-3.9.0 → diff_diff-3.10.0}/pyproject.toml +2 -2
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/Cargo.lock +19 -19
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/Cargo.toml +1 -1
- diff_diff-3.9.0/diff_diff/mmm.py +0 -653
- {diff_diff-3.9.0 → diff_diff-3.10.0}/LICENSE +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_backend.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_base.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_deprecation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_guides_api.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_nprobust_port.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/_rdrobust_port.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/agent_workflow.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/bacon.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/balancing.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/chaisemartin_dhaultfoeuille.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/changes_in_changes.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/changes_in_changes_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/conformal.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/conley.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/continuous_did_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/guides/__init__.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/had.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/had_pretests.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/honest_did.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/imputation_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/local_linear.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/lpdid.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/lpdid_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/power.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/prep.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/profile.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/rdd.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/rdplot.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/survey.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/synthetic_control.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/synthetic_control_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/trop.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/trop_global.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/trop_local.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/trop_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/two_stage_aggregation.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_event_study.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/build.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/alloc_profile.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/batched_solve.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/bootstrap.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/demean.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/lib.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/linalg.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.0}/rust/src/trop.rs +0 -0
- {diff_diff-3.9.0 → diff_diff-3.10.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.
|
|
3
|
+
Version: 3.10.0
|
|
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.3 ; 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'
|
|
@@ -175,11 +175,13 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
|
|
|
175
175
|
- [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html) - Wooldridge (2023, 2025) ETWFE: saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias `ETWFE`.
|
|
176
176
|
- [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing or non-absorbing (reversible) treatment
|
|
177
177
|
- [ChangesInChanges](https://diff-diff.readthedocs.io/en/stable/api/changes_in_changes.html) - Athey & Imbens (2006) nonlinear/distributional DiD for the 2x2 design: full counterfactual distribution and quantile treatment effects via CDF transformation, plus the QDiD comparison estimator via `method="qdid"`; bootstrap inference; R qte parity. Alias `CiC`
|
|
178
|
+
- [LWDiD](https://diff-diff.readthedocs.io/en/stable/api/lwdid.html) - Lee & Wooldridge (2025, 2026) rolling-transformation DiD: unit-specific demean/detrend converts panel to cross-section, staggered adoption, `estimation_method` in `reg`/`ipw`/`dr`/`psm` (the papers' RA/IPW/IPWRA plus propensity-score matching), exact small-N inference on the classical collapsed regression
|
|
178
179
|
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
|
|
179
180
|
|
|
180
181
|
## Diagnostics & Sensitivity
|
|
181
182
|
|
|
182
183
|
- [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
|
|
184
|
+
- [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
185
|
- [Parallel Trends Testing](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - simple and Wasserstein-robust parallel trends tests, equivalence testing (TOST)
|
|
184
186
|
- [Placebo Tests](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - placebo timing, group, permutation, leave-one-out
|
|
185
187
|
- [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
|
|
@@ -120,11 +120,13 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
|
|
|
120
120
|
- [WooldridgeDiD](https://diff-diff.readthedocs.io/en/stable/api/wooldridge_etwfe.html) - Wooldridge (2023, 2025) ETWFE: saturated OLS, logit/Poisson QMLE (ASF-based ATT). Alias `ETWFE`.
|
|
121
121
|
- [LPDiD](https://diff-diff.readthedocs.io/en/stable/api/lpdid.html) - Dube, Girardi, Jorda & Taylor (2025) Local Projections DiD: per-horizon long-difference event study on clean controls (no negative weighting), variance- or equally-weighted ATT, for absorbing or non-absorbing (reversible) treatment
|
|
122
122
|
- [ChangesInChanges](https://diff-diff.readthedocs.io/en/stable/api/changes_in_changes.html) - Athey & Imbens (2006) nonlinear/distributional DiD for the 2x2 design: full counterfactual distribution and quantile treatment effects via CDF transformation, plus the QDiD comparison estimator via `method="qdid"`; bootstrap inference; R qte parity. Alias `CiC`
|
|
123
|
+
- [LWDiD](https://diff-diff.readthedocs.io/en/stable/api/lwdid.html) - Lee & Wooldridge (2025, 2026) rolling-transformation DiD: unit-specific demean/detrend converts panel to cross-section, staggered adoption, `estimation_method` in `reg`/`ipw`/`dr`/`psm` (the papers' RA/IPW/IPWRA plus propensity-score matching), exact small-N inference on the classical collapsed regression
|
|
123
124
|
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
|
|
124
125
|
|
|
125
126
|
## Diagnostics & Sensitivity
|
|
126
127
|
|
|
127
128
|
- [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
|
|
129
|
+
- [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
130
|
- [Parallel Trends Testing](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - simple and Wasserstein-robust parallel trends tests, equivalence testing (TOST)
|
|
129
131
|
- [Placebo Tests](https://diff-diff.readthedocs.io/en/stable/api/diagnostics.html) - placebo timing, group, permutation, leave-one-out
|
|
130
132
|
- [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
|
|
@@ -160,8 +160,11 @@ from diff_diff.local_linear import (
|
|
|
160
160
|
)
|
|
161
161
|
from diff_diff.lpdid import LPDiD
|
|
162
162
|
from diff_diff.lpdid_results import LPDiDResults
|
|
163
|
+
from diff_diff.lwdid import LWDiD
|
|
164
|
+
from diff_diff.lwdid_results import LWDiDResults
|
|
163
165
|
from diff_diff.mmm import (
|
|
164
166
|
MeridianROIPrior,
|
|
167
|
+
meridian_calibration_mask,
|
|
165
168
|
to_meridian_roi_prior,
|
|
166
169
|
to_pymc_marketing_lift_test,
|
|
167
170
|
)
|
|
@@ -223,6 +226,10 @@ from diff_diff.rdd import (
|
|
|
223
226
|
RegressionDiscontinuity,
|
|
224
227
|
RegressionDiscontinuityResults,
|
|
225
228
|
)
|
|
229
|
+
from diff_diff.rddensity import (
|
|
230
|
+
RDDensityTest,
|
|
231
|
+
RDDensityTestResult,
|
|
232
|
+
)
|
|
226
233
|
from diff_diff.rdplot import (
|
|
227
234
|
RDPlot,
|
|
228
235
|
RDPlotResult,
|
|
@@ -361,7 +368,7 @@ def __getattr__(name: str) -> _Any:
|
|
|
361
368
|
raise AttributeError(f"module 'diff_diff' has no attribute {name!r}")
|
|
362
369
|
|
|
363
370
|
|
|
364
|
-
__version__ = "3.
|
|
371
|
+
__version__ = "3.10.0"
|
|
365
372
|
__all__ = [
|
|
366
373
|
# Estimators
|
|
367
374
|
"DifferenceInDifferences",
|
|
@@ -454,6 +461,9 @@ __all__ = [
|
|
|
454
461
|
# LPDiD (Local Projections DiD)
|
|
455
462
|
"LPDiD",
|
|
456
463
|
"LPDiDResults",
|
|
464
|
+
# LWDiD (Lee & Wooldridge rolling transformation DiD)
|
|
465
|
+
"LWDiD",
|
|
466
|
+
"LWDiDResults",
|
|
457
467
|
# Visualization
|
|
458
468
|
"plot_bacon",
|
|
459
469
|
"plot_event_study",
|
|
@@ -570,6 +580,9 @@ __all__ = [
|
|
|
570
580
|
# RDPlot (data-driven RD plots, rdplot parity)
|
|
571
581
|
"RDPlot",
|
|
572
582
|
"RDPlotResult",
|
|
583
|
+
# RDDensityTest (manipulation testing, rddensity parity)
|
|
584
|
+
"RDDensityTest",
|
|
585
|
+
"RDDensityTestResult",
|
|
573
586
|
# HeterogeneousAdoptionDiD pre-test diagnostics (Phase 3)
|
|
574
587
|
"qug_test",
|
|
575
588
|
"stute_test",
|
|
@@ -613,6 +626,7 @@ __all__ = [
|
|
|
613
626
|
# MMM calibration export (interop)
|
|
614
627
|
"to_pymc_marketing_lift_test",
|
|
615
628
|
"to_meridian_roi_prior",
|
|
629
|
+
"meridian_calibration_mask",
|
|
616
630
|
"MeridianROIPrior",
|
|
617
631
|
# LLM guide accessor
|
|
618
632
|
"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
|
|
@@ -142,6 +145,8 @@ class _StaggeredTripleDiffEngineMixin:
|
|
|
142
145
|
unit: Optional[str] = None,
|
|
143
146
|
precomputed: Any = None,
|
|
144
147
|
cband: bool = True,
|
|
148
|
+
*,
|
|
149
|
+
_replay_bitgen_state: Optional[Dict[str, Any]] = None,
|
|
145
150
|
) -> Any: ...
|
|
146
151
|
|
|
147
152
|
def _fit_staggered_core(
|
|
@@ -178,8 +183,10 @@ class _StaggeredTripleDiffEngineMixin:
|
|
|
178
183
|
# the deprecated StaggeredTripleDifference (whose 3.x API SHAPE is
|
|
179
184
|
# frozen through removal - that freeze was never a licence to emit
|
|
180
185
|
# silently-biased numbers) and direct attribute mutation on either
|
|
181
|
-
# class, which bypasses __init__ and set_params alike.
|
|
182
|
-
|
|
186
|
+
# class, which bypasses __init__ and set_params alike. The
|
|
187
|
+
# assignment form also normalizes a numpy scalar to a Python int
|
|
188
|
+
# before any `g - 1 - anticipation` arithmetic can overflow.
|
|
189
|
+
self.anticipation = validate_anticipation(self.anticipation)
|
|
183
190
|
from diff_diff.survey import (
|
|
184
191
|
_resolve_survey_for_fit,
|
|
185
192
|
_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).
|
|
@@ -468,7 +553,11 @@ class AggregationKit:
|
|
|
468
553
|
``cband_crit_value`` is ``None`` both when bands were disabled and
|
|
469
554
|
when no aggregation ran, so it cannot distinguish the two.
|
|
470
555
|
bootstrap : AggregationKit.BootstrapReplaySpec or None
|
|
471
|
-
Value-bound bootstrap replay description
|
|
556
|
+
Value-bound bootstrap replay description. Populated on
|
|
557
|
+
CallawaySantAnna and EfficientDiD bootstrapped fits (their recompute
|
|
558
|
+
levels replay the fit-time multiplier bootstrap from it); ``None``
|
|
559
|
+
on analytical fits and on pre-replay legacy artifacts (whose
|
|
560
|
+
bootstrapped recompute levels fail closed with a refit message).
|
|
472
561
|
"""
|
|
473
562
|
|
|
474
563
|
bookkeeping: Dict[str, Any]
|
|
@@ -489,9 +578,29 @@ class BootstrapReplaySpec:
|
|
|
489
578
|
``set_params(n_bootstrap=...)`` silently changes - and can truncate - the
|
|
490
579
|
replayed stream.
|
|
491
580
|
|
|
492
|
-
This records the generator state plus the parameters BY VALUE
|
|
493
|
-
|
|
494
|
-
|
|
581
|
+
This records the generator state plus the parameters BY VALUE, which
|
|
582
|
+
pickles and is immune to later mutation of the estimator. Two usage
|
|
583
|
+
modes:
|
|
584
|
+
|
|
585
|
+
- ``rebuild()`` reconstructs the plain unit-level stream via
|
|
586
|
+
``iter_weight_blocks`` (it does NOT cover the survey/FPC/PSU-expansion
|
|
587
|
+
branches).
|
|
588
|
+
- The CallawaySantAnna and EfficientDiD post-fit replays are
|
|
589
|
+
STATE-ONLY: they consume
|
|
590
|
+
``bitgen_state``/``n_bootstrap``/``weight_type``/``backend`` and let
|
|
591
|
+
each engine's ``_run_multiplier_bootstrap`` re-derive the generation
|
|
592
|
+
branch from the kit bookkeeping - one branch-selection implementation
|
|
593
|
+
per engine, no drift.
|
|
594
|
+
|
|
595
|
+
``backend`` records the weight-generation backend identity at capture
|
|
596
|
+
(``"rust"``/``"numpy"`` per
|
|
597
|
+
:func:`diff_diff.bootstrap_chunking.effective_weight_backend`, or
|
|
598
|
+
``"portable"`` for provably backend-independent generation branches).
|
|
599
|
+
The Rust and NumPy generators produce DIFFERENT draws from the same
|
|
600
|
+
bit-generator state, so a replay under a different backend must FAIL
|
|
601
|
+
CLOSED rather than silently regenerate another realization. ``None``
|
|
602
|
+
means unknown and also fails closed - a permissive default on a safety
|
|
603
|
+
discriminator would let a future constructor silently bypass the guard.
|
|
495
604
|
"""
|
|
496
605
|
|
|
497
606
|
bitgen_state: Dict[str, Any]
|
|
@@ -500,6 +609,7 @@ class BootstrapReplaySpec:
|
|
|
500
609
|
weight_type: str
|
|
501
610
|
block_size: Optional[int] = None
|
|
502
611
|
expand_index: Optional[np.ndarray] = None
|
|
612
|
+
backend: Optional[str] = None
|
|
503
613
|
|
|
504
614
|
def rebuild(self) -> Any:
|
|
505
615
|
"""Reconstruct the replayable weight stream."""
|
|
@@ -70,6 +70,23 @@ if TYPE_CHECKING:
|
|
|
70
70
|
_TARGET_BLOCK_BYTES = 256 * 1024 * 1024
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
def effective_weight_backend() -> str:
|
|
74
|
+
"""The weight-generation backend :func:`iter_weight_blocks` would use NOW.
|
|
75
|
+
|
|
76
|
+
Returns ``"rust"`` exactly when the generator branch below does — the
|
|
77
|
+
predicate must stay identical to :func:`iter_weight_blocks`'s own
|
|
78
|
+
``rust_gen`` resolution. The two backends produce DIFFERENT draws from
|
|
79
|
+
the same bit-generator state (Rust draws one base seed and row-seeds
|
|
80
|
+
Xoshiro absolutely; the NumPy fallback consumes the PCG64 stream
|
|
81
|
+
directly), so a captured RNG state replays bit-identically only within
|
|
82
|
+
one backend. Post-fit bootstrap replay (the CallawaySantAnna and
|
|
83
|
+
EfficientDiD ``BootstrapReplaySpec``) stamps this value at fit and fails
|
|
84
|
+
closed on a mismatch rather than silently regenerating a different
|
|
85
|
+
realization.
|
|
86
|
+
"""
|
|
87
|
+
return "rust" if (HAS_RUST_BACKEND and _rust_bootstrap_weights is not None) else "numpy"
|
|
88
|
+
|
|
89
|
+
|
|
73
90
|
def compute_block_size(
|
|
74
91
|
n_units: int, n_bootstrap: int, target_bytes: int = _TARGET_BLOCK_BYTES
|
|
75
92
|
) -> int:
|
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Shared bootstrap utilities for multiplier bootstrap inference.
|
|
3
3
|
|
|
4
|
-
Provides weight generation, percentile CI
|
|
5
|
-
|
|
4
|
+
Provides weight generation, percentile statistics (CI / p-value / per-effect
|
|
5
|
+
stats), and the percentile-override appliers shared across the estimator
|
|
6
|
+
bootstrap engines.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
import warnings
|
|
9
|
-
from typing import TYPE_CHECKING, Optional, Tuple
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, Protocol, Tuple
|
|
10
11
|
|
|
11
12
|
import numpy as np
|
|
12
13
|
|
|
13
14
|
from diff_diff._backend import HAS_RUST_BACKEND, _rust_bootstrap_weights
|
|
15
|
+
from diff_diff.utils import safe_inference_batch
|
|
14
16
|
|
|
15
17
|
if TYPE_CHECKING:
|
|
16
18
|
from diff_diff.survey import ResolvedSurveyDesign
|
|
@@ -29,6 +31,8 @@ __all__ = [
|
|
|
29
31
|
"compute_effect_bootstrap_stats_batch",
|
|
30
32
|
"warn_bootstrap_failure_rate",
|
|
31
33
|
"stratified_bootstrap_indices",
|
|
34
|
+
"apply_bootstrap_event_study_overrides",
|
|
35
|
+
"apply_bootstrap_group_overrides",
|
|
32
36
|
]
|
|
33
37
|
|
|
34
38
|
|
|
@@ -366,7 +370,12 @@ def compute_effect_bootstrap_stats(
|
|
|
366
370
|
se = float(np.std(valid_dist, ddof=1))
|
|
367
371
|
|
|
368
372
|
# Guard: if SE is not finite or zero, all inference fields must be NaN.
|
|
369
|
-
|
|
373
|
+
# An EXACTLY CONSTANT distribution is degenerate too, even when its
|
|
374
|
+
# np.std comes back tiny-positive from mean-subtraction roundoff at a
|
|
375
|
+
# non-zero constant level (e.g. census-FPC zero-weight draws leave every
|
|
376
|
+
# replicate at the original effect): a t built on that roundoff SE would
|
|
377
|
+
# be astronomically large and silently "significant".
|
|
378
|
+
if not np.isfinite(se) or se <= 0 or float(valid_dist.max()) == float(valid_dist.min()):
|
|
370
379
|
warnings.warn(
|
|
371
380
|
f"Bootstrap SE is non-finite or zero (n_valid={n_valid}) in {context}. "
|
|
372
381
|
"Returning NaN for SE/CI/p-value.",
|
|
@@ -472,8 +481,13 @@ def compute_effect_bootstrap_stats_batch(
|
|
|
472
481
|
batch_p = np.minimum(2 * batch_p, 1.0)
|
|
473
482
|
batch_p = np.maximum(batch_p, 1 / (n_bootstrap + 1))
|
|
474
483
|
|
|
475
|
-
# Guard: SE must be positive and finite
|
|
476
|
-
|
|
484
|
+
# Guard: SE must be positive and finite, and the distribution must
|
|
485
|
+
# not be EXACTLY CONSTANT (a constant non-zero level can produce a
|
|
486
|
+
# tiny-positive np.std from mean-subtraction roundoff - e.g.
|
|
487
|
+
# census-FPC zero-weight draws - which would otherwise leak a
|
|
488
|
+
# roundoff SE and an astronomically large t past the zero check).
|
|
489
|
+
is_constant = sub.max(axis=0) == sub.min(axis=0)
|
|
490
|
+
se_valid = np.isfinite(batch_ses) & (batch_ses > 0) & ~is_constant
|
|
477
491
|
n_bad_se = int(np.sum(~se_valid))
|
|
478
492
|
if n_bad_se > 0:
|
|
479
493
|
warnings.warn(
|
|
@@ -948,3 +962,96 @@ def generate_rao_wu_weights_batch(
|
|
|
948
962
|
for b in range(n_bootstrap):
|
|
949
963
|
result[b] = generate_rao_wu_weights(resolved_survey, rng)
|
|
950
964
|
return result
|
|
965
|
+
|
|
966
|
+
|
|
967
|
+
# =============================================================================
|
|
968
|
+
# Bootstrap override helpers (shared by fit and the post-fit replay)
|
|
969
|
+
# =============================================================================
|
|
970
|
+
# Extracted verbatim from CallawaySantAnna.fit()'s inline blocks (and adopted
|
|
971
|
+
# by EfficientDiD's fit/replay) so the post-fit aggregate() replay applies
|
|
972
|
+
# EXACTLY the same percentile overrides the fit-time path applies — one
|
|
973
|
+
# implementation, no twin drift. (The deprecated StaggeredTripleDifference
|
|
974
|
+
# keeps its OWN copy of the group replacement loop; unifying it is sequenced
|
|
975
|
+
# with the M-014 container port.) Note on warning attribution: when the
|
|
976
|
+
# engines run under the post-fit replay their fit-tuned stacklevels resolve
|
|
977
|
+
# into library frames rather than the user's aggregate() call — accepted as
|
|
978
|
+
# cosmetic (recorded decision).
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
class _BootstrapOverrideSource(Protocol):
|
|
982
|
+
"""Structural contract for bootstrap containers the appliers consume.
|
|
983
|
+
|
|
984
|
+
Both ``CSBootstrapResults`` and ``EDiDBootstrapResults`` satisfy it by
|
|
985
|
+
field name; a Protocol keeps this module free of estimator imports.
|
|
986
|
+
"""
|
|
987
|
+
|
|
988
|
+
event_study_ses: Optional[Dict[Any, float]]
|
|
989
|
+
event_study_cis: Optional[Dict[Any, Tuple[float, float]]]
|
|
990
|
+
event_study_p_values: Optional[Dict[Any, float]]
|
|
991
|
+
group_effect_ses: Optional[Dict[Any, float]]
|
|
992
|
+
group_effect_cis: Optional[Dict[Any, Tuple[float, float]]]
|
|
993
|
+
group_effect_p_values: Optional[Dict[Any, float]]
|
|
994
|
+
|
|
995
|
+
|
|
996
|
+
def apply_bootstrap_event_study_overrides(
|
|
997
|
+
event_study_effects: Optional[Dict[int, Dict[str, Any]]],
|
|
998
|
+
bootstrap_results: _BootstrapOverrideSource,
|
|
999
|
+
alpha: float,
|
|
1000
|
+
) -> None:
|
|
1001
|
+
"""Overwrite per-event-time se/CI/p with percentile-bootstrap values.
|
|
1002
|
+
|
|
1003
|
+
Mutates ``event_study_effects`` in place; t is recomputed from the
|
|
1004
|
+
percentile SE via ``safe_inference_batch``. No-op when either side has
|
|
1005
|
+
no event-study surface.
|
|
1006
|
+
"""
|
|
1007
|
+
if (
|
|
1008
|
+
event_study_effects is not None
|
|
1009
|
+
and bootstrap_results.event_study_ses is not None
|
|
1010
|
+
and bootstrap_results.event_study_cis is not None
|
|
1011
|
+
and bootstrap_results.event_study_p_values is not None
|
|
1012
|
+
):
|
|
1013
|
+
es_keys = [e for e in event_study_effects if e in bootstrap_results.event_study_ses]
|
|
1014
|
+
if es_keys:
|
|
1015
|
+
es_effects_arr = np.array([float(event_study_effects[e]["effect"]) for e in es_keys])
|
|
1016
|
+
es_ses_arr = np.array([float(bootstrap_results.event_study_ses[e]) for e in es_keys])
|
|
1017
|
+
es_t_stats, _, _, _ = safe_inference_batch(es_effects_arr, es_ses_arr, alpha=alpha)
|
|
1018
|
+
for idx, e in enumerate(es_keys):
|
|
1019
|
+
event_study_effects[e]["se"] = bootstrap_results.event_study_ses[e]
|
|
1020
|
+
event_study_effects[e]["conf_int"] = bootstrap_results.event_study_cis[e]
|
|
1021
|
+
event_study_effects[e]["p_value"] = bootstrap_results.event_study_p_values[e]
|
|
1022
|
+
event_study_effects[e]["t_stat"] = float(es_t_stats[idx])
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
def apply_bootstrap_group_overrides(
|
|
1026
|
+
group_effects: Optional[Dict[Any, Dict[str, Any]]],
|
|
1027
|
+
bootstrap_results: _BootstrapOverrideSource,
|
|
1028
|
+
alpha: float,
|
|
1029
|
+
) -> None:
|
|
1030
|
+
"""Overwrite per-group se/CI/p with percentile-bootstrap values.
|
|
1031
|
+
|
|
1032
|
+
Mutates ``group_effects`` in place and clears each row's ``df_used``
|
|
1033
|
+
(the percentile inference never used the analytical df, so keeping it
|
|
1034
|
+
would claim a t-reference that governed nothing). No-op when either
|
|
1035
|
+
side has no group surface.
|
|
1036
|
+
"""
|
|
1037
|
+
if (
|
|
1038
|
+
group_effects is not None
|
|
1039
|
+
and bootstrap_results.group_effect_ses is not None
|
|
1040
|
+
and bootstrap_results.group_effect_cis is not None
|
|
1041
|
+
and bootstrap_results.group_effect_p_values is not None
|
|
1042
|
+
):
|
|
1043
|
+
grp_keys = [g for g in group_effects if g in bootstrap_results.group_effect_ses]
|
|
1044
|
+
if grp_keys:
|
|
1045
|
+
grp_effects_arr = np.array([float(group_effects[g]["effect"]) for g in grp_keys])
|
|
1046
|
+
grp_ses_arr = np.array([float(bootstrap_results.group_effect_ses[g]) for g in grp_keys])
|
|
1047
|
+
grp_t_stats, _, _, _ = safe_inference_batch(grp_effects_arr, grp_ses_arr, alpha=alpha)
|
|
1048
|
+
for idx, g in enumerate(grp_keys):
|
|
1049
|
+
group_effects[g]["se"] = bootstrap_results.group_effect_ses[g]
|
|
1050
|
+
group_effects[g]["conf_int"] = bootstrap_results.group_effect_cis[g]
|
|
1051
|
+
group_effects[g]["p_value"] = bootstrap_results.group_effect_p_values[g]
|
|
1052
|
+
group_effects[g]["t_stat"] = float(grp_t_stats[idx])
|
|
1053
|
+
# Same clearing rule the ES df provenance follows: these
|
|
1054
|
+
# se/p/CI are now percentile-bootstrap values that never used
|
|
1055
|
+
# the analytical df, so keeping df_used would claim a
|
|
1056
|
+
# t-reference that governed nothing.
|
|
1057
|
+
group_effects[g]["df_used"] = None
|
|
@@ -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:
|