Skip to content

Fix/refuse rank deficient fit - #76

Merged
correspondMerchant merged 2 commits into
mainfrom
fix/refuse-rank-deficient-fit
Aug 10, 2026
Merged

Fix/refuse rank deficient fit#76
correspondMerchant merged 2 commits into
mainfrom
fix/refuse-rank-deficient-fit

Conversation

@correspondMerchant

Copy link
Copy Markdown
Owner

Refuse a rank-deficient outcome model at the fit

A rank-deficient design makes glm() return NA for a coefficient — two
predictors carrying the same information, or a saturated fit. Those NAs flowed
into the optimizer and every predicted outcome became NA, so the run failed
downstream after trying every intervention. The package learned to name the
aliased terms in that downstream failure (#73); it now refuses the fit up front,
in outcome_model_fitting() right after the convergence check, before any
intervention is tried.

Scoped to what the optimizer reads. The refusal fires only when an aliased
coefficient is one the optimization uses: intercept, intervention components and
their interaction terms, fixed centre and time effects, centre characteristics.
An aliased additional covariate does not refuse — the optimizer never reads
it, so the recommendation equals the drop-that-covariate run — it warns instead.
The used-coefficient set is built with the same helpers rec_int_processor()
reads them with, so classification cannot drift and a factor covariate's
level-named coefficient or a covariate named like a fixed effect is handled the
same in both places.

The message is the existing rank_deficient_outcome_message(), reworded from
past-tense ("could be estimated at any of the interventions tried") to present
("can be estimated from the outcome model") so it reads correctly both at the new
fit-site and the two downstream sites still reachable via the exported
get_confidence_set().

A rank-deficient design makes glm() return NA for one or more coefficients,
whether from two predictors carrying the same information or a saturated fit
with as many coefficients as rows. Those NAs then flowed into the optimizer,
every predicted outcome became NA, and the run failed downstream. The package
learned to name the aliased terms in that downstream failure, but only after
letting the NAs propagate through the whole optimization first.

The fit is now refused where it is made, in outcome_model_fitting(), right after
the convergence check and before the non-fatal diagnostics, so a model that
cannot be estimated is rejected before any intervention is tried rather than
after all of them fail. The condition is anyNA(coef(model)), the earliest point
the coefficients exist, and the one caller of outcome_model_fitting() is
lago_optimization(), so nothing else that wanted such a fit is affected.

The message is the existing rank_deficient_outcome_message(), which already
named the aliased terms and gave the remedy. Its opening was past tense and
described a downstream event, "No outcome could be estimated at any of the
interventions tried", which does not fit a refusal made before any intervention
is tried. It now opens "No outcome can be estimated from the outcome model", which
reads correctly both here and at the two downstream sites that still call it, and
neither is weakened: those guards remain for a direct caller of the exported
get_confidence_set(), which does not pass through this fit.

Nothing changes for a model that is estimable: across a sweep of valid
configurations every returned value is identical. The one integration test that
drove a rank-deficient fit through lago_optimization() now meets the refusal at
the fit, with the same message on both optimizer paths, so its assertions still
hold without editing.
…zer uses

The refusal added a commit ago fired on any NA coefficient, which is broader
than the harm it was guarding against. The optimizer reads the intercept, the
intervention components and their interaction terms, the fixed center and time
effects, and the center characteristics; it never reads the additional
covariates. So an aliased additional covariate has an NA coefficient the
optimization never touches, and the recommendation is exactly the one the fit
without that covariate produces. Refusing that run rejected an optimization that
was valid: on the released version the same call returns a recommendation, byte
for byte the drop-the-covariate result.

The check now refuses only when an aliased coefficient is one the optimizer
reads. The set of those names is built with the same helpers rec_int_processor()
uses to read them, so the two cannot drift and a factor covariate's level-named
coefficient or a covariate named like a fixed-effect term is classified the same
way in both places. An aliased intervention component or a saturated center fit
still refuses, since those coefficients are read.

An aliased additional covariate now warns rather than refuses: the recommendation
is unaffected, but a covariate glm() dropped contributes nothing to the outcome
and the caller may not have intended that. This matches the existing model-fit
diagnostics, which warn about a questionable fit and let the optimization
continue.

Valid runs are unchanged, including runs with additional covariates that are not
aliased.
@correspondMerchant
correspondMerchant merged commit ff51f5b into main Aug 10, 2026
1 check passed
@correspondMerchant
correspondMerchant deleted the fix/refuse-rank-deficient-fit branch August 10, 2026 14:55
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