Skip to content

Longitudinal IPCW sandwich: per-period censoring γ block for AIPW + IPW (and IPW propensity-weighting fix)#16

Merged
lorenzoFabbri merged 5 commits into
mainfrom
fix/longitudinal-ipcw-sandwich
Jun 26, 2026
Merged

Longitudinal IPCW sandwich: per-period censoring γ block for AIPW + IPW (and IPW propensity-weighting fix)#16
lorenzoFabbri merged 5 commits into
mainfrom
fix/longitudinal-ipcw-sandwich

Conversation

@lorenzoFabbri

Copy link
Copy Markdown
Contributor

Completes the longitudinal IPCW sandwich so the per-period censoring model's estimation uncertainty is propagated analytically for both longitudinal AIPW and longitudinal IPW.

What's here (3 commits)

  1. bb7fa24 — Longitudinal AIPW IPCW γ block (chunk 1). The ICE-AIPW stacked-EE sandwich gains a per-period censoring γ block in θ = (α, β, γ, μ); the IPCW weight is threaded into each outcome score so the numerical bread captures γ→β→μ. By double-robust orthogonality this cross-term is near-zero (~0.03% of the SE) — wired and carried for an exactly-correct sandwich.

  2. e84c103 — Longitudinal IPW IPCW γ→β correction + propensity-weighting fix (chunk 2).

    • Estimator fix: the IPCW weight no longer enters the propensity. The per-period treatment-density (and stabilization-numerator) models are now ordinary unweighted regressions on all observed rows; the censoring weight reweights the final-period Hájek MSM only — the standard separate-then-multiply IPW+IPCW construction (Hernán & Robins 2020, Ch. 12.6 & 17). Carried by a new propensity_weights argument threaded causat()fit_ipw()fit_longitudinal_ipw() (and through refit_ipw() for the bootstrap). Non-IPCW fits are byte-identical.
    • Sandwich: new compute_ipw_ipcw_correction_longitudinal() (R/variance_if_ipw_longitudinal_ipcw.R) propagates the censoring model's estimation uncertainty through the MSM (γ→β), reusing the shared make_ipcw_weight_fn_longitudinal() γ block and the per-period apply_model_correction() projection. Load-bearing for IPW (~5% of the treated-arm SE on an informatively-censored DGP), unlike the orthogonal AIPW case.
  3. b2aafd4 — Review hardening. Explicit nrow(X_fit) == n_final row-alignment guard (classed causatr_variance_row_mismatch), mirroring the point-IPW primitive's invariant.

Validation

  • delicatessen M-estimation oracle (tests/testthat/fixtures/python/longitudinal_ipw_ipcw_delicatessen.py): point to ~1e-13, per-arm/ATE SE to ~1e-4. Its companion known-weights (γ-fixed) sandwich pins the efficiency-gain magnitude two-sided.
  • Monte-Carlo SE-vs-empirical-SD calibration (ratio ~1.0), bootstrap parity, propensity-unweighted regression anchor (prior weights ≡ 1, coefs = unweighted GLM ~1e-8).
  • New test-longitudinal-ipw-ipcw.R (24 assertions); existing longitudinal IPW / IPCW / lmtp-oracle / multivariate / AIPW-IPCW / NHEFS tests unchanged.
  • Full suite (Tier-1) clean; R CMD check Status: OK.

Docs

NEWS.md, FEATURE_COVERAGE_MATRIX.md, CLAUDE.md, .claude/hard-rules.md, and variance-theory vignette §6.5 (differentiates the IPW load-bearing / AIPW orthogonal / ICE mildly-conservative cases) updated.

Follow-up (separate PR)

A package-wide IPCW/nuisance-weight audit: point IPW IPCW has a confirmed 3–7% sandwich bug (same IPCW-weighted-propensity root cause; delicatessen-verified) — it needs a point-IF re-architecture to standardize. Also point AIPW (orthogonal), ICE a0 conservatism, and SNM+IPCW (silently accepted) to validate/fix.

… AIPW IPCW sandwich

Under IPCW the longitudinal AIPW (ICE-AIPW) stacked-EE sandwich now includes the
per-period censoring model in its parameter vector: theta = (alpha, beta, gamma,
mu). Each period's logistic censoring score pins gamma, and the stabilized IPCW
weight is threaded into every per-step outcome score as `external x ipcw(gamma)`
at the step's fit rows, so the numerical bread captures the censoring-model
estimation cross-terms (gamma -> outcome beta -> marginal mean) mechanically --
no hand-derived cross-derivative, consistent with the existing stacked-EE
construction. A shared helper `make_ipcw_weight_fn_longitudinal()` (the
per-period generalisation of the point `make_ipcw_weight_fn()`) supplies the
censoring scores and the gamma->weight closure; it reproduces
`details$ipcw_weights` exactly at the fitted gamma (~1e-16).

The augmented (doubly-robust) estimator is first-order insensitive to the
censoring nuisance, so the cross-term is near-zero in practice (it moves the
marginal-mean SE by ~0.03%): the bread's (beta, gamma) block is non-zero (the
block is wired) but (mu, gamma) is zero. The block is carried for an exactly
correct sandwich, not to fix a sizeable bias -- the cross-package audit's ~1.03
SE ratio for longitudinal AIPW was Monte-Carlo noise, confirmed here.

Testing: a new test-aipw-longitudinal-ipcw.R adds the first coverage for the
longitudinal AIPW + IPCW sandwich (previously untested): the shared helper
reproduces the IPCW weights and its scores vanish; the stacked EE is faithful
with the gamma block; the (beta, gamma) cross-block is wired; a non-IPCW fit
carries no gamma block; the with-gamma vs known-gamma SE agree to 0.5%
(orthogonality); the per-arm sandwich SE tracks the Monte-Carlo empirical SD;
and it matches the bootstrap. Non-IPCW longitudinal AIPW tests are unchanged
(the gamma block is gated on `ipcw`). Chunk 1 of the longitudinal-IPCW-sandwich
plan; the delicatessen cross-language oracle is reserved for the longitudinal
IPW chunk, where the censoring cross-term is large (~15%) and load-bearing.
…opensity-weighting fix

Two changes to longitudinal IPW under built-in IPCW (estimator = "ipw",
type = "longitudinal", ipcw = TRUE).

Estimator fix -- the IPCW weight no longer enters the propensity. causat()
previously folded the censoring weights into the master weight vector that fed
the per-period treatment-density models, so the propensity was fit IPCW-weighted
on the uncensored rows. The standard IPW+IPCW construction estimates the
treatment and censoring models separately and multiplies their weights, applying
the censoring weight to the marginal structural model only (Hernan & Robins,
Causal Inference: What If, 2020, Ch. 12.6 & 17). The per-period treatment-density
and stabilization-numerator models are now ordinary regressions on all observed
rows; the IPCW factor is carried by a new `propensity_weights` argument threaded
causat() -> fit_ipw() -> fit_longitudinal_ipw() (and through refit_ipw() for the
bootstrap), so only the folded `weights` reach the MSM. This shifts the IPCW
longitudinal IPW point estimate onto the textbook estimator; non-IPCW fits are
byte-identical, and point IPW is unchanged (a separate pre-existing path).

Sandwich -- the censoring cross-term is now propagated. The id-level analytic
sandwich gains compute_ipw_ipcw_correction_longitudinal()
(R/variance_if_ipw_longitudinal_ipcw.R): it propagates the per-period censoring
model's estimation uncertainty through the MSM (the gamma -> beta cross-term),
reusing the shared make_ipcw_weight_fn_longitudinal() gamma block and the same
apply_model_correction() per-period projection the propensity correction uses.
The correction is subtracted, recovering the Robins-Rotnitzky-Zhao (1994)
censoring-estimation efficiency gain. Unlike the doubly-robust AIPW case (where
the cross-term is ~0.03% by orthogonality), the IPW cross-term is large and
load-bearing -- ~5% of the treated-arm SE on an informatively-censored DGP. With
the propensity now IPCW-unweighted, gamma reaches mu only through the MSM, so
there is no gamma -> alpha term.

Testing: new test-longitudinal-ipw-ipcw.R validates against a delicatessen
M-estimation oracle (longitudinal_ipw_ipcw_delicatessen.py) that stacks the
per-period propensity scores, the censoring gamma score, and the IPCW-weighted
Hajek marginal-mean equations -- point to ~1e-13, per-arm/ATE SE to ~1e-4. The
oracle's companion known-weights (gamma-fixed) sandwich pins the efficiency-gain
magnitude two-sided (full < known, ~5% on the treated arm). Also: Monte-Carlo
SE-vs-empirical-SD calibration (ratio ~1.0), bootstrap parity, and a
propensity-is-unweighted regression anchor (prior weights == 1, coefs ==
unweighted GLM to ~1e-8). Existing longitudinal IPW / IPCW / lmtp-oracle /
multivariate / AIPW-IPCW / NHEFS tests unchanged. Full suite (Tier-1) clean;
R CMD check Status: OK. Chunk 2 of the longitudinal-IPCW-sandwich plan.
…orrection

compute_ipw_ipcw_correction_longitudinal() builds the censoring cross-derivative
from per-row products of the MSM score ingredients (X_msm, r_msm, mu_eta_msm,
length nrow(model.matrix)) and the final-period fit-row vectors
(final_global_rows, other_w, length n_final). These coincide for the supported
`Y ~ 1` (and baseline-EM) MSM, where model.frame() drops nothing, but a future
MSM that dropped a final-period row (e.g. an NA effect modifier) would silently
misalign the two and corrupt the correction. Add the explicit
nrow(msm_prep$X_fit) == n_final guard (classed causatr_variance_row_mismatch),
mirroring the invariant the point-IPW primitive compute_ipw_if_self_contained_one()
already enforces, so the condition fails loudly instead of silently.

Defensive only -- no active bug (the supported path satisfies the invariant; the
24 test-longitudinal-ipw-ipcw.R assertions are unchanged). Surfaced by the
critical review of the longitudinal IPW IPCW chunk.

1st-round critical review (longitudinal IPW IPCW)
…red point IPW fix

While building the longitudinal IPCW sandwich (this PR) an audit found a single
root cause: causat() folds the IPCW weights into the master weight vector, which
feeds every nuisance fit including the treatment-density (propensity) models. The
textbook construction estimates the treatment and censoring models separately and
multiplies their weights (Hernan & Robins 2025 Ch 12.6 & 17), so the censoring
weight must reweight the outcome/MSM side only. Sampling/transport weights are
not folded, so they are clean.

Findings (delicatessen-verified): longitudinal IPW + IPCW was both
propensity-IPCW-weighted and missing the gamma->beta cross-term -- FIXED in this
PR. Longitudinal AIPW (orthogonal) and ICE (no propensity; orthogonal outcome
regression) are valid. Point IPW + IPCW has a CONFIRMED 3-7% per-arm sandwich
error (the IPCW-weighted propensity makes the sandwich omit the gamma->alpha
cross-term; the point estimate stays consistent). Point AIPW + IPCW is
non-standard but DR-orthogonal (SE acceptable). SNM + IPCW (18g) carries a
censoring block but was not oracle-validated here.

The point IPW fix is deferred to a focused implement-feature effort: standardize
the propensity to unweighted-on-all-rows (deeper than the IF -- make_weight_fn()
and the point contrast are row-coupled to the uncensored-fit propensity) via a
gated stacked-EE sandwich, then tighten the loose NHEFS IPCW test. Recorded in
PHASE_14_IPCW.md (Post-shipment audit), FEATURE_COVERAGE_MATRIX.md (known
limitation), CLAUDE.md, .claude/hard-rules.md (do-not-rediscover), and the
variance-theory vignette (point IPW + IPCW callout: use bootstrap meanwhile).
No code change -- documentation only.
The 2026-06-25 IPCW audit's deferred work (standardize the point IPW + AIPW
propensity to unweighted-on-all-rows, gated stacked-EE sandwich for point IPW +
IPCW, validate SNM + IPCW, tighten the loose NHEFS test) is now a first-class
design doc, PHASE_27_POINT_IPCW_PROPENSITY.md, rather than a subsection buried in
the completed Phase 14 -- so it stays findable and is a direct target for the
implement-feature workflow. PHASE_14's post-shipment audit now points to Phase 27
for the fix detail; the CLAUDE.md phase-status Pending list adds 27 and clarifies
that the earlier "point IPW/AIPW IPCW need no extra term" note held only for the
multinomial direct dmu/dgamma term -- the gamma->alpha propensity-weighting
cross-term was Monte-Carlo-masked and is a real 3-7% point IPW SE bug.

No code change -- documentation only.
@lorenzoFabbri lorenzoFabbri merged commit 7ffd223 into main Jun 26, 2026
3 checks passed
@lorenzoFabbri lorenzoFabbri deleted the fix/longitudinal-ipcw-sandwich branch June 26, 2026 07:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant