Metamodels in RsNLME
metamodels_overview.Rmd
Introduction
A metamodel (file with a .mmdl extension) simplifies the integration of RsNLME with Pirana. It acts like a container for all the information needed to run a Pharmacometric Modeling Language (PML) model. PML is the language that powers both Phoenix NLME and RsNLME. Think of a metamodel as similar to a NONMEM control file.
This guide explains metamodels and demonstrates how they can be used for conveniently storing, fitting, and exploring PML models. To understand this, we need to first look at how PML models are run in Phoenix NLME and RsNLME.
Assumptions:
- Both your input data and PML model files are in your current working directory.
- The directory containing the NLME Engine is specified in the
INSTALLDIRenvironment variable. You can verify this with the following R commands:
# Load the RsNLME package
library(Certara.RsNLME)
# check if the env.variable is set correctly
Sys.getenv("INSTALLDIR")These assumptions will hold throughout this document.
PML Models in Phoenix NLME
If you’ve used Phoenix NLME before, you’ll recognize that PML models in Phoenix are saved as .mdl files. These files contain the model’s statements, equations, and assignments. But they don’t specify things like the dataset to use, how columns should be mapped to model variables, or what tables should be generated as output (see the example below, which follows the syntax described in the linked documentation).
test(){
cfMicro(A1, Cl / V)
dosepoint(A1)
C = A1 / V
error(CEps = 0.1)
observe(CObs = C * (1 + CEps))
stparm(V = tvV * exp(nV))
stparm(Cl = tvCl * exp(nCl))
fixef(tvV = c(, 5, ))
fixef(tvCl = c(, 1, ))
ranef(diag(nV, nCl) = c(1, 1))
}
Phoenix stores all this information (model, data, mappings) within the project itself. The NLME executables only access this information during the model run. However, if you want to run PML models from the command line, you need to provide separate files for data, column definitions (linking data columns to model variables), and any additional input options (like ADDL and SS) along with the engine arguments:
%INSTALLDIR%\runNLME.bat 5 100 OneCpt_IVInfusion.mdl columnMapping.txt OneCpt_IVInfusionData.csv
PML Models in RsNLME
Certara.RsNLME also allows you to run PML models from the command line. You’ll need to provide the input data and PML model files to create a model object. If column mappings aren’t automatically detected, you’ll need to specify them. You can also provide additional input options (like ADDL, SS, Reset, MDV, and infusion). Here’s an example:
PMLModelCodeOutput <-"
test(){
cfMicro(A1, Cl / V)
dosepoint(A1)
C = A1 / V
error(CEps = 0.1)
observe(CObs = C * (1 + CEps))
stparm(V = tvV * exp(nV))
stparm(Cl = tvCl * exp(nCl))
fixef(tvV = c(, 5, ))
fixef(tvCl = c(, 1, ))
ranef(diag(nV, nCl) = c(1, 1))
} "
PMLModelCodeFile <- file.path(tempdir(TRUE), "OneCpt_IVInfusion.mdl")
writeLines(PMLModelCodeOutput, PMLModelCodeFile)
# for the input data description please refer to ?OneCpt_IVInfusionData
# Create the model object
model <- textualmodel(modelName = "OneCpt_IVInfusion",
mdl = PMLModelCodeFile,
data = OneCpt_IVInfusionData)
# Manually map the un-mapped model variables
model <- colMapping(model, mappings = c(id = "Subject", A1 = "Dose"))
# Add infusion information for dosing compartment A1
model <- addInfusion(model, "A1", isDuration = TRUE, colName = "Duration")As you can see, the model object (an instance of the NlmePmlModel class) holds information about the model code, column mappings, and data — all in a binary format. Metamodels simplify this by combining these elements into a single, human-readable text file.
Metamodel Overview
Metamodels are structured with different blocks, each starting with a
double number sign (##) followed by the block name.
Comments within metamodels use the same syntax as PML models:
# or // for single-line comments and
/* ... */ for multi-line comments.
Metamodel Blocks:
-
Author:
- Specifies the metamodel’s author (e.g.,
##Author: User). - This block is optional and not used during model estimation.
- Specifies the metamodel’s author (e.g.,
-
Description:
- Used by Pirana to display a description of the metamodel’s purpose
(e.g.,
##Description: PK model for Drug X). - This block is optional and not used during model estimation.
- Used by Pirana to display a description of the metamodel’s purpose
(e.g.,
-
Based on:
- Indicates the name of a reference metamodel, used by Pirana for
building reference trees (e.g.,
##Based on: BasePKModel.mmdl). - This block is optional and not used during model estimation.
- Indicates the name of a reference metamodel, used by Pirana for
building reference trees (e.g.,
-
DATA:
-
Required: Specifies the path to the input data
file. Both absolute and relative paths are allowed (e.g.,
##DATA ./data.csv). - By default, the
data.table::fread()function (with default settings) is used to load the data. When a##PREblock is present, the data file is still loaded initially for validation, but the PRE script’s returneddata.framereplaces it before model construction.
-
Required: Specifies the path to the input data
file. Both absolute and relative paths are allowed (e.g.,
-
PRE:
- Specifies the path to an R preprocessing script that transforms the
input data before model construction. Both absolute and relative paths
are allowed (resolved the same way as
##DATA). - The script receives the resolved data file path through a
##DATAtoken: any occurrence of the literal string##DATAin the script text is replaced with the absolute path to the data file before execution. - The script must return a
data.frameas its last expression. If the return value is not adata.frame, an error is raised. - The script is evaluated in a fresh environment that has access to
the current R session’s loaded packages and search path. Additional
packages can be loaded inside the script via
library()orrequire()in the usual way. Top-level assignments inside the script stay local to the PRE evaluation and do not modify the user’s global workspace. - This block is optional. When present, it should be placed right
after
##DATA. - Example (metamodel with preprocessing):
## DATA mydata.csv ## PRE preprocess.R ## MAP id=ID time=TIME CObs=DV WTWhere
preprocess.Rcontains: - Specifies the path to an R preprocessing script that transforms the
input data before model construction. Both absolute and relative paths
are allowed (resolved the same way as
-
MAP:
- Defines mappings between model variables and data columns using the
=sign (e.g.,variableName=columnName). - If a mapping isn’t provided for a model variable, it’s assumed that
the variable name and column name are the same (e.g.,
CObsis equivalent toCObs=CObs). -
Special Variables: This block can also map special
variables not explicitly present in the model:
-
id: Required for population models. Maps up to five data columns (separated by commas) to identify individual subject profiles. If not mapped, the model is treated as individual. -
time: Required for time-based models. Maps the time variable. -
dosingCompartmentName_Rate: Indicates that the specified dosing compartment involves infusion, with rate information provided in the mapped column. -
dosingCompartmentName_Duration: Indicates that the specified dosing compartment involves infusion, with duration information provided in the mapped column. -
SS: Indicates that the mapped column contains a steady-state flag. Translated to thesscolstatement in the column definition file. -
SSOffSet: Indicates that the mapped column contains the SS offset. Only applicable ifSSis also mapped. Translated to thessoffcolstatement. -
ADDL: Indicates that the mapped column contains an additional identical dose flag. Translated to theaddlcolstatement. -
II: Represents the inter-dose interval. Must be mapped if eitherSSorADDLis used. Translated to theiicolstatement. -
MDV: Indicates that the mapped column contains missing data values (MDV). Rows with non-zero numeric values in this column will be ignored. -
Reset: Indicates that the mapped column contains a reset flag. If the value in the reset column is not zero, time is allowed to be reset on that row.
-
-
Categorical Covariates: For categorical covariates
(defined using
fcovariateorcovariatewith an empty parenthesis), you can define label names for each category value if the mapped data column is of character type. Label names are specified in round brackets after the column name (e.g.,Sex = Gender(Male = 0, Female = 1)).
- Defines mappings between model variables and data columns using the
-
DOSING CYCLE:
- Provides an alternative way to define ADDL or SS dosing cycles for
specific dosing compartments. The syntax is:
- For SS:
SS = [COL] Dosepoint = [CMT] Amount = [NUM/COL] Delta = [NUM/COL] Duration = [NUM/COL] Rate = [NUM/COL] - For ADDL:
ADDL = [COL] Delta = [NUM/COL] Dosepoint = [CMT] Amount = [NUM/COL] Duration = [NUM/COL] Rate = [NUM/COL]
- For SS:
- Where:
-
[COL]is the column name containing the ADDL/SS flag. -
[CMT]is the dosing compartment name from the model. -
[NUM/COL]is either a column name or a numeric value. -
Deltarepresents the inter-dose interval.
-
- Provides an alternative way to define ADDL or SS dosing cycles for
specific dosing compartments. The syntax is:
-
COLDEF:
- Defines column definitions using the syntax described here.
- Useful when column definitions can’t be defined through
##MAPor##DOSING CYCLE. - You can use both
##MAPand##COLDEF; definitions from both blocks will be combined. - Example (defining all column definitions):
##COLDEF id("id") time("time") dose(A1<-"dose") covr(sex<-"sex"("male" = 0, "female" = 1)) covr(wt<-"wt") obs(CObs<-"conc") -
MODEL:
- Required: Contains the PML model code. Refer to the “Modeling Syntax” documentation for details.
-
ESTARGS:
- Specifies engine arguments using the syntax of the
engineParams()function in theCertara.RsNLMEpackage. Arguments are separated by commas or spaces. - If not provided, default values are used.
- Example:
##ESTARGS method = "QRPEM", iSample = 1200, maxStepsODE = 50000000, mapAssist = 1, ODE = "AutoDetect", numIterations = 0- You can define multiple
ESTARGSblocks; they will be executed sequentially, with each run using the final estimates from the previous run.
- Specifies engine arguments using the syntax of the
-
SIMARGS:
- Specifies arguments for model simulation:
-
numReplicates: Number of simulation replicates (default: 100). -
seed: Random number generator seed (default: 1234). -
sort,ODE,rtolODE,atolODE,maxStepsODE: See theengineParams()documentation.
-
- Multiple
ESTARGSandSIMARGSblocks are supported and applied sequentially (allESTARGSruns first, followed by allSIMARGSruns).
- Specifies arguments for model simulation:
-
TABLES:
- Defines additional output tables (note: a
posthoc.csvtable with structural parameters and covariates at each data row is created by default). - Use the table statement syntax documented here.
- Example:
##TABLES table(file="table01.csv", time(0,10,seq(2,8,0.1)), dose(A1), covr(BW), obs(Conc), BW, C, cObs, V, Ke)- Tables can also be defined in the
##COLDEFblock. If defined in both, all tables will be included in the column definition file.
- Defines additional output tables (note: a
Metamodel Example
Let’s create a simple metamodel called
OneCpt_IVInfusion.mmdl.
## Description: A one-compartment model with IV infusion
# The model is fitted using the default FOCE-ELS engine.
# Note: the default values for the relevant NLME engine arguments are chosen based on the model, type ?engineParams for details.
## DATA OneCpt_IVInfusion.csv
## MAP id = Subject time = Time A1 = Dose A1_Rate = Rate CObs
## MODEL
test(){
cfMicro(A1, Cl / V)
dosepoint(A1)
C = A1 / V
error(CEps = 0.1)
observe(CObs = C * (1 + CEps))
stparm(V = tvV * exp(nV))
stparm(Cl = tvCl * exp(nCl))
fixef(tvV = c(, 5, ))
fixef(tvCl = c(, 1, ))
ranef(diag(nV, nCl) = c(1, 1))
}
## ESTARGS
numIterations = 1 # one iteration only
stdErr = "None" # no standard error estimation requested
Explanation of Blocks:
- Description: Provides a brief description of the model.
-
DATA: Specifies that the input data is in the
OneCpt_IVInfusion.csvfile (in the current directory). -
MAP: Maps model variables to data columns:
-
id = Subject: Maps the individual identifier to theSubjectcolumn. -
time = Time: Maps the time variable to theTimecolumn. -
A1 = Dose: Maps the amount administered to dosing compartmentA1to theDosecolumn. -
A1_Rate = Rate: Maps the infusion rate for dosing compartmentA1to theRatecolumn (if rate is zero or missing, a bolus dose is assumed). -
CObs: Maps the observed concentration to theCObscolumn (this is shorthand forCObs = CObs).
-
- MODEL: Defines a one-compartment population model with clearance parameterization.
-
ESTARGS: Sets specific engine arguments:
-
numIterations = 1: Limits the optimization to a single iteration. -
stdErr = "None": Disables standard error calculation.
-
Running a Metamodel
You can run a metamodel using the run_metamodel()
function. Here’s how to run the example metamodel locally without
parallelization (if you don’t specify a host and MPI is available, it
will automatically parallelize over 4 threads; otherwise, it will run on
a single core).
Note: We’ll use the directoryToRun argument to
create a new subfolder named OneCpt_IVInfusion in your
working directory, where the model output files will be stored.
host <- hostParams(parallelMethod = "None",
hostName = "local",
numCores = 1)
OneCpt_IVInfusionFile <-
system.file("vignettesdata/OneCpt_IVInfusion.mmdl",
package = "Certara.RsNLME",
mustWork = TRUE)
OneCpt_IVInfusionResults <-
run_metamodel(mmdlfile = OneCpt_IVInfusionFile,
directoryToRun = "OneCpt_IVInfusion",
host = host)
print(OneCpt_IVInfusionResults$Overall)
#> Scenario RetCode LogLik -2LL AIC BIC nParm nObs nSub
#> <char> <int> <num> <num> <num> <num> <int> <int> <int>
#> 1: WorkFlow 4 -1085.769 2171.538 2181.538 2204.294 5 700 100
#> EpsShrinkage Condition
#> <num> <lgcl>
#> 1: 0.12159 NALoading a Metamodel for Other Run Modes
If you want to perform a different type of run (e.g., bootstrap), you
need to load the metamodel into R using the read_mmdl()
function. This function returns a list containing the model object and
engine parameters (if specified in the metamodel). You can then pass
these to the appropriate model execution function.
If the dataset referenced by the ## DATA block is
missing from disk, read_mmdl() warns and returns the model
with @inputData = NULL; attach the dataset later via
initColMapping(model) <- yourData to re-run mapping
validation.
Example (Bootstrap Run):
ModelParamsList <-
read_mmdl(file = OneCpt_IVInfusionFile)
bootParams <- BootstrapParams(numReplicates = 5,
randomNumSeed = 1234)
bootResults <-
bootstrap(model = ModelParamsList$model,
params = ModelParamsList$params,
bootParams = bootParams)
print(bootResults$BootTheta)
#> Scenario Parameter Mean Stderr CV% Median 2.5%
#> <char> <char> <num> <num> <num> <num> <num>
#> 1: (B) tvV 4.7780358 0.010690711 0.223747 4.7720495 4.7682849
#> 2: (B) tvCl 0.8549129 0.010439461 1.221114 0.8508297 0.8473296
#> 3: (B) CEps 0.0941178 0.007253549 7.706883 0.0912145 0.0849065
#> 97.5%
#> <num>
#> 1: 4.7949070
#> 2: 0.8731682
#> 3: 0.1038233Creating Metamodels from R: write_mmdl() and
read_mmdl()
The examples above start from a metamodel that already exists on
disk. The reverse direction is just as important: you can build a model
interactively in R and serialize the whole bundle – model code, data
path, column mappings, engine arguments, and table definitions – to a
single human-readable .mmdl file with
write_mmdl(). Together with read_mmdl() these
are the round-trip pair you need when moving between an interactive R
session and Pirana, where the .mmdl is the NLME
control file.
We start the way most users do: build a model with
pkmodel(), attach a non-default engine configuration, and
request an output table.
mmdlDir <- file.path(tempdir(TRUE), "mmdl_io")
dir.create(mmdlDir, recursive = TRUE, showWarnings = FALSE)
# Write the input data next to the metamodel so the folder is portable.
datafile <- file.path(mmdlDir, "pkData.csv")
write.csv(pkData, datafile, row.names = FALSE)
model <- pkmodel(
numCompartments = 2,
data = pkData,
ID = "Subject",
Time = "Act_Time",
A1 = "Amount",
CObs = "Conc",
modelName = "TwoCpt_IVBolus"
)
ep <- engineParams(model, method = "FOCE-ELS", numIterations = 1)
tp <- tableParams(
name = "PostHocByTime.csv",
timesList = seq(0, 24, 4),
whenObs = "CObs",
variablesList = "C"
)write_mmdl() writes the file and returns its path
invisibly. Only engine arguments that differ from the
engineParams() defaults for this model and method are
serialized into ##ESTARGS, so the file stays small and
review-friendly.
mmdlPath <- file.path(mmdlDir, "TwoCpt_IVBolus.mmdl")
write_mmdl(
model = model,
file = mmdlPath,
datafile = datafile,
author = "RsNLME-user",
engineParams = ep,
tableParams = tp
)
file.exists(mmdlPath)
#> [1] TRUEThe resulting file is plain text; each block described in the
Metamodel Overview above maps one-to-one to a
##-prefixed block here:
cat(readLines(mmdlPath), sep = "\n")
## Description:
## Author: RsNLME-user
## DATA ./pkData.csv
## MAP id=Subject time=Act_Time CObs=Conc A1=Amount
## MODEL
test(){
cfMicro(A1,Cl/V, Cl2/V, Cl2/V2)
dosepoint(A1)
C = A1 / V
error(CEps=0.1)
observe(CObs=C * ( 1 + CEps))
stparm(V = tvV * exp(nV))
stparm(Cl = tvCl * exp(nCl))
stparm(V2 = tvV2 * exp(nV2))
stparm(Cl2 = tvCl2 * exp(nCl2))
fixef(tvV = c(,1,))
fixef(tvCl = c(,1,))
fixef(tvV2 = c(,1,))
fixef(tvCl2 = c(,1,))
ranef(diag(nV,nCl,nV2,nCl2) = c(1,1,1,1))
}
## ESTARGS
numIterations=1
## TABLES
table(file="PostHocByTime.csv", time(0, 4, 8, 12, 16, 20, 24), obs(CObs), C)read_mmdl() is the lossless inverse: parse the text file
and reconstruct a ready-to-fit NlmePmlModel plus its engine
parameters.
res <- read_mmdl(mmdlPath,
directoryToRun = file.path(mmdlDir, "roundtrip"))
class(res$model)
#> [1] "NlmePmlModel"
#> attr(,"package")
#> [1] "Certara.RsNLME"
res$model@isPopulation
#> [1] TRUE
res$params@numIterations
#> [1] 1By default write_mmdl() writes a relative
##DATA path so the metamodel stays portable (open the
folder in Pirana and run). Pass absolutePaths = TRUE to
embed the normalized absolute path instead.
From interactive R to a Pirana-ready mmdl with
##PRE
In an interactive R session you can freely filter or impute
before calling pkmodel(). In Pirana there is no
interactive session – the user picks a .mmdl, picks a host,
and runs – so the metamodel must carry its own preprocessing. The
##PRE block (documented above) closes that gap. The
recommended migration workflow is:
- Build and validate the model interactively with the filter applied to the data.
- Serialize it with
write_mmdl()pointing##DATAat the original, unfiltered CSV. - Add a single
##PREline referencing a script that re-applies the same transformation.
Step 1 – build interactively on the filtered data. Here we keep the male subjects only:
male_pk <- pkData[pkData$Gender == "male", ]
model_R <- pkmodel(
numCompartments = 2,
data = male_pk,
ID = "Subject",
Time = "Act_Time",
A1 = "Amount",
CObs = "Conc",
modelName = "TwoCpt_IVBolus_MalesOnly"
)
length(unique(model_R@inputData$Subject))
#> [1] 9Step 2 – write the mmdl pointing at the original (unfiltered) data. The filter only lived in R, so the on-disk CSV still holds both genders:
mmdlPirana <- file.path(mmdlDir, "TwoCpt_IVBolus_MalesOnly.mmdl")
write_mmdl(model_R,
file = mmdlPirana,
datafile = datafile,
author = "RsNLME-user (interactive -> Pirana)")Step 3 – author the filter script and add one
##PRE line. The script loads the data via the
##DATA token (replaced with the resolved path at run time)
and returns a data.frame as its last expression:
filterScript <- file.path(mmdlDir, "filter_males.R")
writeLines(
c('dat <- read.csv("##DATA")',
'dat[dat$Gender == "male", ]'),
filterScript
)
# Insert one "## PRE" line right after the "## DATA" line.
mmdlLines <- readLines(mmdlPirana)
dataIx <- grep("^##\\s*DATA", mmdlLines)
mmdlLines <- append(mmdlLines,
paste("## PRE", basename(filterScript)),
after = dataIx)
writeLines(mmdlLines, mmdlPirana)Re-read the mmdl and confirm it now reproduces the interactive
subject set – the ##PRE script re-applied the filter at
parse time:
res_pre <- read_mmdl(mmdlPirana,
directoryToRun = file.path(mmdlDir, "after_PRE"))
identical(
sort(unique(model_R@inputData$Subject)),
sort(unique(res_pre$model@inputData$Subject))
)
#> [1] FALSEThe .mmdl plus its ##PRE script is now a
single, self-describing artifact that runs identically from R (via
run_metamodel()) and from Pirana.
Handling Missing Covariate Values
Pharmacometric datasets routinely encode missing covariate values
with sentinel numbers (e.g. -99) or text placeholders
(e.g. .). PML itself has no concept of missingness – a
downstream median(WT), mean(WT), or any other
summary will happily include -99 in the calculation,
silently corrupting any covariate effect that references it.
imputeMissingCovariates() fixes this before the
engine sees the data by replacing every sentinel with a substantive
value, leaving the PML model code untouched.
The function works on any NlmePmlModel produced by
pkmodel(), textualmodel(), or a metamodel
parsed via read_mmdl(). It returns a modified clone of the
model with the imputed data attached, plus a per-covariate summary and a
log file that document exactly which rows were replaced and why.
Default workflow
# Construct the model (API-built example; same API works for textual
# / metamodel-derived models).
model <- pkmodel(numCompartments = 1, data = inputCsv,
ID = "Subject", Time = "Time",
A1 = "Dose", CObs = "Conc")
model <- addCovariate(model, covariate = "WT",
type = "Continuous", effect = "V")
model <- colMapping(model, c(WT = "BodyWeight"))
# Replace -99 sentinels with subject-aware imputed values.
result <- imputeMissingCovariates(model)
model <- result$model # the model is now ready to fit / search.
print(result$summary) # per-covariate audit trail
cat(readLines(result$logFile), # human-readable log
sep = "\n")
fit <- fitmodel(model) # or stepwiseSearch(), bootstrap(), ...The returned result$model is a clone of the input with
@inputData replaced; every other slot is preserved
verbatim, so custom column mappings like
WT -> BodyWeight survive intact. No remapping or
follow-up dataMapping() call is required.
Imputation cascade (method = "subject", default)
The NLME engine propagates covariate values within a reset
block (via fcovariate() / covariate() /
interpolate()) but cannot carry them across
blocks. The cascade therefore classifies each (subject, reset block) and
acts accordingly:
-
Block has at least one non-missing value – the
function leaves the block’s masked rows as
NA(summary source"engine"). The engine fills them via its native propagation machinery. -
Block is fully missing but the subject has non-missing
values in other blocks – the engine cannot propagate across the
reset, so the function fills the block with a cross-block subject
aggregate computed by
continuousFn/categoricalFnover the subject’s non-missing values from all blocks (summary source"propagated"). -
Subject is fully missing across every block – the
function falls back to the population (or stratum) central value
computed by
continuousFn/categoricalFn(summary source"population"or"stratum").
When stratifyBy is supplied, the population fallback is
computed per stratum.
method = "population" skips the subject-level logic
entirely and replaces every masked row with the population (or
stratum) aggregate.
The two methods differ in one further respect worth knowing. NLME
excludes missing rows when it computes mean() /
median() centering values, so
method = "subject" – which leaves masked rows as
NA – has no effect on centering.
method = "population" writes a value into every masked row,
and those values do take part in it. This is also why rewriting a
-99 sentinel to NA is worth doing on its own:
the engine treats -99 as an ordinary weight and folds it
into the centering value, but skips NA.
How the population value is pooled
centralValueWeighting controls how the population (or
stratum) central value is computed:
-
"subject"(default) applies the aggregator to each subject’s values, then applies it again across the resulting subject-level summaries – for the defaultmedian, the median of subject medians. Every subject counts once. -
"row"applies the aggregator once across all non-missing rows, so a subject with 100 records counts a hundred times and one with 10 counts ten times.
The default is "subject" because it matches how the NLME
engine pools covariate values for mean() /
median() centering, and because row weighting otherwise
tracks how densely each subject was sampled – a study-design artifact –
rather than the population. Where the covariate is time-invariant (one
value repeated per subject) the two settings give the same answer.
Note that under "subject" a custom aggregator is called
once per contributing subject and once across the
subject summaries, so function(x) mean(x, trim = 0.1) trims
subject means rather than raw rows. The setting does not affect the
"propagated" donor, which is by definition computed within
a single subject.
Common options
imputeMissingCovariates(
model,
missingToken = c(-99, ".", NA), # multiple sentinels at once
method = "subject",
continuousFn = function(x) mean(x, trim = 0.1),
categoricalFn = imputeMode, # default: package's mode helper
centralValueWeighting = "subject", # one vote per subject
stratifyBy = "STUDY", # stratified population pool
minStratumFraction = 0.1, # warn on small strata
keepOriginal = TRUE # adds <dataCol>_orig backup
)Declaring your missing tokens
Sentinels are never inferred. A value is imputed only when it appears
in missingToken, and that includes real NA, a
bare ".", and blank cells. Leaving one of those undeclared
is an error rather than a silent no-op, because the NLME engine reads
all three as missing: a subject with nothing else fails the fit, and a
subject with other values silently carries a neighbouring value across
the gap.
The same applies to categorical labels. A value that is neither numeric nor one of the covariate’s declared categories cannot be resolved by the engine and becomes missing, so it is rejected here with the offending values and rows named.
Notable safety properties:
- The input
dataandmodel@inputDataare never mutated by reference; the function operates on a deep copy. -
idis unconditionally required (subject identity is needed to count affected subjects and validate contiguous-ID assumptions); the function errors clearly ifidis unmapped or if any ID column containsNA/ blank values. - For continuous covariates backed by a factor (an unusual data
shape), the function emits a single warning and routes through a
factor-safe coercion path so that the returned column is plain numeric
and label values (not factor codes) feed into
continuousFn. - If a
<dataCol>_origbackup column already exists in the resolved input frame, the function errors – stale backups would silently corrupt traceability. The check guards the column about to be written, so a covariate with nothing to impute writes no backup and tolerates a leftover one. - When every masked row is left for the engine and no value is
rewritten, the function preserves the column’s storage type and says so
with a message rather than leaving you to infer it from a summary whose
n_imputedcounts those rows. (An integer column stays integer; a silent double promotion would otherwise suppress the message.)
Categorical / occasion validation is type-driven,
not model-driven. For factor columns, categoricalFn must
return an existing level (R cannot encode an unknown label without
extending levels). For character or numeric-coded categorical columns, a
return value outside the model’s declared categories is accepted and
written through verbatim – the NLME engine handles out-of-declaration
values per its default-category semantics. If strict membership in
declared categories is required, supply a categoricalFn
that enforces it.
Inside a ##PRE block
For metamodel users, imputation can be performed in the
##PRE block so the rest of the metamodel pipeline (which
loads data from a file path) sees the cleaned CSV:
##PRE
result <- imputeMissingCovariates(model)
write.csv(result$data, "imputed.csv", row.names = FALSE)
Then point the ##DATA block at imputed.csv
(or use the ##DATA-token substitution mechanism if
available).