| Title: | Aggregate Data Modelling |
|---|---|
| Description: | Fit pharmacokinetic/pharmacodynamic (PK/PD) models to aggregate-level data (mean vector and covariance matrix per study) rather than individual-level data, for meta-analysis across studies. Integrates with the 'nlmixr2'/'rxode2' ecosystem via four estimation methods: a First-Order ('FO') analytical estimator, a Monte Carlo (MC) estimator, a Gauss-Hermite quadrature ('GH') estimator, and an Iterative Reweighting Monte Carlo ('IRMC') estimator. Methods are based on Välitalo (2021) <doi:10.1007/s10928-021-09760-1>; software described in van de Beek et al. (2025) <doi:10.1007/s10928-025-10011-w>. |
| Authors: | H. van de Beek [aut, cre], P.A.J. Välitalo [aut], L.B. Zwep [aut], J.G.C. van Hasselt [aut] |
| Maintainer: | H. van de Beek <[email protected]> |
| License: | GPL (>= 3) |
| Version: | 0.4.1 |
| Built: | 2026-09-22 10:18:10 UTC |
| Source: | https://github.com/leidenpharmacology/admixr2 |
Creates a control object for nlmixr2(est = "adfo"). The FO estimator
linearises model predictions at : it is faster than the MC
estimator but less accurate for models with large IIV or strongly
non-linear individual predictions.
adfoControl( studies = list(), grad = c("analytical", "none", "fd"), algorithm = NULL, maxeval = 500L, ftol_rel = .Machine$double.eps^(1/2), print = 10L, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad_h = 1e-04, grad_bounds = 5, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/5), covMethod = c("r,s", "r", "none"), n_restarts = 1L, restart_sd = 0.5, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, xtol_rel = .Machine$double.eps^(1/2), ... )adfoControl( studies = list(), grad = c("analytical", "none", "fd"), algorithm = NULL, maxeval = 500L, ftol_rel = .Machine$double.eps^(1/2), print = 10L, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad_h = 1e-04, grad_bounds = 5, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/5), covMethod = c("r,s", "r", "none"), n_restarts = 1L, restart_sd = 0.5, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, xtol_rel = .Machine$double.eps^(1/2), ... )
studies |
Named list of study specifications (same format as
|
grad |
Gradient mode. The default was |
algorithm |
nloptr algorithm, or |
maxeval |
Maximum function evaluations (default 500). |
ftol_rel |
Relative tolerance (default |
print |
Print-frequency for live progress (0 = silent). |
seed |
Random seed (used for restarts). |
cores |
OpenMP threads for |
nDisplayProgress |
Passed to |
grad_h |
Finite-difference step for unpaired struct theta gradient and FD Jacobian. |
grad_bounds |
Box-constraint half-width when using gradients: the fit is
confined to |
cov_h |
Inner FD step for the gradient-based Hessian (only used when
|
cov_h_outer |
Outer step scale for NLL-FD Hessian. |
covMethod |
It is the default because it is the conservative choice, not the aggressive
one. Under correct specification Applies to every residual family whose conditional law is independent across
timepoints, which is all of them except
All three blocks are reported on the scale the ESTIMATES are printed on, as
An adfo standard error describes scatter, not accuracy. FO linearises the
model at eta = 0, and on a non-additive residual (or a saturating endpoint,
or a large omega) the resulting point estimates carry a bias of several
standard errors – measured 5-20 SE, giving 0% coverage for a nominal 95%
interval even where the SE itself matches the sampling SD. Use |
n_restarts |
Number of optimizer restarts (1 = no multi-start). |
restart_sd |
Standard deviation for random perturbations of initial struct thetas at each restart (> 1). |
workers |
Number of parallel workers (mirai daemons) for multi-restart
(default 1 = sequential). Requires the |
rxControl |
|
calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix
|
Passed to |
sigdig |
Significant digits asked of the ODE solver, or It is a speed lever, and an opt-in one because it is not free. The
estimators finite-difference the solve with steps of the same order:
|
addProp |
How combined additive+proportional error is parameterised in
the nlmixr2 output tables: |
returnAdmr |
If |
resid_nodes |
Gauss-Hermite nodes used to integrate the RESIDUAL for a
transform-both-sides endpoint ( This is an ACCURACY dial, not a speed one. The quadrature is linear in
|
xtol_rel |
Relative parameter tolerance (default
|
... |
Unused arguments (trigger an error). |
An adfoControl object (a named list).
rxode2::rxSolve() estimates free RAM on every call. When the memuse
package is not installed its fallback ends up shelling out to vm_stat, a
macOS-only command, so on Windows and Linux every solve spawns a process that
can only fail. Because the FO estimator issues many small solves, this
overhead is measurable (roughly 17% of an FO gradient). Installing memuse
makes the fallback unreachable:
install.packages("memuse")
# Inspect defaults ctl <- adfoControl() ctl$grad ctl$maxeval # Analytical gradient, more evaluations ctl2 <- adfoControl(grad = "analytical", maxeval = 1000L) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adfo", control = adfoControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), maxeval = 100L ) ) print(fit)# Inspect defaults ctl <- adfoControl() ctl$grad ctl$maxeval # Analytical gradient, more evaluations ctl2 <- adfoControl(grad = "analytical", maxeval = 1000L) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adfo", control = adfoControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), maxeval = 100L ) ) print(fit)
Creates a control object for nlmixr2(est = "adgh"). The GH estimator
integrates model predictions against the random-effects prior
using a deterministic tensor-product
Gauss-Hermite quadrature grid. It is unbiased at any IIV magnitude (unlike
FO), noise-free (unlike MC), and much faster than MC for models with up to
~4 etas.
adghControl( studies = list(), n_nodes = 5L, grad = c("analytical", "fd", "none"), algorithm = NULL, maxeval = 500L, ftol_rel = .Machine$double.eps^(1/2), print = 10L, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad_h = 1e-04, grad_bounds = 5, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/4), covMethod = c("r,s", "r", "none"), n_restarts = 1L, restart_sd = 0.5, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, cov_nodes = 7L, cov_integration = c("on", "sparse", "off"), cov_sparse_level = 3L, xtol_rel = .Machine$double.eps^(1/2), ... )adghControl( studies = list(), n_nodes = 5L, grad = c("analytical", "fd", "none"), algorithm = NULL, maxeval = 500L, ftol_rel = .Machine$double.eps^(1/2), print = 10L, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad_h = 1e-04, grad_bounds = 5, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/4), covMethod = c("r,s", "r", "none"), n_restarts = 1L, restart_sd = 0.5, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, cov_nodes = 7L, cov_integration = c("on", "sparse", "off"), cov_sparse_level = 3L, xtol_rel = .Machine$double.eps^(1/2), ... )
studies |
Named list of study specifications (same format as
|
||||||||||||
n_nodes |
Number of quadrature nodes per eta dimension (default 5).
For a transform-both-sides endpoint ( |
||||||||||||
grad |
Gradient mode. |
||||||||||||
algorithm |
nloptr algorithm, or |
||||||||||||
maxeval |
Maximum function evaluations (default 500). |
||||||||||||
ftol_rel |
Relative tolerance (default |
||||||||||||
print |
Print-frequency for live progress (0 = silent). |
||||||||||||
seed |
Random seed (used for restarts). |
||||||||||||
cores |
OpenMP threads for |
||||||||||||
nDisplayProgress |
Passed to |
||||||||||||
grad_h |
Finite-difference step for unpaired struct theta gradient and FD Jacobian fallback. |
||||||||||||
grad_bounds |
Box-constraint half-width when using gradients: the fit is
confined to |
||||||||||||
cov_h |
Inner FD step for the gradient-based Hessian (only used when
|
||||||||||||
cov_h_outer |
Outer step scale for numerical Hessian. Default
|
||||||||||||
covMethod |
Applies to every residual family whose conditional law is independent across
timepoints, which is all of them except
All three blocks are reported on the scale the ESTIMATES are printed on, as
|
||||||||||||
n_restarts |
Number of optimizer restarts (1 = no multi-start). |
||||||||||||
restart_sd |
SD of random perturbations of initial struct thetas at each restart. |
||||||||||||
workers |
Number of parallel workers (mirai daemons) for multi-restart
(default 1 = sequential). Requires the |
||||||||||||
rxControl |
|
||||||||||||
calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix
|
Passed to |
||||||||||||
sigdig |
Significant digits asked of the ODE solver, or It is a speed lever, and an opt-in one because it is not free. The
estimators finite-difference the solve with steps of the same order:
|
||||||||||||
addProp |
How combined additive+proportional error is parameterised in
the nlmixr2 output tables: |
||||||||||||
returnAdmr |
If |
||||||||||||
resid_nodes |
Gauss-Hermite nodes used to integrate the RESIDUAL for a
transform-both-sides endpoint ( This is an ACCURACY dial, not a speed one. The quadrature is linear in
|
||||||||||||
cov_nodes |
Gauss-Hermite nodes per covariate used to integrate the
COVARIATE distribution when a study declares It is a nodes-per-DIRECTION budget rather than a literal node count.
Where the covariates reach the model through fewer scalars than there
are covariates, admixr2 integrates over those directions instead of
over a product grid, and each direction is given |
||||||||||||
cov_integration |
How a study's covariate distribution is integrated. Three states.
Both are Gauss–Hermite rules; they differ in which product terms are kept. Measured against an exact reference (lognormal margins, allometric plus a saturable term), relative error on the mean and the covariance:
At four covariates level 3 is both cheaper than the 3-node product grid
and roughly 40x more accurate, and the advantage grows with DEPENDENT covariates ( The cost of a sparse rule is SIGNED weights: they sum to 1 exactly, but the sum of their absolute values is 2.5 at level 3 for three covariates and 4.1 for four, so the answer is a difference of terms several times its own size and solver noise is amplified accordingly. A sandwich covariance whose weight matrix comes out indefinite as a result is refused rather than reported. |
||||||||||||
cov_sparse_level |
Smolyak level for |
||||||||||||
xtol_rel |
Relative parameter tolerance (default
|
||||||||||||
... |
Unused arguments (trigger an error). |
An adghControl object (a named list).
admControl(), adfoControl(), adirmcControl()
ctl <- adghControl() ctl$n_nodes ctl$grad # More nodes for large IIV, analytical gradient ctl2 <- adghControl(n_nodes = 7L, grad = "analytical", maxeval = 300L) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adgh", control = adghControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))) ) )ctl <- adghControl() ctl$n_nodes ctl$grad # More nodes for large IIV, analytical gradient ctl2 <- adghControl(n_nodes = 7L, grad = "analytical", maxeval = 300L) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adgh", control = adghControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))) ) )
Constructs a control object for est = "adirmc", the Iterative Reweighting
Monte Carlo estimator.
adirmcControl( studies = list(), n_sim = 2500L, outer_iter = 50L, sampling = c("sobol", "halton", "torus", "lhs", "rnorm"), algorithm = NULL, maxeval = 5000L, ftol_rel = .Machine$double.eps, print = 1L, omega_expansion = 1, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad = c("analytical", "none", "fd"), kappa_method = c("exact", "linearized", "linearized_gh"), kappa_n_nodes = 5L, grad_h = 1e-06, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/5), phases = c(2, 1, 0.5, 0.01), convcrit = 1e-05, max_worse = 5L, covMethod = c("r,s", "r", "none"), cov_n_sim = 10000L, n_restarts = 1L, restart_sd = 0.2, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, xtol_rel = .Machine$double.eps^(1/2), ... )adirmcControl( studies = list(), n_sim = 2500L, outer_iter = 50L, sampling = c("sobol", "halton", "torus", "lhs", "rnorm"), algorithm = NULL, maxeval = 5000L, ftol_rel = .Machine$double.eps, print = 1L, omega_expansion = 1, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad = c("analytical", "none", "fd"), kappa_method = c("exact", "linearized", "linearized_gh"), kappa_n_nodes = 5L, grad_h = 1e-06, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/5), phases = c(2, 1, 0.5, 0.01), convcrit = 1e-05, max_worse = 5L, covMethod = c("r,s", "r", "none"), cov_n_sim = 10000L, n_restarts = 1L, restart_sd = 0.2, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, xtol_rel = .Machine$double.eps^(1/2), ... )
studies |
Named list of study specifications. Each element is a list with:
Multi-compartment (multiple observed outputs). To fit several observed
compartments simultaneously (e.g. plasma and brain/CSF), give the study an
Long format (one row per endpoint/time). As an alternative to the
# independent blocks: per-row variances; optional per-endpoint `n` column
# and per-endpoint `ev` (a list of event tables keyed by endpoint)
list(n = 60L, ev = ev,
data = data.frame(DVID = c("cp", "cp", "cCSF"), TIME = c(1, 2, 2),
E = c(9.1, 7.4, 2.2), V = c(1.2, 0.9, 0.1)))
# joint (same subjects): ONE stacked covariance whose rows/cols align with
# the rows of `data` -- no `cross` blocks to assemble by hand
list(n = 60L, ev = ev, data = data.frame(DVID = ..., TIME = ..., E = ...),
V = V_joint)
A study-level |
n_sim |
Number of Monte Carlo samples per NLL evaluation. |
outer_iter |
Maximum inner optimiser iterations per phase. |
sampling |
Sampling method for eta draws: |
algorithm |
nloptr algorithm string, or |
maxeval |
Maximum number of optimizer function evaluations. |
ftol_rel |
Relative function-value tolerance for convergence. |
print |
Print progress every this many evaluations (0 = silent). |
omega_expansion |
Inflate proposal Omega by this factor (>= 1). |
seed |
Random seed for reproducibility. |
cores |
Number of OpenMP threads for |
nDisplayProgress |
Passed to |
grad |
Gradient mode for the inner optimiser: |
kappa_method |
Kappa correction method for models with non-mu-referenced
struct thetas: |
kappa_n_nodes |
Number of GH nodes per eta dimension for
|
grad_h |
Step size for the inner optimiser's finite-difference gradient
( |
cov_h |
Inner FD step for the gradient-based Hessian (only used when
|
cov_h_outer |
Outer step scale for the numerical Hessian. The actual step
for parameter |
phases |
Numeric vector of box-constraint half-widths, one per phase. Phases progressively tighten the search region. |
convcrit |
Convergence criterion: phase ends when |
max_worse |
Stop a phase after this many consecutive worsening iterations. |
covMethod |
|
cov_n_sim |
Number of MC samples for the covariance (Hessian) step.
More samples reduce MC noise in NLL evaluations. The NLL-based Hessian
( |
n_restarts |
Number of optimization restarts. Runs in parallel when
|
restart_sd |
Standard deviation of structural theta perturbations for restart initialisation. |
workers |
Number of parallel workers for multi-restart. |
rxControl |
|
calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix
|
Passed to |
sigdig |
Significant digits asked of the ODE solver, or It is a speed lever, and an opt-in one because it is not free. The
estimators finite-difference the solve with steps of the same order:
|
addProp |
How combined additive+proportional error is parameterised in
the nlmixr2 output tables: |
returnAdmr |
If |
resid_nodes |
Gauss-Hermite nodes used to integrate the RESIDUAL for a
transform-both-sides endpoint ( This is an ACCURACY dial, not a speed one. The quadrature is linear in
|
xtol_rel |
Relative parameter tolerance for convergence (default
|
... |
Additional arguments (none allowed; triggers an error). |
Multi-compartment fits (a study observations list with several observed
outputs) are not supported by adirmc; use est = "admc", "adfo", or
"adgh" for those. Single-output studies are fit as usual.
An object of class adirmcControl.
# Inspect defaults ctl <- adirmcControl() ctl$phases ctl$omega_expansion # Tighter phases, more restarts ctl2 <- adirmcControl( n_sim = 1000L, omega_expansion = 1.5, phases = c(2, 1, 0.5, 0.01), n_restarts = 3L ) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- diag(diag(cov.wt(dv_mat, method = "ML")$cov)) pk_model <- function() { ini({ tcl <- log(5); tv1 <- log(12); tv2 <- log(25) tq <- log(12); tka <- log(1.2) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v1 ~ 0.09; eta.v2 ~ 0.09 eta.q ~ 0.09; eta.ka ~ 0.09 }) model({ cl <- exp(tcl + eta.cl); v1 <- exp(tv1 + eta.v1) v2 <- exp(tv2 + eta.v2); q <- exp(tq + eta.q) ka <- exp(tka + eta.ka) d/dt(depot) <- -ka * depot d/dt(central) <- ka * depot - (cl/v1 + q/v1) * central + (q/v2) * peripheral d/dt(peripheral) <- (q/v1) * central - (q/v2) * peripheral cp <- central / v1 cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adirmc", control = adirmcControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), n_sim = 500L ) ) print(fit)# Inspect defaults ctl <- adirmcControl() ctl$phases ctl$omega_expansion # Tighter phases, more restarts ctl2 <- adirmcControl( n_sim = 1000L, omega_expansion = 1.5, phases = c(2, 1, 0.5, 0.01), n_restarts = 3L ) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- diag(diag(cov.wt(dv_mat, method = "ML")$cov)) pk_model <- function() { ini({ tcl <- log(5); tv1 <- log(12); tv2 <- log(25) tq <- log(12); tka <- log(1.2) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v1 ~ 0.09; eta.v2 ~ 0.09 eta.q ~ 0.09; eta.ka ~ 0.09 }) model({ cl <- exp(tcl + eta.cl); v1 <- exp(tv1 + eta.v1) v2 <- exp(tv2 + eta.v2); q <- exp(tq + eta.q) ka <- exp(tka + eta.ka) d/dt(depot) <- -ka * depot d/dt(central) <- ka * depot - (cl/v1 + q/v1) * central + (q/v2) * peripheral d/dt(peripheral) <- (q/v1) * central - (q/v2) * peripheral cp <- central / v1 cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adirmc", control = adirmcControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), n_sim = 500L ) ) print(fit)
Constructs a control object for est = "admc", the Monte Carlo aggregate
data modelling estimator.
admControl( studies = list(), n_sim = 5000L, sampling = c("sobol", "halton", "torus", "lhs", "rnorm"), algorithm = NULL, maxeval = 500L, ftol_rel = .Machine$double.eps^2, print = 10L, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad = c("sens", "fd", "none"), grad_h = 1e-04, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/5), grad_bounds = 5, covMethod = c("r,s", "r", "none"), cov_n_sim = 10000L, n_restarts = 1L, restart_sd = 0.5, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, xtol_rel = .Machine$double.eps^(1/2), ... )admControl( studies = list(), n_sim = 5000L, sampling = c("sobol", "halton", "torus", "lhs", "rnorm"), algorithm = NULL, maxeval = 500L, ftol_rel = .Machine$double.eps^2, print = 10L, seed = 12345L, cores = rxode2::rxCores(), nDisplayProgress = .Machine$integer.max, grad = c("sens", "fd", "none"), grad_h = 1e-04, cov_h = 0.001, cov_h_outer = .Machine$double.eps^(1/5), grad_bounds = 5, covMethod = c("r,s", "r", "none"), cov_n_sim = 10000L, n_restarts = 1L, restart_sd = 0.5, workers = 1L, rxControl = NULL, calcTables = FALSE, compress = TRUE, ci = 0.95, sigdig = NULL, sigdigTable = NULL, addProp = c("combined2", "combined1"), optExpression = TRUE, sumProd = FALSE, literalFix = TRUE, returnAdmr = FALSE, resid_nodes = 81L, xtol_rel = .Machine$double.eps^(1/2), ... )
studies |
Named list of study specifications. Each element is a list with:
Multi-compartment (multiple observed outputs). To fit several observed
compartments simultaneously (e.g. plasma and brain/CSF), give the study an
Long format (one row per endpoint/time). As an alternative to the
# independent blocks: per-row variances; optional per-endpoint `n` column
# and per-endpoint `ev` (a list of event tables keyed by endpoint)
list(n = 60L, ev = ev,
data = data.frame(DVID = c("cp", "cp", "cCSF"), TIME = c(1, 2, 2),
E = c(9.1, 7.4, 2.2), V = c(1.2, 0.9, 0.1)))
# joint (same subjects): ONE stacked covariance whose rows/cols align with
# the rows of `data` -- no `cross` blocks to assemble by hand
list(n = 60L, ev = ev, data = data.frame(DVID = ..., TIME = ..., E = ...),
V = V_joint)
A study-level |
n_sim |
Number of Monte Carlo samples per NLL evaluation. |
sampling |
Sampling method for eta draws: |
algorithm |
nloptr algorithm string, or |
maxeval |
Maximum number of optimizer function evaluations. |
ftol_rel |
Relative function-value tolerance for convergence. |
print |
Print progress every this many evaluations (0 = silent). |
seed |
Random seed for reproducibility. |
cores |
Number of OpenMP threads for |
nDisplayProgress |
Passed to |
grad |
Gradient mode: |
grad_h |
Step size for finite-difference gradient evaluation during
optimization (used by |
cov_h |
Inner FD step for the gradient-based Hessian (only used when
|
cov_h_outer |
Outer step scale for the numerical Hessian. The actual step
for parameter |
grad_bounds |
Box-constraint half-width when using gradients: the fit is
confined to |
covMethod |
Applies to every residual family whose conditional law is independent across
timepoints, which is all of them except
All three blocks are reported on the scale the ESTIMATES are printed on, as
|
cov_n_sim |
Number of MC samples for the covariance (Hessian) step.
More samples reduce MC noise in NLL evaluations. The NLL-based Hessian
( |
n_restarts |
Number of optimization restarts. Runs in parallel when
|
restart_sd |
Standard deviation of structural theta perturbations for restart initialisation. |
workers |
Number of parallel workers for multi-restart. |
rxControl |
|
calcTables, compress, ci, sigdigTable, optExpression, sumProd, literalFix
|
Passed to |
sigdig |
Significant digits asked of the ODE solver, or It is a speed lever, and an opt-in one because it is not free. The
estimators finite-difference the solve with steps of the same order:
|
addProp |
How combined additive+proportional error is parameterised in
the nlmixr2 output tables: |
returnAdmr |
If |
resid_nodes |
Gauss-Hermite nodes used to integrate the RESIDUAL for a
transform-both-sides endpoint ( This is an ACCURACY dial, not a speed one. The quadrature is linear in
|
xtol_rel |
Relative parameter tolerance for convergence (default
|
... |
Additional arguments (none allowed; triggers an error). |
An object of class admControl.
# Minimal control object -- inspect defaults ctl <- admControl() ctl$n_sim ctl$algorithm # Override key settings without fitting ctl2 <- admControl( n_sim = 2000L, maxeval = 300L, grad = "fd", seed = 42L ) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv1 <- log(12); tv2 <- log(25) tq <- log(12); tka <- log(1.2) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v1 ~ 0.09; eta.v2 ~ 0.09 eta.q ~ 0.09; eta.ka ~ 0.09 }) model({ cl <- exp(tcl + eta.cl); v1 <- exp(tv1 + eta.v1) v2 <- exp(tv2 + eta.v2); q <- exp(tq + eta.q) ka <- exp(tka + eta.ka) d/dt(depot) <- -ka * depot d/dt(central) <- ka * depot - (cl/v1 + q/v1) * central + (q/v2) * peripheral d/dt(peripheral) <- (q/v1) * central - (q/v2) * peripheral cp <- central / v1 cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "admc", control = admControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), n_sim = 1000L, maxeval = 200L ) ) print(fit)# Minimal control object -- inspect defaults ctl <- admControl() ctl$n_sim ctl$algorithm # Override key settings without fitting ctl2 <- admControl( n_sim = 2000L, maxeval = 300L, grad = "fd", seed = 42L ) library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv1 <- log(12); tv2 <- log(25) tq <- log(12); tka <- log(1.2) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v1 ~ 0.09; eta.v2 ~ 0.09 eta.q ~ 0.09; eta.ka ~ 0.09 }) model({ cl <- exp(tcl + eta.cl); v1 <- exp(tv1 + eta.v1) v2 <- exp(tv2 + eta.v2); q <- exp(tq + eta.q) ka <- exp(tka + eta.ka) d/dt(depot) <- -ka * depot d/dt(central) <- ka * depot - (cl/v1 + q/v1) * central + (q/v2) * peripheral d/dt(peripheral) <- (q/v1) * central - (q/v2) * peripheral cp <- central / v1 cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "admc", control = admControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), n_sim = 1000L, maxeval = 200L ) ) print(fit)
Returns a minimal NONMEM-style data frame that satisfies nlmixr2's data
argument requirement. The single observation row carries a non-NA
placeholder DV (1); the dose row keeps DV = NA. The placeholder is
purely for dispatch and output construction and never enters the reported
objective – each estimator overwrites fit$env$objective (and
OBJF/logLik/AIC/BIC) with its own aggregate -2LL. A non-NA observation is
required because nlmixr2's post-fit output construction
(nlmixr2CreateOutputFromUi) solves the model over this frame, and rxode2's
event-table translation rejects a dataset with no non-NA observation rows
("no rows in event table or input data"), the same reason the multi-endpoint
frame below uses a placeholder DV.
admData(outputs = NULL)admData(outputs = NULL)
outputs |
Optional character vector of observed output (endpoint) names
for a multi-compartment model with several prediction lines (e.g.
|
A data frame with columns ID, TIME, DV, AMT, EVID, CMT
(single-endpoint), plus a DVID endpoint column when outputs is given.
admData() admData(c("cp", "cCSF"))admData() admData(c("cp", "cCSF"))
The numbers every diagnostic panel is drawn from, as one row per study and
observation time. plot() draws a fixed set of panels; this returns the
moments behind them so you can draw your own.
admMoments(fit, n_sim = NULL, seed = 1L, by = c("source", "stratum"))admMoments(fit, n_sim = NULL, seed = 1L, by = c("source", "stratum"))
fit |
An |
n_sim, seed
|
Simulation size and seed for the predicted moments. Defaults to the fit's own. |
by |
|
A data frame with one row per study and observation time.
obs_sd and pred_sd are the SD of one observation across subjects –
not between-subject variability. They carry BSV, residual error, and,
wherever a study marginalises a covariate, the spread that covariate
induces. struct_sd is the same quantity before residual error is
composed on, so pred_sd - struct_sd is what sigma contributes: a predicted
spread that misses the observed one can then be attributed. struct_sd is
NA for a transforming error model, which moves the mean as well and leaves
the pre-sigma variance on a different scale.
z is the mean standardised residual, (obs - pred) / sqrt(pred_var / n).
stratify cuts a source into one study per covariate level, because a
covariate a study marginalises over is not identified against a random
effect on the same parameter. Those strata are the unit of the likelihood,
not a unit a reader recognises, so by = "source" (the default) puts them
back together: the mean is the n-weighted mean, and the variance is the law
of total variance – within plus between, the between term being the
covariate effect conditioning created.
by = "stratum" returns them separately, which is what you want when the
between-level contrast is the thing you are looking at.
## Not run: m <- admMoments(fit) library(ggplot2) ggplot(m, aes(pred_sd, obs_sd, colour = study)) + geom_abline(slope = 1, intercept = 0) + geom_point() ## End(Not run)## Not run: m <- admMoments(fit) library(ggplot2) ggplot(m, aes(pred_sd, obs_sd, colour = study)) + geom_abline(slope = 1, intercept = 0) + geom_point() ## End(Not run)
Written the way a baseline demographics table reads. Each covariate takes
whichever summary the paper printed — mean/sd, median/iqr,
a cv as a percent, or a proportion for a binary one — and correlations
are given for the PAIRS that were reported, everything else being
independent.
admPopulation(..., cor = NULL, dist = c("lnorm", "normal"), data = NULL)admPopulation(..., cor = NULL, dist = c("lnorm", "normal"), data = NULL)
... |
Named covariates. A continuous one takes a named vector, e.g.
|
cor |
Correlations between covariate PAIRS, named |
dist |
|
data |
A data frame of individual covariates to derive the table
FROM, instead of typing it out — a digitised baseline listing, or the
cohort itself in a simulation study. Each numeric column becomes a margin
(a 0/1 column becomes a proportion, everything else a continuous margin
matching that column's mean and SD), and every continuous PAIR gets its
correlation — taken on the LATENT scale, so on the logs for a lognormal
margin, which is the conversion easiest to get wrong by hand. Anything
named in |
A covariate specification, as covDist() returns.
admStudy(), which takes one; covDraw() to inspect it.
Stops any worker processes (mirai daemons) started by a parallel-restart fit
(admControl(workers = N)). Workers are stopped automatically after the
restart phase completes, so this function is only needed if a fit was
interrupted before cleanup could run.
admStopWorkers()admStopWorkers()
NULL, invisibly.
# Safe to call at any time; no-op if no workers are running admStopWorkers()# Safe to call at any time; no-op if no workers are running admStopWorkers()
Takes admStudy() objects and names them. Names come from the argument names
where given, so admStudies(smith2019, jones2021) labels them by the objects
they were built as.
admStudies(...)admStudies(...)
... |
|
Nothing is generated here either. The studies are materialised once, inside the fit, so the whole specification stays cheap to build and inspect.
An admStudies object, to pass as studies to any of
admControl(), adghControl(), adfoControl() or adirmcControl().
One study, transcribed from one paper. It holds what the paper reported and nothing else, is checked as you build it, and prints so the transcription can be read back against the source before anything is fitted.
admStudy( model = NULL, est = NULL, E = NULL, V = NULL, sd = NULL, sem = NULL, n = NULL, times = NULL, dose = NULL, ev = NULL, population = NULL, at = NULL, by = NULL, strata_nodes = NULL, range = NULL, label = NULL, v_denom = NULL )admStudy( model = NULL, est = NULL, E = NULL, V = NULL, sd = NULL, sem = NULL, n = NULL, times = NULL, dose = NULL, ev = NULL, population = NULL, at = NULL, by = NULL, strata_nodes = NULL, range = NULL, label = NULL, v_denom = NULL )
model |
The published model, as an nlmixr2-style function (or a parsed
|
est |
Named vector of the paper's parameter estimates, on the scale the
model's |
E, V, sd, sem
|
Digitised aggregate data: the reported mean profile |
n |
Number of subjects the study reports on. Taken from |
times |
Observation times. |
dose |
Dose amount, as shorthand for a single-dose |
ev |
A dosing event table from |
population |
The population the study enrolled — see
|
at |
Named list pinning a covariate at a value, for a study reported in
one subgroup, e.g. |
by |
Covariate the paper reports results SEPARATELY by, e.g.
|
strata_nodes |
Quadrature resolution for a conditional CONTINUOUS
covariate, i.e. how many nodes it is cut into. Default 9. Raise it for
precision, at a multiplicative cost: a source conditional on two continuous
covariates becomes Where the nodes are cut on the span of the source and analysis models
rather than the product grid, two error sources remain and neither is
reduced by raising |
range |
Optional named list giving the covariate span the source
ENROLLED, e.g. |
label |
Optional display name; otherwise taken from the argument name in
|
v_denom |
Which denominator the supplied spread uses:
|
A study contributes in one of two currencies, and this takes either:
a published model — model, with the paper's parameter table as
est;
digitised aggregate data — E with sd (or sem, or V).
A study generated from a model is not a sample: its mean and covariance are
exact functions of that model's parameters, and n sets its RELATIVE WEIGHT
against the other studies rather than its precision. No standard error is
reported for a fit that includes one — see admStudies().
Nothing is solved here. The study is generated when it reaches the fit, so
building one is cheap and a mistake surfaces on print() rather than after a
long run.
An admStudy object.
Whether a covariate is conditional or marginal for a source is a property of that source's own model, so admixr2 works it out and there is nothing to set.
A covariate is conditional when the source's model ESTIMATED its coefficient. That paper reports a contrast along it, so the source is cut into nodes and the contrast is carried into the fit. A covariate the model merely READS is not conditional — weight at a fixed allometric exponent carries no fitted effect to recover, and conditioning on it would buy nodes and no evidence.
A covariate is marginal when the model did not estimate it. There is no
contrast in that paper to condition on, and splitting it on a covariate its model
never saw would manufacture evidence, so admixr2 integrates over the
population instead. That covers both the covariate the model never mentions
and the one it reads at an asserted coefficient; print() names them
separately, because only the second is easy to mistake for conditional.
Which covariates a source is conditional on comes from that source. Which of them admixr2 has to cut into nodes is narrowed to the ones the analysis model reads, and that narrowing is exact: if the model's prediction does not move across a source's nodes, the mixture those nodes collapse to is a sufficient statistic for it, so the objective is unchanged to eight decimal places while the study count falls. Measured on a source conditional on two covariates with an analysis model reading one, collapsing the unread one moved the objective by 0.00004 and collapsing the read one by 73.6.
This matters and is why it is not left to the caller: a covariate every source marginalises is not identified against a random effect on the same parameter. Measured over 720 replicates, coverage was 0.933 with one source conditional and 0.925 with all of them conditional, against 0.817 with none.
A source given as digitised E/V has no model to derive from and is
marginal over whatever it declares. Use one admStudy(..., at = ...) per
reported subgroup to enter a paper that published by subgroup.
stratify was removed; covStrata() still takes it for working with a
covariate distribution directly. strata_nodes and range remain: the
first is a precision setting and the second is a fact about the source,
neither of which is a statement about which covariates are conditional.
admStudies() to collect several, admPopulation() for the
baseline table.
The ordinary LRT: the objective difference against a chi-squared reference
with Df equal to the number of parameters the larger model adds.
## S3 method for class 'admFit' anova(object, ...)## S3 method for class 'admFit' anova(object, ...)
object |
An |
... |
Further |
Both fits must come from the same estimator and, for the quadrature
estimators, the same node count (n_nodes) or, for the Monte Carlo ones
(admc, adirmc), the same sample size (n_sim). Each scores its own
approximation to the likelihood, so objectives from different ones are not
comparable and the comparison is refused rather than reported.
The stratum resolution is checked per covariate, and only where the two fits overlap. A covariate a model does not read cannot put the two on different scales: if its prediction does not move across a source's nodes, the mixture those nodes collapse to is a sufficient statistic for it, so its objective is the same at either resolution. That is what makes the nested pair of a covariate test comparable — the null model drops the term, so its sources are not cut along it. Two fits that both read a covariate and cut it differently, including one cutting it and the other integrating over it whole, are refused.
Testing a variance AT ZERO puts the null on the boundary of the parameter space, where the exact reference is a chi-bar-squared mixture rather than a chi-squared. The p-value reported there is CONSERVATIVE – too large – so a significant result stays significant, but treat a borderline one with care.
A data frame of class anova.admFit, smallest model first.
Builds the cov_dist a study carries, from what a publication actually
reports: a mean and a standard deviation per covariate, and optionally a
correlation between them. Validation happens here, where an error can name
the covariate, rather than at the first objective evaluation of a fit.
covDist(..., cor = NULL, joint = NULL, dist = c("normal", "lnorm"))covDist(..., cor = NULL, joint = NULL, dist = c("normal", "lnorm"))
... |
One argument per covariate, named as the model reads it. Each is one of:
Alternatively a single data.frame with |
cor |
Correlation between the covariates: a scalar for two of them, or a correlation matrix (named, in any order). Realised through a Gaussian copula on the declared margins. |
joint |
Not accepted yet, and an error if given. It is the place an
arbitrary sampler will attach — a function receiving the matrix of
uniforms admixr2 supplies and returning one named column per covariate, on
each covariate's own scale, which is the shape a vine copula produces —
but the contract around it is not settled, so the argument refuses rather
than half-works. Use |
dist |
Default margin for the Choose |
A validated cov_dist, ready to pass to a study. Printing it shows
each covariate's realised mean, SD and type.
A distribution may name covariates the analysis model never uses. The prediction cannot depend on them, so integrating over them changes nothing and they are left off the design, with a message naming them. This is what lets a NESTED pair of models share one set of studies: the null model of a covariate test simply omits the term, while the population it was fitted to still describes everyone who was enrolled. Marginalising a covariate out of a correlated specification is exact — every surviving margin keeps the distribution it was given.
A covariate the model does read must still be described, by a cov_dist
entry or a fixed cov value, so a mistyped name is reported as the
covariate it left undescribed rather than passing unnoticed.
covDraw() to see what a specification describes,
covStrata() to cut it into strata.
# what a baseline-characteristics table reports cd <- covDist(WT = c(mean = 72, sd = 16), CRCL = c(mean = 90, sd = 25), cor = 0.6) cd # a categorical covariate: labels are the levels, values their proportions covDist(SEX = c(female = 0.55, male = 0.45)) # or transcribe the table itself covDist(data.frame(covariate = c("WT", "CRCL"), mean = c(72, 90), sd = c(16, 25)))# what a baseline-characteristics table reports cd <- covDist(WT = c(mean = 72, sd = 16), CRCL = c(mean = 90, sd = 25), cor = 0.6) cd # a categorical covariate: labels are the levels, values their proportions covDist(SEX = c(female = 0.55, male = 0.45)) # or transcribe the table itself covDist(data.frame(covariate = c("WT", "CRCL"), mean = c(72, 90), sd = c(16, 25)))
cov_dist specificationSimulates the covariate values admixr2 itself would use for n subjects,
from the same specification a study carries in cov_dist. It is the way to
see what a specification actually describes — to check that a reported mean
and standard deviation were transcribed correctly, that a correlation points
the way round you meant, or that a copula sampler returns what you think it
does — before a fit depends on it.
covDraw(cov_dist, n = 1000L, n_eta = 0L)covDraw(cov_dist, n = 1000L, n_eta = 0L)
cov_dist |
A covariate specification, as given to a study. Each element
names a covariate and describes its distribution, either as |
n |
Number of subjects to draw. |
n_eta |
Number of random effects in the model the specification belongs
to. The draws are deterministic and come from Sobol dimensions after the
random effects', so passing the model's own |
A numeric matrix with n rows and one named column per covariate.
# a baseline-characteristics table, transcribed directly X <- covDraw(list(WT = list(mean = 72, sd = 16), CRCL = list(mean = 90, sd = 25), cor = 0.6), n = 500) colMeans(X) stats::cor(log(X[, "WT"]), log(X[, "CRCL"]))# a baseline-characteristics table, transcribed directly X <- covDraw(list(WT = list(mean = 72, sd = 16), CRCL = list(mean = 90, sd = 25), cor = 0.6), n = 500) colMeans(X) stats::cor(log(X[, "WT"]), log(X[, "CRCL"]))
Shows the strata datagen() builds for a study declaring stratify — the
covariate value each stratum is pinned at, its effective sample size, and the
distribution the remaining covariates follow within it. It is the way to
see what a stratification actually describes before a fit depends on it.
covStrata( cov_dist, stratify, n_nodes = .ADM_STRATA_NODES, n = 1, n_pool = 32768L, cov_range = NULL )covStrata( cov_dist, stratify, n_nodes = .ADM_STRATA_NODES, n = 1, n_pool = 32768L, cov_range = NULL )
cov_dist |
A covariate specification, as given to a study — see
|
stratify |
Character vector naming the covariates to stratify on. The
rest are left in each stratum's own |
n_nodes |
Strata per stratified covariate (default 9). A discrete covariate ignores it and is cut at its levels, exactly, with the level probabilities as weights. For a continuous one it sets how finely the covariate is resolved, and the
rule depends on what is known about the joint distribution. Where the
latent structure is known — an explicit This is a convergence parameter, not a modelling choice. The
well-defined object is the limit as the count grows; any finite value
approximates it, and under misspecification the answer can jump between
basins rather than drift. Raise it until the estimates stop moving rather
than picking a value you like. Cost is How much the objective depends on it turns on which rule applies. Where the
latent structure is known — an explicit A declared discrete covariate no longer forces the slow rule: as long as it is latently independent of the others it enumerates exactly at its levels, which took a study declaring one sex covariate from 515 units of movement, still rising at a count of 50, to 0.009. An opaque |
n |
Total sample size to divide among the strata. The default of |
n_pool |
Minimum size of the deterministic pool the strata are cut
from. Each stratum's covariate distribution is that pool restricted to its
own bin, so this sets how finely the within-stratum distribution is
resolved — and, because no stratum can produce a covariate value beyond
the pool's extremes, how far into the tails it reaches. The pool is grown
automatically with the number of strata, and a stratum left with too few
draws is an error rather than a silently coarse answer; It applies only where a pool is used at all: an opaque |
cov_range |
Optional named list giving the range each stratified
covariate was actually ENROLLED over, e.g. Without it the strata are cut over the whole declared distribution, and a
source is credited with evidence at covariate values it never sampled. The
overstatement is exactly More strata resolve the covariate range more finely but do not buy accuracy, and each one costs a solve: on a matched one-covariate fit the coefficient came back at 0.7000 / 0.7002 / 0.7005 / 0.7005 for 3 / 4 / 10 / 16 strata against a true 0.700, while the fit took 7.2 / 4.1 / 8.0 / 11.5 seconds. Raise it when the covariate effect is strongly nonlinear over the range; the only hard limit is that a stratum must keep enough pooled draws to describe itself, which admixr2 checks. |
Stratify on the covariates a source's own published model conditions on, and leave the rest to be marginalised. A source that never fitted a covariate has no contrast in it to report, and evaluating it at nodes that vary that covariate manufactures a null one — measured, with no sampling noise, as an attenuation from 0.450 to 0.211 in the fitted coefficient.
A list with one element per stratum, each containing cov (the
value each stratified covariate is held at — a quadrature node, a level,
or a bin mean), cov_dist (the distribution of ALL covariates within
that stratum, carrying the full joint dependence), n and weight.
covDraw() to inspect a covariate specification, datagen() to
generate the strata as studies.
# a source that fitted weight but not renal function, the two correlated st <- covStrata(list(WT = list(mean = 72, sd = 15), CRCL = list(mean = 90, sd = 22), cor = 0.7), stratify = "WT", n_nodes = 4L, n = 300) vapply(st, function(s) s$cov$WT, numeric(1)) # where each stratum sits vapply(st, function(s) s$n, numeric(1)) # and how big it is # within a stratum, CRCL follows its CONDITIONAL distribution: a heavier # stratum has a higher creatinine clearance, which is the whole point vapply(st, function(s) mean(covDraw(s$cov_dist, n = 2000L)[, "CRCL"]), numeric(1))# a source that fitted weight but not renal function, the two correlated st <- covStrata(list(WT = list(mean = 72, sd = 15), CRCL = list(mean = 90, sd = 22), cor = 0.7), stratify = "WT", n_nodes = 4L, n = 300) vapply(st, function(s) s$cov$WT, numeric(1)) # where each stratum sits vapply(st, function(s) s$n, numeric(1)) # and how big it is # within a stratum, CRCL follows its CONDITIONAL distribution: a heavier # stratum has a higher creatinine clearance, which is the whole point vapply(st, function(s) mean(covDraw(s$cov_dist, n = 2000L)[, "CRCL"]), numeric(1))
Generates population mean vectors (E) and covariance matrices
(V) for each study by integrating over the IIV distribution – either by
Monte Carlo (the default) or by a deterministic First-Order expansion
(method = "fo", see datagenControl()). Each study may specify its own PK/PD model (as would be the
case when digitising data from several published studies, each fit with a
different structural model). True parameter values are taken from the
ini() block of each study's model. Each element of the returned list
is ready to supply directly to admControl(studies = ...).
datagen(studies, model = NULL, control = datagenControl())datagen(studies, model = NULL, control = datagenControl())
studies |
A named list of study specifications. Each element is a list with:
|
model |
Optional default model function used for any study that does not
supply its own |
control |
A |
With control = datagenControl(method = "mc") (the default) population
moments are computed via the same Monte Carlo engine as est = "admc":
where and are the model and initial estimates
from the ini() block of study , the sample covariance uses the
ML denominator n_sim, and is diagonal with entries
determined by that study model's residual error type (additive, proportional,
or log-normal).
With method = "fo" the moments are instead the deterministic First-Order
expansion used by est = "adfo":
with the Jacobian obtained from the sensitivity model (or finite
differences if that is unavailable). This is the natural choice for design
evaluation and optimal design: the moments are fast and reproducible, and
because the data-generating and data-analytic models coincide, the FO Hessian
of the log-likelihood (the expected information matrix) is evaluated at the
true maximum rather than at a point that is not an MLE of the generated data.
Note est = "adfo" always adds to its predicted covariance, so
for a consistent FIM keep the residual error in the generating model; omit it
only when residual-free (IIV-only) moments are genuinely what you want.
With method = "gh" the moments are computed by deterministic
Gauss-Hermite quadrature over the random-effects prior :
where are the Cholesky-scaled tensor-product GH nodes and
weights. Unlike FO this is unbiased at any IIV magnitude; unlike MC the result
is noise-free and exactly reproducible. Matching the moments of est = "adgh"
makes method = "gh" the natural choice for optimal design with that estimator.
Models are compiled and cached on first use (keyed by model expression digest), so repeated calls or multiple studies sharing the same model incur only a single compilation.
A named list with one element per study. Each element contains:
EPopulation mean vector at times.
VPopulation covariance matrix
(length(times) x length(times); ML denominator n_sim for
method = "mc", the analytical FO covariance for method = "fo",
or the GH weighted covariance for method = "gh").
The diagonal carries the model's residual-error variance; to generate
residual-free (IIV-only) moments, omit the error term from the model.
nSample size (NA_integer_ if not supplied).
timesObservation times.
evDosing event table.
samplesRaw n_sim x length(times) prediction matrix
(only when control$return_samples = TRUE).
datagenControl(), admControl()
library(rxode2) pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } study_data <- datagen( studies = list( study1 = list(times = c(1, 2, 4, 8, 12, 24), ev = rxode2::et(amt = 100), n = 200L) ), model = pk_model, control = datagenControl(n_sim = 2000L) ) # E and V plug directly into admControl(studies = ...) round(study_data$study1$E, 2)library(rxode2) pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } study_data <- datagen( studies = list( study1 = list(times = c(1, 2, 4, 8, 12, 24), ev = rxode2::et(amt = 100), n = 200L) ), model = pk_model, control = datagenControl(n_sim = 2000L) ) # E and V plug directly into admControl(studies = ...) round(study_data$study1$E, 2)
datagen()
Control parameters for datagen()
datagenControl( method = c("mc", "fo", "gh"), n_sim = 5000L, n_nodes = 5L, sampling = c("sobol", "halton", "torus", "lhs", "rnorm"), seed = 12345L, cores = 1L, return_samples = FALSE, resid_nodes = 81L, cov_nodes = 7L )datagenControl( method = c("mc", "fo", "gh"), n_sim = 5000L, n_nodes = 5L, sampling = c("sobol", "halton", "torus", "lhs", "rnorm"), seed = 12345L, cores = 1L, return_samples = FALSE, resid_nodes = 81L, cov_nodes = 7L )
method |
Moment approximation used to generate |
n_sim |
Number of Monte Carlo samples used to approximate population
moments. Ignored when |
n_nodes |
Number of Gauss-Hermite nodes per eta dimension for
|
sampling |
Quasi-random sampling method: |
seed |
Integer seed. Applied before stochastic methods
( |
cores |
Number of |
return_samples |
Include the raw |
resid_nodes |
Gauss-Hermite nodes used to integrate the RESIDUAL for a
transform-both-sides endpoint ( |
cov_nodes |
Gauss-Hermite nodes per covariate when |
A list of class "datagenControl".
ctrl <- datagenControl(n_sim = 2000L) ctrl$sampling # "sobol" # Deterministic FO moments for design evaluation: datagenControl(method = "fo")$method # "fo" # GH quadrature moments (unbiased, noise-free): datagenControl(method = "gh", n_nodes = 5L)$n_nodesctrl <- datagenControl(n_sim = 2000L) ctrl$sampling # "sobol" # Deterministic FO moments for design evaluation: datagenControl(method = "fo")$method # "fo" # GH quadrature moments (unbiased, noise-free): datagenControl(method = "gh", n_nodes = 5L)$n_nodes
A simulated pharmacokinetic dataset for the fictional drug examplomycin,
intended as a worked example for aggregate data modelling with admixr2.
The dataset contains 500 subjects, each with 9 observation time points,
generated from a two-compartment model with first-order absorption.
examplomycinexamplomycin
A data frame with 5000 rows and 6 columns:
ID: Subject identifier (integer, 1–500).
TIME: Time after dose (hours).
DV: Observed plasma concentration (mg/L).
AMT: Dose amount (mg); 100 for dosing records, 0 otherwise.
EVID: Event type (101 = dose, 0 = observation).
CMT: Compartment (1 = depot, 2 = central).
True population parameters:
| Parameter | Value |
| CL (L/hr) | 5 |
| V1 (L) | 10 |
| V2 (L) | 30 |
| Q (L/hr) | 10 |
| ka (1/hr) | 1 |
| IIV (all, SD on log scale) | 0.3 |
| Proportional error (SD) | 0.2 |
Single oral dose of 100 mg; sampling at 0.1, 0.25, 0.5, 1, 2, 3, 5, 8, and 12 hours post-dose.
Generated from a two-compartment PK model using rxode2::rxSolve().
See vignette("admixr2") for a full modelling example.
data("examplomycin") head(examplomycin) # Compute aggregate statistics obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) E <- sapply(times, function(t) mean(obs$DV[obs$TIME == t])) round(E, 3)data("examplomycin") head(examplomycin) # Compute aggregate statistics obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) E <- sapply(times, function(t) mean(obs$DV[obs$TIME == t])) round(E, 3)
Called automatically by nlmixr2(model, admData(), est = "adfo", control = adfoControl(...)). Not typically called directly.
## S3 method for class 'adfo' nlmixr2Est(env, ...)## S3 method for class 'adfo' nlmixr2Est(env, ...)
env |
nlmixr2 environment containing |
... |
Unused. |
An admFit nlmixr2 fit object.
Called automatically by nlmixr2(model, admData(), est = "adgh", control = adghControl(...)). Not typically called directly.
## S3 method for class 'adgh' nlmixr2Est(env, ...)## S3 method for class 'adgh' nlmixr2Est(env, ...)
env |
nlmixr2 environment containing |
... |
Unused. |
An admFit nlmixr2 fit object.
Called automatically by
nlmixr2(model, admData(), est = "adirmc", control = adirmcControl(...)).
Not typically called directly.
## S3 method for class 'adirmc' nlmixr2Est(env, ...)## S3 method for class 'adirmc' nlmixr2Est(env, ...)
env |
nlmixr2 environment containing |
... |
Unused. |
An admFit nlmixr2 fit object.
Called automatically by nlmixr2(model, admData(), est = "admc", control = admControl(...)).
Not typically called directly.
## S3 method for class 'admc' nlmixr2Est(env, ...)## S3 method for class 'admc' nlmixr2Est(env, ...)
env |
nlmixr2 environment containing |
... |
Unused. |
An admFit nlmixr2 fit object.
Generates up to five diagnostic panels:
## S3 method for class 'admFit' plot( x, which = c("mean", "cov", "covariate", "nll", "par"), n_sim = NULL, seed = 1L, ... )## S3 method for class 'admFit' plot( x, which = c("mean", "cov", "covariate", "nll", "par"), n_sim = NULL, seed = 1L, ... )
x |
An |
which |
Character vector selecting which panel types to produce.
Any subset of |
n_sim |
Number of MC samples for the final prediction. Defaults to the
value used during fitting. Only used when |
seed |
Random seed for reproducibility. |
... |
Unused. |
"mean" – Observed vs predicted mean per study (2x2 grid). Upper row:
observed and predicted mean lines with +/-1 SD ribbon on a shared y scale
(black throughout). Lower row: raw residual lollipop with +/-2 SE band and
standardised residual z-scores with +/-1.96 reference lines.
"cov" – Observed vs predicted (co)variance heatmaps per study (2x2
grid). Upper row shares a common colour scale (blue-white-red). Lower row
uses distinct diverging scales: residual (red-white-green) and
standardised residual (gold-white-purple). Significance stars overlaid on
the standardised residual panel.
"covariate" – Two BETWEEN-study covariate panels, one facet per
covariate any source describes – including one the model does not read,
which is exactly the case covariate_resid exists for.
covariate_effect sweeps each covariate across the range the sources
between them cover and draws every model parameter that reads it, at the
fitted thetas, as ONE line per parameter with the other covariates held at
the pooled centre; the region no source sampled is shaded as
extrapolation, and a discrete covariate is drawn on its levels rather than
swept through the values between them. covariate_resid plots each
study's mean standardised residual against the covariate value it sits at,
with an lm trend on a continuous axis: a slope there is a mis-specified
covariate form. Produced only for a fit whose studies declare covariates,
so it is silently absent otherwise.
"nll" – NLL trace per restart over optimizer evaluations. Restarts
coloured with the Okabe-Ito palette.
"par" – Parameter trace per restart on the natural scale (struct thetas
back-transformed, sigma as SD, omega diagonal as variance labelled
V(eta.x)). Facets ordered as in the model ini() block. Restarts
coloured with the Okabe-Ito palette.
A named list of ggplot2 objects, invisibly. Prints each selected
top-level panel. For the "mean" and "cov" panels the returned list also
contains each sub-panel individually so a single panel (or a few) can be
extracted in code without reprinting the whole grid. Elements can be pulled
out by name – plot(fit, which = "mean")$mean_study1_pred or
plot(fit, which = "cov")$cov_study1_std_resid – or by position, with the
combined 2x2 grid stored first per study
(plot(fit, which = "mean")[[1]] is the full grid, [1] the length-1
named sub-list). The sub-panel keys are <type>_<source>_obs, _pred,
_resid, and _std_resid; the combined grid stays under
<type>_<source>. <source> is the study name, and for a source cut into
nodes it is the name of the SOURCE rather than of a node – these panels
are about the paper, so the nodes are put back together first and there is
no mean_study1_s1. The extra sub-panel keys are not printed on their own.
With aggregate data a covariate effect is identified BETWEEN studies: the
renal exponent in vignette("covariates") is recovered from three cohorts
sitting at three different creatinine-clearance medians, none of which fitted
a renal term at all. The "mean" and "cov" panels are per study and in
isolation, so every source can sit beautifully on its own panel while the
relation tying them together is wrong. That contrast is what "covariate"
draws.
Both panels distinguish how each study enters a covariate, because the two
are different statements about the source rather than degrees of the same
one. A marginalised covariate – one the estimator integrates over a
declared distribution – is drawn as a round point at the study's median with
a 10th-90th percentile bar and a 2.5th-97.5th whisker: the source constrains
the effect through the spread it induces, and the whole span is what it
speaks for. A conditional covariate – cut into nodes, or at or by, so the model
is solved at one value – is drawn as a diamond at that value with no spread,
because it has none; what that source reports along it is a relationship,
drawn as its own regression over the range it covers.
A SOURCE IS ONE MARK, not one per stratum. Conditioning cuts a source into one study per quadrature node, and a node is an internal discretisation of the very distribution the source already stands for: drawn straight it became nine small dots for one paper, each carrying a ninth of its patients, with the outermost node setting the axis. On a LEVEL axis the positions are values the paper actually reported, so a source appears once per level.
covariate_resid
A continuous covariate is read as a trend: the dashed lm across studies,
where a slope is a mis-specified covariate form.
A discrete one is read as a contrast instead, and is drawn that way. Its
axis is ticked at its levels and nowhere else, no regression is fitted
through it – a line across the levels of a factor reports as a slope what is
a difference between groups – and the strata stratify cut from one source
are joined, because that pairing is the evidence conditioning creates: the same
study, one covariate moved. Several sources tilting the same way is the
mis-specification. A regression over the pooled cloud of strata cannot show
it, since it averages the pairs away.
With few sources, covariates whose study medians happen to move together cannot be told apart here – three cohorts whose weights and renal function both decline will show a slope in both facets whichever one is mis-specified. The panel localises the problem to a set of covariates, not to one; separating them needs a source that breaks the pattern.
Every admixr2 fit also carries the observed and predicted aggregate data in
fit$env$aggData, a named list with one entry per study. Each entry holds the
observation times, the study n, and two moment sets – obs (from the
data) and pred (predicted at the fitted parameters) – each a list with the
mean vector E and the (co)variance matrix V:
fit$env$aggData$study1$obs$E # observed mean vector fit$env$aggData$study1$obs$V # observed covariance matrix fit$env$aggData$study1$pred$E # predicted mean vector fit$env$aggData$study1$pred$V # predicted covariance matrix
The predicted moments are computed by one MC simulation at the fitted
parameters using the fit's own n_sim and a fixed seed. The slot is absent
only when the fit cannot be simulated (no simulation model available).
It is per study, which for a source cut into nodes means per node –
where the "mean" and "cov" panels are per SOURCE, the nodes put back
together by the mixture law. So the two no longer line up entry for entry on
such a fit, and the panel keys are mean_<source> rather than
mean_<source>_s1. admMoments() returns whichever of the two you want,
tidied: by = "source" matches the panels, by = "stratum" matches this
slot.
traceplot()
admixr2 fits also plug into the nlmixr2 traceplot() generic. During fitting
the parameter iteration history of the best restart is stored on the fit in
the standard parHistData slot (natural scale), so traceplot(fit) produces
the familiar per-parameter, free-y facetted trace used elsewhere in the
nlmixr2 ecosystem. There is no burn-in marker (admixr2 records optimizer
evaluations, not SAEM iterations), and only the best restart is shown – the
per-restart overlay and the NLL trace remain available via
plot(fit, which = c("par", "nll")). The trace stores only improving
evaluations (steps that lowered the best NLL), so the iter axis indexes
those improvement steps rather than raw optimizer iterations.
library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adfo", control = adfoControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), maxeval = 100L ) ) plot(fit)library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adfo", control = adfoControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), maxeval = 100L ) ) plot(fit)
Delegates to print.nlmixr2FitCore for the standard nlmixr2 coloured
output. admFit class is kept on the object during the call so that
head.admFit intercepts any head(fit) calls that arise in the paged-
output path (R Markdown / notebooks), preventing the
[.data.frame(.subset2(env, integer)) crash that occurs when an
environment-backed fit is subscripted like a plain list.
## S3 method for class 'admFit' print(x, ...)## S3 method for class 'admFit' print(x, ...)
x |
An |
... |
Passed to |
x, invisibly.
library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adfo", control = adfoControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), maxeval = 100L ) ) print(fit)library(rxode2) library(nlmixr2) data("examplomycin") obs <- examplomycin[examplomycin$EVID == 0, ] obs <- obs[order(obs$ID, obs$TIME), ] times <- sort(unique(obs$TIME)) ids <- unique(obs$ID) dv_mat <- do.call(rbind, lapply(ids, function(i) { sub <- obs[obs$ID == i, ]; sub$DV[order(sub$TIME)] })) E <- colMeans(dv_mat) V <- cov.wt(dv_mat, method = "ML")$cov pk_model <- function() { ini({ tcl <- log(5); tv <- log(30) prop.sd <- c(0, 0.2) eta.cl ~ 0.09; eta.v ~ 0.04 }) model({ cl <- exp(tcl + eta.cl) v <- exp(tv + eta.v) d/dt(central) <- -(cl/v) * central cp <- central / v cp ~ prop(prop.sd) }) } fit <- nlmixr2( pk_model, admData(), est = "adfo", control = adfoControl( studies = list(study1 = list(E = E, V = V, n = length(ids), times = times, ev = et(amt = 100))), maxeval = 100L ) ) print(fit)