diff-diff 3.11.0__tar.gz → 3.11.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.11.0 → diff_diff-3.11.1}/PKG-INFO +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/__init__.py +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_staggered_triple_diff_engine.py +14 -2
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/chaisemartin_dhaultfoeuille.py +6 -5
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/continuous_did.py +92 -17
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/continuous_did_results.py +18 -2
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/dml_did.py +127 -37
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/dml_did_results.py +6 -3
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/efficient_did.py +39 -4
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/efficient_did_results.py +25 -7
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/estimators.py +2 -2
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/guides/llms-autonomous.txt +6 -5
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/guides/llms-full.txt +6 -3
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/guides/llms-practitioner.txt +2 -2
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/guides/llms.txt +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/had.py +19 -4
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/had_pretests.py +6 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/honest_did.py +21 -60
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/imputation.py +13 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/imputation_results.py +24 -7
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lpdid.py +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/results_base.py +12 -9
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/spillover.py +4 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/staggered.py +28 -4
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/sun_abraham.py +5 -5
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/survey.py +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/synthetic_did.py +5 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/triple_diff.py +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/trop.py +4 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/twfe.py +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/two_stage.py +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/wooldridge.py +4 -2
- {diff_diff-3.11.0 → diff_diff-3.11.1}/pyproject.toml +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/Cargo.lock +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/Cargo.toml +1 -1
- {diff_diff-3.11.0 → diff_diff-3.11.1}/LICENSE +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/README.md +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_backend.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_base.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_crossfit.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_deprecation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_dr_scores.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_guides_api.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_learners.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_nprobust_port.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_rdrobust_port.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/_reporting_helpers.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/agent_workflow.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/aggregation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/bacon.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/balancing.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/bootstrap_chunking.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/bootstrap_utils.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/business_report.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/chaisemartin_dhaultfoeuille_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/changes_in_changes.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/changes_in_changes_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/conformal.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/conley.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/continuous_did_aggregation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/datasets.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/diagnostic_report.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/efficient_did_aggregation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/efficient_did_bootstrap.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/efficient_did_covariates.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/guides/__init__.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/imputation_aggregation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/linalg.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/local_linear.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lpdid_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid_randomization.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid_sensitivity.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid_staggered.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid_visualization.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/lwdid_wild_bootstrap.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/mmm.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/power.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/practitioner.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/prep.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/profile.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/rdd.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/rddensity.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/rdplot.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/stacked_did.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/staggered_aggregation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/staggered_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/staggered_triple_diff.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/synthetic_control.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/synthetic_control_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/trop_global.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/trop_local.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/trop_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/two_stage_aggregation.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/two_stage_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/utils.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_event_study.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/build.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/alloc_profile.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/batched_solve.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/bootstrap.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/demean.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/lib.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/linalg.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/trop.rs +0 -0
- {diff_diff-3.11.0 → diff_diff-3.11.1}/rust/src/weights.rs +0 -0
|
@@ -295,8 +295,20 @@ class _StaggeredTripleDiffEngineMixin:
|
|
|
295
295
|
if resolved_survey is not None and survey_metadata is not None:
|
|
296
296
|
resolved_survey_unit = precomputed.get("resolved_survey_unit")
|
|
297
297
|
if resolved_survey_unit is not None:
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
from diff_diff.survey import _extract_unit_survey_weights
|
|
299
|
+
|
|
300
|
+
# Raw (pre-normalization) unit weights for metadata
|
|
301
|
+
# provenance: compute_survey_metadata expects the ORIGINAL
|
|
302
|
+
# scale (resolve() rescales pweights to mean 1, so the
|
|
303
|
+
# resolved weights would misreport sum_weights/
|
|
304
|
+
# weight_range; scale-invariant fields are unaffected
|
|
305
|
+
# either way). Read from ``data``: ``df`` has first_treat
|
|
306
|
+
# overwritten by this point.
|
|
307
|
+
assert survey_design is not None
|
|
308
|
+
raw_unit_w = _extract_unit_survey_weights(
|
|
309
|
+
data, unit, survey_design, precomputed["all_units"]
|
|
310
|
+
)
|
|
311
|
+
survey_metadata = compute_survey_metadata(resolved_survey_unit, raw_unit_w)
|
|
300
312
|
|
|
301
313
|
# Survey df for t-distribution critical values
|
|
302
314
|
df_survey = precomputed.get("df_survey")
|
|
@@ -51,7 +51,7 @@ from diff_diff.chaisemartin_dhaultfoeuille_results import (
|
|
|
51
51
|
DCDHBootstrapResults,
|
|
52
52
|
)
|
|
53
53
|
from diff_diff.linalg import solve_ols
|
|
54
|
-
from diff_diff.utils import safe_inference
|
|
54
|
+
from diff_diff.utils import safe_inference, validate_n_bootstrap
|
|
55
55
|
|
|
56
56
|
__all__ = [
|
|
57
57
|
"ChaisemartinDHaultfoeuille",
|
|
@@ -762,8 +762,8 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin, BaseE
|
|
|
762
762
|
)
|
|
763
763
|
if not 0.0 < alpha < 1.0:
|
|
764
764
|
raise ValueError(f"alpha must be in (0, 1), got {alpha}")
|
|
765
|
-
|
|
766
|
-
|
|
765
|
+
# Shared type guard (rejects bool/float, floor stays 0).
|
|
766
|
+
validate_n_bootstrap(n_bootstrap)
|
|
767
767
|
if by_path is not None:
|
|
768
768
|
if isinstance(by_path, bool) or not isinstance(by_path, int):
|
|
769
769
|
raise ValueError(
|
|
@@ -836,8 +836,9 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin, BaseE
|
|
|
836
836
|
)
|
|
837
837
|
if not 0.0 < self.alpha < 1.0:
|
|
838
838
|
raise ValueError(f"alpha must be in (0, 1), got {self.alpha}")
|
|
839
|
-
|
|
840
|
-
|
|
839
|
+
# Shared type guard (kept aligned with __init__ so the two sites
|
|
840
|
+
# cannot drift).
|
|
841
|
+
validate_n_bootstrap(self.n_bootstrap)
|
|
841
842
|
if self.by_path is not None:
|
|
842
843
|
if isinstance(self.by_path, bool) or not isinstance(self.by_path, int):
|
|
843
844
|
raise ValueError(
|
|
@@ -506,6 +506,18 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
506
506
|
if col not in df.columns:
|
|
507
507
|
raise ValueError(f"Column '{col}' not found in data.")
|
|
508
508
|
|
|
509
|
+
# Snapshot the raw survey-weight column BEFORE any df mutation
|
|
510
|
+
# (never-treated dose zeroing, first_treat inf->0, to_numeric): a
|
|
511
|
+
# design whose weight column aliases a mutable role column (e.g.
|
|
512
|
+
# weights == dose) must still surface the user's ORIGINAL values in
|
|
513
|
+
# survey_metadata. Per-unit via groupby-first so the later
|
|
514
|
+
# dose-filter (which drops whole units) cannot desync alignment.
|
|
515
|
+
raw_unit_w_meta: Optional[pd.Series] = None
|
|
516
|
+
if survey_design is not None and survey_design.weights is not None:
|
|
517
|
+
# `is not None`, not truthiness: resolve() treats any non-None
|
|
518
|
+
# string — an empty-string column name included — as a column.
|
|
519
|
+
raw_unit_w_meta = data.groupby(unit)[survey_design.weights].first()
|
|
520
|
+
|
|
509
521
|
# Covariate-path guards (conditional parallel trends).
|
|
510
522
|
if cov_cols:
|
|
511
523
|
if survey_design is not None:
|
|
@@ -770,11 +782,20 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
770
782
|
)
|
|
771
783
|
lowest_dose = d_L
|
|
772
784
|
|
|
773
|
-
# Re-resolve survey design on filtered
|
|
774
|
-
# (survey arrays must align with df, not the original data)
|
|
785
|
+
# Re-resolve survey design on the filtered rows if rows were dropped
|
|
786
|
+
# (survey arrays must align with df, not the original data). Resolve
|
|
787
|
+
# from PRISTINE ``data`` rows, not the mutated working frame: the
|
|
788
|
+
# unfiltered path resolves from ``data``, and df's role-column
|
|
789
|
+
# coercions (never-treated dose zeroing, first_treat inf->0,
|
|
790
|
+
# to_numeric) must not leak into a design whose column aliases a
|
|
791
|
+
# mutated role column — resolving on df previously zero-weighted
|
|
792
|
+
# every never-treated unit when ``weights == dose``. The dose filter
|
|
793
|
+
# drops whole units and preserves row order, so the pristine
|
|
794
|
+
# unit-mask selection is row-for-row identical to df.
|
|
775
795
|
if resolved_survey is not None and len(df) < len(data):
|
|
796
|
+
_kept_row_mask = data[unit].isin(set(df[unit].unique())).to_numpy()
|
|
776
797
|
resolved_survey, survey_weights, survey_weight_type, survey_metadata = (
|
|
777
|
-
_resolve_survey_for_fit(survey_design,
|
|
798
|
+
_resolve_survey_for_fit(survey_design, data[_kept_row_mask], "analytical")
|
|
778
799
|
)
|
|
779
800
|
|
|
780
801
|
# 2. Precompute structures
|
|
@@ -973,6 +994,39 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
973
994
|
|
|
974
995
|
_survey_df = None # Set by analytical branch when survey is active
|
|
975
996
|
|
|
997
|
+
# Recompute survey_metadata from the UNIT-level design on EVERY arm
|
|
998
|
+
# (degenerate no-post-cells, bootstrap, analytic) so reported
|
|
999
|
+
# sum_weights/effective_n/n_psu/df_survey describe one granularity —
|
|
1000
|
+
# the CS/EfficientDiD convention. Construction is byte-identical to
|
|
1001
|
+
# the ones inside _run_bootstrap and _compute_analytical_se.
|
|
1002
|
+
_unit_resolved_shared = None
|
|
1003
|
+
if resolved_survey is not None:
|
|
1004
|
+
# Built ONCE and threaded into the analytical/bootstrap helpers
|
|
1005
|
+
# below (they previously rebuilt it — on replicate designs that
|
|
1006
|
+
# copied the unit-by-replicate matrix twice).
|
|
1007
|
+
_unit_resolved_shared = resolved_survey.subset_to_units_by_row_idx(
|
|
1008
|
+
precomp["unit_first_panel_row"],
|
|
1009
|
+
unit_weights=precomp.get("unit_survey_weights"),
|
|
1010
|
+
)
|
|
1011
|
+
if resolved_survey is not None and survey_metadata is not None:
|
|
1012
|
+
from diff_diff.survey import compute_survey_metadata
|
|
1013
|
+
|
|
1014
|
+
_unit_resolved_meta = _unit_resolved_shared
|
|
1015
|
+
# Raw (pre-normalization) unit weights for metadata provenance:
|
|
1016
|
+
# compute_survey_metadata expects the ORIGINAL scale (resolve()
|
|
1017
|
+
# rescales pweights to mean 1; scale-invariant fields are
|
|
1018
|
+
# unaffected either way). ``raw_unit_w_meta`` was snapshotted
|
|
1019
|
+
# from pristine ``data`` before the df mutations; reindexing to
|
|
1020
|
+
# ``all_units`` (the dose-filtered unit order) keeps alignment —
|
|
1021
|
+
# survey weights are unit-constant (validated at resolve time).
|
|
1022
|
+
assert survey_design is not None
|
|
1023
|
+
raw_w_unit = (
|
|
1024
|
+
raw_unit_w_meta.reindex(precomp["all_units"]).to_numpy(dtype=np.float64)
|
|
1025
|
+
if raw_unit_w_meta is not None
|
|
1026
|
+
else np.ones(precomp["n_units"], dtype=np.float64)
|
|
1027
|
+
)
|
|
1028
|
+
survey_metadata = compute_survey_metadata(_unit_resolved_meta, raw_w_unit)
|
|
1029
|
+
|
|
976
1030
|
if len(post_gt) == 0:
|
|
977
1031
|
warnings.warn(
|
|
978
1032
|
"No post-treatment (g,t) cells available for aggregation. "
|
|
@@ -1040,6 +1094,7 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
1040
1094
|
agg_acrt_d,
|
|
1041
1095
|
event_study_effects,
|
|
1042
1096
|
resolved_survey=resolved_survey,
|
|
1097
|
+
pre_unit_resolved=_unit_resolved_shared,
|
|
1043
1098
|
)
|
|
1044
1099
|
att_d_se = boot_result["att_d_se"]
|
|
1045
1100
|
att_d_ci_lower = boot_result["att_d_ci_lower"]
|
|
@@ -1080,6 +1135,7 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
1080
1135
|
agg_att_d,
|
|
1081
1136
|
agg_acrt_d,
|
|
1082
1137
|
resolved_survey=resolved_survey,
|
|
1138
|
+
pre_unit_resolved=_unit_resolved_shared,
|
|
1083
1139
|
)
|
|
1084
1140
|
att_d_se = analytic["att_d_se"]
|
|
1085
1141
|
acrt_d_se = analytic["acrt_d_se"]
|
|
@@ -1097,14 +1153,9 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
1097
1153
|
):
|
|
1098
1154
|
_survey_df = 0
|
|
1099
1155
|
|
|
1100
|
-
#
|
|
1101
|
-
#
|
|
1102
|
-
|
|
1103
|
-
if _unit_resolved is not None:
|
|
1104
|
-
from diff_diff.survey import compute_survey_metadata
|
|
1105
|
-
|
|
1106
|
-
raw_w_unit = _unit_resolved.weights
|
|
1107
|
-
survey_metadata = compute_survey_metadata(_unit_resolved, raw_w_unit)
|
|
1156
|
+
# (Unit-level survey_metadata is recomputed once for ALL
|
|
1157
|
+
# arms before the post_gt split; only the replicate-df
|
|
1158
|
+
# propagation below is analytic-arm-specific.)
|
|
1108
1159
|
|
|
1109
1160
|
# Propagate replicate df override to survey_metadata for display
|
|
1110
1161
|
# (but not the df=0 sentinel — keep metadata as None for undefined df)
|
|
@@ -1234,6 +1285,15 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
1234
1285
|
rank_deficient_action=self.rank_deficient_action,
|
|
1235
1286
|
event_study_effects=event_study_effects,
|
|
1236
1287
|
survey_metadata=survey_metadata,
|
|
1288
|
+
# Per-row ES df provenance (M-092 completion): the survey df the
|
|
1289
|
+
# ES rows' safe_inference used. None when no ES surface was
|
|
1290
|
+
# built, on bootstrap fits (_survey_df stays None there), on
|
|
1291
|
+
# non-survey fits, and for the replicate-undefined 0 sentinel.
|
|
1292
|
+
event_study_df=(
|
|
1293
|
+
float(_survey_df)
|
|
1294
|
+
if (event_study_effects is not None and _survey_df is not None and _survey_df > 0)
|
|
1295
|
+
else None
|
|
1296
|
+
),
|
|
1237
1297
|
)
|
|
1238
1298
|
# Post-fit aggregation kit (row M-025): attached on EVERY fit;
|
|
1239
1299
|
# scalars-only on bootstrap fits (the ES route fails closed there).
|
|
@@ -1942,6 +2002,7 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
1942
2002
|
agg_att_d: np.ndarray,
|
|
1943
2003
|
agg_acrt_d: np.ndarray,
|
|
1944
2004
|
resolved_survey: Optional["ResolvedSurveyDesign"] = None,
|
|
2005
|
+
pre_unit_resolved: Optional["ResolvedSurveyDesign"] = None,
|
|
1945
2006
|
) -> Dict[str, Any]:
|
|
1946
2007
|
"""Compute analytical SEs using influence functions."""
|
|
1947
2008
|
n_units = precomp["n_units"]
|
|
@@ -2042,9 +2103,16 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
2042
2103
|
# The resolved_survey has panel-level arrays (n_obs = n_units * n_periods),
|
|
2043
2104
|
# but influence functions are unit-level (n_units). Build a unit-level
|
|
2044
2105
|
# ResolvedSurveyDesign by subsetting to one obs per unit.
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2106
|
+
# Reuse the fit-level collapse when supplied (avoids copying
|
|
2107
|
+
# the unit-by-replicate matrix a second time on replicate
|
|
2108
|
+
# designs); construction is byte-identical.
|
|
2109
|
+
unit_resolved = (
|
|
2110
|
+
pre_unit_resolved
|
|
2111
|
+
if pre_unit_resolved is not None
|
|
2112
|
+
else resolved_survey.subset_to_units_by_row_idx(
|
|
2113
|
+
precomp["unit_first_panel_row"],
|
|
2114
|
+
unit_weights=precomp.get("unit_survey_weights"),
|
|
2115
|
+
)
|
|
2048
2116
|
)
|
|
2049
2117
|
|
|
2050
2118
|
X_ones = np.ones((n_units, 1))
|
|
@@ -2142,6 +2210,7 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
2142
2210
|
original_acrt_d: np.ndarray,
|
|
2143
2211
|
event_study_effects: Optional[Dict[int, Dict]],
|
|
2144
2212
|
resolved_survey: Optional["ResolvedSurveyDesign"] = None,
|
|
2213
|
+
pre_unit_resolved: Optional["ResolvedSurveyDesign"] = None,
|
|
2145
2214
|
) -> Dict[str, Any]:
|
|
2146
2215
|
"""Run multiplier bootstrap inference."""
|
|
2147
2216
|
if self.n_bootstrap < 50:
|
|
@@ -2172,9 +2241,15 @@ class ContinuousDiD(_ContinuousDiDAggregationMixin, BaseEstimator):
|
|
|
2172
2241
|
# Build unit-level ResolvedSurveyDesign for survey-aware bootstrap
|
|
2173
2242
|
unit_resolved = None
|
|
2174
2243
|
if resolved_survey is not None:
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2244
|
+
# Reuse the fit-level collapse when supplied (byte-identical
|
|
2245
|
+
# construction; avoids a second unit-by-replicate copy).
|
|
2246
|
+
unit_resolved = (
|
|
2247
|
+
pre_unit_resolved
|
|
2248
|
+
if pre_unit_resolved is not None
|
|
2249
|
+
else resolved_survey.subset_to_units_by_row_idx(
|
|
2250
|
+
precomp["unit_first_panel_row"],
|
|
2251
|
+
unit_weights=precomp.get("unit_survey_weights"),
|
|
2252
|
+
)
|
|
2178
2253
|
)
|
|
2179
2254
|
|
|
2180
2255
|
# Generate bootstrap weights — PSU-level when survey design is present
|
|
@@ -125,6 +125,11 @@ class ContinuousDiDResults(BaseResults, AggregationMixin):
|
|
|
125
125
|
Random seed used for bootstrap.
|
|
126
126
|
rank_deficient_action : str
|
|
127
127
|
How rank deficiency is handled (``"warn"``, ``"error"``, ``"silent"``).
|
|
128
|
+
event_study_df : float or None
|
|
129
|
+
Scalar survey df governing the event-study rows' t-inference.
|
|
130
|
+
``None`` on non-survey fits, on bootstrapped fits, when no
|
|
131
|
+
fit-time event-study surface was built, and for the
|
|
132
|
+
replicate-undefined ``0`` sentinel.
|
|
128
133
|
"""
|
|
129
134
|
|
|
130
135
|
dose_response_att: DoseResponseCurve
|
|
@@ -176,9 +181,16 @@ class ContinuousDiDResults(BaseResults, AggregationMixin):
|
|
|
176
181
|
# Survey design metadata (SurveyMetadata instance from diff_diff.survey)
|
|
177
182
|
survey_metadata: Optional[Any] = field(default=None)
|
|
178
183
|
# Post-fit aggregation kit (row M-025), attached by ContinuousDiD.fit().
|
|
179
|
-
#
|
|
180
|
-
# 'event_study' recompute reads it;
|
|
184
|
+
# New fields are appended AFTER this one (positional-__init__
|
|
185
|
+
# compatibility). Only the 'event_study' recompute reads it;
|
|
186
|
+
# 'simple'/'dose' are views.
|
|
181
187
|
_aggregation_kit: Optional[Any] = field(default=None, repr=False, compare=False)
|
|
188
|
+
# Scalar survey df governing the event-study rows' t-inference. None on
|
|
189
|
+
# non-survey fits, on bootstrapped fits (percentile inference; the ES
|
|
190
|
+
# recompute also fails closed there), when no event-study surface was
|
|
191
|
+
# built, and for the replicate-undefined 0 sentinel. Appended last per
|
|
192
|
+
# the positional-__init__ convention above.
|
|
193
|
+
event_study_df: Optional[float] = None
|
|
182
194
|
|
|
183
195
|
# Post-fit aggregation routing (M-122 contract). ContinuousDiD's extra
|
|
184
196
|
# 'dose' level is documented in the ledger row and v4-design section 6;
|
|
@@ -683,12 +695,16 @@ class ContinuousDiDResults(BaseResults, AggregationMixin):
|
|
|
683
695
|
# cells exist.
|
|
684
696
|
meta = bk["survey_metadata"]
|
|
685
697
|
meta = dataclasses.replace(meta) if meta is not None else None
|
|
698
|
+
# Per-row df provenance: the kit's survey df is the value this
|
|
699
|
+
# route's safe_inference calls received; 0-sentinel normalized.
|
|
700
|
+
_es_df = bk["survey_df"]
|
|
686
701
|
carrier = dataclasses.replace(
|
|
687
702
|
self,
|
|
688
703
|
event_study_effects=es,
|
|
689
704
|
survey_metadata=meta,
|
|
690
705
|
alpha=kit.alpha,
|
|
691
706
|
anticipation=kit.anticipation,
|
|
707
|
+
event_study_df=(float(_es_df) if _es_df is not None and _es_df > 0 else None),
|
|
692
708
|
# _provenance_kwargs reads base_period off the carrier - it
|
|
693
709
|
# rides the kit like its siblings alpha/anticipation so
|
|
694
710
|
# post-fit mutation of the public field cannot reach
|
|
@@ -1364,15 +1364,43 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1364
1364
|
"control_inf": inf_full[control_idx],
|
|
1365
1365
|
}
|
|
1366
1366
|
|
|
1367
|
-
# Per-cell SE.
|
|
1368
|
-
#
|
|
1369
|
-
#
|
|
1370
|
-
#
|
|
1371
|
-
#
|
|
1372
|
-
#
|
|
1367
|
+
# Per-cell SE. Replicate designs use IF-reweighting on the Hajek
|
|
1368
|
+
# payload (compute_replicate_if_variance; the same psi the aggregate
|
|
1369
|
+
# _se_from_psi call consumes) — a zero or non-finite replicate
|
|
1370
|
+
# variance is degenerate and fails closed to NaN (stricter than the
|
|
1371
|
+
# shared aggregate clamp; REGISTRY DMLDiD Note). PSU designs
|
|
1372
|
+
# (declared OR bare cluster=) route through the CS per-cell CR1
|
|
1373
|
+
# helper (3-valued contract: float = use it, NaN = unidentified
|
|
1374
|
+
# clustered variance and MUST propagate, None = malformed -> fall
|
|
1375
|
+
# back). Non-PSU survey designs use the weighted sqrt-sum (CS
|
|
1376
|
+
# mirror: the full design enters aggregate SEs only); the no-survey
|
|
1377
|
+
# branch is verbatim.
|
|
1373
1378
|
se: float
|
|
1374
1379
|
with np.errstate(over="ignore", invalid="ignore"):
|
|
1375
|
-
if
|
|
1380
|
+
if resolved_survey_unit is not None and resolved_survey_unit.uses_replicate_variance:
|
|
1381
|
+
from diff_diff.survey import compute_replicate_if_variance
|
|
1382
|
+
|
|
1383
|
+
variance, n_valid_rep = compute_replicate_if_variance(
|
|
1384
|
+
inf_full, resolved_survey_unit
|
|
1385
|
+
)
|
|
1386
|
+
if not np.isfinite(variance) or variance <= 0.0:
|
|
1387
|
+
se = float("nan")
|
|
1388
|
+
else:
|
|
1389
|
+
se = float(np.sqrt(variance))
|
|
1390
|
+
# Per-cell df: min(design df, n_valid - 1) — the dCDH
|
|
1391
|
+
# _effective_df_survey rule, inlined. n_valid is computed
|
|
1392
|
+
# over the WHOLE replicate columns, so it equals R for every
|
|
1393
|
+
# cell in practice (defensive; REGISTRY Note). df_survey is
|
|
1394
|
+
# a CELL-LOCAL binding — never mutate
|
|
1395
|
+
# precomputed["df_survey"], which feeds the post-fit
|
|
1396
|
+
# aggregation kit.
|
|
1397
|
+
if df_survey is not None:
|
|
1398
|
+
df_survey = min(int(df_survey), int(n_valid_rep) - 1)
|
|
1399
|
+
else:
|
|
1400
|
+
# Undefined replicate df (QR rank <= 1): df=0 sentinel
|
|
1401
|
+
# -> NaN inference (CS sentinel parity).
|
|
1402
|
+
df_survey = 0
|
|
1403
|
+
elif (
|
|
1376
1404
|
resolved_survey_unit is not None
|
|
1377
1405
|
and getattr(resolved_survey_unit, "psu", None) is not None
|
|
1378
1406
|
):
|
|
@@ -1386,9 +1414,10 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1386
1414
|
else:
|
|
1387
1415
|
se = float(np.sqrt(np.mean(psi_bar**2) / n_cell))
|
|
1388
1416
|
# NOTE: `se` is deliberately OUTSIDE the non_finite_score gate on the
|
|
1389
|
-
# design-based branches — a NaN from the CR1 helper
|
|
1390
|
-
# unidentified-variance signal and must
|
|
1391
|
-
# NaN-consistent inference tuple on a
|
|
1417
|
+
# design-based branches — a NaN from the CR1 helper (or a degenerate
|
|
1418
|
+
# replicate variance) is the unidentified-variance signal and must
|
|
1419
|
+
# flow to safe_inference as a NaN-consistent inference tuple on a
|
|
1420
|
+
# RETAINED cell.
|
|
1392
1421
|
if resolved_survey_unit is None and not np.isfinite(se):
|
|
1393
1422
|
diagnostics["skip_reason"] = "non_finite_score"
|
|
1394
1423
|
return (
|
|
@@ -1760,12 +1789,28 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1760
1789
|
}
|
|
1761
1790
|
|
|
1762
1791
|
# Per-cell SE (same dispatch as the panel cell; see the comment
|
|
1763
|
-
# there):
|
|
1792
|
+
# there): replicate designs -> IF-reweighting with the degenerate
|
|
1793
|
+
# fail-closed guard and the cell-local min(df, n_valid - 1) rule;
|
|
1794
|
+
# PSU designs -> CS per-cell CR1 helper (NaN propagates as
|
|
1764
1795
|
# the deliberate unidentified-variance signal on a RETAINED cell);
|
|
1765
1796
|
# non-PSU survey -> weighted sqrt-sum; no-survey verbatim.
|
|
1766
1797
|
se: float
|
|
1767
1798
|
with np.errstate(over="ignore", invalid="ignore"):
|
|
1768
|
-
if
|
|
1799
|
+
if resolved_survey_unit is not None and resolved_survey_unit.uses_replicate_variance:
|
|
1800
|
+
from diff_diff.survey import compute_replicate_if_variance
|
|
1801
|
+
|
|
1802
|
+
variance, n_valid_rep = compute_replicate_if_variance(
|
|
1803
|
+
inf_full, resolved_survey_unit
|
|
1804
|
+
)
|
|
1805
|
+
if not np.isfinite(variance) or variance <= 0.0:
|
|
1806
|
+
se = float("nan")
|
|
1807
|
+
else:
|
|
1808
|
+
se = float(np.sqrt(variance))
|
|
1809
|
+
if df_survey is not None:
|
|
1810
|
+
df_survey = min(int(df_survey), int(n_valid_rep) - 1)
|
|
1811
|
+
else:
|
|
1812
|
+
df_survey = 0
|
|
1813
|
+
elif (
|
|
1769
1814
|
resolved_survey_unit is not None
|
|
1770
1815
|
and getattr(resolved_survey_unit, "psu", None) is not None
|
|
1771
1816
|
):
|
|
@@ -1831,19 +1876,24 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1831
1876
|
Parameters
|
|
1832
1877
|
----------
|
|
1833
1878
|
survey_design : SurveyDesign, optional
|
|
1834
|
-
Complex survey design (pweight-only
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
PSU is strictly coarser than the
|
|
1841
|
-
per-cell and aggregate variances
|
|
1842
|
-
kernels with ``df = n_PSU - n_strata``
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1879
|
+
Complex survey design (pweight-only). Declared designs weight
|
|
1880
|
+
the moment kernels (Hajek p-hat/lambda-hat/theta) and pass
|
|
1881
|
+
``sample_weight`` into the nuisance learners (user learner
|
|
1882
|
+
objects must accept ``sample_weight`` by keyword — a learner
|
|
1883
|
+
without it is rejected up front). Two variance lanes:
|
|
1884
|
+
full-design TSL (weights/strata/PSU/FPC) switches cross-fitting
|
|
1885
|
+
to PSU-cohesive folds when the PSU is strictly coarser than the
|
|
1886
|
+
sampling unit and routes the per-cell and aggregate variances
|
|
1887
|
+
through the design-based kernels with ``df = n_PSU - n_strata``
|
|
1888
|
+
t-inference; replicate-weight designs (BRR / Fay / JK1 / JKn /
|
|
1889
|
+
SDR) compute per-cell AND aggregate variances by IF-reweighting
|
|
1890
|
+
the cross-fitted scores with ``df = rank(replicate matrix) - 1``
|
|
1891
|
+
t-inference (nuisances are not re-estimated per replicate;
|
|
1892
|
+
REGISTRY DMLDiD Note). Replicate designs reject ``cluster=``
|
|
1893
|
+
and ``n_bootstrap > 0`` combinations. Survey support is a
|
|
1894
|
+
documented library extension of Chang (2020), which assumes
|
|
1895
|
+
i.i.d. sampling — Theorem 2's coverage claim does not carry
|
|
1896
|
+
over (REGISTRY DMLDiD Notes).
|
|
1847
1897
|
"""
|
|
1848
1898
|
df, covariates = self._validate_and_prepare(
|
|
1849
1899
|
data, outcome, unit, time, first_treat, covariates
|
|
@@ -1866,13 +1916,18 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1866
1916
|
survey_metadata,
|
|
1867
1917
|
) = _resolve_survey_for_fit(survey_design, data, "analytical")
|
|
1868
1918
|
|
|
1869
|
-
# Replicate
|
|
1870
|
-
# variance
|
|
1871
|
-
|
|
1919
|
+
# Replicate + bootstrap rejected FIRST (before any fit work) —
|
|
1920
|
+
# replicate variance is an analytical alternative, not compatible
|
|
1921
|
+
# with bootstrap (CS parity, staggered.py).
|
|
1922
|
+
if (
|
|
1923
|
+
self.n_bootstrap > 0
|
|
1924
|
+
and resolved_survey is not None
|
|
1925
|
+
and resolved_survey.uses_replicate_variance
|
|
1926
|
+
):
|
|
1872
1927
|
raise NotImplementedError(
|
|
1873
|
-
"DMLDiD
|
|
1874
|
-
"
|
|
1875
|
-
"
|
|
1928
|
+
"DMLDiD bootstrap (n_bootstrap > 0) is not supported "
|
|
1929
|
+
"with replicate-weight survey designs. Replicate weights provide "
|
|
1930
|
+
"analytical variance; use n_bootstrap=0 instead."
|
|
1876
1931
|
)
|
|
1877
1932
|
|
|
1878
1933
|
# Raw (pre-normalization) per-obs design weights, for metadata
|
|
@@ -1886,7 +1941,7 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1886
1941
|
assert survey_design is not None
|
|
1887
1942
|
raw_obs_weights = (
|
|
1888
1943
|
data[survey_design.weights].values.astype(np.float64)
|
|
1889
|
-
if survey_design.weights
|
|
1944
|
+
if survey_design.weights is not None
|
|
1890
1945
|
else np.ones(len(data), dtype=np.float64)
|
|
1891
1946
|
)
|
|
1892
1947
|
|
|
@@ -1902,6 +1957,25 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
1902
1957
|
"drop or impute them before fitting"
|
|
1903
1958
|
)
|
|
1904
1959
|
cluster_ids_for_check = _cluster_col.to_numpy()
|
|
1960
|
+
# Reject replicate-weight + cluster= AFTER the column checks (CS
|
|
1961
|
+
# ordering: a bogus cluster name raises ValueError first).
|
|
1962
|
+
# Replicate IF variance is computed by replicate reweighting and
|
|
1963
|
+
# ignores PSU/cluster entirely (replicate_weights are mutually
|
|
1964
|
+
# exclusive with strata/psu/fpc) — honoring cluster= would
|
|
1965
|
+
# silently have no effect on the variance, and the inject-as-PSU
|
|
1966
|
+
# paths below would violate that mutual exclusion.
|
|
1967
|
+
if resolved_survey is not None and resolved_survey.uses_replicate_variance:
|
|
1968
|
+
raise NotImplementedError(
|
|
1969
|
+
f"DMLDiD(cluster={self.cluster!r}) is not "
|
|
1970
|
+
"supported with replicate-weight survey designs. "
|
|
1971
|
+
"Replicate-weight variance is computed by replicate "
|
|
1972
|
+
"reweighting (BRR / Fay / JK1 / JKn / SDR) and ignores "
|
|
1973
|
+
"PSU/cluster entirely — setting cluster= would silently "
|
|
1974
|
+
"have no effect on the variance estimate. Either omit "
|
|
1975
|
+
"cluster= (the replicate weights encode the design "
|
|
1976
|
+
"structure implicitly) or use a non-replicate survey "
|
|
1977
|
+
"design (with explicit strata/psu/fpc)."
|
|
1978
|
+
)
|
|
1905
1979
|
if resolved_survey is None:
|
|
1906
1980
|
# Bare cluster=: synthesize a PSU-only design. survey_metadata
|
|
1907
1981
|
# stays None DELIBERATELY (it is the declared-survey marker:
|
|
@@ -2188,15 +2262,31 @@ class DMLDiD(CallawaySantAnnaBootstrapMixin, CallawaySantAnnaAggregationMixin, B
|
|
|
2188
2262
|
|
|
2189
2263
|
# Overall ATT (simple aggregation over post-treatment finite cells).
|
|
2190
2264
|
# overall_effective_df is non-None only when replicate variance
|
|
2191
|
-
# dropped replicates
|
|
2192
|
-
#
|
|
2265
|
+
# dropped replicates (n_valid < R). MIN-CAP, not CS's replace: the
|
|
2266
|
+
# QR-rank design df stays the ceiling (CS's replace convention can
|
|
2267
|
+
# RAISE df above the design df — anti-conservative; deliberate
|
|
2268
|
+
# documented divergence, REGISTRY DMLDiD Note + CS-parity TODO row).
|
|
2193
2269
|
overall_att, overall_se, overall_effective_df = self._aggregate_simple(
|
|
2194
2270
|
group_time_effects, influence_func_info, df, unit, precomputed
|
|
2195
2271
|
)
|
|
2196
|
-
if overall_effective_df is not None:
|
|
2197
|
-
df_survey = overall_effective_df
|
|
2272
|
+
if overall_effective_df is not None and df_survey is not None:
|
|
2273
|
+
df_survey = min(int(df_survey), int(overall_effective_df))
|
|
2274
|
+
# Propagate to survey_metadata for display consistency (CS
|
|
2275
|
+
# parity) — the capped value, never the sentinel below.
|
|
2276
|
+
if survey_metadata is not None:
|
|
2277
|
+
survey_metadata.df_survey = df_survey
|
|
2278
|
+
# Replicate design with undefined df (QR rank <= 1): df=0 sentinel
|
|
2279
|
+
# -> NaN inference, applied to the LOCAL df only (survey_metadata
|
|
2280
|
+
# keeps None).
|
|
2281
|
+
df_overall = df_survey
|
|
2282
|
+
if (
|
|
2283
|
+
df_survey is None
|
|
2284
|
+
and resolved_survey is not None
|
|
2285
|
+
and resolved_survey.uses_replicate_variance
|
|
2286
|
+
):
|
|
2287
|
+
df_overall = 0
|
|
2198
2288
|
overall_t_stat, overall_p_value, overall_conf_int = safe_inference(
|
|
2199
|
-
overall_att, overall_se, alpha=self.alpha, df=
|
|
2289
|
+
overall_att, overall_se, alpha=self.alpha, df=df_overall
|
|
2200
2290
|
)
|
|
2201
2291
|
|
|
2202
2292
|
# Optional multiplier bootstrap (keyword form; aggregate=None is the
|
|
@@ -15,9 +15,12 @@ definition (REGISTRY.md "IF-based variance estimators..." — the default),
|
|
|
15
15
|
and DMLDiD's augmented-score SE ``sqrt(mean(psi_bar**2)/n)`` is exactly
|
|
16
16
|
that on NO-DESIGN fits — per UNIT on panel fits, per OBSERVATION on
|
|
17
17
|
repeated-cross-section fits (rows are the sampling units there). Under a
|
|
18
|
-
``survey_design=``/``cluster=`` the per-cell SE is
|
|
19
|
-
weighted-IF variance
|
|
20
|
-
``SurveyDesign(psu=...)`` routed through the
|
|
18
|
+
``survey_design=``/``cluster=`` the per-cell SE is design-based instead:
|
|
19
|
+
full-design TSL fits use the CR1 / weighted-IF variance (the CS
|
|
20
|
+
clustered-``hc1`` convention: ``SurveyDesign(psu=...)`` routed through the
|
|
21
|
+
shared stratified-PSU meat), while replicate-weight fits use IF-reweighting
|
|
22
|
+
via ``compute_replicate_if_variance`` on the same per-cell payload
|
|
23
|
+
(``df = rank(replicate matrix) - 1``; REGISTRY DMLDiD Note).
|
|
21
24
|
"""
|
|
22
25
|
|
|
23
26
|
from dataclasses import dataclass, field
|
|
@@ -1397,6 +1397,32 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1397
1397
|
apply_bootstrap_group_overrides(group_effects, bootstrap_results, self.alpha)
|
|
1398
1398
|
|
|
1399
1399
|
# ----- Build results -----
|
|
1400
|
+
# Raw (pre-normalization) unit weights for the metadata recompute:
|
|
1401
|
+
# compute_survey_metadata expects the ORIGINAL scale (resolve()
|
|
1402
|
+
# rescales pweights to mean 1, so the resolved unit weights would
|
|
1403
|
+
# misreport sum_weights/weight_range; scale-invariant fields are
|
|
1404
|
+
# unaffected either way).
|
|
1405
|
+
raw_unit_w_meta: Optional[np.ndarray] = None
|
|
1406
|
+
if self._unit_resolved_survey is not None:
|
|
1407
|
+
assert survey_design is not None
|
|
1408
|
+
# `is not None`, not truthiness: resolve() treats any non-None
|
|
1409
|
+
# string — an empty-string column name included — as a column.
|
|
1410
|
+
raw_obs_w_meta = (
|
|
1411
|
+
data[survey_design.weights].values.astype(np.float64)
|
|
1412
|
+
if survey_design.weights is not None
|
|
1413
|
+
else np.ones(len(data), dtype=np.float64)
|
|
1414
|
+
)
|
|
1415
|
+
raw_unit_w_meta = raw_obs_w_meta[self._unit_first_panel_row]
|
|
1416
|
+
# Per-row ES df provenance (M-092 completion): the post-overall
|
|
1417
|
+
# survey-df snapshot is the df the ES rows' safe_inference used.
|
|
1418
|
+
# None when no ES surface was built, under bootstrap (percentile
|
|
1419
|
+
# inference used no df — the shipped producer convention), and for
|
|
1420
|
+
# the replicate-undefined 0 sentinel (representable only via
|
|
1421
|
+
# survey_metadata.df_survey).
|
|
1422
|
+
_es_df_final: Optional[float] = None
|
|
1423
|
+
if event_study_effects is not None and bootstrap_results is None:
|
|
1424
|
+
if _survey_df_post_overall is not None and _survey_df_post_overall > 0:
|
|
1425
|
+
_es_df_final = float(_survey_df_post_overall)
|
|
1400
1426
|
self.results_ = EfficientDiDResults(
|
|
1401
1427
|
group_time_effects=group_time_effects,
|
|
1402
1428
|
overall_att=overall_att,
|
|
@@ -1416,6 +1442,7 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1416
1442
|
bootstrap_weights=self.bootstrap_weights,
|
|
1417
1443
|
seed=self.seed,
|
|
1418
1444
|
event_study_effects=event_study_effects,
|
|
1445
|
+
event_study_df=_es_df_final,
|
|
1419
1446
|
group_effects=group_effects,
|
|
1420
1447
|
efficient_weights=stored_weights if stored_weights else None,
|
|
1421
1448
|
omega_condition_numbers=stored_cond if stored_cond else None,
|
|
@@ -1452,7 +1479,7 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1452
1479
|
kernel_bandwidth=self.kernel_bandwidth,
|
|
1453
1480
|
omega_ridge=self.omega_ridge,
|
|
1454
1481
|
survey_metadata=(
|
|
1455
|
-
self._recompute_unit_survey_metadata(survey_metadata)
|
|
1482
|
+
self._recompute_unit_survey_metadata(survey_metadata, raw_unit_w_meta)
|
|
1456
1483
|
if survey_metadata is not None
|
|
1457
1484
|
else None
|
|
1458
1485
|
),
|
|
@@ -1483,14 +1510,22 @@ class EfficientDiD(EfficientDiDBootstrapMixin, _EfficientAggregationMixin, BaseE
|
|
|
1483
1510
|
self.is_fitted_ = True
|
|
1484
1511
|
return self.results_
|
|
1485
1512
|
|
|
1486
|
-
def _recompute_unit_survey_metadata(self, panel_metadata):
|
|
1487
|
-
"""Recompute survey metadata from unit-level design if available.
|
|
1513
|
+
def _recompute_unit_survey_metadata(self, panel_metadata, raw_unit_weights=None):
|
|
1514
|
+
"""Recompute survey metadata from unit-level design if available.
|
|
1515
|
+
|
|
1516
|
+
``raw_unit_weights`` carries the ORIGINAL-scale (pre-normalization)
|
|
1517
|
+
unit weights and MUST be passed whenever ``_unit_resolved_survey``
|
|
1518
|
+
is set — never fall back to ``_unit_resolved_survey.weights``, which
|
|
1519
|
+
resolve() rescaled to mean 1 and would misreport sum_weights/
|
|
1520
|
+
weight_range (scale-invariant fields are unaffected either way).
|
|
1521
|
+
"""
|
|
1488
1522
|
if self._unit_resolved_survey is not None:
|
|
1489
1523
|
from diff_diff.survey import compute_survey_metadata
|
|
1490
1524
|
|
|
1525
|
+
assert raw_unit_weights is not None
|
|
1491
1526
|
meta = compute_survey_metadata(
|
|
1492
1527
|
self._unit_resolved_survey,
|
|
1493
|
-
|
|
1528
|
+
raw_unit_weights,
|
|
1494
1529
|
)
|
|
1495
1530
|
# Propagate effective replicate df if available
|
|
1496
1531
|
# (but not the df=0 sentinel — keep metadata as None for undefined df)
|