You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
perf(staggered): O(n_units) CS aggregation IF assembly + R did 2.5.1 yardstick
Rewrite CallawaySantAnna's combined influence-function assembly (also
inherited by StaggeredTripleDifference aggregation) - previously 56-85% of
analytical fit time at scale:
- Per-fit cohort tables (np.unique + np.bincount) cached on the precomputed
structures with array-identity validation (safe against triple-diff's
shallow-copy + zeroed-cohort aggregation pattern)
- Closed-form WIF (wif_i = w_i * (E(c_i)/S - K(c_i)*d/S^2)) replacing dense
(n_units x n_gt) indicator/outer-product matrices - algebraically identical,
documented in REGISTRY
- Fancy-index scatter replacing np.add.at (per-cell index arrays are
duplicate-free by construction; invariant documented at all IF producers)
- Pre-rewrite general path preserved verbatim as fallback for direct callers
Point estimates bit-identical; aggregated SEs <=5e-16 relative (drift-bound
frozen-copy tests at rtol=0 atol=1e-9 across panel/survey/RCS/unbalanced/
triple-diff/inf-coded fixtures). Analytical fits 2.3-6.3x faster at 2-5M rows,
bootstrap fits 1.4-1.6x, RCS peak memory -55 to -68%. 3-11x faster than R did
2.5.1 at equal work (single- and multi-core R; benchmarks/R/benchmark_did.R
extended with bootstrap/covariate/parallelism flags for the yardstick).
Golden aggregated-SE assertions enabled for dr scenarios (match R fixtures at
1e-12..7e-6). Pre-existing reg-method aggregated-SE gap vs R fixtures (3-20%,
ATTs exact) documented in TODO.md for follow-up investigation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X4AzrFUMqJxcUumSH31mSr
Copy file name to clipboardExpand all lines: TODO.md
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -32,6 +32,7 @@ The `Origin` column (Actionable tables) and the `PR` column (Deferred tables) bo
32
32
|`ContinuousDiD` CGBS-2024 extensions. (a) `covariates=` kwarg — **DONE (reg/dr)**; remaining: (b) discrete-treatment saturated regression (integer dose currently warned, not routed to per-level coefficients); (c) lowest-dose-as-control per Remark 3.1 when `P(D=0)=0`. Also deferred from the covariate work: `estimation_method="ipw"` on the dose curve (scalar-adjustment / degenerate — documented `NotImplementedError`), and `covariates=` × `survey_design=` (weighted OR + weighted nuisance IF). |`continuous_did.py`| CGBS-2024 | Heavy | Low |
33
33
|`ImputationDiD` LOO conservative-variance refinement (BJS 2024 Supp. Appendix A.9) — a finite-sample improvement to the auxiliary-model residuals reducing overfit of `tau_tilde_g` to `epsilon`. Asymptotic Theorem-3 variance is implemented and matches R `didimputation` (which also omits LOO by default). |`imputation.py`| imputation-validation | Mid | Low |
34
34
|`TwoWayFixedEffects(vcov_type in {hc2, hc2_bm})` with replicate-weight designs raises `NotImplementedError` (`twfe.py:~233`). The replicate path re-demeans per replicate, which doesn't compose with the full-dummy HC2/HC2-BM build — a correct impl needs per-replicate full-dummy refit. Workaround: `hc1` for replicate-weight CR1. |`twfe.py::fit`| follow-up | Heavy | Low |
35
+
|`CallawaySantAnna` reg-method aggregated SEs sit 3-20% from the R golden fixtures (`two_period` simple/group/dynamic, `dynamic_effects` dynamic) while the ATTs match at 1e-11 and the dr-method aggregated SEs match at ≤7e-6. Pre-existing (byte-identical deltas on the pre-fast-path tree); golden aggregated-SE assertions were therefore enabled for dr scenarios only (`test_golden_simple_aggregation_se`, `test_golden_event_study_aggregation_se`). Investigate the reg-path per-cell IF vs R `DRDID::reg_did_panel`'s. |`staggered.py::_outcome_regression`, `tests/test_csdid_ported.py`| CS-scaling | Mid | Medium |
35
36
| TWFE's HC2/HC2-BM inline full-dummy build (`twfe.py:280-315`) duplicates the dummy-construction logic in `DifferenceInDifferences(fixed_effects=...)` (`estimators.py:478-486`). Extract a shared helper, or delegate TWFE's HC2/HC2-BM path to DiD's `fixed_effects=` branch (with TWFE-specific cluster-default threading), to reduce drift risk on FE naming / survey behavior / result-surface conventions. Substantive refactor — touches both estimators. |`twfe.py::fit`, `estimators.py::DifferenceInDifferences.fit`| follow-up | Heavy | Low |
| Multiplier-bootstrap weight chunking (CallawaySantAnna, EfficientDiD, and HAD — all wired through `diff_diff/bootstrap_chunking.py`) covers the **unstratified** survey-PSU generation (the default unit-level bootstrap — `cluster=None`, equivalently `cluster="unit"` — the large-`n_units` OOM case). Remaining gap: the **stratified** survey-PSU generator (`generate_survey_multiplier_weights_batch`, per-stratum + lonely-PSU pooling + FPC) still materializes the full `(n_bootstrap × n_psu)` matrix (consumed via sliced blocks). Stratified designs have few PSUs so this rarely OOMs; tile per-stratum generation over draws (each stratum's draws are independent → contiguous draw-blocks reproduce the stream bit-identically) if a large-PSU stratified design hits memory. |`diff_diff/bootstrap_chunking.py::iter_survey_multiplier_weight_blocks`| follow-up | Mid | Low |
54
55
| Migrate `spillover._iterative_fe_subset` onto the shared `diff_diff.utils._iterative_fe_solve` (same Gauss-Seidel-on-codes recursion, specialized to a masked Butts subsample; ImputationDiD/TwoStageDiD already route through the shared helper — this takes the FE-solver copy count from 2 to 1). Preserve the SpilloverDiD positive-weight/NaN-FE REGISTRY contract and `_FE_ITER_MAX=100` budget (or align it to 10k with a REGISTRY note). |`spillover.py`| demean-modernization | Mid | Low |
55
56
| Adopt `snap_absorbed_regressors` (FE-spanned regressor two-stage snap + LSMR confirmation) on the ImputationDiD lead-indicator path — lead columns are the most plausible FE-spanned regressors, and the truncated-MAP-iterate exposure documented at `utils.py` applies; today only `solve_ols` rank detection guards it. Behavior change beyond the demean-modernization refactor, so deferred from that PR. |`imputation.py::_compute_lead_coefficients`| demean-modernization | Mid | Low |
57
+
|`_cluster_robust_se_from_per_gt_if` scatters per-cell IFs with `np.add.at` (unbuffered ufunc, 5-20x slower than fancy `+=`; same slow-scatter class the aggregation fast path fixed). Index arrays come from `np.where` over boolean masks (duplicate-free), so per-cell `psi[idx] += vals` is exact. Runs once per (g,t) cell when `cluster=` is set — noticeable at many-cell fits. |`staggered.py::_cluster_robust_se_from_per_gt_if`| CS-scaling | Quick | Low |
58
+
| Per-cell `treated_units`/`control_units` label arrays (`all_units[positions]`, ~O(n_control) alloc per (g,t) cell) are consumed only by the precomputed-None fallback of the combined-IF assembly, which no in-package caller reaches — build them lazily (or drop from the IF-info dict) to cut per-cell allocation at high cell counts. |`staggered.py::_compute_att_gt_fast`| CS-scaling | Mid | Low |
59
+
|`_compute_aggregated_se` is dead code (zero in-package callers; superseded by `_compute_aggregated_se_with_wif`) — remove it, or fold its docstring into the WIF variant. Its `np.add.at` scatter also predates the fancy-`+=` convention. |`staggered_aggregation.py::_compute_aggregated_se`| CS-scaling | Quick | Low |
0 commit comments