Skip to content

Latest commit

 

History

History
442 lines (320 loc) · 9.48 KB

tools_optimization_indomethacin.md

File metadata and controls

442 lines (320 loc) · 9.48 KB

Pooled analysis of indomethacin PK data

Metrum Research Group

Packages

library(tidyverse)
theme_set(theme_bw())
library(mrgsolve)

Load indomethacin data set

data(Indometh)
  • Take a look at what is there
head(Indometh)
. Grouped Data: conc ~ time | Subject
.   Subject time conc
. 1       1 0.25 1.50
. 2       1 0.50 0.94
. 3       1 0.75 0.78
. 4       1 1.00 0.48
. 5       1 1.25 0.37
. 6       1 2.00 0.19
count(Indometh, Subject)
. Grouped Data: conc ~ time | Subject
.   Subject  n
. 1       1 11
. 2       4 11
. 3       2 11
. 4       5 11
. 5       6 11
. 6       3 11
ggplot(Indometh, aes(time,conc,group=Subject)) + 
  geom_point() + geom_line() +
  scale_y_continuous(trans = "log", breaks = 10^seq(-4,4))

This is individual-level data, but we are going to do naive pooled analysis.

Data assembly

data <- readRDS("data/indometh.RDS")

head(data)
.   time conc evid cmt ID amt
. 1 0.00   NA    1   2  1  25
. 2 0.25 1.50    0   0  1  NA
. 3 0.50 0.94    0   0  1  NA
. 4 0.75 0.78    0   0  1  NA
. 5 1.00 0.48    0   0  1  NA
. 6 1.25 0.37    0   0  1  NA

Load a PK model

  • We’ll try out one-compartment first
mod <- modlib("pk1")

param(mod)
. 
.  Model parameters (N=3):
.  name value . name value
.  CL   1     | V    20   
.  KA   1     | .    .

Pick some parameters to estimate:

theta <- log(c(CL = 1, V = 100))
names(theta)
. [1] "CL" "V"

Create an objective function function

  • For starters, just do OLS estimation
  • Note that we need to name the parameters (p)
    • Parameter updates require names in mrgsolve
    • Generally, don’t expect p to retain any names that you might pass in through the initial estimates
  • We also pass in the data and the dependent variable (dv)
obj <- function(p, theta, data, dv ="conc", pred = FALSE) {
  
  names(p) <- names(theta)
  
  p <- lapply(p,exp)
  
  mod <- param(mod, p)
  
  out <- mrgsim_q(mod, data, output="df")
  
  if(pred) return(out)
  
  sqr <- (out[["CP"]] - data[[dv]])^2
  
  sum(sqr, na.rm=TRUE)
}

Fit with one-compartment model

  • First generate some initial estimates
  • These need to be named in a way that is consistent with the model we are using
  • I usually run a test with the objective function function to make sure the logic works out
obj(theta,theta,data)
. [1] 33.69619
  • Nelder-Mead optimization
fit <- optim(par = theta, fn=obj, theta = theta, data=data)
  • And generate some predictions based on the final estimates
pred <- obj(fit$par, theta, data, pred = TRUE)

data$pred <- pred$CP

head(data)
.   time conc evid cmt ID amt      pred
. 1 0.00   NA    1   2  1  25 2.7771715
. 2 0.25 1.50    0   0  1  NA 1.9814023
. 3 0.50 0.94    0   0  1  NA 1.4136524
. 4 0.75 0.78    0   0  1  NA 1.0085852
. 5 1.00 0.48    0   0  1  NA 0.7195857
. 6 1.25 0.37    0   0  1  NA 0.5133960

Make a plot of the output

  • What do you think? Good fit?
ggplot(data = data) + 
  geom_point(aes(time,conc)) + 
  scale_y_log10() + 
  geom_line(aes(time,pred),col="firebrick", lwd=1)

Your turn

  • Try fitting the same indomethacin data with a 2-compartment model
mod <- modlib("pk2")
  • Take a look at the model and generate a call to minqa::newuoa using the OLS objective function above to fit the data

  • You will also need try out a new set of initial estimates for all of the volumes and clearances for 2-compartment, IV bolus model

  • What do you think of the fit using the the OLS objective function?

    • Can you make a simple modification to the OLS objective function that might make the fit look a little better?
  • Suppose we’re worried about the newuoa optimizer and want to try a global search algorithm

    • Can you construct a call to RcppDE::DEoptim that will also fit the data?
    • Remember that DEoptim doesn’t use initial estimates the same way stats::optim or minqa::newuoa does; you have to specify one vector of lower boundaries and one vector of upper boundaries, with a lower and upper bound for each parameter

Answer

  • Set the initial estimates for two compartment model
param(mod)
. 
.  Model parameters (N=5):
.  name value . name value
.  CL   1     | V2   20   
.  KA   1     | V3   10   
.  Q    2     | .    .
theta <- log(c(CL = 2, V2 = 50, Q = 10, V3 = 50))
fit <- optim(par = theta, fn=obj, theta = theta, data=data)
  • And generate some predictions based on the final estimates
pred <- obj(fit$par, theta, data, pred = TRUE)

data$pred <- pred$CP

ggplot(data = data) + 
  geom_point(aes(time,conc)) + 
  scale_y_log10() + 
  geom_line(aes(time,pred),col="firebrick", lwd=1)
. Warning: Removed 6 rows containing missing values (geom_point).

  • Try weighted least squares
obj <- function(p, theta, data, wt, pred = FALSE) {
  names(p) <- names(theta)
  p <- lapply(p,exp)
  out <- mod %>% param(p) %>% mrgsim_q(data, output="df")
  if(pred) return(out)
  return(sum(((out$CP - data[["conc"]])*wt)^2, na.rm=TRUE))
}
dv <- data[["conc"]]

fit_wt <- minqa::newuoa(par = theta, fn=obj, theta = theta, data=data, wt=1/dv)

Final estimates and final value of objective function

exp(fit_wt$par)
. [1]  8.926545  8.873479  6.319330 19.684318
obj(fit_wt$par,theta,data,dv)
. [1] 8.630392
  • Generate predictions for the final and initial estimates
pred <-  obj(fit$par, theta, data, wt = 1/dv, pred = TRUE)
predi <- obj(theta,  theta, data, wt = 1/dv, pred = TRUE)
predw <- obj(fit_wt$par, theta, data, wt = 1/dv, pred = TRUE) 


data$pred <- pred$CP
data$predi <- predi$CP
data$predw <- predw$CP
head(data)
.   time conc evid cmt ID amt      pred     predi     predw
. 1 0.00   NA    1   2  1  25 3.2236198 0.5000000 2.8173842
. 2 0.25 1.50    0   0  1  NA 2.0547326 0.4714730 1.8483732
. 3 0.50 0.94    0   0  1  NA 1.3586833 0.4456942 1.2371868
. 4 0.75 0.78    0   0  1  NA 0.9405602 0.4223898 0.8505654
. 5 1.00 0.48    0   0  1  NA 0.6860398 0.4013128 0.6049255
. 6 1.25 0.37    0   0  1  NA 0.5280509 0.3822414 0.4478397
  • Plot the predictions
pred <- distinct(data, time, .keep_all = TRUE)

ggplot(data = data) + 
  geom_point(aes(time,conc)) + 
  scale_y_log10() + 
  geom_line(data=pred,aes(time,pred),col="black", lwd=1, alpha = 0.6) +
  geom_line(data=pred,aes(time,predi),col="darkgreen", lwd=1) + 
  geom_line(data = pred, aes(time,predw), col="firebrick", lwd = 1)
. Warning: Removed 6 rows containing missing values (geom_point).

Fit the data with RcppDE::DEoptim

fit <- DEoptim::DEoptim(
  obj, 
  lower = rep(-4,4), 
  upper = rep(4,4), 
  theta = theta, data = data, wt = 1/dv, 
  control = DEoptim::DEoptim.control(itermax=120,trace=20)
)

Check the estimates and the final value of the objective function

tibble(
  DE = exp(fit$optim$bestmem), 
  Nelder = exp(fit_wt$par)
)

tibble(
  DE = obj(fit$optim$bestmem, theta,data,1/dv),
  Nelder = obj(fit_wt$par, theta, data, 1/dv)
)

Some global search with NLOPTR

https://nlopt.readthedocs.io/en/latest/NLopt_Algorithms/

library(nloptr)
a0 <- obj(theta,theta=theta,data=data,wt = 1/dv, pred = FALSE)

lowr <- rep(-5,length(theta))
uppr <- rep(5, length(theta))

x <- isres(
  x0 = theta, 
  fn=obj, 
  lower = lowr, 
  upper = uppr, 
  theta=theta, 
  data=data, 
  wt = 1/dv, 
  maxeval=20000
)

y <- crs2lm(
  x0 = theta, 
  fn=obj, 
  lower = lowr, 
  upper = uppr, 
  theta=theta, 
  data=data, 
  wt = 1/dv, 
  maxeval=5000
)

z <- newuoa(x0 = y$par, fn = obj,theta = theta, data = data, wt = 1/dv)


tibble(a0 = theta, a = fit_wt$par, x = x$par, y = y$par, z= z$par) %>% exp
tibble(a0 = a0,a = fit_wt$fval, x = x$value, y= y$value, z = z$value)


direct <- directL( 
  fn = obj, 
  lower = lowr, 
  upper = uppr, 
  theta = theta, 
  data = data, 
  wt = 1/dv, 
  control = list(maxeval=2500)
)


tibble(a0 = theta, a = fit_wt$par, x = x$par, y = y$par, z= z$par,d = direct$par) %>% exp
tibble(a0 = a0,a = fit_wt$fval, x = x$value, y= y$value, z = z$value, d = direct$value)