Stepwise covariate modeling with nlmixr2scm
By Justin Wilkins in nlmixr2 nlmixr2scm
October 23, 2026
nlmixr2scm is on CRAN!
Happily, nlmixr2scm, which does stepwise covariate
modeling (SCM) for nlmixr2 models, is now on CRAN. It took a while -
seems they’re very busy now that everyone is churning out packages with
AI - but it’s finally there.
install.packages("nlmixr2scm")
If you’ve built covariate models in NONMEM, you’ve probably used scm
in Perl-speaks-NONMEM.
nlmixr2scm follows the same approach
(Jonsson & Karlsson, 1998), and
it works directly on an nlmixr2 fit. You don’t need to write control
streams or config files or any of that kind of thing, although the command
will probably be quite lengthy…
How SCM works
SCM searches in two phases, and both use likelihood-ratio tests (you probably know this, but a bit of underlining is always useful):
- Forward inclusion. Starting from the base model, it tests every candidate parameter-covariate relationship. The one that gives the largest significant drop in OFV is added, and the search repeats until no remaining candidate passes the forward threshold (p < 0.05 by default).
- Backward elimination. Starting from the final forward model, it removes each included relationship one at a time. Any relationship whose removal does not raise the OFV enough to pass a stricter threshold (p < 0.01 by default) is dropped.
runSCM() writes the covariate terms into the model body for you.
Continuous covariates are centered (at the median, or at a reference value
you choose), and categorical covariates are expanded into indicator columns.
It takes the original, untransformed data.
A quick example
We’ll start from a one-compartment warfarin PK model:
library(nlmixr2)
library(nlmixr2scm)
pkdata <- nlmixr2data::warfarin[nlmixr2data::warfarin$dvid == "cp", ]
warf_pk <- function() {
ini({
tka <- log(1.15)
tcl <- log(0.135)
tv <- log(7.0)
eta.ka ~ 0.40
eta.cl ~ 0.25
eta.v ~ 0.10
prop.err <- 0.10
})
model({
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
linCmt() ~ prop(prop.err)
})
}
fit_base <- nlmixr2(warf_pk, pkdata, est = "focei",
control = foceiControl(print = 0))
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====] 0:00:00
## done
Let’s test body weight (as a power or linear relationship) and sex on clearance and volume…
scm <- runSCM(
fit = fit_base,
data = pkdata, # needed: the base model doesn't use wt or sex
varsVec = c("cl", "v"), # parameters to test
covarsVec = "wt", # continuous covariates
catvarsVec = "sex", # categorical covariates
shapes = c("power", "lin"),
pVal = list(fwd = 0.05, bck = 0.01),
saveModels = FALSE, # quick exploratory run: nothing written to disk
print = 0,
confirm = FALSE
)
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## step covar var shape objf deltObjf AIC BIC
## cov_wt_power_v 1 wt_power v power 449.113 -25.97642 926.4202 954.6238
## numParams qchisqr pchisqr included searchType
## cov_wt_power_v 8 3.841459 3.456124e-07 yes forward
## covNames covarEffect bsvReduction
## cov_wt_power_v cov_wt_power_v 0.8814415 61.74603
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## step covar var shape objf deltObjf AIC BIC
## cov_wt_power_cl 2 wt_power cl power 442.6788 -6.434287 921.9859 953.715
## numParams qchisqr pchisqr included searchType
## cov_wt_power_cl 9 3.841459 0.01119381 yes forward
## covNames covarEffect bsvReduction
## cov_wt_power_cl cov_wt_power_cl 0.5844377 17.27476
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## step covar var shape objf deltObjf AIC BIC
## cov_wt_power_cl 1 wt_power cl power 448.2978 5.619047 925.605 953.8086
## numParams qchisqr pchisqr included searchType
## cov_wt_power_cl 8 6.634897 0.01776631 dropped backward
## covNames covarEffect bsvReduction
## cov_wt_power_cl cov_wt_power_cl 0.5844377 20.15977
## calculating covariance matrix
## [====|====|====|====|====|====|====|====|====|====done
## Direction Step Relation Ref OFV OFV dOFV p-value Decision
## -------------------------------------------------------------------------------------------
## Forward 1 wt_power~v [power] 475.089 449.113 -25.976 0.0000 Added
## Forward 2 wt_power~cl [power] 449.113 442.679 -6.434 0.0112 Added
## Forward 3 sex_female~v [cat] 442.679 440.495 -2.184 0.1395 Not selected
## Backward 1 wt_power~cl [power] 442.679 448.298 5.619 0.0178 Removed
## Backward 2 wt_power~v [power] 448.298 474.521 26.223 0.0000 Retained
## Direction Step Relation Ref OFV OFV dOFV p-value Decision
## -------------------------------------------------------------------------------------------
## Forward 1 wt_power~v [power] 475.089 449.113 -25.976 0.0000 Added
## Forward 1 wt_lin~v [lin] 475.089 450.010 -25.080 0.0000 Not selected
## Forward 1 sex_female~v [cat] 475.089 458.423 -16.666 0.0000 Not selected
## Forward 1 wt_power~cl [power] 475.089 470.203 -4.887 0.0271 Not selected
## Forward 1 wt_lin~cl [lin] 475.089 470.712 -4.378 0.0364 Not selected
## Forward 1 sex_female~cl [cat] 475.089 474.173 -0.917 0.3383 Not selected
##
## Forward 2 wt_power~cl [power] 449.113 442.679 -6.434 0.0112 Added
## Forward 2 wt_lin~cl [lin] 449.113 444.150 -4.963 0.0259 Not selected
## Forward 2 sex_female~v [cat] 449.113 448.361 -0.752 0.3858 Not selected
## Forward 2 sex_female~cl [cat] 449.113 448.984 -0.129 0.7190 Not selected
##
## Forward 3 sex_female~v [cat] 442.679 440.495 -2.184 0.1395 Not selected
## Forward 3 sex_female~cl [cat] 442.679 441.515 -1.164 0.2807 Not selected
##
## Backward 1 wt_power~v [power] 442.679 470.173 27.494 0.0000 Retained
## Backward 1 wt_power~cl [power] 442.679 448.298 5.619 0.0178 Removed
##
## Backward 2 wt_power~v [power] 448.298 474.521 26.223 0.0000 Retained
Printing the result gives a short overview:
scm
## ── nlmixr2scm result ───────────────────────────────────────────────────────────
## Search : scm (focei), 6 candidate relations
## OFV : base 475.089 -> final 448.298 (dOFV -26.792)
## Covariates : wt_power~v [power]
## Use summary() for options, model comparison and step tables.
For the full picture, use summary(). It lists the options the search ran
with, compares the base model with the final forward and final backward
models (OFV, ΔOFV, AIC, BIC, parameter count and covariates), prints the
step-by-step and all-candidate tables, and tells you where the models and
report files were saved on disk, if they were:
summary(scm)
## ── nlmixr2scm summary ──────────────────────────────────────────────────────────
## ── Options ─────────────────────────────────────────────────────────────────────
## Search type : scm
## p-value fwd / bck : 0.05 / 0.01
## Estimation : focei; control: foceiControl (inherited from fit)
## Candidates : 6: wt_power~cl [power], wt_lin~cl [lin], wt_power~v
## [power], wt_lin~v [lin], sex_female~cl [cat], sex_female~v
## [cat]
## Included relations : none
## Shapes : power, lin
## Centers : median of each continuous covariate
## Categorical cutoff : 0.05
## Profile warm-start : stalled candidates only (stallTol = 0)
## Retries : up to 3; maxDeltaOFV = Inf; OFV tolerance = 0; p-value
## underflow retried; on exhaustion keep best attempt
## Parallel : workers = sequential; rxThreads per worker = 16
## Save models : no
## ── Model comparison ────────────────────────────────────────────────────────────
## Model OFV dOFV AIC BIC Params
## Base 475.089 0.000 950.397 975.075 7
## Forward final 442.679 -32.411 921.986 953.715 9
## Backward final 448.298 -26.792 925.605 953.809 8
##
## SCM covariates:
## Base : (none)
## Forward final : wt_power~v [power], wt_power~cl [power]
## Backward final : wt_power~v [power]
## ── SCM Step Summary ────────────────────────────────────────────────────────────
## Direction Step Relation Ref OFV OFV dOFV p-value Decision
## -------------------------------------------------------------------------------------------
## Forward 1 wt_power~v [power] 475.089 449.113 -25.976 0.0000 Added
## Forward 2 wt_power~cl [power] 449.113 442.679 -6.434 0.0112 Added
## Forward 3 sex_female~v [cat] 442.679 440.495 -2.184 0.1395 Not selected
## Backward 1 wt_power~cl [power] 442.679 448.298 5.619 0.0178 Removed
## Backward 2 wt_power~v [power] 448.298 474.521 26.223 0.0000 Retained
## ── SCM All Candidates ──────────────────────────────────────────────────────────
## Direction Step Relation Ref OFV OFV dOFV p-value Decision
## -------------------------------------------------------------------------------------------
## Forward 1 wt_power~v [power] 475.089 449.113 -25.976 0.0000 Added
## Forward 1 wt_lin~v [lin] 475.089 450.010 -25.080 0.0000 Not selected
## Forward 1 sex_female~v [cat] 475.089 458.423 -16.666 0.0000 Not selected
## Forward 1 wt_power~cl [power] 475.089 470.203 -4.887 0.0271 Not selected
## Forward 1 wt_lin~cl [lin] 475.089 470.712 -4.378 0.0364 Not selected
## Forward 1 sex_female~cl [cat] 475.089 474.173 -0.917 0.3383 Not selected
##
## Forward 2 wt_power~cl [power] 449.113 442.679 -6.434 0.0112 Added
## Forward 2 wt_lin~cl [lin] 449.113 444.150 -4.963 0.0259 Not selected
## Forward 2 sex_female~v [cat] 449.113 448.361 -0.752 0.3858 Not selected
## Forward 2 sex_female~cl [cat] 449.113 448.984 -0.129 0.7190 Not selected
##
## Forward 3 sex_female~v [cat] 442.679 440.495 -2.184 0.1395 Not selected
## Forward 3 sex_female~cl [cat] 442.679 441.515 -1.164 0.2807 Not selected
##
## Backward 1 wt_power~v [power] 442.679 470.173 27.494 0.0000 Retained
## Backward 1 wt_power~cl [power] 442.679 448.298 5.619 0.0178 Removed
##
## Backward 2 wt_power~v [power] 448.298 474.521 26.223 0.0000 Retained
## ── Final model ─────────────────────────────────────────────────────────────────
## Retained:
## wt_power~v [power]
## ℹ Final model OFV: 448.298
## ────────────────────────────────────────────────────────────────────────────────
## ── Files ───────────────────────────────────────────────────────────────────────
## None: models and reports were not saved (saveModels = FALSE).
Underneath, the result is a list you can dig into:
summaryTablehas one row per candidate tested in both phases, with the OFV, ΔOFV, p-value, AIC/BIC, and whether the relationship was included.resFwd[[1]]andresBck[[1]]are the final forward and backward fits. These are ordinary nlmixr2 fit objects, so you can use your usual diagnostics and plots on them.
scm$resBck[[1]]$parFixedDf
## Estimate SE %RSE Back-transformed CI Lower
## tka -0.4775266 0.25661205 53.737750 0.6203158 0.3751321
## tcl -1.9991278 0.05323381 2.662852 0.1354534 0.1220330
## tv 2.1282396 0.03044324 1.430442 8.4000660 7.9135138
## prop.err 0.2233200 0.02903750 13.002646 0.2233200 0.1664075
## cov_wt_power_v 0.8686918 0.13432749 15.463193 0.8686918 0.6054147
## CI Upper BSV(CV%) Shrink(SD)%
## tka 1.0257497 71.587391 49.660122
## tcl 0.1503497 27.228417 3.203639
## tv 8.9165332 9.061567 51.577626
## prop.err 0.2802324 NA NA
## cov_wt_power_v 1.1319688 NA NA
If you want to control exactly which relationships are tested, pass
pairsVec instead of the full varsVec × covarsVec grid. You can also use
includedRelations to force known relationships (allometric weight on
clearance, for example) into the backward-elimination model.
Real-world searches
Real-world SCM runs often fit hundreds of candidate models, and nlmixr2scm has
several features for that:
Caching and resume. With
saveModels = TRUE(the default), every candidate fit and step table is saved tooutputDir. If a run is interrupted, callrunSCM()again with the sameoutputDirand it picks up where it stopped.restart = TRUEstarts from scratch instead.Parallel fitting.
workersfits the candidates within each step in parallel throughfuture.rxThreadssets how manyrxode2threads each worker uses.runSCM()checksworkers * rxThreadsagainst your physical core count before it starts, so that it doesn’t quietly murder your computer by maxing out its capacity.runSCM(fit_base, data = pkdata, varsVec = c("cl", "v"), covarsVec = "wt", workers = 4, rxThreads = 1)Protection against bad fits. Some candidate fits stall at the zero-effect initial estimate. This happens sometimes with ODE models.
runSCM()detects these stalled candidates and refits them from a starting value obtained with a quick 1-D profile. Candidates that land on an implausible OFV are retried from perturbed initial estimates, and the best attempt is kept. Both behaviours are switched on by default. By default, a p-value that underflows to zero also counts as implausible, which means very strong effects (an OFV drop of more than about 70 for one parameter) get retried too. If you trust those,retryOnUnderflow = FALSEsaves the extra fits.Fixed centering.
centers = c(wt = 70)centers weight at 70 kg instead of the per-dataset median. The coefficients then keep the same reference across datasets.
Learn more
Acknowledgements
nlmixr2scm was written together with Matthew Fidler and Yaping Liu and contains
contributions from Bill Denney, Vipul Mann, Vishal Sarsani, and Christian
Bartels. The design owes a great deal to the original PsN scm implementation by
Lars Lindbom, Niclas Jonsson, Pontus Pihlgren, Mats Karlsson, Andrew Hooker,
Kajsa Harling, Rikard Nordgren and Svetlana Freiberga. Thank you all!
- Posted on:
- October 23, 2026
- Length:
- 10 minute read, 1974 words
- Categories:
- nlmixr2 nlmixr2scm
- See Also: