Skip to content

Fix glmnet lambda handling: predict-time fallback and fit-time cold-start convergence failure - #41

Open
ielbadisy wants to merge 4 commits into
mainfrom
fix/glmnet-predict-lambda-fallback
Open

Fix glmnet lambda handling: predict-time fallback and fit-time cold-start convergence failure#41
ielbadisy wants to merge 4 commits into
mainfrom
fix/glmnet-predict-lambda-fallback

Conversation

@ielbadisy

@ielbadisy ielbadisy commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

Two related bugs in the glmnet learner, both surfaced by the same downstream failure: an Elastic Net learner in a real analysis pipeline (CLAVUS, VUS-reclassification prediction) produced a single constant predicted probability for every test row (AUROC exactly 0.5, sensitivity 0).

1. predict_xy lambda fallback. If spec$lambda wasn't threaded through to predict_xy, it fell back to state$state$lambda[1] -- the largest (most-regularized) value in glmnet's default path, not the tuned/intended one.

2. fit_xy cold-lambda convergence failure (the actual root cause on real data). fit_xy requested a single lambda directly via glmnet::glmnet(..., lambda = spec$lambda), cold-starting the coordinate-descent solver with no warm-started path. On a wide/high-cardinality design matrix (~35 raw predictors, ~150+ columns after one-hot encoding several categorical variables with many levels), this failed to converge for the CV-selected alpha=0.5, lambda=0.01 and glmnet silently returned an empty, all-zero-coefficient model (fit$lambda == Inf), which then predicted the same constant probability for every observation -- independent of the predict_xy fix in (1).

Fix (1) alone was not sufficient to resolve the real pipeline's degenerate result; fix (2) was required and is the one that actually restores correct behavior on that data.

Fix

  • fit_xy now always fits the full regularization path (never passes a bare lambda= to glmnet::glmnet()).
  • The lambda intended for prediction (spec$lambda, or the smallest/least-regularized path value if unset) is recorded at fit time and extracted via predict(..., s=) at predict time -- the supported, warm-started way to get predictions at an arbitrary path point.

Verification

  • Verified directly against the real dataset that triggered this: funcml::tune() + predict() now reaches AUROC 0.781 / AUPRC 0.681 through the standard funcml path, exactly matching an independent glmnet::cv.glmnet() reference fit (s = 0.01) on the identical training/test data.
  • tests/testthat/test-glmnet-predict-lambda.R: 3 tests -- non-degenerate predictions when spec$lambda is unset at fit time, correct lambda reuse when spec$lambda is set, and a direct structural check that fit_xy always produces a multi-point path (length(fit$state$state$lambda) > 1) rather than a single cold-fit lambda.
  • Confirmed the degenerate-prediction test fails against the pre-fix code (AUROC = 0.50 exactly) and passes after the fix.
  • tests/testthat/test-learner-audit-contract.R (glmnet fit/predict contract) and test-estimate*.R (which also exercise glmnet as a meta-learner) still pass.
  • Note: I did not attempt to reproduce the convergence failure in a small synthetic example -- several attempts with wide one-hot designs, extreme-scale numeric predictors, and rare categorical levels did not trigger it, so it appears specific to structure in the real data. The structural test above asserts the invariant that prevents the failure by construction instead.

Bumped to 0.7.3, NEWS.md updated with both fixes.

predict_xy fell back to state$state$lambda[1] when spec$lambda was not
passed at prediction time. glmnet orders its default regularization path
largest-to-smallest, so lambda[1] is the *most* regularized value, not
the tuned/intended one -- silently collapsing predictions toward an
intercept-only model whenever a learner was fit with the default full
path (lambda = NULL).

Discovered via a downstream analysis pipeline where an Elastic Net
learner produced a single constant predicted probability for every test
row (AUROC exactly 0.5, sensitivity 0), traced to this fallback.

Fix: record the lambda actually used at fit_xy time (either spec$lambda,
or the smallest/least-regularized value in the default path) and always
reuse that recorded value at predict_xy time, rather than re-deriving it
from spec or guessing from the fitted path.

Adds a regression test that fits without spec$lambda and asserts
predictions are non-degenerate; confirmed this test fails (AUROC = 0.50)
against the pre-fix code and passes after the fix.
The predict_xy fallback fixed in the previous commit was necessary but
not sufficient: fit_xy itself requested a single lambda directly in
glmnet::glmnet(..., lambda = spec$lambda), which cold-starts the
coordinate-descent solver without a warm-start path. On the real
downstream dataset that surfaced this bug (CLAVUS, ~35 raw predictors
expanding to ~150+ columns after one-hot encoding of several
high-cardinality categorical variables), this failed to converge for
alpha=0.5, lambda=0.01 and glmnet silently returned an empty, all-zero
model (fit$lambda == Inf), which predict_xy then dutifully used to
produce a constant probability for every row (AUROC exactly 0.5).

fit_xy now always fits the full regularization path (no lambda= passed
to glmnet::glmnet directly); the intended lambda is recorded from spec
and extracted via predict(..., s=) at predict_xy time, which is the
supported, warm-started way to get predictions at an arbitrary point on
the path. Verified against the real dataset that triggered this: the
previously degenerate Elastic Net learner now reaches AUROC 0.781 /
AUPRC 0.681 through funcml::tune() + predict(), matching a
glmnet::cv.glmnet() reference fit on the same data exactly.

Also strengthens the regression test to assert the fit_xy invariant
directly (fit$state$state$lambda has length > 1, i.e. a full path was
computed) rather than only checking predict_xy output, since the
convergence failure is data-dependent and did not reproduce reliably in
small synthetic examples.
@ielbadisy ielbadisy changed the title Fix glmnet predict_xy lambda fallback collapsing to null model Fix glmnet lambda handling: predict-time fallback and fit-time cold-start convergence failure Aug 3, 2026
ielbadisy and others added 2 commits August 3, 2026 22:53
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… DESCRIPTION

The hand-written CITATION was pinned to an old version and had no URL.
Dropping it lets R build the citation from DESCRIPTION automatically,
which stays in sync and includes the CRAN URL.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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