diff-diff 3.6.0__tar.gz → 3.6.2__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.6.0 → diff_diff-3.6.2}/PKG-INFO +4 -4
- {diff_diff-3.6.0 → diff_diff-3.6.2}/README.md +3 -3
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/__init__.py +1 -1
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/_backend.py +12 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/_nprobust_port.py +13 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/_reporting_helpers.py +20 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/bacon.py +15 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/business_report.py +13 -4
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/chaisemartin_dhaultfoeuille.py +13 -6
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/chaisemartin_dhaultfoeuille_results.py +5 -3
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/continuous_did.py +46 -81
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/diagnostic_report.py +181 -7
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/efficient_did.py +4 -23
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/efficient_did_covariates.py +33 -10
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/estimators.py +112 -52
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/guides/llms-autonomous.txt +15 -6
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/guides/llms-full.txt +15 -11
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/guides/llms-practitioner.txt +6 -3
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/guides/llms.txt +5 -5
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/had.py +89 -44
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/linalg.py +21 -4
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/lpdid.py +461 -59
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/lpdid_results.py +43 -2
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/results.py +5 -3
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/stacked_did.py +4 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/staggered.py +185 -33
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/staggered_aggregation.py +16 -7
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/staggered_results.py +9 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/sun_abraham.py +16 -14
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/survey.py +65 -2
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/synthetic_control.py +198 -21
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/synthetic_control_results.py +717 -72
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/trop.py +153 -22
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/trop_global.py +17 -1
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/trop_local.py +179 -19
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/trop_results.py +33 -2
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/twfe.py +29 -35
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/utils.py +568 -95
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/wooldridge.py +21 -1
- {diff_diff-3.6.0 → diff_diff-3.6.2}/pyproject.toml +1 -1
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/Cargo.lock +8 -8
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/Cargo.toml +5 -1
- diff_diff-3.6.2/rust/src/alloc_profile.rs +70 -0
- diff_diff-3.6.2/rust/src/demean.rs +333 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/src/lib.rs +17 -1
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/src/linalg.rs +70 -48
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/src/trop.rs +212 -87
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/src/weights.rs +227 -61
- {diff_diff-3.6.0 → diff_diff-3.6.2}/LICENSE +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/_guides_api.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/agent_workflow.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/balancing.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/bootstrap_chunking.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/bootstrap_utils.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/chaisemartin_dhaultfoeuille_bootstrap.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/conformal.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/conley.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/continuous_did_bspline.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/continuous_did_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/datasets.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/diagnostics.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/efficient_did_bootstrap.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/efficient_did_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/efficient_did_weights.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/guides/__init__.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/had_pretests.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/honest_did.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/imputation.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/imputation_bootstrap.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/imputation_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/local_linear.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/power.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/practitioner.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/prep.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/prep_dgp.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/pretrends.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/profile.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/spillover.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/stacked_did_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/staggered_bootstrap.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/staggered_triple_diff.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/staggered_triple_diff_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/synthetic_did.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/triple_diff.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/two_stage.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/two_stage_bootstrap.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/two_stage_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/__init__.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_common.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_continuous.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_diagnostic.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_event_study.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_power.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_staggered.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/visualization/_synthetic.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/diff_diff/wooldridge_results.py +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/build.rs +0 -0
- {diff_diff-3.6.0 → diff_diff-3.6.2}/rust/src/bootstrap.rs +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: diff-diff
|
|
3
|
-
Version: 3.6.
|
|
3
|
+
Version: 3.6.2
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Science/Research
|
|
6
6
|
Classifier: Operating System :: OS Independent
|
|
@@ -155,7 +155,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
|
|
|
155
155
|
- [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - panel data DiD with unit and time fixed effects via within-transformation or dummies
|
|
156
156
|
- [MultiPeriodDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - event study design with period-specific treatment effects for dynamic analysis
|
|
157
157
|
- [CallawaySantAnna](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Callaway & Sant'Anna (2021) group-time ATT estimator for staggered adoption
|
|
158
|
-
- [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The
|
|
158
|
+
- [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The most general option for treatments that switch on AND off (see also `LPDiD`/`TROP` `non_absorbing`). Alias `DCDH`.
|
|
159
159
|
- [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
|
|
160
160
|
- [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html) - Borusyak, Jaravel & Spiess (2024) imputation estimator, most efficient under homogeneous effects
|
|
161
161
|
- [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
|
|
@@ -170,7 +170,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
|
|
|
170
170
|
- [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
|
|
171
171
|
- [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
|
|
172
172
|
- [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`.
|
|
173
|
-
- [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 treatment
|
|
173
|
+
- [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
|
|
174
174
|
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
|
|
175
175
|
|
|
176
176
|
## Diagnostics & Sensitivity
|
|
@@ -198,7 +198,7 @@ No other Python or R DiD package offers design-based variance estimation for mod
|
|
|
198
198
|
- Python 3.9 - 3.14
|
|
199
199
|
- numpy >= 1.20
|
|
200
200
|
- pandas >= 1.3
|
|
201
|
-
- scipy >= 1.
|
|
201
|
+
- scipy >= 1.10
|
|
202
202
|
|
|
203
203
|
## Development
|
|
204
204
|
|
|
@@ -102,7 +102,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
|
|
|
102
102
|
- [TwoWayFixedEffects](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - panel data DiD with unit and time fixed effects via within-transformation or dummies
|
|
103
103
|
- [MultiPeriodDiD](https://diff-diff.readthedocs.io/en/stable/api/estimators.html) - event study design with period-specific treatment effects for dynamic analysis
|
|
104
104
|
- [CallawaySantAnna](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Callaway & Sant'Anna (2021) group-time ATT estimator for staggered adoption
|
|
105
|
-
- [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The
|
|
105
|
+
- [ChaisemartinDHaultfoeuille](https://diff-diff.readthedocs.io/en/stable/api/chaisemartin_dhaultfoeuille.html) - de Chaisemartin & D'Haultfœuille (2020/2022) for **reversible (non-absorbing) treatments** with multi-horizon event study, normalized effects, cost-benefit delta, sup-t bands, and dynamic placebos. The most general option for treatments that switch on AND off (see also `LPDiD`/`TROP` `non_absorbing`). Alias `DCDH`.
|
|
106
106
|
- [SunAbraham](https://diff-diff.readthedocs.io/en/stable/api/staggered.html) - Sun & Abraham (2021) interaction-weighted estimator for heterogeneity-robust event studies
|
|
107
107
|
- [ImputationDiD](https://diff-diff.readthedocs.io/en/stable/api/imputation.html) - Borusyak, Jaravel & Spiess (2024) imputation estimator, most efficient under homogeneous effects
|
|
108
108
|
- [TwoStageDiD](https://diff-diff.readthedocs.io/en/stable/api/two_stage.html) - Gardner (2022) two-stage estimator with GMM sandwich variance
|
|
@@ -117,7 +117,7 @@ Full guide: `diff_diff.get_llm_guide("practitioner")`.
|
|
|
117
117
|
- [TROP](https://diff-diff.readthedocs.io/en/stable/api/trop.html) - Triply Robust Panel estimator (Athey et al. 2025) with nuclear norm factor adjustment
|
|
118
118
|
- [StaggeredTripleDifference](https://diff-diff.readthedocs.io/en/stable/api/staggered.html#staggeredtripledifference) - Ortiz-Villavicencio & Sant'Anna (2025) staggered DDD with group-time ATT
|
|
119
119
|
- [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`.
|
|
120
|
-
- [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 treatment
|
|
120
|
+
- [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
|
|
121
121
|
- [BaconDecomposition](https://diff-diff.readthedocs.io/en/stable/api/bacon.html) - Goodman-Bacon (2021) decomposition for diagnosing TWFE bias in staggered settings
|
|
122
122
|
|
|
123
123
|
## Diagnostics & Sensitivity
|
|
@@ -145,7 +145,7 @@ No other Python or R DiD package offers design-based variance estimation for mod
|
|
|
145
145
|
- Python 3.9 - 3.14
|
|
146
146
|
- numpy >= 1.20
|
|
147
147
|
- pandas >= 1.3
|
|
148
|
-
- scipy >= 1.
|
|
148
|
+
- scipy >= 1.10
|
|
149
149
|
|
|
150
150
|
## Development
|
|
151
151
|
|
|
@@ -65,6 +65,14 @@ except ImportError:
|
|
|
65
65
|
_rust_sc_weight_fw_weighted_with_convergence = None
|
|
66
66
|
_rust_backend_info = None
|
|
67
67
|
|
|
68
|
+
# FE-absorption MAP demeaning kernel: imported independently so a stale or
|
|
69
|
+
# mixed-version extension missing only this newer symbol degrades to the
|
|
70
|
+
# numpy demeaning engine WITHOUT disabling the older Rust accelerations.
|
|
71
|
+
try:
|
|
72
|
+
from diff_diff._rust_backend import demean_map as _rust_demean_map
|
|
73
|
+
except ImportError:
|
|
74
|
+
_rust_demean_map = None
|
|
75
|
+
|
|
68
76
|
# Determine final backend based on environment variable and availability
|
|
69
77
|
if _backend_env == "python":
|
|
70
78
|
# Force pure Python mode - disable Rust even if available
|
|
@@ -73,6 +81,8 @@ if _backend_env == "python":
|
|
|
73
81
|
_rust_project_simplex = None
|
|
74
82
|
_rust_solve_ols = None
|
|
75
83
|
_rust_compute_robust_vcov = None
|
|
84
|
+
# FE-absorption MAP demeaning kernel
|
|
85
|
+
_rust_demean_map = None
|
|
76
86
|
# TROP estimator acceleration (local method)
|
|
77
87
|
_rust_unit_distance_matrix = None
|
|
78
88
|
_rust_loocv_grid_search = None
|
|
@@ -124,6 +134,8 @@ __all__ = [
|
|
|
124
134
|
"_rust_project_simplex",
|
|
125
135
|
"_rust_solve_ols",
|
|
126
136
|
"_rust_compute_robust_vcov",
|
|
137
|
+
# FE-absorption MAP demeaning kernel
|
|
138
|
+
"_rust_demean_map",
|
|
127
139
|
# TROP estimator acceleration (local method)
|
|
128
140
|
"_rust_unit_distance_matrix",
|
|
129
141
|
"_rust_loocv_grid_search",
|
|
@@ -1361,6 +1361,19 @@ def lprobust(
|
|
|
1361
1361
|
se_cl = float(np.sqrt((deriv_fact**2) * V_Y_cl[deriv, deriv]))
|
|
1362
1362
|
se_rb = float(np.sqrt((deriv_fact**2) * V_Y_bc[deriv, deriv]))
|
|
1363
1363
|
|
|
1364
|
+
# Cluster-robust variance is unidentified when fewer than two clusters
|
|
1365
|
+
# contribute to the ACTIVE kernel window (``eC = cluster[ind]``): the
|
|
1366
|
+
# between-cluster meat is degenerate, so a finite ``se`` here would report
|
|
1367
|
+
# unidentified clustered inference as if identified. NaN both SEs so any
|
|
1368
|
+
# downstream inference (the ``safe_inference`` gate in
|
|
1369
|
+
# ``bias_corrected_local_linear``; HAD's beta-scale rescale) is NaN-coupled.
|
|
1370
|
+
# Unclustered fits (``eC is None``) are unaffected, and a clustered window
|
|
1371
|
+
# with >= 2 distinct clusters is bit-identical, so the DGP-4 golden parity
|
|
1372
|
+
# is preserved.
|
|
1373
|
+
if eC is not None and len(np.unique(eC)) < 2:
|
|
1374
|
+
se_cl = float("nan")
|
|
1375
|
+
se_rb = float("nan")
|
|
1376
|
+
|
|
1364
1377
|
# --- Per-observation influence function for the BIAS-CORRECTED point
|
|
1365
1378
|
# estimate at ``deriv`` (Phase 4.5 survey composition).
|
|
1366
1379
|
# Aligned with ``V_Y_bc`` (NOT ``V_Y_cl``) so survey-composed variance
|
|
@@ -635,6 +635,26 @@ def describe_target_parameter(results: Any) -> Dict[str, Any]:
|
|
|
635
635
|
"reference": "REGISTRY.md Sec. SyntheticControl",
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
+
if name == "SpilloverDiDResults":
|
|
639
|
+
return {
|
|
640
|
+
"name": "total effect on the treated (Butts spillover-aware ATT)",
|
|
641
|
+
"definition": (
|
|
642
|
+
"The total effect on the treated ``tau_total`` from Butts (2021) "
|
|
643
|
+
"ring-indicator spillover DiD, identified off FAR-AWAY control "
|
|
644
|
+
"observations (``d_it > d_bar``, Assumption 5) rather than any "
|
|
645
|
+
"not-yet-/never-treated pool. The estimator decomposes into the "
|
|
646
|
+
"DIRECT effect on treated units plus per-ring spillover-on-control "
|
|
647
|
+
"effects that relax SUTVA within the treated units' spatial "
|
|
648
|
+
"neighborhood; ``att`` is the headline total effect, while the "
|
|
649
|
+
"per-ring ``spillover_effects`` and (when ``event_study=True``) the "
|
|
650
|
+
"per-event-time direct dynamics are available on the result object "
|
|
651
|
+
"for disaggregated inference."
|
|
652
|
+
),
|
|
653
|
+
"aggregation": "spillover",
|
|
654
|
+
"headline_attribute": "att",
|
|
655
|
+
"reference": "Butts (2021); REGISTRY.md Sec. SpilloverDiD",
|
|
656
|
+
}
|
|
657
|
+
|
|
638
658
|
# Default: unrecognized result class. Fall through with a neutral
|
|
639
659
|
# block — agents / downstream consumers can still dispatch on
|
|
640
660
|
# ``aggregation="unknown"`` and fall back to generic ATT narration.
|
|
@@ -18,6 +18,7 @@ import numpy as np
|
|
|
18
18
|
import pandas as pd
|
|
19
19
|
|
|
20
20
|
from diff_diff.results import _format_survey_block
|
|
21
|
+
from diff_diff.utils import pre_demean_norms, snap_absorbed_regressors
|
|
21
22
|
from diff_diff.utils import within_transform as _within_transform_util
|
|
22
23
|
|
|
23
24
|
|
|
@@ -795,6 +796,7 @@ class BaconDecomposition:
|
|
|
795
796
|
) -> float:
|
|
796
797
|
"""Compute TWFE estimate using within-transformation."""
|
|
797
798
|
# Apply two-way within transformation (weighted if survey weights provided)
|
|
799
|
+
_pre_norms = pre_demean_norms(df, [treat_col], weights=weights)
|
|
798
800
|
df_dm = _within_transform_util(
|
|
799
801
|
df,
|
|
800
802
|
[outcome, treat_col],
|
|
@@ -803,6 +805,19 @@ class BaconDecomposition:
|
|
|
803
805
|
suffix="_within",
|
|
804
806
|
weights=weights,
|
|
805
807
|
)
|
|
808
|
+
# Snap an FE-spanned treatment to exact zero: the d_var == 0 guard
|
|
809
|
+
# below then returns its deterministic 0.0 (with the cause warning)
|
|
810
|
+
# instead of an arbitrary junk/junk division.
|
|
811
|
+
snap_absorbed_regressors(
|
|
812
|
+
df_dm,
|
|
813
|
+
[treat_col],
|
|
814
|
+
_pre_norms,
|
|
815
|
+
absorbed_desc=f"unit '{unit}' and time '{time}' fixed effects",
|
|
816
|
+
group_vars=[unit, time],
|
|
817
|
+
suffix="_within",
|
|
818
|
+
display_names={treat_col: "treatment"},
|
|
819
|
+
weights=weights,
|
|
820
|
+
)
|
|
806
821
|
|
|
807
822
|
# Extract within-transformed values
|
|
808
823
|
y_within = df_dm[f"{outcome}_within"].values
|
|
@@ -353,12 +353,21 @@ class BusinessReport:
|
|
|
353
353
|
"""Return a structured multi-section markdown report."""
|
|
354
354
|
base = _render_full_report(self.to_dict())
|
|
355
355
|
if self._include_appendix:
|
|
356
|
+
appendix_text = None
|
|
356
357
|
try:
|
|
357
358
|
appendix = self._results.summary()
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
359
|
+
if appendix:
|
|
360
|
+
appendix_text = str(appendix)
|
|
361
|
+
except Exception as exc: # noqa: BLE001
|
|
362
|
+
appendix_error = type(exc).__name__ or "Exception"
|
|
363
|
+
base = (
|
|
364
|
+
base
|
|
365
|
+
+ "\n\n## Technical Appendix\n\n"
|
|
366
|
+
+ "Technical appendix unavailable: estimator summary rendering failed "
|
|
367
|
+
+ f"({appendix_error}).\n"
|
|
368
|
+
)
|
|
369
|
+
if appendix_text:
|
|
370
|
+
base = base + "\n\n## Technical Appendix\n\n```\n" + appendix_text + "\n```\n"
|
|
362
371
|
return base
|
|
363
372
|
|
|
364
373
|
def export_markdown(self) -> str:
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
"""
|
|
2
2
|
de Chaisemartin-D'Haultfoeuille (dCDH) estimator for reversible-treatment DiD.
|
|
3
3
|
|
|
4
|
-
The dCDH estimator is the
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
The dCDH estimator is the most general DiD estimator in the diff-diff library
|
|
5
|
+
for **non-absorbing (reversible) treatments** — treatment can switch on AND off
|
|
6
|
+
over time, switcher vs non-switcher comparisons are its primitive object, and it
|
|
7
|
+
allows dynamic (carryover) effects with explicit joiner/leaver (``DID_+`` /
|
|
8
|
+
``DID_-``) decomposition. ``LPDiD`` (``non_absorbing="first_entry"`` /
|
|
9
|
+
``"effect_stabilization"``) and ``TROP`` (``non_absorbing=True``, under a
|
|
10
|
+
no-dynamic-effects assumption) also accept non-absorbing treatment under stronger
|
|
11
|
+
assumptions. The remaining staggered estimators in the library
|
|
7
12
|
(``CallawaySantAnna``, ``SunAbraham``, ``ImputationDiD``, ``TwoStageDiD``,
|
|
8
13
|
``EfficientDiD``, ``WooldridgeDiD``) assume treatment is absorbing.
|
|
9
14
|
|
|
@@ -354,9 +359,11 @@ class ChaisemartinDHaultfoeuille(ChaisemartinDHaultfoeuilleBootstrapMixin):
|
|
|
354
359
|
"""
|
|
355
360
|
de Chaisemartin-D'Haultfoeuille (dCDH) estimator.
|
|
356
361
|
|
|
357
|
-
The
|
|
358
|
-
|
|
359
|
-
|
|
362
|
+
The most general library estimator for **reversible (non-absorbing)
|
|
363
|
+
treatments** - treatment may switch on AND off over time, with explicit
|
|
364
|
+
joiner/leaver (``DID_+`` / ``DID_-``) decomposition (``LPDiD`` and ``TROP``
|
|
365
|
+
also support non-absorbing treatment under stronger assumptions; see their
|
|
366
|
+
``non_absorbing`` parameters). Computes the contemporaneous-switch DiD ``DID_M`` from the
|
|
360
367
|
AER 2020 paper (equivalently ``DID_1`` at horizon ``l = 1`` of the
|
|
361
368
|
dynamic companion paper, NBER WP 29873) plus the full multi-horizon
|
|
362
369
|
event study ``DID_l`` for ``l = 1..L_max`` via the ``L_max`` parameter
|
|
@@ -4,9 +4,11 @@ Result containers for the de Chaisemartin-D'Haultfoeuille (dCDH) estimator.
|
|
|
4
4
|
This module contains ``ChaisemartinDHaultfoeuilleResults`` and
|
|
5
5
|
``DCDHBootstrapResults`` dataclasses produced by the
|
|
6
6
|
``ChaisemartinDHaultfoeuille`` (alias ``DCDH``) estimator. The dCDH
|
|
7
|
-
estimator is the
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
estimator is the most general library estimator for non-absorbing
|
|
8
|
+
(reversible) treatments (``LPDiD`` and ``TROP`` also support non-absorbing
|
|
9
|
+
treatment under stronger assumptions; see their ``non_absorbing`` parameters).
|
|
10
|
+
Phase 1 ships the contemporaneous-switch case ``DID_M`` (= ``DID_1`` of the
|
|
11
|
+
dynamic companion paper).
|
|
10
12
|
|
|
11
13
|
References
|
|
12
14
|
----------
|
|
@@ -31,9 +31,9 @@ from diff_diff.continuous_did_results import (
|
|
|
31
31
|
)
|
|
32
32
|
from diff_diff.linalg import _rank_guarded_inv, solve_ols
|
|
33
33
|
from diff_diff.survey import (
|
|
34
|
-
ResolvedSurveyDesign,
|
|
35
34
|
_resolve_survey_for_fit,
|
|
36
35
|
_validate_unit_constant_survey,
|
|
36
|
+
build_unit_first_row_index,
|
|
37
37
|
compute_survey_vcov,
|
|
38
38
|
)
|
|
39
39
|
from diff_diff.utils import safe_inference
|
|
@@ -413,8 +413,7 @@ class ContinuousDiD:
|
|
|
413
413
|
|
|
414
414
|
# Filter out NaN cells (e.g., from zero effective survey mass)
|
|
415
415
|
gt_results = {
|
|
416
|
-
gt: r for gt, r in gt_results.items()
|
|
417
|
-
if np.isfinite(r.get("att_glob", np.nan))
|
|
416
|
+
gt: r for gt, r in gt_results.items() if np.isfinite(r.get("att_glob", np.nan))
|
|
418
417
|
}
|
|
419
418
|
|
|
420
419
|
if len(gt_results) == 0:
|
|
@@ -573,9 +572,12 @@ class ContinuousDiD:
|
|
|
573
572
|
# Survey df for t-distribution inference (unit-level, not panel-level)
|
|
574
573
|
_survey_df = analytic.get("df_survey")
|
|
575
574
|
# Guard: replicate design with undefined df → NaN inference
|
|
576
|
-
if (
|
|
577
|
-
|
|
578
|
-
|
|
575
|
+
if (
|
|
576
|
+
_survey_df is None
|
|
577
|
+
and resolved_survey is not None
|
|
578
|
+
and hasattr(resolved_survey, "uses_replicate_variance")
|
|
579
|
+
and resolved_survey.uses_replicate_variance
|
|
580
|
+
):
|
|
579
581
|
_survey_df = 0
|
|
580
582
|
|
|
581
583
|
# Recompute survey_metadata from unit-level design so reported
|
|
@@ -589,8 +591,7 @@ class ContinuousDiD:
|
|
|
589
591
|
|
|
590
592
|
# Propagate replicate df override to survey_metadata for display
|
|
591
593
|
# (but not the df=0 sentinel — keep metadata as None for undefined df)
|
|
592
|
-
if
|
|
593
|
-
and survey_metadata is not None):
|
|
594
|
+
if _survey_df is not None and _survey_df != 0 and survey_metadata is not None:
|
|
594
595
|
if survey_metadata.df_survey != _survey_df:
|
|
595
596
|
survey_metadata.df_survey = _survey_df
|
|
596
597
|
|
|
@@ -624,30 +625,8 @@ class ContinuousDiD:
|
|
|
624
625
|
unit_resolved_es = None
|
|
625
626
|
if resolved_survey is not None:
|
|
626
627
|
row_idx = precomp["unit_first_panel_row"]
|
|
627
|
-
|
|
628
|
-
precomp.get("unit_survey_weights")
|
|
629
|
-
if precomp.get("unit_survey_weights") is not None
|
|
630
|
-
else np.ones(n_units)
|
|
631
|
-
)
|
|
632
|
-
us = (
|
|
633
|
-
resolved_survey.strata[row_idx]
|
|
634
|
-
if resolved_survey.strata is not None
|
|
635
|
-
else None
|
|
636
|
-
)
|
|
637
|
-
up = (
|
|
638
|
-
resolved_survey.psu[row_idx]
|
|
639
|
-
if resolved_survey.psu is not None
|
|
640
|
-
else None
|
|
641
|
-
)
|
|
642
|
-
uf = (
|
|
643
|
-
resolved_survey.fpc[row_idx]
|
|
644
|
-
if resolved_survey.fpc is not None
|
|
645
|
-
else None
|
|
646
|
-
)
|
|
647
|
-
n_strata_u = len(np.unique(us)) if us is not None else 0
|
|
648
|
-
n_psu_u = len(np.unique(up)) if up is not None else 0
|
|
649
|
-
unit_resolved_es = resolved_survey.subset_to_units(
|
|
650
|
-
row_idx, uw, us, up, uf, n_strata_u, n_psu_u,
|
|
628
|
+
unit_resolved_es = resolved_survey.subset_to_units_by_row_idx(
|
|
629
|
+
row_idx, unit_weights=precomp.get("unit_survey_weights")
|
|
651
630
|
)
|
|
652
631
|
|
|
653
632
|
for e_val, info_e in event_study_effects.items():
|
|
@@ -711,13 +690,21 @@ class ContinuousDiD:
|
|
|
711
690
|
|
|
712
691
|
# Score-scale: psi = w * if_es (matches TSL bread)
|
|
713
692
|
psi_es = unit_resolved_es.weights * if_es
|
|
714
|
-
variance, _nv = compute_replicate_if_variance(
|
|
715
|
-
|
|
693
|
+
variance, _nv = compute_replicate_if_variance(
|
|
694
|
+
psi_es, unit_resolved_es
|
|
695
|
+
)
|
|
696
|
+
es_se = (
|
|
697
|
+
float(np.sqrt(max(variance, 0.0)))
|
|
698
|
+
if np.isfinite(variance)
|
|
699
|
+
else np.nan
|
|
700
|
+
)
|
|
716
701
|
else:
|
|
717
702
|
X_ones_es = np.ones((n_units, 1))
|
|
718
703
|
tsl_scale_es = float(unit_resolved_es.weights.sum())
|
|
719
704
|
if_es_tsl = if_es * tsl_scale_es
|
|
720
|
-
vcov_es = compute_survey_vcov(
|
|
705
|
+
vcov_es = compute_survey_vcov(
|
|
706
|
+
X_ones_es, if_es_tsl, unit_resolved_es
|
|
707
|
+
)
|
|
721
708
|
es_se = float(np.sqrt(np.abs(vcov_es[0, 0])))
|
|
722
709
|
else:
|
|
723
710
|
es_se = float(np.sqrt(np.sum(if_es**2)))
|
|
@@ -831,15 +818,11 @@ class ContinuousDiD:
|
|
|
831
818
|
unit_cohorts[i] = unit_first.loc[u, first_treat]
|
|
832
819
|
dose_vector[i] = unit_first.loc[u, dose]
|
|
833
820
|
|
|
834
|
-
# Build unit-to-first-panel-row mapping (for subsetting panel-level
|
|
835
|
-
#
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
u = row[unit]
|
|
840
|
-
if u not in seen_units:
|
|
841
|
-
seen_units.add(u)
|
|
842
|
-
unit_first_panel_row[unit_to_idx[u]] = pos_idx
|
|
821
|
+
# Build unit-to-first-panel-row mapping (for subsetting panel-level
|
|
822
|
+
# arrays): the positional index of each unit's first row in df, aligned
|
|
823
|
+
# to ``all_units`` (== ``unit_to_idx`` order since
|
|
824
|
+
# ``unit_to_idx = {u: i for i, u in enumerate(all_units)}``).
|
|
825
|
+
unit_first_panel_row = build_unit_first_row_index(df[unit].values, all_units)
|
|
843
826
|
|
|
844
827
|
# Per-unit survey weights (take first obs per unit from panel data)
|
|
845
828
|
unit_survey_weights = None
|
|
@@ -949,8 +932,10 @@ class ContinuousDiD:
|
|
|
949
932
|
# Guard against zero effective mass (e.g., after subpopulation)
|
|
950
933
|
if np.sum(w_treated) <= 0 or np.sum(w_control) <= 0:
|
|
951
934
|
return {
|
|
952
|
-
"att_glob": np.nan,
|
|
953
|
-
"
|
|
935
|
+
"att_glob": np.nan,
|
|
936
|
+
"acrt_glob": np.nan,
|
|
937
|
+
"n_treated": 0,
|
|
938
|
+
"n_control": 0,
|
|
954
939
|
"att_d": np.full(len(dvals), np.nan),
|
|
955
940
|
"acrt_d": np.full(len(dvals), np.nan),
|
|
956
941
|
}
|
|
@@ -1293,23 +1278,8 @@ class ContinuousDiD:
|
|
|
1293
1278
|
# but influence functions are unit-level (n_units). Build a unit-level
|
|
1294
1279
|
# ResolvedSurveyDesign by subsetting to one obs per unit.
|
|
1295
1280
|
row_idx = precomp["unit_first_panel_row"]
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
unit_weights = np.ones(n_units)
|
|
1299
|
-
|
|
1300
|
-
unit_strata = (
|
|
1301
|
-
resolved_survey.strata[row_idx] if resolved_survey.strata is not None else None
|
|
1302
|
-
)
|
|
1303
|
-
unit_psu = resolved_survey.psu[row_idx] if resolved_survey.psu is not None else None
|
|
1304
|
-
unit_fpc = resolved_survey.fpc[row_idx] if resolved_survey.fpc is not None else None
|
|
1305
|
-
|
|
1306
|
-
# Count unique strata/PSU in the unit-level subset
|
|
1307
|
-
n_strata_unit = len(np.unique(unit_strata)) if unit_strata is not None else 0
|
|
1308
|
-
n_psu_unit = len(np.unique(unit_psu)) if unit_psu is not None else 0
|
|
1309
|
-
|
|
1310
|
-
unit_resolved = resolved_survey.subset_to_units(
|
|
1311
|
-
row_idx, unit_weights, unit_strata, unit_psu, unit_fpc,
|
|
1312
|
-
n_strata_unit, n_psu_unit,
|
|
1281
|
+
unit_resolved = resolved_survey.subset_to_units_by_row_idx(
|
|
1282
|
+
row_idx, unit_weights=precomp.get("unit_survey_weights")
|
|
1313
1283
|
)
|
|
1314
1284
|
|
|
1315
1285
|
X_ones = np.ones((n_units, 1))
|
|
@@ -1370,7 +1340,11 @@ class ContinuousDiD:
|
|
|
1370
1340
|
|
|
1371
1341
|
# Return unit-level survey df and resolved design for metadata recomputation
|
|
1372
1342
|
# Only override with n_valid-based df when replicates were actually dropped
|
|
1373
|
-
if
|
|
1343
|
+
if (
|
|
1344
|
+
resolved_survey is not None
|
|
1345
|
+
and hasattr(resolved_survey, "uses_replicate_variance")
|
|
1346
|
+
and resolved_survey.uses_replicate_variance
|
|
1347
|
+
):
|
|
1374
1348
|
if _rep_n_valid < unit_resolved.n_replicates:
|
|
1375
1349
|
unit_df_survey = _rep_n_valid - 1 if _rep_n_valid > 1 else None
|
|
1376
1350
|
else:
|
|
@@ -1415,7 +1389,11 @@ class ContinuousDiD:
|
|
|
1415
1389
|
|
|
1416
1390
|
# Reject replicate-weight designs for bootstrap — replicate variance
|
|
1417
1391
|
# is an analytical alternative to bootstrap, not compatible with it
|
|
1418
|
-
if
|
|
1392
|
+
if (
|
|
1393
|
+
resolved_survey is not None
|
|
1394
|
+
and hasattr(resolved_survey, "uses_replicate_variance")
|
|
1395
|
+
and resolved_survey.uses_replicate_variance
|
|
1396
|
+
):
|
|
1419
1397
|
raise NotImplementedError(
|
|
1420
1398
|
"ContinuousDiD bootstrap (n_bootstrap > 0) is not supported "
|
|
1421
1399
|
"with replicate-weight survey designs. Replicate weights provide "
|
|
@@ -1429,22 +1407,9 @@ class ContinuousDiD:
|
|
|
1429
1407
|
# Build unit-level ResolvedSurveyDesign for survey-aware bootstrap
|
|
1430
1408
|
unit_resolved = None
|
|
1431
1409
|
if resolved_survey is not None:
|
|
1432
|
-
from diff_diff.survey import ResolvedSurveyDesign
|
|
1433
|
-
|
|
1434
1410
|
row_idx = precomp["unit_first_panel_row"]
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
unit_weights = np.ones(n_units)
|
|
1438
|
-
unit_strata = (
|
|
1439
|
-
resolved_survey.strata[row_idx] if resolved_survey.strata is not None else None
|
|
1440
|
-
)
|
|
1441
|
-
unit_psu = resolved_survey.psu[row_idx] if resolved_survey.psu is not None else None
|
|
1442
|
-
unit_fpc = resolved_survey.fpc[row_idx] if resolved_survey.fpc is not None else None
|
|
1443
|
-
n_strata_u = len(np.unique(unit_strata)) if unit_strata is not None else 0
|
|
1444
|
-
n_psu_u = len(np.unique(unit_psu)) if unit_psu is not None else 0
|
|
1445
|
-
unit_resolved = resolved_survey.subset_to_units(
|
|
1446
|
-
row_idx, unit_weights, unit_strata, unit_psu, unit_fpc,
|
|
1447
|
-
n_strata_u, n_psu_u,
|
|
1411
|
+
unit_resolved = resolved_survey.subset_to_units_by_row_idx(
|
|
1412
|
+
row_idx, unit_weights=precomp.get("unit_survey_weights")
|
|
1448
1413
|
)
|
|
1449
1414
|
|
|
1450
1415
|
# Generate bootstrap weights — PSU-level when survey design is present
|
|
@@ -1682,7 +1647,7 @@ class ContinuousDiD:
|
|
|
1682
1647
|
boot_es[e],
|
|
1683
1648
|
alpha=self.alpha,
|
|
1684
1649
|
context=f"event study e={e}",
|
|
1685
|
-
|
|
1650
|
+
)
|
|
1686
1651
|
es_se[e] = se_e
|
|
1687
1652
|
es_ci[e] = ci_e
|
|
1688
1653
|
es_p[e] = p_e
|