Skip to content

feat(dml): replicate-weight survey designs via IF-reweighting (per-cell + aggregate) - #807

Merged
igerber merged 2 commits into
mainfrom
dml-replicate-weights
Aug 30, 2026
Merged

feat(dml): replicate-weight survey designs via IF-reweighting (per-cell + aggregate)#807
igerber merged 2 commits into
mainfrom
dml-replicate-weights

Conversation

@igerber

@igerber igerber commented Aug 30, 2026

Copy link
Copy Markdown
Owner

Summary

  • Lift DMLDiD's blanket replicate-weight NotImplementedError: replicate SurveyDesigns (BRR / Fay / JK1 / JKn / SDR) now compute per-cell AND aggregate variances by IF-reweighting the augmented cross-fitted scores (compute_replicate_if_variance on the same Hájek payload the aggregate _se_from_psi route consumes), on both lanes (panel + declared repeated cross sections).
  • Per-cell arm leads the SE dispatch chain; degenerate cells (zero or non-finite replicate variance) fail closed to a full NaN inference tuple — stricter than the shared aggregate clamp (documented asymmetry, REGISTRY DMLDiD replicate Note).
  • Df conventions: QR-rank − 1 design df with min(df_survey, n_valid − 1) capping per cell and at the overall relay. The overall MIN-CAP deliberately diverges from CS's replace convention (anti-conservative — it can raise df above the design df); the CS-side flip plus the shared ES/group and staggered-DDD replace sites are tracked in the new TODO CS-parity row, and the inherited ES replace behavior is pinned by a test cross-linking that row. df_survey is None (rank ≤ 1) → df=0 sentinel local to safe_inference; survey_metadata.df_survey keeps None.
  • Targeted rejections replace the blanket gate: replicate + n_bootstrap > 0 (before any fit work) and replicate + cluster= (after the cluster column checks — CS ordering, bogus names stay ValueError); message parity with CS.
  • Docs in the same diff: REGISTRY replicate Note (linearized Hájek equation with FIXED full-weight denominator, both combined_weights branches, verified against the implementation to 1e-15; Neyman-orthogonality justification for no per-replicate re-cross-fitting; ridge weight-scale caveat; df + degeneracy conventions) and support-matrix insertion (14 of 21); survey-theory roster + dispatch Notes cell; api rst two-lane variance split; choosing_estimator matrix + narrative; all four wheel-shipped LLM guides + the llms-full tally; survey-roadmap limitation row removed; practitioner decision tree; variance-conventions ROWS reason literals + regenerated CI-pinned table; tutorial 32 markdown (code-cell hash pins untouched); fit()/results-module docstrings; changelog.d/ fragment.

Methodology references (required if estimator / math changes)

  • Method name(s): DMLDiD (Chang 2020) replicate-weight survey variance — IF-reweighting on augmented cross-fitted scores
  • Paper / source link(s): Chang (2020), "Double/debiased machine learning for difference-in-differences models" (arXiv:1812.10846); replicate-weight IF-reweighting per the library's shared compute_replicate_if_variance machinery (REGISTRY "Replicate-weight support matrix")
  • Any intentional deviations from the source (and why): Survey support is a documented LIBRARY EXTENSION of Chang's i.i.d. theory (pre-existing Note); new deviations documented in the REGISTRY DMLDiD replicate Note — per-cell replicate SEs exceed CS's per-cell convention, the overall effective-df relay MIN-CAPS instead of CS's replace (anti-conservative), and nuisances are not re-estimated per replicate (Neyman-orthogonality rationale recorded)

Validation

  • Tests added/updated: tests/test_survey_dml.py (new §10 replicate suite: acceptance both lanes + BRR + combined_weights=False discriminator, per-cell SE cross-check against the helper, learner-parametrized scale invariance with ridge bounded, df spy matrix incl. cap-vs-replace discriminator and rank-one NaN, degenerate-cell fail-closed with explicit isnan, targeted rejections, aggregations, PSU-fold non-interaction; blanket fail-closed pins replaced), tests/test_variance_conventions.py (ROWS reason literals). Full battery green on Rust and pure-Python backends (test_survey_dml, test_dml_did, test_survey_phase6, test_methodology_dml_did, test_variance_conventions, test_t32_dml_did_drift, test_guides, test_changelog_fragments).
  • Backtest / simulation / notebook evidence (if applicable): per-cell SEs verified to match direct compute_replicate_if_variance calls exactly (rtol 1e-12); the REGISTRY equation verified against the implementation to 1e-15; scale-invariance probe at ×100 (linear/sieve ≤4e-16 drift, ridge 1.5e-5..2.5e-4 documented).

Security / privacy

  • Confirm no secrets/PII in this PR: Yes

Changelog

  • changelog.d/ fragment added (or N/A - no user-visible change): Yes (changelog.d/20260830-dml-replicate-weights.md)

…ll + aggregate)

Lift DMLDiD's blanket replicate-weight NotImplementedError. Replicate
SurveyDesigns (BRR / Fay / JK1 / JKn / SDR) now compute per-cell AND
aggregate variances by IF-reweighting the augmented cross-fitted scores
(compute_replicate_if_variance on the Hajek payload), on both lanes.

- Per-cell arm leads the SE dispatch chain; degenerate cells (zero or
  non-finite replicate variance) fail closed to NaN inference (stricter
  than the shared aggregate clamp; documented asymmetry).
- Df: QR-rank - 1 design df with min(df_survey, n_valid - 1) capping
  per cell and at the overall relay (deliberate documented divergence
  from CS's replace convention, which is anti-conservative; CS-side flip
  tracked in the TODO CS-parity row together with the shared ES/group
  and staggered-DDD replace sites). df_survey None (rank <= 1) -> df=0
  sentinel local to safe_inference; survey_metadata keeps None.
- Targeted rejections replace the blanket gate: replicate + bootstrap
  (before any fit work) and replicate + cluster= (after the cluster
  column checks — CS ordering, bogus names stay ValueError).
- Docs: REGISTRY replicate Note (linearized Hajek equation with fixed
  full-weight denominator, both combined_weights branches; Neyman-
  orthogonality justification; ridge weight-scale caveat; df and
  degeneracy conventions) + support matrix insertion (14 of 21);
  survey-theory roster + dispatch Notes cell; api rst two-lane split;
  choosing_estimator matrix + narrative; 4 LLM guides + llms-full tally;
  survey-roadmap row removed; practitioner decision tree; variance-
  conventions ROWS literals + regenerated table; tutorial 32 markdown;
  fit()/results docstrings; changelog.d fragment.
- Tests: JK1/BRR builders, acceptance (both lanes + combined_weights=
  False discriminator), per-cell cross-check vs the helper, df spy
  matrix (tightening reaches cells/overall/metadata; cap-vs-replace
  discriminator on a rank-deficient design; rank-one -> NaN), learner-
  parametrized scale invariance (linear/sieve pinned, ridge bounded),
  degenerate-cell fail-closed (explicit isnan), targeted rejections,
  aggregations (ES vcov None, total closed, inherited ES replace df
  pinned), PSU-fold non-interaction.
@github-actions

Copy link
Copy Markdown

Overall assessment: ✅ Looks good

Executive summary

  • No unmitigated P0/P1 findings.
  • DMLDiD panel and RCS replicate variance correctly reweights the augmented Chang influence scores.
  • Chang (2020) is i.i.d.-only; the survey extension and deviations are explicitly documented.
  • One P2 tutorial wording issue incorrectly generalizes PSU-cohesive folds to replicate designs.
  • Existing dropped-replicate df parity work is documented and tracked.

Methodology

  • Severity: P3 — Documented library extension.
    Impact: Chang (2020) does not cover survey weighting or clustering, but the Registry clearly disclaims paper-level coverage and documents fixed-denominator IF reweighting, non-refitted nuisances, df rules, and degeneracy behavior. The implementation matches that specification on both lanes (diff_diff/dml_did.py:L1344-L1433, diff_diff/dml_did.py:L1768-L1838, diff_diff/survey.py:L2430-L2465, docs/methodology/REGISTRY.md:L3029-L3101).
    Concrete fix: None required.

Code Quality

No findings.

Performance

No findings.

Maintainability

No findings.

Tech Debt

  • Severity: P3 — Tracked df-parity limitation.
    Impact: Event-study/group aggregation can replace rather than cap design df when replicates are dropped (diff_diff/staggered_aggregation.py:L1103-L1105, L1305-L1307). This is documented and tracked in TODO.md:L40, so it is informational under the review policy.
    Concrete fix: Follow the tracked work item and apply min(df_survey, effective_df) across the listed shared sites.

Security

No findings.

Documentation/Tests

  • Severity: P2 — Tutorial overgeneralizes PSU-cohesive folds.
    Impact: The RCS tutorial says any declared survey design makes folds PSU-cohesive, but replicate designs have no PSU and deliberately retain D×T-stratified folds, as the new test confirms. Users may misunderstand the replicate cross-fitting behavior (docs/tutorials/32_dml_did.ipynb:L1248-L1250, tests/test_survey_dml.py:L1225-L1228).
    Concrete fix: Qualify the statement as applying to full designs with a PSU, then state that replicate designs retain ordinary D×T-stratified folds and use rank-minus-one df.

The new tests otherwise cover both data lanes, weight interpretation, df handling, degeneracy, aggregation, and rejection paths. They could not be executed in this review environment because pytest and NumPy are unavailable.

The RCS tutorial's survey cell said any declared design makes
cross-fitting folds PSU-cohesive; replicate designs have no PSU (mutual
exclusion) and retain D x T-stratified folds with rank-1 df, as
test_replicate_fit_keeps_stratified_folds pins. Markdown-only edit;
code-cell hash pins untouched (drift test green). CI review P2.
@github-actions

Copy link
Copy Markdown

🔁 AI review rerun (requested by @igerber)

Head SHA: 679ba78f9f3fcc3fedc71db7329019f9e35dec56


Overall assessment: ✅ Looks good

Executive summary

  • No unmitigated P0/P1 findings.
  • Panel and RCS paths correctly reweight augmented Chang influence scores.
  • Degenerate variance and undefined-df inference fail closed consistently.
  • The prior tutorial wording issue is resolved.
  • Tests could not run because pytest/NumPy are unavailable; changed Python and notebook JSON parse successfully.

Methodology

  • Severity: P3 — Documented library extension.
    Impact: Chang (2020) assumes i.i.d. sampling; replicate-weight inference is not paper-derived. The Registry explicitly documents fixed-denominator IF reweighting, nuisance non-refitting, df rules, and degeneracy behavior. Both lanes match that specification (diff_diff/dml_did.py:L1380-L1433, L1799-L1838, L2263-L2289; docs/methodology/REGISTRY.md:L3029-L3101).
    Concrete fix: None required.

  • Severity: P3 — Documented diagonal-covariance fallback.
    Impact: Replicate event studies expose marginal SEs but no multivariate VCV, so HonestDiD uses its documented diagonal fallback (diff_diff/staggered_aggregation.py, event-study VCV construction; docs/methodology/REGISTRY.md:L5771).
    Concrete fix: None required.

Code Quality

No findings. Both per-cell paths use safe_inference() and preserve all-or-nothing NaN inference.

Performance

No findings. Reweighting retained influence scores avoids per-replicate nuisance refits as documented.

Maintainability

No findings. The implementation reuses the shared replicate-variance and aggregation machinery.

Tech Debt

  • Severity: P3 — Tracked df-parity work.
    Impact: Shared event-study/group aggregation retains the replace-style effective-df convention, which can exceed design df when replicates are dropped. This is explicitly tracked (TODO.md:L40; diff_diff/staggered_aggregation.py:L1103-L1105, L1305-L1307).
    Concrete fix: Follow the existing TODO row; no action required for this PR.

Security

No findings.

Documentation/Tests

No findings. The prior PSU-fold wording is now correctly limited to full PSU designs, with replicate designs described as retaining stratified folds (docs/tutorials/32_dml_did.ipynb:L1246-L1253). A compliant changelog fragment is present, and tests cover both lanes, weighting branches, df handling, degeneracy, aggregation, and rejection paths.

@igerber igerber added the ready-for-ci Triggers CI test workflows label Aug 30, 2026
@igerber
igerber merged commit 32db10b into main Aug 30, 2026
39 of 40 checks passed
@igerber
igerber deleted the dml-replicate-weights branch August 30, 2026 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-for-ci Triggers CI test workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant