nlmixr2 augmented plot

By Matthew Fidler in nlmixr2

March 31, 2025

This month I will on a single nlmixr2’s plot function that is shared with nlme, augPred(). I think this is useful but also harder to find like the rxode2 plots discussed last month.

The example of this feature is the phenobarbitol data:

library(nlmixr2)
pheno <- function() {
  ini({
    tcl <- log(0.008) # typical value of clearance
    tv <-  log(0.6)   # typical value of volume
    ## var(eta.cl)
    eta.cl + eta.v ~ c(1,
                       0.01, 1) ## cov(eta.cl, eta.v), var(eta.v)
                      # interindividual variability on clearance and volume
    add.err <- 0.1    # residual variability
  })
  model({
    cl <- exp(tcl + eta.cl) # individual value of clearance
    v <- exp(tv + eta.v)    # individual value of volume
    ke <- cl / v            # elimination rate constant
    d/dt(A1) = - ke * A1    # model differential equation
    cp = A1 / v             # concentration in plasma
    cp ~ add(add.err)       # define error model
  })
}

# Note the suppress messages simply removes the output from the
# fit so it is easier to read in the blog.
fit <- suppressMessages(nlmixr(pheno, pheno_sd, "saem",
                               control=list(print=0),
                               table=list(cwres=TRUE, npde=TRUE)))

You can see the basic plots including the individual plots:

p <- plot(fit)

# Here I am subsetting the plots to show only individual plots
p <- p[["All Data"]]

# In this case the list of plots is named starting with "individual"
w <- which(vapply(names(p), function(x) grepl("individual", x), logical(1)))

# This creates a new list of plots, and changes it to the same class
# as output by nlmixr2
p <- lapply(w,function(x) p[[x]])
class(p) <- "nlmixr2PlotList"


#This is a hack to suppress the warnings & messages from the plot function
suppressMessages(suppressWarnings(print(p)))

In particular the individual plots does not show the complete prediction.

One way to get the complete prediction is to add more points to the rxode2 prediction than what is in the original dataset.

In NONMEM (and in nlmixr2) you can add EVID=2 predictions into your input dataset, though the ODE solving mesh may change your nlme solution.

Another method is to add the observations after the nlme fit is complete by using augPred()

This is simple:

ap <- augPred(fit)

head(ap)
##     values        ind id   time Endpoint
## 1 18.71656 Individual  1  0.000       A1
## 2 18.54663 Individual  1  2.000       A1
## 3 18.06285 Individual  1  7.796       A1
## 4 20.01561 Individual  1 15.592       A1
## 5 19.31653 Individual  1 23.388       A1
## 6 21.18352 Individual  1 31.184       A1

You can see this is a dataset that you can plot yourself with any package you would like. Like rxode2 there is a ggplot2 method attached to plot for augPred() datasets from nlmixr2:

# The suppressWarnings, supress messages and print is made
# to abbreviate the output, you can also simply use plot(ap)
suppressWarnings(suppressMessages(print(plot(ap))))

Here you see the affect of dosing on the outcome more clearly than the traditional individual predictions.

This also works with multiple endpoint models:

pk.turnover.emax3 <- function() {
  ini({
    tktr <- log(1)
    tka <- log(1)
    tcl <- log(0.1)
    tv <- log(10)
    ##
    eta.ktr ~ 1
    eta.ka ~ 1
    eta.cl ~ 2
    eta.v ~ 1
    prop.err <- 0.1
    pkadd.err <- 0.1
    ##
    temax <- logit(0.8)
    tec50 <- log(0.5)
    tkout <- log(0.05)
    te0 <- log(100)
    ##
    eta.emax ~ .5
    eta.ec50  ~ .5
    eta.kout ~ .5
    eta.e0 ~ .5
    ##
    pdadd.err <- 10
  })
  model({
    ktr <- exp(tktr + eta.ktr)
    ka <- exp(tka + eta.ka)
    cl <- exp(tcl + eta.cl)
    v <- exp(tv + eta.v)
    emax = expit(temax+eta.emax)
    ec50 =  exp(tec50 + eta.ec50)
    kout = exp(tkout + eta.kout)
    e0 = exp(te0 + eta.e0)
    ##
    DCP = center/v
    PD=1-emax*DCP/(ec50+DCP)
    ##
    effect(0) = e0
    kin = e0*kout
    ##
    d/dt(depot) = -ktr * depot
    d/dt(gut) =  ktr * depot -ka * gut
    d/dt(center) =  ka * gut - cl / v * center
    d/dt(effect) = kin*PD -kout*effect
    ##
    cp = center / v
    cp ~ prop(prop.err) + add(pkadd.err)
    effect ~ add(pdadd.err) | pca
  })
}

# Like the prior model, we wish to suppress messages too:
fit.TOS <- suppressMessages(nlmixr(pk.turnover.emax3, warfarin, "saem", control=list(print=0),
                                   table=list(cwres=TRUE, npde=TRUE)))

Just like you can see the individuals from the standard plot:

p <- plot(fit.TOS)

# Here I am subsetting the plots to show only individual plots
cp <- p[["Endpoint:  cp"]]
pca <- p[["Endpoint:  pca"]]

# In this case the list of plots is named starting with "individual"
w <- which(vapply(names(cp), function(x) grepl("individual", x), logical(1)))

# This creates a new list of plots, and changes it to the same class
# as output by nlmixr2
cp <- lapply(w,function(x) cp[[x]])
class(cp) <- "nlmixr2PlotList"


# In this case the list of plots is named starting with "individual"
w <- which(vapply(names(pca), function(x) grepl("individual", x), logical(1)))

# This creates a new list of plots, and changes it to the same class
# as output by nlmixr2
pca <- lapply(w, function(x) pca[[x]])
class(pca) <- "nlmixr2PlotList"



#This is a hack to suppress the warnings & messages from the plot function
suppressMessages(suppressWarnings(print(cp)))

suppressMessages(suppressWarnings(print(pca)))

Now you see the augmented predictions separated by endpoint:

suppressWarnings(suppressMessages(plot(augPred(fit.TOS))))

In this case, where there is more rich data, the differences are a bit less drastic than the phenobarbitol example, but still more full profiles are present for patients who discontinued when using augPred()

Also, as a note, the plots made inside nlmixr2 are subset by endpoints and can be subset to which-ever plot that you wish to have. Perhaps someday easier method (with say filter()) can be used to select the correct plots.

Overall, augPred() is an easy and useful way to add more complete predictions to any model.

Posted on:
March 31, 2025
Length:
5 minute read, 906 words
Categories:
nlmixr2
See Also: