Hello,
I am fitting a non-linear “one compartment extravenous pharmacokinetic model” to drug data. It’s a time series – basically following a certain drug Dose, D, the body incorporates the drug over Time, T, given a certain absorption rate, K_a, but simultaneously eliminates the drug following an elimination rate, K_e, and a clearance, C_l:
\frac{D K_e K_a (e^{-K_e T} - e^{-K_a T})}{C_l (K_a - K_e)}
The model seems to be doing a good job using brms
(please see fully reproducible example below), but there are two conditions that I would like to implement to improve the likelihood estimates:
1 – Reject values of mu
that are negative, simply because the response variable is drug concentration in the body, hence it cannot be negative. I tried fitting the model using a Gamma distribution, but it returns unrealistic values and I do not know why. Is there a way to fit it using the current Gaussian distribution but rejecting parameter estimates when mu < 0
?
2 – As far as I understand this equation, the actual model fit can be achieved by multiple combinations of K_a, K_e, and C_l, making this a complicated parameter space to get right answers. I have successfully implemented an initial constraint on the prior of K_a (see below), but I would also like to add a second condition here that depends on a secondary parameter that is calculated based on the estimated parameters at each iteration. Basically, one can derive the “time to the peak” (T_p) and the “peak” (P):
T_p = \frac{ln(K_a) - ln(K_e)}{K_a - K_e} # in time units
P = \frac{D K_e K_a (e^{-K_e T_p} - e^{-K_a T_p})}{C_l (K_a - K_e)} # in concentration units
Given these two equations, one can calculate how efficient (E) an organism is at absorbing the drug up to the peak:
E = \frac{P}{K_a T_p}
Efficiencies by definition are bounded between 0 and 1, so I would like to add the above calculations to the brms
code to generate some type of rejection statement, e.g.:
if (E < 0 || E > 1)
reject("E is bounded between 0 and 1; found E=", E);
I would really appreciate your assistance implementing conditions 1 and 2. Here’s the fully reproducible example, but first here’s my sessionInfo()
:
sessionInfo()
R version 3.5.3 (2019-03-11)
Platform: x86_64-apple-darwin15.6.0 (64-bit)
Running under: macOS Mojave 10.14.4
Matrix products: default
BLAS: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRblas.0.dylib
LAPACK: /Library/Frameworks/R.framework/Versions/3.5/Resources/lib/libRlapack.dylib
locale:
[1] en_AU.UTF-8/en_AU.UTF-8/en_AU.UTF-8/C/en_AU.UTF-8/en_AU.UTF-8
attached base packages:
[1] stats graphics grDevices utils datasets methods base
other attached packages:
[1] brms_2.8.0 Rcpp_1.0.1
loaded via a namespace (and not attached):
[1] mvtnorm_1.0-10 lattice_0.20-38 prettyunits_1.0.2 ps_1.3.0 zoo_1.8-5 gtools_3.8.1 assertthat_0.2.1
[8] digest_0.6.18 mime_0.6 R6_2.4.0 plyr_1.8.4 backports_1.1.3 ggridges_0.5.1 stats4_3.5.3
[15] coda_0.19-2 ggplot2_3.1.0 colourpicker_1.0 pillar_1.3.1 rlang_0.3.3 lazyeval_0.2.2 miniUI_0.1.1.1
[22] callr_3.2.0 Matrix_1.2-15 DT_0.5 shinythemes_1.1.2 shinyjs_1.0 stringr_1.4.0 htmlwidgets_1.3
[29] loo_2.1.0 igraph_1.2.4 munsell_0.5.0 shiny_1.2.0 compiler_3.5.3 httpuv_1.5.0 rstan_2.18.2
[36] pkgconfig_2.0.2 base64enc_0.1-3 pkgbuild_1.0.3 rstantools_1.5.1 htmltools_0.3.6 tidyselect_0.2.5 tibble_2.1.1
[43] gridExtra_2.3 threejs_0.3.1 matrixStats_0.54.0 crayon_1.3.4 dplyr_0.8.0.1 later_0.8.0 grid_3.5.3
[50] nlme_3.1-137 xtable_1.8-3 gtable_0.3.0 magrittr_1.5 StanHeaders_2.18.1 scales_1.0.0 cli_1.1.0
[57] stringi_1.4.3 reshape2_1.4.3 promises_1.0.1 dygraphs_1.1.1.6 xts_0.11-2 tools_3.5.3 glue_1.3.1
[64] shinystan_2.5.0 markdown_0.9 purrr_0.3.2 crosstalk_1.0.0 processx_3.3.0 rsconnect_0.8.13 abind_1.4-5
[71] parallel_3.5.3 inline_0.3.15 colorspace_1.4-1 bridgesampling_0.6-0 bayesplot_1.6.0 Brobdingnag_1.2-6
library(brms)
rstan::rstan_options(auto_write = TRUE)
options(mc.cores = parallel::detectCores())
calculateMinimumlKa <- function (data, minimumScaling = 1) {
dt <- data$Time_days[which.max(data$Drug_concentration_mg.ml)] - data$Time_days[1]
dy <- max(data$Drug_concentration_mg.ml) - data$Drug_concentration_mg.ml[1]
log(ifelse(dy == 0, data$Drug_concentration_mg.ml[1], dy) / ifelse(dt == 0, data$Time_days[1], dt) * minimumScaling)
}
data <- read.csv(text =
'Drug_concentration_mg.ml,Dose_mg,Time_days
0.0006773291,13.57695,0.01
0.4894254798,13.57695,0.88
1.0282572998,13.57695,1.24
1.6008287008,13.57695,1.77
1.5079260525,13.57695,2.10
1.4089264576,13.57695,2.85
1.1498402861,13.57695,3.93
0.8579193906,13.57695,4.95
0.5450494898,13.57695,7.05
0.2563178348,13.57695,11.97
0.1634733689,13.57695,15.98
0.0706932426,13.57695,23.74
0.0774802343,13.57695,28.96
0.0364683985,13.57695,53.01',
header = TRUE, stringsAsFactors = FALSE)
minLKa <- calculateMinimumlKa(data)
maxLKa <- log(exp(minLKa) * 1e3)
kineticsPriors <- c(prior(normal(0, 2), nlpar = 'lKe'),
eval(parse(text = paste0('prior(normal(', minLKa * 1.5, ', 5), nlpar = \'lKa\', lb = ', minLKa, ', ub = ', maxLKa,')'))),
prior(normal(10, 5), nlpar = 'lCl'))
kineticsInits <- list(list(lKe = -1, lKa = -4, lCl = 2),
list(lKe = 0, lKa = -1, lCl = 15),
list(lKe = -5, lKa = 6, lCl = 10))
# define one compartment extravenous pharmacokinetic model
# notice that parameters are fitted on the log scale to force them to be positive
kineticsEquation <- brms::bf(Drug_concentration_mg.ml ~ Dose_mg * exp(lKe + lKa - lCl) * (exp(-exp(lKe) * Time_days) - exp(-exp(lKa) * Time_days)) / (exp(lKa) - exp(lKe)), lKa ~ 1, lKe ~ 1, lCl ~ 1, nl = TRUE)
set.seed(1)
model <- brms::brm(kineticsEquation, data = data, family = gaussian(), prior = kineticsPriors, sample_prior = TRUE, chains = 3, cores = 3, iter = 1.5e4, warmup = 7.5e3, inits = kineticsInits, control = list(adapt_delta = 0.9))
# add time to peak, and peak, then plot the data
pars <- as.data.frame(t(brms::fixef(model))['Estimate', , drop = FALSE])
names(pars) <- gsub('_Intercept', '', names(pars))
T_p <- (pars$lKa - pars$lKe) / (exp(pars$lKa) - exp(pars$lKe)) # time to peak in days
P <- data$Dose_mg[1] * exp(pars$lKe + pars$lKa - pars$lCl) * (exp(-exp(pars$lKe) * T_p) - exp(-exp(pars$lKa) * T_p)) / (exp(pars$lKa) - exp(pars$lKe)) # peak in mg/ml
xseq <- seq(min(data$Time_days), max(data$Time_days), length.out = 200)
modelPrediction <- predict(model, newdata = data.frame(Time_days = xseq, Dose_mg = unique(data$Dose_mg)), re_formula = NA, summary = TRUE, probs = c(0.025, 0.975))
transparentCol <- rgb(255, 99, 71, max = 255, alpha = 125)
plot(data$Time_days, data$Drug_concentration_mg.ml, ylim = range(modelPrediction), xlab = 'Time (days)', ylab = 'Drug concentration (mg / ml)', las = 1, pch = 16, col = 'tomato')
polygon(c(xseq, rev(xseq), xseq[1]), c(modelPrediction[, 'Q2.5'], rev(modelPrediction[, 'Q97.5']), modelPrediction[1, 'Q2.5']), border = NA, col = transparentCol) # notice negative values on posterior predictions
lines(xseq, modelPrediction[, 'Estimate'], lty = 2, lwd = 1.5, col = 'tomato')
usr <- par('usr')
polygon(c(usr[1:2], rev(usr[1:2]), usr[1]), c(rep(usr[3], 2), rep(0, 2), usr[3]), col = rgb(77, 77, 77, max = 255, alpha = 50))
lines(usr[1:2], c(0, 0), lty = 2, col = 'grey30')
text(mean(usr[1:2]), -0.1, 'Unrealistic posterior values', adj = c(0.5, 0.5), cex = 2)
postSamps <- brms::posterior_samples(model)
lKa <- postSamps[, 'b_lKa_Intercept']
lKe <- postSamps[, 'b_lKe_Intercept']
lCl <- postSamps[, 'b_lCl_Intercept']
postT_p <- (lKa - lKe) / (exp(lKa) - exp(lKe))
postP <- data$Dose_mg[1] * exp(lKe + lKa - lCl) * (exp(-exp(lKe) * postT_p) - exp(-exp(lKa) * postT_p)) / (exp(lKa) - exp(lKe))
postE <- postP / (exp(lKa) * postT_p)
hist(postE, xlab = 'Efficiency of drug absorption', xaxs = 'i', yaxs = 'i', las = 1, main = '', xpd = NA)
usr <- par('usr')
polygon(c(c(1, usr[2]), rev(c(1, usr[2])), 1), c(rep(0, 2), rep(usr[4], 2), 0), col = rgb(77, 77, 77, max = 255, alpha = 50))
lines(c(1, usr[2]), c(0, 0), lty = 2, col = 'grey30')
text(mean(c(1, usr[2])), mean(usr[3:4]), 'Unrealistic efficiency values', adj = c(0.5, 0.5), cex = 2, srt = 90)