Skip to contents

Why multiple studies?

Passing several studies to admControl() fits the model to all of them simultaneously, minimising the sum of per-study NLLs under a shared set of population parameters. This is the core use case for aggregate-data modelling: you have summary statistics from multiple trials (which may differ in dose, sample size, or observation schedule) and want a single population model consistent with all of them.

Splitting examplomycin into two cohorts

We partition the 500 examplomycin subjects into two cohorts of 250 and compute separate aggregate statistics for each:

library(admixr2)
library(rxode2)
library(nlmixr2)
library(ggplot2)

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 <- matrix(NA_real_, nrow = length(ids), ncol = length(times))
for (i in seq_along(ids)) {
  sub         <- obs[obs$ID == ids[i], ]
  dv_mat[i, ] <- sub$DV[order(sub$TIME)]
}

# Alternate subjects into two equal cohorts
idx1 <- seq(1, length(ids), by = 2)   # rows 1, 3, 5, ... → cohort 1
idx2 <- seq(2, length(ids), by = 2)   # rows 2, 4, 6, ... → cohort 2

E1 <- colMeans(dv_mat[idx1, ]); V1 <- cov(dv_mat[idx1, ]); n1 <- length(idx1)
E2 <- colMeans(dv_mat[idx2, ]); V2 <- cov(dv_mat[idx2, ]); n2 <- length(idx2)

Comparing observed profiles across cohorts

Before fitting, visualise the raw summary statistics to confirm the two cohorts are comparable (both drawn from the same population here):

df_obs <- rbind(
  data.frame(cohort = "Cohort 1", time = times,
             mean = E1,
             lo   = E1 - sqrt(diag(V1)),
             hi   = E1 + sqrt(diag(V1))),
  data.frame(cohort = "Cohort 2", time = times,
             mean = E2,
             lo   = E2 - sqrt(diag(V2)),
             hi   = E2 + sqrt(diag(V2)))
)

ggplot(df_obs, aes(x = time, y = mean, colour = cohort, fill = cohort)) +
  geom_ribbon(aes(ymin = lo, ymax = hi), alpha = 0.15, colour = NA) +
  geom_line(linewidth = 1) +
  geom_point(size = 2.5) +
  scale_x_log10(breaks = times, labels = times) +
  scale_colour_manual(values = c("Cohort 1" = "#0072B2", "Cohort 2" = "#D55E00")) +
  scale_fill_manual(  values = c("Cohort 1" = "#0072B2", "Cohort 2" = "#D55E00")) +
  labs(title    = "Observed mean ± 1 SD by cohort",
       x        = "Time (h, log scale)",
       y        = "Concentration",
       colour   = NULL, fill = NULL) +
  theme_bw()
Observed mean ± 1 SD for each cohort on a log time axis.

Observed mean ± 1 SD for each cohort on a log time axis.

Model definition

pk_model <- function() {
  ini({
    tcl     <- log(5)  ; label("Log clearance (L/hr)")
    tv1     <- log(10) ; label("Log central volume (L)")
    tv2     <- log(30) ; label("Log peripheral volume (L)")
    tq      <- log(10) ; label("Log inter-compartmental CL (L/hr)")
    tka     <- log(1)  ; label("Log absorption rate constant (1/hr)")
    prop.sd <- c(0, 0.2); label("Proportional residual error SD")
    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)
  })
}

Fitting with two studies

Pass both cohorts as a named list. Each entry may independently specify times, ev, V, n, and method:

fit_multi <- nlmixr2(
  pk_model, admData(), est = "admc",
  control = admControl(
    studies = list(
      cohort1 = list(E = E1, V = V1, n = n1,
                     times = times, ev = rxode2::et(amt = 100)),
      cohort2 = list(E = E2, V = V2, n = n2,
                     times = times, ev = rxode2::et(amt = 100))
    ),
    n_sim     = 5000L,
    cov_n_sim = 10000L,
    maxeval   = 300L,
    seed      = 1L
  )
)
#> [====|====|====|====|====|====|====|====|====|====] 0:00:07 
#> 
#> [====|====|====|====|====|====|====|====|====|====] 0:00:07

print(fit_multi)
#> ── nlmix admc ──
#> 
#>           OBJF       AIC       BIC Log-likelihood
#> admc -3672.877 -3650.877 -3580.346       1836.438
#> 
#> ── Time (sec fit_multi$time): ──
#> 
#>   optimize covariance elapsed
#> 1    90.54     21.165 111.705
#> 
#> ── Population Parameters (fit_multi$parFixed or fit_multi$parFixedDf): ──
#> 
#>                                   Parameter    Est.      SE  %RSE
#> tcl                    Log clearance (L/hr)   1.601 0.01637 1.022
#> tv1                  Log central volume (L)   2.316 0.08773 3.787
#> tv2               Log peripheral volume (L)   3.402 0.04027 1.184
#> tq        Log inter-compartmental CL (L/hr)   2.284 0.02134 0.934
#> tka     Log absorption rate constant (1/hr) 0.02593 0.08251 318.1
#> prop.sd      Proportional residual error SD  0.1988              
#>         Back-transformed(95%CI) BSV(CV%) Shrink(SD)%
#> tcl         4.958 (4.802, 5.12)     32.9            
#> tv1        10.14 (8.536, 12.04)     33.8            
#> tv2        30.01 (27.74, 32.48)     32.1            
#> tq         9.819 (9.417, 10.24)     33.5            
#> tka        1.026 (0.873, 1.206)     31.3            
#> prop.sd                  0.1988                     
#>  
#>   Covariance Type (fit_multi$covMethod): r
#>   No correlations in between subject variability (BSV) matrix
#>   Full BSV covariance (fit_multi$omega) 
#>     or correlation (fit_multi$omegaR; diagonals=SDs)
#>   Distribution stats (mean/skewness/kurtosis/p-value) available in $shrink 
#>   Censoring (fit_multi$censInformation): No censoring
#>   Minimization message (fit_multi$message):  
#>     NLOPT_XTOL_REACHED: Optimization stopped because xtol_rel or xtol_abs (above) was reached.

Per-study diagnostic plots

plot() automatically produces separate panels for each study. Panel names follow the pattern mean_<study> and cov_<study>:

plots <- plot(fit_multi, which = "mean")
Mean diagnostics for both cohorts (one panel per study).

Mean diagnostics for both cohorts (one panel per study).

Mean diagnostics for both cohorts (one panel per study).

Mean diagnostics for both cohorts (one panel per study).

names(plots)
#> [1] "mean_cohort1" "mean_cohort2"

Access individual panels to compare studies side by side:

plots$mean_cohort1
plots$mean_cohort2

# Combine with patchwork if installed
if (requireNamespace("patchwork", quietly = TRUE)) {
  patchwork::wrap_plots(plots, ncol = 1)
}

Different doses and schedules

Studies may differ in any aspect. A typical multi-study setup from a drug development programme:

fit_program <- nlmixr2(
  pk_model, admData(), est = "admc",
  control = admControl(
    studies = list(
      phase1_50mg  = list(E = E_50,  V = V_50,  n = 30L,
                          times = c(1, 2, 4, 8),
                          ev    = rxode2::et(amt = 50)),
      phase2_100mg = list(E = E_100, V = V_100, n = 120L,
                          times = c(0.5, 1, 2, 4, 8, 12),
                          ev    = rxode2::et(amt = 100)),
      phase2_200mg = list(E = E_200, V = V_200, n = 115L,
                          times = c(0.5, 1, 2, 4, 8, 12),
                          ev    = rxode2::et(amt = 200))
    ),
    n_sim   = 5000L,
    maxeval = 1000L,
    seed    = 1L
  )
)

Studies with a diagonal V (or a plain vector of variances) are automatically assigned method = "var", avoiding the O(n_t³) Cholesky solve when the off-diagonal covariance structure is unavailable.