Repository navigation
Conversation
…ve no new group levels
…mgcv setup and predict them from one linear-predictor matrix
This was referenced Oct 27, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR was assisted by Claude.
Summary
For a multilevel submodel,
divmin()fits each projected draw withlme4::lmer()(Gaussian) orlme4::glmer(), andinit_submodl()then predicts each draw's fit for the training data withpredict.merMod()andrepair_re(). Eachlmer()call builds the model frame, the fixed-effects model matrix and the random-effects structure (lme4::lFormula()), although only the response differs between the draws; and each prediction builds the fixed-effects matrix and the random-effects design again, twice. In a profile ofcv_varsel(validate_search = TRUE)for a Gaussian multilevel model, the predictions took about 60 % of the time andlFormula()about 15 %.This PR has three commits.
Predictions ($X \beta_s + Z b_s$ of all draws from one fixed-effects model matrix and one random-effects design ($Z$ by the names of the group levels, and checks the mapping against
subprd_mer_direct(), called first insubprd()). If all fits arelmerModorglmerModfits of the same submodel andnewdatahas no new group levels (and the global optionprojpred.mlvl_pred_newisFALSE),repair_re()would add exactly zero and draw no random numbers.subprd_mer_direct()then skips it and computes the predictionslme4::mkNewReTrms()), with the steps ofpredict.merMod(). It maps the conditional modes of each draw onto the rows ofmkNewReTrms()for the first draw. In all other cases, the existing code runs, with the same random numbers. The predictions are identical to master (largest absolute difference 0 in the checks), including the dimension names.Shared
lFormula()forlmer()submodels (lmer_shared_callback()indivmin(), Gaussian submodels with the identity link).lme4::lmer()islFormula()followed bymkLmerDevfun(),optimizeLmer(),checkConv()andmkMerMod(). This commit callslFormula()once per projection and repeats the other steps oflmer()for each draw.lmer_shared_fit()transcribes these steps from the body oflmer()in lme4 2.0-6 to 2.1-1 (lme4 is GPL-2 | GPL-3), with the response column replaced, fresh copies of the vectors that lme4's C++ objects modify in place (Lambdat@x,theta,Zt@x,X), and the samecalland warning texts aslmer(). The fits areall.equal()(withtolerance = 0) to those oflmer(), with identicaltheta. (Twolmer()fits of the same data are notidentical()either, because of lme4's reference-class objects.) Errors fall back to the draw-wiselmer()fit.Shared setup for
mgcv::gam()submodels (gam_shared_callback()indivmin(), additive submodels without group-level terms). This commit builds the setup ofgam()(model frame, smooth bases, penalties) once per projection withmgcv::gam(fit = FALSE)and fits each draw withmgcv::gam(G = ), the public mgcv interface for a prepared setup. It sets the elements that contain the response name andcallas in a fit byfit_gam_callback(), so the fits areall.equal()(withtolerance = 0) to the draw-wise fits.subprd()predictsgamfits of one submodel from one linear-predictor matrix (predict.gam(type = "lpmatrix")) for all draws; the predictions are identical to the per-drawpredict.gam(). The internal optionprojpred.gam_shared = FALSEswitches the shared setup off.In commits 2 and 3,
shared_setup_callback()builds the shared setup from the data of the first draw-wise fitter call indivmin(). #580 removes the observations with weight 0 indivmin()before the draw-wise fitter calls. A setup built before this removal would include these observations. If the data of a later call differ from the data of the first call,divmin()fits that draw with the draw-wise fitter.Commit 2 repeats steps of
lmer(), so it has three guards:lmer_shared_check()(0.2 s, a fixed problem without random numbers: two grouping factors, a random slope, observation weights, 3 draws) compareslmer_shared_fit()withlmer(). If they differ, for example after a change in lme4, projpred uses onelmer()call per draw as before. (Without the fresh copies, the check returnsFALSE.)tests/testthat/test_lmer_shared.Rruns on CRAN (0.75 s): the check itself and a comparison ofdivmin()with and without sharing. So a change inlmer()or in lme4's in-place behavior also makes projpred's checks fail, including the reverse-dependency checks of lme4.projpred.lmer_shared = FALSEswitches commit 2 off.The steps of
lmer()are the same in lme4 2.0-6, 2.1-0 and 2.1-1.Results
Predictions and fits are unchanged. In the bench below, the rankings, the suggested sizes and the CV ranking proportions are identical to master, and the largest relative difference in the ELPD estimates is 1.6e-16.
Bench: simulated data, n = 300, 6 predictors, grouping factors with 20 and 8 levels; rstanarm
stan_lmer()(random intercepts and a random slope),stan_glmer()(Bernoulli) andstan_gamm4()(smooth term and random intercept) reference models; forward search,nterms_max = 6;cvt10=cv_varsel(validate_search = TRUE, nloo = 10, nterms_max = 4, refit_prj = TRUE); 1 CPU per run, all versions in the same job on one node (CSC Roihu, AMD EPYC 9965), lme4 2.0-6. Times in seconds.The speedup of a commit compares it with the column before it; the last column compares with master.
stan_lmer()stan_lmer()stan_gamm4()stan_gamm4()stan_glmer()stan_glmer()For the additive models (bench
m1, same setup; commit 3 is measured against commits 1 + 2):stan_gamm4()without group termstan_gamm4()without group termstan_gamm4()with group term(The Bernoulli times are from a separate run with master and commit 1; commit 2 does not apply to
glmer().) After this PR, lme4's optimizer andmkMerMod()take most of the time of the Gaussian runs.Tests
The test suite passes with
NOT_CRAN=true: 264 tests, 55 912 expectations, 0 failures, 0 errors, the same 4 warnings as master. Tested with this change applied to master 73cf0ee (projpred 2.11.0.9000), R 4.6.1, lme4 2.0-6, rstanarm 2.32.2, brms 2.23.0, on CSC Roihu (Linux, AMD EPYC).New tests:
tests/testthat/test_mer_predict.R:subprd_mer_direct()andsubprd()give the predictions of the draw-wise path, including the dimension names, forlmerfits (random slope and two grouping factors, random intercepts only, no fixed-effects predictors, a special formula withpoly(),I()and a factor) andglmerfits, for the full data and a subset; the random number state is unchanged;NULLfor a new group level, withprojpred.mlvl_pred_new = TRUE, for other fits, withoutnewdataand for fits of different submodels.tests/testthat/test_lmer_shared.R(runs on CRAN):lmer_shared_check()isTRUE;divmin()gives fits that areall.equal(tolerance = 0)with identicalthetaand formulas with and without sharing (random slope, two grouping factors,poly(), a factor, weights), and also for data with one observation with weight 0;lmer_shared_callback()returnsNULLwith the option set toFALSEand for a missing response value.tests/testthat/test_gam_shared.R:divmin()givesgamfits that areall.equal(tolerance = 0)with identical formulas with and without sharing (two smooth terms, a factor, weights), and also for data with one observation with weight 0;shared_setup_callback()builds the setup one time, from the data of its first call, and fits a call with different data with the draw-wise fitter;subprd()gives the predictions ofpredict.gam()for the full data and a subset, andsubprd_gam_direct()returnsNULLfor fits of different submodels;gam_shared_callback()returnsNULLwith the option set toFALSEand for a missing response value.The new tests fail on master (the functions do not exist there).
Overlap with other PRs
divmin()inR/divergence_minimizers.R. The second PR to merge needs a rebase. The resolution keeps both sets of functions. With Leave the left-out observation out of the fold projections in validated LOO CV #580, the shared setups of commits 2 and 3 use the data after Leave the left-out observation out of the fold projections in validated LOO CV #580 removes the observations with weight 0. A trial merge with Leave the left-out observation out of the fold projections in validated LOO CV #580 passestest_lmer_shared.Randtest_gam_shared.R. These tests fail if the setups use the data before the removal.