nlmixr uses a unified interface for specifying and running models. Let's start with a very simple PK example, using the single-dose theophylline dataset generously provided by Dr. Robert A. Upton of the University of California, San Francisco:
## Load libraries
library(ggplot2)
#> Registered S3 methods overwritten by 'ggplot2':
#> method from
#> [.quosures rlang
#> c.quosures rlang
#> print.quosures rlang
library(nlmixr)
str(theo_sd)
#> 'data.frame': 144 obs. of 7 variables:
#> $ ID : int 1 1 1 1 1 1 1 1 1 1 ...
#> $ TIME: num 0 0 0.25 0.57 1.12 2.02 3.82 5.1 7.03 9.05 ...
#> $ DV : num 0 0.74 2.84 6.57 10.5 9.66 8.58 8.36 7.47 6.89 ...
#> $ AMT : num 320 0 0 0 0 ...
#> $ EVID: int 101 0 0 0 0 0 0 0 0 0 ...
#> $ CMT : int 1 2 2 2 2 2 2 2 2 2 ...
#> $ WT : num 79.6 79.6 79.6 79.6 79.6 79.6 79.6 79.6 79.6 79.6 ...
ggplot(theo_sd, aes(TIME, DV)) + geom_line(aes(group=ID), col="red") + scale_x_continuous("Time (h)") + scale_y_continuous("Concentration") + labs(title="Theophylline single-dose", subtitle="Concentration vs. time by individual")
We can try fitting a simple one-compartment PK model to this small dataset. We write the model as follows:
one.cmt <- function() {
ini({
tka <- 0.45 # Log Ka
tcl <- 1 # Log Cl
tv <- 3.45 # Log V
eta.ka ~ 0.6
eta.cl ~ 0.3
eta.v ~ 0.1
add.err <- 0.7
})
model({
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
linCmt() ~ add(add.err)
})
}
We can now run the model…
fit <- nlmixr(one.cmt, theo_sd, est="nlme")
#> Loading model already run (/tmp/Rtmp5N8lKH/Rinst4a8718e23adc/nlmixr/nlmixr-one.cmt-theo_sd-nlme-fafbd54a79996a01ea5b7de6a8d7212c.rds)
print(fit)
#> ── nlmixr nlme by maximum likelihood (Solved; mu-ref & covs) nlme OBF fit ─
#> OBJF AIC BIC Log-likelihood Condition Number
#> nlme 116.8727 373.4725 393.6521 -179.7363 17.08747
#>
#> ── Time (sec; $time): ─────────────────────────────────────────────────────
#> nlme setup table other
#> elapsed 13.61 31.552 0.03 6.848
#>
#> ── Population Parameters ($parFixed or $parFixedDf): ──────────────────────
#> Registered S3 method overwritten by 'R.oo':
#> method from
#> throw.default R.methodsS3
#> Parameter Est. SE %RSE Back-transformed(95%CI) BSV(CV%)
#> tka Log Ka 0.447 0.192 43 1.56 (1.07, 2.28) 68.7
#> tcl Log Cl 1.02 0.0847 8.31 2.77 (2.35, 3.27) 26.9
#> tv Log V 3.45 0.0464 1.35 31.5 (28.7, 34.5) 13.6
#> add.err 0.697 0.697
#> Shrink(SD)%
#> tka 0.241%
#> tcl 3.78%
#> tv 10.0%
#> add.err
#>
#> Covariance Type ($covMethod): nlme
#> No correlations in between subject variability (BSV) matrix
#> Full BSV covariance ($omega) or correlation ($omegaR; diagonals=SDs)
#> Distribution stats (mean/skewness/kurtosis/p-value) available in $shrink
#>
#> ── Fit Data (object is a modified tibble): ────────────────────────────────
#> # A tibble: 132 x 18
#> ID TIME DV EVID PRED RES IPRED IRES IWRES eta.ka eta.cl
#> <fct> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 0 0.74 0 0 0.74 0 0.74 1.06 0.101 -0.479
#> 2 1 0.25 2.84 0 3.25 -0.410 3.84 -1.00 -1.44 0.101 -0.479
#> 3 1 0.570 6.57 0 5.83 0.744 6.78 -0.212 -0.305 0.101 -0.479
#> # … with 129 more rows, and 7 more variables: eta.v <dbl>, rx1c <dbl>,
#> # ka <dbl>, cl <dbl>, v <dbl>, depot <dbl>, central <dbl>
We can alternatively express the same model by ordinary differential equations (ODEs):
one.compartment <- function() {
ini({
tka <- 0.45 # Log Ka
tcl <- 1 # Log Cl
tv <- 3.45 # Log V
eta.ka ~ 0.6
eta.cl ~ 0.3
eta.v ~ 0.1
add.err <- 0.7
})
model({
ka <- exp(tka + eta.ka)
cl <- exp(tcl + eta.cl)
v <- exp(tv + eta.v)
d/dt(depot) = -ka * depot
d/dt(center) = ka * depot - cl / v * center
cp = center / v
cp ~ add(add.err)
})
}
We can try the Stochastic Approximation EM (SAEM) method to this model:
fit <- nlmixr(one.compartment, theo_sd, est="saem")
#> Loading model already run (/tmp/Rtmp5N8lKH/Rinst4a8718e23adc/nlmixr/nlmixr-one.compartment-theo_sd-saem-1cb146fbba324a1fe760d0b6e8f83240.rds)
And if we wanted to, we could even apply the First-Order Conditional Estimation (FOCEi) method to this model:
fitF <- nlmixr(one.compartment, theo_sd, est="focei")
#> Loading model already run (/tmp/Rtmp5N8lKH/Rinst4a8718e23adc/nlmixr/nlmixr-one.compartment-theo_sd-focei-a1f34f114ba648e90a70b82a36d8eb5b.rds)
This example delivers a complete model fit as the fit
object, including parameter history, a set of fixed effect estimates,
and random effects for all included subjects.
Now back to the saem
fit; Let's look at the fit using nlmixr's built-in diagnostics…
plot(fit)
print(fit)
#> ── nlmixr SAEM(ODE); OBJF by Gaussian Quadrature (n.nodes=3, n.sd=1.6) fit
#> OBJF AIC BIC Log-likelihood Condition Number
#> gauss3_1.6 122.9719 379.5717 399.7513 -182.7858 18.16231
#>
#> ── Time (sec; $time): ─────────────────────────────────────────────────────
#> saem setup table covariance logLik other
#> elapsed 57.44 22.53 0.02 0.04 0.08 4.31
#>
#> ── Population Parameters ($parFixed or $parFixedDf): ──────────────────────
#> Parameter Est. SE %RSE Back-transformed(95%CI) BSV(CV%)
#> tka Log Ka 0.451 0.196 43.5 1.57 (1.07, 2.31) 71.9
#> tcl Log Cl 1.02 0.0836 8.22 2.77 (2.35, 3.26) 27.0
#> tv Log V 3.45 0.0469 1.36 31.5 (28.7, 34.5) 14.0
#> add.err 0.692 0.692
#> Shrink(SD)%
#> tka 0.411%
#> tcl 3.36%
#> tv 10.0%
#> add.err
#>
#> Covariance Type ($covMethod): linFim
#> No correlations in between subject variability (BSV) matrix
#> Full BSV covariance ($omega) or correlation ($omegaR; diagonals=SDs)
#> Distribution stats (mean/skewness/kurtosis/p-value) available in $shrink
#>
#> ── Fit Data (object is a modified tibble): ────────────────────────────────
#> # A tibble: 132 x 18
#> ID TIME DV EVID PRED RES IPRED IRES IWRES eta.ka eta.cl
#> <fct> <dbl> <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 1 0 0.74 0 0 0.74 0 0.74 1.07 0.105 -0.487
#> 2 1 0.25 2.84 0 3.26 -0.423 3.86 -1.02 -1.48 0.105 -0.487
#> 3 1 0.570 6.57 0 5.84 0.725 6.81 -0.235 -0.340 0.105 -0.487
#> # … with 129 more rows, and 7 more variables: eta.v <dbl>, ka <dbl>,
#> # cl <dbl>, v <dbl>, cp <dbl>, depot <dbl>, center <dbl>
fit$eta
#> Warning in knit_print.huxtable(ht): Unrecognized output format "rmarkdown::html". Using `to_screen` to print huxtables.
#> Set options("huxtable.knitr_output_format") manually to "latex", "html", "rtf", "docx", "pptx", "md" or "screen".
┌─────────────────────────────────────┐ │ ID eta.ka eta.cl eta.v │ ├─────────────────────────────────────┤ │ 1 0.105 -0.487 -0.08 │ │ 2 0.221 0.144 0.0206 │ │ 3 0.368 0.0311 0.058 │ │ 4 -0.277 -0.015 -0.00723 │ │ 5 -0.0458 -0.155 -0.142 │ │ 6 -0.382 0.367 0.203 │ │ 7 -0.791 0.16 0.0466 │ │ 8 -0.181 0.168 0.0958 │ │ 9 1.42 0.0423 0.0121 │ │ 10 -0.738 -0.391 -0.17 │ │ 11 0.79 0.281 0.146 │ │ 12 -0.527 -0.126 -0.198 │ └─────────────────────────────────────┘
Column names: ID, eta.ka, eta.cl, eta.v
Default trace plots can be generated using:
traceplot(fit)
but with a little more work, we can get a nicer set of iteration trace plots (“wriggly worms”)…
iter <- fit$par.hist.stacked
iter$Parameter[iter$par=="add.err"] <- "Additive error"
iter$Parameter[iter$par=="eta.cl"] <- "IIV CL/F"
iter$Parameter[iter$par=="eta.v"] <- "IIV V/F"
iter$Parameter[iter$par=="eta.ka"] <- "IIV ka"
iter$Parameter[iter$par=="tcl"] <- "log(CL/F)"
iter$Parameter[iter$par=="tv"] <- "log(V/F)"
iter$Parameter[iter$par=="tka"] <- "log(ka)"
iter$Parameter <- ordered(iter$Parameter, c("log(CL/F)", "log(V/F)", "log(ka)",
"IIV CL/F", "IIV V/F", "IIV ka",
"Additive error"))
ggplot(iter, aes(iter, val)) +
geom_line(col="red") +
scale_x_continuous("Iteration") +
scale_y_continuous("Value") +
facet_wrap(~ Parameter, scales="free_y") +
labs(title="Theophylline single-dose", subtitle="Parameter estimation iterations")
… and some random-effects histograms…
etas <- data.frame(eta = c(fit$eta$eta.ka, fit$eta$eta.cl, fit$eta$eta.v),
lab = rep(c("eta(ka)", "eta(CL/F)", "eta(V/F)"), each=nrow(fit$eta)))
etas$lab <- ordered(etas$lab, c("eta(CL/F)","eta(V/F)","eta(ka)"))
ggplot(etas, aes(eta)) +
geom_histogram(fill="red", col="white") +
geom_vline(xintercept=0) +
scale_x_continuous(expression(paste(eta))) +
scale_y_continuous("Count") +
facet_grid(~ lab) +
coord_cartesian(xlim=c(-1.75,1.75)) +
labs(title="Theophylline single-dose", subtitle="IIV distributions")
#> `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
This is all very nice. But what we really want is a complete suite of model diagnostic tools, like those available in xpose, right?
Restart R, and install xpose from CRAN, if you haven't already…
## install.packages("xpose")
library(xpose)
#>
#> Attaching package: 'xpose'
#> The following object is masked from 'package:nlmixr':
#>
#> vpc
#> The following object is masked from 'package:stats':
#>
#> filter
Now install the extension for nlmixr:
devtools::install_github("nlmixrdevelopment/xpose.nlmixr")
… and convert your nlmixr fit object into an xpose fit object.
library(xpose.nlmixr)
xp <- xpose_data_nlmixr(fit);
save(xp, file=xpdbLoc)
xpdbLoc <- file.path(system.file(package="nlmixr"), "xpdb.rds");
if (file.exists(xpdbLoc)){
load(xpdbLoc);
} else {
stop("Need to generate nlmixr.xpose example")
}
dv_vs_pred(xp)
dv_vs_ipred(xp)
dv_vs_pred(xp)
absval_res_vs_pred(xp, res="IWRES")
We can also replicate some of nlmixr's internal plots…
ind_plots(xp, res="IWRES")
For more information about using xpose, see the Uppsala pharmacometrics group's comprehensive site here.
The nlmixr modeling dialect, inspired by R and NONMEM, can be used to fit models using all current and future estimation alogorithms within nlmixr. Using these widely-used tools as inspiration has the advantage of delivering a model specification syntax taht is instantly familira to the majority of analysts working in pharmacometrics and related fields.
Model specifications for nlmixr are written using functions containing
ini
and model
blocks. These functions can be called anything, but
must contain these two components. Let's look at a very simple
one-compartment model with no covariates.
f <- function() {
ini({ # Initial conditions/variables
# are specified here
})
model({ # The model is specified
# here
})
}
The ini
block specifies initial conditions, including initial
estimates and boundaries for those algorithms which support them
(currently, the built-in nlme
and saem
methods do
not). Nomenclature is similar to that used in NONMEM, Monolix and
other similar packages. In the NONMEM world, the ini
block is
analogous to $THETA
, $OMEGA
and $SIGMA
blocks.
f <- function(){ # Note that arguments to the function are currently
# ignored by nlmixr
ini({
# Initial conditions for population parameters (sometimes
# called THETA parameters) are defined by either '<-' or '='
lCl <- 1.6 # log Cl (L/hr)
# Note that simple expressions that evaluate to a number are
# OK for defining initial conditions (like in R)
lVc = log(90) # log V (L)
## Also, note that a comment on a parameter is captured as a parameter label
lKa <- 1 # log Ka (1/hr)
# Bounds may be specified by c(lower, est, upper), like NONMEM:
# Residuals errors are assumed to be population parameters
prop.err <- c(0, 0.2, 1)
# IIV terms will be discussed in the next example
})
# The model block will be discussed later
model({})
}
As shown in the above example:
c(lower, est, upper)
.c(lower,est)
is equivalent to c(lower,est,Inf)
c(est)
does not specify a lower bound, and is
equivalent to specifying the parameter without using R's c()
function.These parameters can be named using almost any R-compatible name. Please note that:
=
or <-
, not ~
)._
are not supported. Note that R
does not allow variable starting with _
to be assigned without
quoting them.rx_
or nlmixr_
is not allowed,
since RxODE and nlmixr use these prefixes internally for certain
estimation routines and for calculating residuals.CL
is
not the same as Cl
.In mixture models, multivariate normal individual deviations from the
normal population and parameters are estimated (in NONMEM these are
called “ETA” parameters). Additionally, the variance/covariance matrix
of these deviations are is also estimated (in NONMEM this is the
“OMEGA” matrix). These also take initial estimates. In nlmixr, these
are specified by the ~
operator. This that is typically used in
statistics R for “modeled by”, and was chosen to distinguish these
estimates from the population and residual error parameters.
Continuing from the prior example, we can annotate the estimates for the between-subject error distribution…
f <- function(){
ini({
lCl <- 1.6 # log Cl (L/hr)
lVc = log(90) # log V (L)
lKa <- 1 # log Ka (1/hr)
prop.err <- c(0, 0.2, 1)
# Initial estimate for ka IIV variance
# Labels work for single parameters
eta.ka ~ 0.1 ## BSV Ka
# For correlated parameters, you specify the names of each
# correlated parameter separated by a addition operator `+`
# and the left handed side specifies the lower triangular
# matrix initial of the covariance matrix.
eta.cl + eta.vc ~ c(0.1,
0.005, 0.1)
# Note that labels do not currently work for correlated
# parameters. Also, do not put comments inside the lower
# triangular matrix as this will currently break the model.
})
# The model block will be discussed later
model({})
}
As shown in the above example:
~
.~
.Currently, comments inside the lower triangular matrix are not allowed.
The model
block specifies the model, and is analogous to the $PK
,
$PRED
and $ERROR
blocks in NONMEM.
Once the initialization block has been defined, you can define a model
in terms of the variables defined in the ini
block. You can also mix
RxODE blocks into the model if needed.
The current method of defining a nlmixr model is to specify the parameters, and then any required RxODE lines. Continuing the annotated example:
f <- function(){
ini({
lCl <- 1.6 # log Cl (L/hr)
lVc <- log(90) # log Vc (L)
lKA <- 0.1 # log Ka (1/hr)
prop.err <- c(0, 0.2, 1)
eta.Cl ~ 0.1 # BSV Cl
eta.Vc ~ 0.1 # BSV Vc
eta.KA ~ 0.1 # BSV Ka
})
model({
# Parameters are defined in terms of the previously-defined
# parameter names:
Cl <- exp(lCl + eta.Cl)
Vc = exp(lVc + eta.Vc)
KA <- exp(lKA + eta.KA)
# Next, the differential equations are defined:
kel <- Cl / Vc;
d/dt(depot) = -KA*depot;
d/dt(centr) = KA*depot-kel*centr;
# And the concentration is then calculated
cp = centr / Vc;
# Finally, we specify that the plasma concentration follows
# a proportional error distribution (estimated by the parameter
# prop.err)
cp ~ prop(prop.err)
})
}
A few points to note:
rx_
or nlmixr_
since these are used internally in
some estimation routines.~
. Currently you can use
either add(parameter)
for additive error, prop(parameter)
for
proportional error or add(parameter1) + prop(parameter2)
for
combined additive and proportional error. You can also specify
norm(parameter)
for additive error, since it follows a normal
distribution.saem
, require parameters expressed in terms of
Pop.Parameter + Individual.Deviation.Parameter +
Covariate*Covariate.Parameter
. The order of these parameters does
not matter. This is similar to NONMEM's mu-referencing, though not
as restrictive. This means that for saem
, a parameterization of
the form Cl <- Cl*exp(eta.Cl)
is not allowed.ini
block;
covariates used in the model are not included in the ini
block. These variables need to be present in the modeling dataset
for the model to run.Models can be fitted several ways, including via the [magrittr] forward-pipe operator.
fit <- nlmixr(one.compartment) %>% saem.fit(data=theo_sd)
fit2 <- nlmixr(one.compartment, data=theo_sd, est="saem")
fit3 <- one.compartment %>% saem.fit(data=theo_sd)
Options to the estimation routines can be specified using nlmeControl for nlme estimation:
fit4 <- nlmixr(one.compartment, theo_sd,est="nlme",control = nlmeControl(pnlsTol = .5))
where options are specified in the nlme
documentation.
Options for saem can be specified using saemControl
:
fit5 <- nlmixr(one.compartment,theo_sd,est="saem",control=saemControl(n.burn=250,n.em=350,print=50))
this example specifies 250 burn-in iterations, 350 em iterations and a print progress every 50 runs.
Solved PK systems are also currently supported by nlmixr with the 'linCmt()' pseudo-function. An annotated example of a solved system is below:
f <- function(){
ini({
lCl <- 1.6 #log Cl (L/hr)
lVc <- log(90) #log Vc (L)
lKA <- 0.1 #log Ka (1/hr)
prop.err <- c(0, 0.2, 1)
eta.Cl ~ 0.1 # BSV Cl
eta.Vc ~ 0.1 # BSV Vc
eta.KA ~ 0.1 # BSV Ka
})
model({
Cl <- exp(lCl + eta.Cl)
Vc = exp(lVc + eta.Vc)
KA <- exp(lKA + eta.KA)
## Instead of specifying the ODEs, you can use
## the linCmt() function to use the solved system.
##
## This function determines the type of PK solved system
## to use by the parameters that are defined. In this case
## it knows that this is a one-compartment model with first-order
## absorption.
linCmt() ~ prop(prop.err)
})
}
A few things to keep in mind:
Vc
/Vp
, Clearances in terms of both Cl
and
Q
/CLD
. Additionally nlmixr knows about elimination
micro-constants (ie K12
). Mixing of these parameters for these
models is currently not supported.After specifying the model syntax you can check that nlmixr is interpreting it correctly by using the nlmixr function on it. Using the above function we can get:
nlmixr(f)
#> ▂▂ RxODE-based 1-compartment model with first-order absorption ▂▂▂▂▂▂▂▂▂▂▂▂
#> ── Initialization: ────────────────────────────────────────────────────────
#> Fixed Effects ($theta):
#> lCl lVc lKA
#> 1.60000 4.49981 0.10000
#>
#> Omega ($omega):
#> eta.Cl eta.Vc eta.KA
#> eta.Cl 0.1 0.0 0.0
#> eta.Vc 0.0 0.1 0.0
#> eta.KA 0.0 0.0 0.1
#> ── μ-referencing ($muRefTable): ───────────────────────────────────────────
#> ┌─────────┬─────────┐
#> │ theta │ eta │
#> ├─────────┼─────────┤
#> │ lCl │ eta.Cl │
#> ├─────────┼─────────┤
#> │ lVc │ eta.Vc │
#> ├─────────┼─────────┤
#> │ lKA │ eta.KA │
#> └─────────┴─────────┘
#> ── Model: ─────────────────────────────────────────────────────────────────
#> Cl <- exp(lCl + eta.Cl)
#> Vc = exp(lVc + eta.Vc)
#> KA <- exp(lKA + eta.KA)
#> ## Instead of specifying the ODEs, you can use
#> ## the linCmt() function to use the solved system.
#> ##
#> ## This function determines the type of PK solved system
#> ## to use by the parameters that are defined. In this case
#> ## it knows that this is a one-compartment model with first-order
#> ## absorption.
#> linCmt() ~ prop(prop.err)
#> ▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂
In general this gives you information about the model (what type of solved system/RxODE), initial estimates as well as the code for the model block.