diff-diff 3.9.1__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.1 → diff_diff-3.10.0}/PKG-INFO +3 -2
- {diff_diff-3.9.1 → diff_diff-3.10.0}/README.md +1 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/__init__.py +6 -1
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_staggered_triple_diff_engine.py +2 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/aggregation.py +29 -4
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/bootstrap_chunking.py +17 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/bootstrap_utils.py +113 -6
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/datasets.py +5 -1
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/diagnostic_report.py +9 -3
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/efficient_did.py +57 -52
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/efficient_did_aggregation.py +36 -7
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/efficient_did_bootstrap.py +99 -15
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/efficient_did_results.py +114 -23
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/estimators.py +16 -4
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/guides/llms-autonomous.txt +41 -1
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/guides/llms-full.txt +110 -18
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/guides/llms-practitioner.txt +38 -14
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/guides/llms.txt +2 -1
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/imputation.py +9 -3
- {diff_diff-3.9.1 → 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.9.1 → diff_diff-3.10.0}/diff_diff/practitioner.py +28 -28
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/results.py +2 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/results_base.py +3 -2
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/spillover.py +32 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/staggered.py +54 -80
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/staggered_bootstrap.py +126 -9
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/staggered_results.py +126 -25
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/twfe.py +14 -14
- {diff_diff-3.9.1 → diff_diff-3.10.0}/pyproject.toml +2 -2
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/Cargo.lock +7 -7
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/Cargo.toml +1 -1
- {diff_diff-3.9.1 → diff_diff-3.10.0}/LICENSE +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_backend.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_base.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_deprecation.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_guides_api.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_nprobust_port.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_rdrobust_port.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/_reporting_helpers.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/agent_workflow.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/bacon.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/balancing.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/business_report.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/chaisemartin_dhaultfoeuille.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/changes_in_changes.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/changes_in_changes_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/conformal.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/conley.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/continuous_did.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/continuous_did_aggregation.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/guides/__init__.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/had.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/had_pretests.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/honest_did.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/imputation_aggregation.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/imputation_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/local_linear.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/lpdid.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/lpdid_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/mmm.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/power.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/prep.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/profile.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/rdd.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/rddensity.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/rdplot.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/stacked_did.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/staggered_triple_diff.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/sun_abraham.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/survey.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/synthetic_control.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/synthetic_control_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/triple_diff.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/trop.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/trop_global.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/trop_local.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/trop_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/two_stage.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/two_stage_aggregation.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/two_stage_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/utils.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_event_study.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/wooldridge.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/build.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/alloc_profile.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/batched_solve.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/bootstrap.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/demean.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/lib.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/linalg.rs +0 -0
- {diff_diff-3.9.1 → diff_diff-3.10.0}/rust/src/trop.rs +0 -0
- {diff_diff-3.9.1 → 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,6 +175,7 @@ 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
|
|
@@ -120,6 +120,7 @@ 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
|
|
@@ -160,6 +160,8 @@ 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,
|
|
165
167
|
meridian_calibration_mask,
|
|
@@ -366,7 +368,7 @@ def __getattr__(name: str) -> _Any:
|
|
|
366
368
|
raise AttributeError(f"module 'diff_diff' has no attribute {name!r}")
|
|
367
369
|
|
|
368
370
|
|
|
369
|
-
__version__ = "3.
|
|
371
|
+
__version__ = "3.10.0"
|
|
370
372
|
__all__ = [
|
|
371
373
|
# Estimators
|
|
372
374
|
"DifferenceInDifferences",
|
|
@@ -459,6 +461,9 @@ __all__ = [
|
|
|
459
461
|
# LPDiD (Local Projections DiD)
|
|
460
462
|
"LPDiD",
|
|
461
463
|
"LPDiDResults",
|
|
464
|
+
# LWDiD (Lee & Wooldridge rolling transformation DiD)
|
|
465
|
+
"LWDiD",
|
|
466
|
+
"LWDiDResults",
|
|
462
467
|
# Visualization
|
|
463
468
|
"plot_bacon",
|
|
464
469
|
"plot_event_study",
|
|
@@ -553,7 +553,11 @@ class AggregationKit:
|
|
|
553
553
|
``cband_crit_value`` is ``None`` both when bands were disabled and
|
|
554
554
|
when no aggregation ran, so it cannot distinguish the two.
|
|
555
555
|
bootstrap : AggregationKit.BootstrapReplaySpec or None
|
|
556
|
-
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).
|
|
557
561
|
"""
|
|
558
562
|
|
|
559
563
|
bookkeeping: Dict[str, Any]
|
|
@@ -574,9 +578,29 @@ class BootstrapReplaySpec:
|
|
|
574
578
|
``set_params(n_bootstrap=...)`` silently changes - and can truncate - the
|
|
575
579
|
replayed stream.
|
|
576
580
|
|
|
577
|
-
This records the generator state plus the parameters BY VALUE
|
|
578
|
-
|
|
579
|
-
|
|
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.
|
|
580
604
|
"""
|
|
581
605
|
|
|
582
606
|
bitgen_state: Dict[str, Any]
|
|
@@ -585,6 +609,7 @@ class BootstrapReplaySpec:
|
|
|
585
609
|
weight_type: str
|
|
586
610
|
block_size: Optional[int] = None
|
|
587
611
|
expand_index: Optional[np.ndarray] = None
|
|
612
|
+
backend: Optional[str] = None
|
|
588
613
|
|
|
589
614
|
def rebuild(self) -> Any:
|
|
590
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
|
|
@@ -1681,7 +1681,11 @@ def load_walmart(force_download: bool = False) -> pd.DataFrame:
|
|
|
1681
1681
|
... )
|
|
1682
1682
|
"""
|
|
1683
1683
|
url = "http://fmwww.bc.edu/repec/bocode/l/lw_walmart.dta"
|
|
1684
|
-
|
|
1684
|
+
# Re-pinned 2026-08-20: upstream re-saved the .dta (container bytes
|
|
1685
|
+
# changed); every shared column verified numerically identical to the
|
|
1686
|
+
# previously pinned copy (29,371 rows, 1,277 counties) before adopting.
|
|
1687
|
+
# The Stata golden's provenance block keeps the capture-time hash.
|
|
1688
|
+
sha256 = "cf1b2d4aee396f0858fb315d0413ff82359e16e84dad68ce1d9be51e8c3e35ac"
|
|
1685
1689
|
|
|
1686
1690
|
source = "lwdid_ssc_ancillary"
|
|
1687
1691
|
try:
|
|
@@ -16,7 +16,10 @@ Design principles:
|
|
|
16
16
|
recompute — for ImputationDiD a panel-backed recompute, for TwoStageDiD
|
|
17
17
|
a fresh Stage-2 OLS + GMM sandwich over the retained frame; used only
|
|
18
18
|
when the raw ``event_study_effects`` field is absent, and failing
|
|
19
|
-
closed to an explicit skip on
|
|
19
|
+
closed to an explicit skip on kit-less/legacy-pickle fits, the sibling
|
|
20
|
+
estimators' bootstrap gates, and backend-mismatched CS bootstrap
|
|
21
|
+
replays — bootstrapped CS fits themselves derive successfully via the
|
|
22
|
+
percentile-bootstrap replay), or
|
|
20
23
|
produced by an existing diff-diff utility. May call
|
|
21
24
|
``check_parallel_trends`` / ``BaconDecomposition`` /
|
|
22
25
|
``EfficientDiD.hausman_pretest`` when the caller supplies the panel +
|
|
@@ -840,8 +843,11 @@ class DiagnosticReport:
|
|
|
840
843
|
surface = candidate
|
|
841
844
|
surface_dict = _surface_to_event_study_dict(candidate)
|
|
842
845
|
except Exception as exc: # noqa: BLE001 — fail-soft by design:
|
|
843
|
-
# expected failures are NotImplementedError (
|
|
844
|
-
# gates, pretrends+replicate
|
|
846
|
+
# expected failures are NotImplementedError (the sibling
|
|
847
|
+
# estimators' bootstrap gates, pretrends+replicate, CS
|
|
848
|
+
# legacy-pickle/backend-mismatch replay refusals — a
|
|
849
|
+
# bootstrapped CS fit itself now derives via the replay)
|
|
850
|
+
# and ValueError (missing kit),
|
|
845
851
|
# but the surface builder can raise bare TypeError and this
|
|
846
852
|
# resolver runs on the applicable_checks path with no outer
|
|
847
853
|
# guard; an escaped exception would hard-fail the report.
|
|
@@ -30,7 +30,11 @@ import pandas as pd
|
|
|
30
30
|
|
|
31
31
|
from diff_diff._base import BaseEstimator
|
|
32
32
|
from diff_diff._deprecation import NOT_SUPPLIED
|
|
33
|
-
from diff_diff.aggregation import AggregationKit
|
|
33
|
+
from diff_diff.aggregation import AggregationKit, BootstrapReplaySpec
|
|
34
|
+
from diff_diff.bootstrap_utils import (
|
|
35
|
+
apply_bootstrap_event_study_overrides,
|
|
36
|
+
apply_bootstrap_group_overrides,
|
|
37
|
+
)
|
|
34
38
|
from diff_diff.efficient_did_aggregation import (
|
|
35
39
|
_cluster_aggregate,
|
|
36
40
|
_compute_se_from_eif,
|
|
@@ -118,6 +122,7 @@ def _build_edid_aggregation_kit(
|
|
|
118
122
|
df_survey: Optional[float],
|
|
119
123
|
alpha: float,
|
|
120
124
|
anticipation: int,
|
|
125
|
+
bootstrap_results: Optional[EDiDBootstrapResults] = None,
|
|
121
126
|
) -> Optional[AggregationKit]:
|
|
122
127
|
"""Bundle the retained EIF payload + bookkeeping for post-fit aggregate().
|
|
123
128
|
|
|
@@ -138,6 +143,21 @@ def _build_edid_aggregation_kit(
|
|
|
138
143
|
# Unreachable after fit()'s empty-effects raise; kept for the CS
|
|
139
144
|
# guard shape (a kit with nothing to re-aggregate is not attached).
|
|
140
145
|
return None
|
|
146
|
+
# STATE-ONLY replay carrier (the CS contract): the spec retains the RNG
|
|
147
|
+
# snapshot + generation-branch identity the run recorded, BY VALUE, so
|
|
148
|
+
# post-fit aggregate() can replay the fit-time multiplier bootstrap
|
|
149
|
+
# through the same engine. Its rebuild() factory is unused here — the
|
|
150
|
+
# engine re-derives the generation branch from the kit bookkeeping.
|
|
151
|
+
# None on analytical fits, where the recompute levels stay analytical.
|
|
152
|
+
replay_spec = None
|
|
153
|
+
if bootstrap_results is not None and bootstrap_results._replay_bitgen_state is not None:
|
|
154
|
+
replay_spec = BootstrapReplaySpec(
|
|
155
|
+
bitgen_state=bootstrap_results._replay_bitgen_state,
|
|
156
|
+
n_bootstrap=bootstrap_results.n_bootstrap,
|
|
157
|
+
n_units=int(n_units),
|
|
158
|
+
weight_type=bootstrap_results.weight_type,
|
|
159
|
+
backend=bootstrap_results._replay_backend,
|
|
160
|
+
)
|
|
141
161
|
return AggregationKit(
|
|
142
162
|
bookkeeping={
|
|
143
163
|
# PRIVATE SNAPSHOTS of the aggregation inputs (CI review P0):
|
|
@@ -165,7 +185,7 @@ def _build_edid_aggregation_kit(
|
|
|
165
185
|
alpha=alpha,
|
|
166
186
|
anticipation=anticipation,
|
|
167
187
|
cband=False,
|
|
168
|
-
bootstrap=
|
|
188
|
+
bootstrap=replay_spec,
|
|
169
189
|
)
|
|
170
190
|
|
|
171
191
|
|
|
@@ -513,20 +533,23 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
513
533
|
``.aggregate('group')`` / ``.aggregate('simple')`` /
|
|
514
534
|
``.aggregate('total')``. On
|
|
515
535
|
bootstrapped fits (``n_bootstrap > 0``) the post-fit
|
|
516
|
-
RECOMPUTE levels (``'event_study'``/``'group'``)
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
``aggregate('simple')``
|
|
520
|
-
``aggregate('total')`` relay the
|
|
521
|
-
|
|
522
|
-
|
|
536
|
+
RECOMPUTE levels (``'event_study'``/``'group'``) REPLAY the
|
|
537
|
+
fit-time multiplier bootstrap from the kit-retained RNG
|
|
538
|
+
state (percentile inference, allclose to a fit-time
|
|
539
|
+
aggregation; no refit needed), while ``aggregate('simple')``
|
|
540
|
+
and, where supported, ``aggregate('total')`` relay the
|
|
541
|
+
stored bootstrap inference (the per-level policy converged
|
|
542
|
+
with row M-027).
|
|
523
543
|
balance_e : int, optional
|
|
524
544
|
DEPRECATED (3.9, removed in 4.0, row M-120): moves onto
|
|
525
545
|
post-fit ``aggregate()`` —
|
|
526
546
|
``results.aggregate('event_study', balance_e=2)``. EDiD's
|
|
527
547
|
balance rule is the ANCHOR-HORIZON rule (keep cohorts with a
|
|
528
|
-
finite effect at
|
|
529
|
-
CallawaySantAnna uses
|
|
548
|
+
finite effect at the anchor horizon), the same rule shape
|
|
549
|
+
CallawaySantAnna uses — with one keying-granularity
|
|
550
|
+
difference: EDiD anchors on the ``int(t - g)`` bucket while
|
|
551
|
+
CS keys raw ``t - g`` (identical on integer-period panels;
|
|
552
|
+
see the REGISTRY truncation Note).
|
|
530
553
|
survey_design : SurveyDesign, optional
|
|
531
554
|
Survey design specification for design-based inference.
|
|
532
555
|
Applies survey weights to all means, covariances, and cohort
|
|
@@ -1364,46 +1387,14 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1364
1387
|
se = float(group_time_effects[gt]["se"])
|
|
1365
1388
|
group_time_effects[gt]["t_stat"] = safe_inference(eff, se, alpha=self.alpha)[0]
|
|
1366
1389
|
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
)
|
|
1375
|
-
for e in event_study_effects:
|
|
1376
|
-
if e in bootstrap_results.event_study_ses:
|
|
1377
|
-
event_study_effects[e]["se"] = bootstrap_results.event_study_ses[e]
|
|
1378
|
-
event_study_effects[e]["conf_int"] = es_cis[e]
|
|
1379
|
-
event_study_effects[e]["p_value"] = es_pvs[e]
|
|
1380
|
-
eff = float(event_study_effects[e]["effect"])
|
|
1381
|
-
se = float(event_study_effects[e]["se"])
|
|
1382
|
-
event_study_effects[e]["t_stat"] = safe_inference(
|
|
1383
|
-
eff, se, alpha=self.alpha
|
|
1384
|
-
)[0]
|
|
1385
|
-
|
|
1386
|
-
g_cis = bootstrap_results.group_effect_cis
|
|
1387
|
-
g_pvs = bootstrap_results.group_effect_p_values
|
|
1388
|
-
if (
|
|
1389
|
-
group_effects is not None
|
|
1390
|
-
and bootstrap_results.group_effect_ses is not None
|
|
1391
|
-
and g_cis is not None
|
|
1392
|
-
and g_pvs is not None
|
|
1393
|
-
):
|
|
1394
|
-
for g in group_effects:
|
|
1395
|
-
if g in bootstrap_results.group_effect_ses:
|
|
1396
|
-
group_effects[g]["se"] = bootstrap_results.group_effect_ses[g]
|
|
1397
|
-
group_effects[g]["conf_int"] = g_cis[g]
|
|
1398
|
-
group_effects[g]["p_value"] = g_pvs[g]
|
|
1399
|
-
eff = float(group_effects[g]["effect"])
|
|
1400
|
-
se = float(group_effects[g]["se"])
|
|
1401
|
-
group_effects[g]["t_stat"] = safe_inference(eff, se, alpha=self.alpha)[0]
|
|
1402
|
-
# Percentile-bootstrap inference has no analytical df;
|
|
1403
|
-
# clear the provenance key the analytical pass recorded
|
|
1404
|
-
# (the CS precedent) so bootstrap rows never publish an
|
|
1405
|
-
# analytical survey df beside percentile p/CI.
|
|
1406
|
-
group_effects[g]["df_used"] = None
|
|
1390
|
+
# ES/group percentile overrides via the shared appliers (the same
|
|
1391
|
+
# implementations the post-fit aggregate() replay runs — one
|
|
1392
|
+
# code path, no fit-vs-replay drift). The appliers carry the
|
|
1393
|
+
# availability guards and the group df_used clearing internally.
|
|
1394
|
+
apply_bootstrap_event_study_overrides(
|
|
1395
|
+
event_study_effects, bootstrap_results, self.alpha
|
|
1396
|
+
)
|
|
1397
|
+
apply_bootstrap_group_overrides(group_effects, bootstrap_results, self.alpha)
|
|
1407
1398
|
|
|
1408
1399
|
# ----- Build results -----
|
|
1409
1400
|
self.results_ = EfficientDiDResults(
|
|
@@ -1487,6 +1478,7 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1487
1478
|
df_survey=_survey_df_post_overall,
|
|
1488
1479
|
alpha=self.alpha,
|
|
1489
1480
|
anticipation=self.anticipation,
|
|
1481
|
+
bootstrap_results=bootstrap_results,
|
|
1490
1482
|
)
|
|
1491
1483
|
self.is_fitted_ = True
|
|
1492
1484
|
return self.results_
|
|
@@ -1637,8 +1629,12 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1637
1629
|
) -> Dict[int, Tuple[float, np.ndarray]]:
|
|
1638
1630
|
"""Aggregate (g,t) effects to post-treatment ES(e) with WIF-corrected EIF."""
|
|
1639
1631
|
by_e: Dict[int, List[Tuple[Tuple, float, float, np.ndarray]]] = {}
|
|
1632
|
+
_has_fractional = False
|
|
1640
1633
|
for (g, t), d in gt_effects.items():
|
|
1641
|
-
|
|
1634
|
+
raw_e = t - g
|
|
1635
|
+
e = int(raw_e)
|
|
1636
|
+
if raw_e != e:
|
|
1637
|
+
_has_fractional = True
|
|
1642
1638
|
if e < -ant:
|
|
1643
1639
|
continue
|
|
1644
1640
|
if not np.isfinite(d["effect"]):
|
|
@@ -1653,6 +1649,15 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1653
1649
|
by_e[e] = []
|
|
1654
1650
|
by_e[e].append(((g, t), d["effect"], pg, eif_vec))
|
|
1655
1651
|
|
|
1652
|
+
if _has_fractional:
|
|
1653
|
+
warnings.warn(
|
|
1654
|
+
"Fractional relative times detected: Hausman pre-test "
|
|
1655
|
+
"horizons are bucketed by int(t - g) (truncation toward "
|
|
1656
|
+
"zero). See the EfficientDiD REGISTRY truncation Note.",
|
|
1657
|
+
UserWarning,
|
|
1658
|
+
stacklevel=3,
|
|
1659
|
+
)
|
|
1660
|
+
|
|
1656
1661
|
result: Dict[int, Tuple[float, np.ndarray]] = {}
|
|
1657
1662
|
for e, items in by_e.items():
|
|
1658
1663
|
if e < 0:
|
|
@@ -22,10 +22,13 @@ Contents:
|
|
|
22
22
|
is what keeps ``aggregate()`` off an ``_estimator_ref``.
|
|
23
23
|
|
|
24
24
|
The numerical content of every function in this module is byte-identical to
|
|
25
|
-
its pre-extraction form, with
|
|
26
|
-
|
|
25
|
+
its pre-extraction form, with the exceptions recorded in the M-023 ledger
|
|
26
|
+
notes: ``_aggregate_by_group`` records a per-row ``df_used`` key (the
|
|
27
27
|
``self._survey_df`` value at that row's ``safe_inference`` call) so the
|
|
28
|
-
post-fit group relay can publish exact per-row df provenance
|
|
28
|
+
post-fit group relay can publish exact per-row df provenance;
|
|
29
|
+
``_aggregate_event_study`` counts DISTINCT cohorts in ``n_groups`` (identity
|
|
30
|
+
on integer panels) and warns once when fractional horizons are truncation-
|
|
31
|
+
bucketed (see the EfficientDiD REGISTRY truncation Note).
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
34
|
import warnings
|
|
@@ -325,7 +328,12 @@ class _EfficientAggregationMixin:
|
|
|
325
328
|
cluster_indices: Optional[np.ndarray] = None,
|
|
326
329
|
n_clusters: Optional[int] = None,
|
|
327
330
|
) -> Dict[int, Dict[str, Any]]:
|
|
328
|
-
"""Aggregate ATT(g,t) by relative time e = t - g
|
|
331
|
+
"""Aggregate ATT(g,t) by relative time ``e = int(t - g)``.
|
|
332
|
+
|
|
333
|
+
On integer-period panels the ``int()`` is the identity. Fractional-
|
|
334
|
+
period panels are truncation-bucketed toward zero (a documented
|
|
335
|
+
deviation from the exact-relative-time equation — see the
|
|
336
|
+
EfficientDiD REGISTRY truncation Note) and emit a ``UserWarning``.
|
|
329
337
|
|
|
330
338
|
Parameters
|
|
331
339
|
----------
|
|
@@ -346,15 +354,30 @@ class _EfficientAggregationMixin:
|
|
|
346
354
|
unit_cohorts : ndarray, optional
|
|
347
355
|
Cohort assignment for each unit (for WIF correction).
|
|
348
356
|
"""
|
|
349
|
-
# Organize by relative time
|
|
357
|
+
# Organize by relative time. Fractional horizons truncation-bucket
|
|
358
|
+
# (int() toward zero) — a lossy, documented convention that must not
|
|
359
|
+
# stay invisible to the user (no-silent-failures).
|
|
360
|
+
_has_fractional = False
|
|
350
361
|
effects_by_e: Dict[int, List[Tuple[Tuple[Any, Any], float, float]]] = {}
|
|
351
362
|
for (g, t), data in group_time_effects.items():
|
|
352
363
|
if not np.isfinite(data["effect"]):
|
|
353
364
|
continue
|
|
354
|
-
|
|
365
|
+
raw_e = t - g
|
|
366
|
+
e = int(raw_e)
|
|
367
|
+
if raw_e != e:
|
|
368
|
+
_has_fractional = True
|
|
355
369
|
if e not in effects_by_e:
|
|
356
370
|
effects_by_e[e] = []
|
|
357
371
|
effects_by_e[e].append(((g, t), data["effect"], cohort_fractions.get(g, 0.0)))
|
|
372
|
+
if _has_fractional:
|
|
373
|
+
warnings.warn(
|
|
374
|
+
"Fractional relative times detected: event-study horizons are "
|
|
375
|
+
"bucketed by int(t - g) (truncation toward zero), pooling "
|
|
376
|
+
"fractional horizons into integer buckets. See the "
|
|
377
|
+
"EfficientDiD REGISTRY truncation Note.",
|
|
378
|
+
UserWarning,
|
|
379
|
+
stacklevel=2,
|
|
380
|
+
)
|
|
358
381
|
|
|
359
382
|
# Balance if requested
|
|
360
383
|
if balance_e is not None:
|
|
@@ -441,7 +464,13 @@ class _EfficientAggregationMixin:
|
|
|
441
464
|
"t_stat": t_stat,
|
|
442
465
|
"p_value": p_val,
|
|
443
466
|
"conf_int": ci,
|
|
444
|
-
|
|
467
|
+
# DISTINCT cohorts in the bucket (the cohort-count n column): identity
|
|
468
|
+
# with len(elist) on integer panels (one cell per cohort per
|
|
469
|
+
# bucket); on fractional panels truncation-bucketing pools
|
|
470
|
+
# multiple cells per cohort and a raw cell count would
|
|
471
|
+
# over-count. Weights above remain per-cell (cell-mass within
|
|
472
|
+
# the bucket — see the REGISTRY truncation Note).
|
|
473
|
+
"n_groups": len({gt[0] for gt in gt_pairs}),
|
|
445
474
|
}
|
|
446
475
|
|
|
447
476
|
return result
|