Skip to contents

Model and source

#> ℹ parameter labels from comments will be replaced by 'label()'
  • Citation: Bustinduy AL, Kolamunnage-Dona R, Mirochnick MH, Capparelli EV, Tallo V, Acosta LP, Olveda RM, Friedman JF, Hope WW. Population pharmacokinetics of praziquantel in pregnant and lactating Filipino women infected with Schistosoma japonicum. Antimicrob Agents Chemother. 2020;64(9):e00566-20. doi:10.1128/AAC.00566-20. PMCID: PMC7449211.

  • Description: Two-compartment oral population PK model with a first-order absorption (gut) compartment, an absorption lag and a reversible breast-milk compartment for praziquantel (racemic, total PZQ) in 45 Filipino women with Schistosoma japonicum infection – 15 in early pregnancy (12-16 weeks gestation), 15 in late pregnancy (30-36 weeks) and 15 lactating postpartum women (5-7 months postpartum) – given 60 mg/kg as two 30 mg/kg oral doses 3 h apart. Plasma and breast milk were co-modelled non-parametrically with NPAG in Pmetrics. Drug moves from central to milk and back with first-order rate constants and is NOT eliminated through milk (the authors’ identifiability choice), so the milk compartment behaves as a sampled second peripheral compartment with its own apparent volume. Clearance and volumes are apparent (CL/F, Vc/F, Vmilk/F); bioavailability was not estimated. No covariate was retained: weight showed no relationship with CL/F or Vc/F, and the higher CL/F of early-pregnancy women was reported only as a post hoc comparison of Bayesian posteriors, not built into the model. Residual variability is fixed(0) because the Pmetrics assay-error model was not published.

  • Article: https://doi.org/10.1128/AAC.00566-20 (open access; PMCID PMC7449211)

This is the first description of praziquantel (PZQ) pharmacokinetics in pregnant and lactating women. It was nested in a randomised controlled trial of PZQ in pregnancy in Leyte, Philippines. Women with Schistosoma japonicum received the trial regimen of 60 mg/kg, given as two 30 mg/kg oral doses 3 h apart. Plasma PK was studied in three cohorts of 15 women each: early pregnancy, late pregnancy and lactating postpartum. Breast milk was also sampled in the lactating cohort. Plasma and milk were co-modelled with the non-parametric NPAG algorithm in Pmetrics.

The same senior author’s group published the paediatric model Bustinduy_2016_praziquantel (Ugandan children, S. mansoni). The two models share the gut / central / peripheral skeleton and Pmetrics parameter names. This paper adds a breast-milk compartment.

Population

45 women entered the PK analysis. Forty-seven were enrolled; two early-pregnancy women vomited shortly after dosing and were not sampled. The Table 1 baseline data cover all 47 enrolled women. Median age was 24.0 years (range 18-44) and mean weight 48.5 kg (SD 7.69); cohort means were 47.6, 51.5 and 46.6 kg. All participants were Asian (Filipino) and non-Hispanic. Infection intensity was low (< 100 eggs per gram) in 46 of the 47 and moderate in one. The early-pregnancy cohort was 12-16 weeks gestation, the late-pregnancy cohort 30-36 weeks, and the lactating cohort 5-7 months postpartum.

Population metadata carried in the model file.
Field Value
species human
n_subjects 45
n_studies 1
age_range 18-44 years
age_median 24.0 years (mean 25.5, SD 6.39; 47 enrolled)
weight_range approximately 36-63 kg (read from Fig. 5; not tabulated)
weight_median 47.9 kg (median used for the Monte Carlo simulations); mean 48.5 kg (SD 7.69; 47 enrolled)
sex_female_pct 100
race_ethnicity Asian (Filipino), 100%
disease_state Stool-positive Schistosoma japonicum infection, otherwise healthy; infection intensity low (< 100 eggs per gram) in 46 of 47 enrolled and moderate in 1.
dose_range 60 mg/kg praziquantel given orally as two 30 mg/kg doses approximately 3 h apart, after a carbohydrate-rich snack. Absolute total doses about 2100-3800 mg (Fig. 6A).
regions Philippines (northeastern Leyte)
reproductive_status Early pregnancy 12-16 weeks gestation (n = 15 analysed; 17 enrolled, 2 vomited and were not sampled), late pregnancy 30-36 weeks gestation (n = 15), lactating 5-7 months postpartum (n = 15).
notes Baseline demographics from Table 1 (47 enrolled). Plasma sampled pre-dose and at 1, 2, 3 (before the second dose), 4, 5, 6, 7, 8, 9, 12, 15 and 24 h after the first dose; breast milk hand-expressed at 3, 6, 9, 12, 15 and 24 h in the lactating group only. LC-MS assay; LLOQ 31.3 ng/mL in plasma and 4.3 ng/mL in milk (Methods). Posterior CL/F was higher in early pregnancy (median about 425 L/h vs about 245 L/h; Fig. 6C) and AUC0-24 lower (median about 7 vs 11-12 mg*h/L; Fig. 6D), but this was not encoded as a covariate effect.

Source trace

All structural values are the mean of the NPAG parameter distribution in Table 2. The next section explains why the mean is used rather than the median.

Equation / parameter Value Source location
lka (Ka) 2.012 1/h Table 2, row Ka (h-1), mean (median 0.395)
lcl (SCL/F) 324.075 L/h Table 2, row SCL/F (liter/h), mean (median 277.447)
lvc (Vc/F) 183.006 L Table 2, row Vc/F (liter), mean (median 142.618)
lk12 (Kcp) 19.313 1/h Table 2, row Kcp (h-1), mean (median 18.941)
lk21 (Kpc) 15.816 1/h Table 2, row Kpc (h-1), mean (median 13.996)
lk_central_milk (Kcb) 18.750 1/h Table 2, row Kcb (h-1), mean (median 19.301)
lk_milk_central (Kbc) 17.816 1/h Table 2, row Kbc (h-1), mean (median 17.077)
lvmilk (Vb/F) 612.130 L Table 2, row Vb/F (liter), mean (median 563.802)
ltlag (Lag) 0.772 h Table 2, row Lag (h), mean (median 0.868)
lfdepot (F) fixed(log(1)) Not estimated; all clearances and volumes in Table 2 are /F
eta* variances log(CV^2 + 1) Table 2 CV (%) column (see identity check below)
propSd, addSd, propSd_Cmilk, addSd_Cmilk fixed(0) Not reported; see Errata
d/dt(depot), d/dt(central), d/dt(peripheral1), d/dt(milk) n/a Methods, equations (1)-(4), with the typesetting corrections listed in Errata
alag(depot) n/a Methods: “A lag function … was applied between the oral administration of PZQ and the appearance of drug in the central compartment”
Cc <- central / vc n/a Methods, output equation Y(1) (printed with X(1); see Errata)
Cmilk <- milk / vmilk n/a Methods, output equation Y(2) = X(4) / Vb
No covariates n/a Results: “Hence, covariates were not incorporated into the structural model”

Table 2 prints CV (%) alongside Mean and SD. For every row, SD / Mean reproduces the printed CV to the rounding of the printed SD. So the CV is on the linear scale, which supports the log(CV^2 + 1) conversion.

tab2 <- tibble::tribble(
  ~parameter, ~mean, ~median, ~sd, ~cv_pct,
  "Ka", 2.012, 0.395, 4.301, 213.750,
  "SCL/F", 324.075, 277.447, 175.373, 54.115,
  "Vc/F", 183.006, 142.618, 93.211, 50.933,
  "Kcp", 19.313, 18.941, 10.167, 52.644,
  "Kpc", 15.816, 13.996, 9.447, 59.733,
  "Kcb", 18.750, 19.301, 9.387, 50.067,
  "Kbc", 17.816, 17.077, 7.845, 44.031,
  "Vb/F", 612.130, 563.802, 395.661, 64.637,
  "Lag", 0.772, 0.868, 0.233, 30.202
) |>
  mutate(
    cv_from_sd_over_mean = 100 * sd / mean,
    omega2 = log((cv_pct / 100)^2 + 1)
  )

# The printed CV% is SD/mean on the linear scale. The largest gap is Lag (the SD
# is printed to only three digits, 0.233).
stopifnot(max(abs(tab2$cv_from_sd_over_mean - tab2$cv_pct)) < 0.05)

# The encoded omegas are exactly these values.
om <- diag(ui$omega)
stopifnot(max(abs(om - tab2$omega2)) < 1e-5)

tab2 |>
  dplyr::rename(
    "Parameter" = parameter,
    "Mean" = mean,
    "Median" = median,
    "SD" = sd,
    "CV% (printed)" = cv_pct,
    "100 x SD/mean" = cv_from_sd_over_mean,
    "omega^2 encoded" = omega2
  ) |>
  knitr::kable(digits = 4, caption = "Table 2 of Bustinduy 2020, with the CV% identity check.")
Table 2 of Bustinduy 2020, with the CV% identity check.
Parameter Mean Median SD CV% (printed) 100 x SD/mean omega^2 encoded
Ka 2.012 0.395 4.301 213.750 213.7674 1.7172
SCL/F 324.075 277.447 175.373 54.115 54.1149 0.2568
Vc/F 183.006 142.618 93.211 50.933 50.9333 0.2306
Kcp 19.313 18.941 10.167 52.644 52.6433 0.2446
Kpc 15.816 13.996 9.447 59.733 59.7307 0.3051
Kcb 18.750 19.301 9.387 50.067 50.0640 0.2237
Kbc 17.816 17.077 7.845 44.031 44.0335 0.1772
Vb/F 612.130 563.802 395.661 64.637 64.6368 0.3491
Lag 0.772 0.868 0.233 30.202 30.1813 0.0873

Structural verification

These checks use typical values with the random effects zeroed. They do not depend on the simulated cohort, so they are tightly gated.

mod <- readModelDb("Bustinduy_2020_praziquantel")
mod_typ <- rxode2::zeroRe(mod)
#> ℹ parameter labels from comments will be replaced by 'label()'

p_mean <- c(
  ka = 2.012, cl = 324.075, vc = 183.006, kcp = 19.313, kpc = 15.816,
  kcb = 18.750, kbc = 17.816, vb = 612.130, tlag = 0.772
)
p_median <- c(
  ka = 0.395, cl = 277.447, vc = 142.618, kcp = 18.941, kpc = 13.996,
  kcb = 19.301, kbc = 17.077, vb = 563.802, tlag = 0.868
)
as_theta <- function(p) {
  c(
    lka = log(p[["ka"]]), lcl = log(p[["cl"]]), lvc = log(p[["vc"]]),
    lk12 = log(p[["kcp"]]), lk21 = log(p[["kpc"]]),
    lk_central_milk = log(p[["kcb"]]), lk_milk_central = log(p[["kbc"]]),
    lvmilk = log(p[["vb"]]), ltlag = log(p[["tlag"]])
  )
}

# The trial regimen: two 30 mg/kg oral doses 3 h apart. The paper's worked
# numbers use the median weight of 47.9 kg.
wt_ref <- 47.9
# The model declares two error endpoints (Cc and Cmilk), so every observation
# row must nominate one through `dvid`; both concentrations are returned as
# columns on every row regardless.
split_dose <- function(wt, tmax = 72, dt = 0.01) {
  rxode2::et(amt = 30 * wt, time = c(0, 3), cmt = "depot") |>
    rxode2::et(seq(0, tmax, by = dt), cmt = "central") |>
    as.data.frame() |>
    dplyr::mutate(dvid = ifelse(evid == 0, 1L, NA_integer_))
}
solve_typ <- function(theta, wt = wt_ref, tmax = 72) {
  rxode2::rxSolve(mod_typ, split_dose(wt, tmax),
    params = theta,
    returnType = "data.frame", rtol = 1e-10, atol = 1e-14
  )
}

typ <- solve_typ(as_theta(p_mean))
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalcl', 'etalvc', 'etalk12', 'etalk21', 'etalk_central_milk', 'etalk_milk_central', 'etalvmilk', 'etaltlag'
dose_tot <- 60 * wt_ref

trap <- function(y, t) sum(diff(t) * (head(y, -1) + tail(y, -1)) / 2)

# 1. Mass balance. Milk has no elimination, so all drug leaves through CL and
#    AUC(0-inf) of plasma equals Dose / CL (F = 1). By 72 h the typical profile
#    is many half-lives down.
auc_p <- trap(typ$Cc, typ$time)
mb_err <- abs(auc_p / (dose_tot / p_mean[["cl"]]) - 1)
stopifnot(mb_err < 1e-3)

# 2. Milk partition identity. With no milk elimination, the milk AMOUNT
#    integrates to (Kcb / Kbc) x the central AMOUNT, so the concentration AUC
#    ratio is (Kcb / Kbc) x (Vc / Vb). This checks the sign fix to the printed
#    eq. (4). The printed sign would drive milk negative.
auc_m <- trap(typ$Cmilk, typ$time)
ratio_closed <- (p_mean[["kcb"]] / p_mean[["kbc"]]) * (p_mean[["vc"]] / p_mean[["vb"]])
ratio_err <- abs(auc_m / auc_p / ratio_closed - 1)
stopifnot(ratio_err < 1e-3, all(typ$Cmilk >= 0))

# 3. Absorption lag: nothing in plasma before Tlag.
stopifnot(all(typ$Cc[typ$time < p_mean[["tlag"]] - 1e-9] == 0))

# 4. The milk compartment is load-bearing for plasma: removing the milk
#    exchange changes the plasma profile.
no_milk <- solve_typ(replace(as_theta(p_mean), "lk_central_milk", log(1e-9)))
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalcl', 'etalvc', 'etalk12', 'etalk21', 'etalk_central_milk', 'etalk_milk_central', 'etalvmilk', 'etaltlag'
milk_effect <- max(abs(no_milk$Cc - typ$Cc)) / max(typ$Cc)
stopifnot(milk_effect > 0.05)

tibble::tibble(
  Check = c(
    "Plasma AUC(0-72) recovers Dose / CL (rel. error)",
    "Milk:plasma AUC ratio vs (Kcb/Kbc)(Vc/Vb) (rel. error)",
    "Closed-form milk:plasma AUC ratio",
    "Plasma profile change when milk exchange is removed (fraction of Cmax)"
  ),
  Value = signif(c(mb_err, ratio_err, ratio_closed, milk_effect), 4)
) |>
  knitr::kable(caption = "Deterministic structural checks (all gated).")
Deterministic structural checks (all gated).
Check Value
Plasma AUC(0-72) recovers Dose / CL (rel. error) 0.0000023
Milk:plasma AUC ratio vs (Kcb/Kbc)(Vc/Vb) (rel. error) 0.0000028
Closed-form milk:plasma AUC ratio 0.3146000
Plasma profile change when milk exchange is removed (fraction of Cmax) 0.2524000

Mean or median? What the paper’s own numbers show

Table 2 prints both a mean and a median for each parameter, and they differ a lot for Ka (2.012 vs 0.395 1/h). The Methods say “both the mean and median parameter values were interrogated”. They do not say which vector drives the reported simulations, so the maintainers tested each vector against the paper’s numeric statements.

det_summary <- function(theta, label) {
  s <- solve_typ(theta, tmax = 48)
  s24 <- s[s$time <= 24, ]
  at <- function(t) s$Cmilk[which.min(abs(s$time - t))]
  tibble::tibble(
    vector = label,
    plasma_cmax = max(s$Cc),
    plasma_auc24 = trap(s24$Cc, s24$time),
    milk_cavg24 = trap(s24$Cmilk, s24$time) / 24,
    milk_plasma_ratio = trap(s24$Cmilk, s24$time) / trap(s24$Cc, s24$time),
    milk_thalf = log(2) * 5 / log(at(15) / at(20)),
    milk_c24 = at(24),
    milk_c48 = at(48)
  )
}
mvm <- dplyr::bind_rows(
  det_summary(as_theta(p_mean), "Table 2 mean"),
  det_summary(as_theta(p_median), "Table 2 median"),
  tibble::tibble(
    vector = "Published", plasma_cmax = NA, plasma_auc24 = NA,
    milk_cavg24 = 0.185, milk_plasma_ratio = 0.36, milk_thalf = 1.90,
    milk_c24 = 4e-4, milk_c48 = 3e-7
  )
)
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalcl', 'etalvc', 'etalk12', 'etalk21', 'etalk_central_milk', 'etalk_milk_central', 'etalvmilk', 'etaltlag'
#> ℹ omega/sigma items treated as zero: 'etalka', 'etalcl', 'etalvc', 'etalk12', 'etalk21', 'etalk_central_milk', 'etalk_milk_central', 'etalvmilk', 'etaltlag'
mvm |>
  dplyr::mutate(dplyr::across(c(milk_c24, milk_c48), ~ formatC(.x, format = "e", digits = 2))) |>
  dplyr::rename(
    "Parameter vector" = vector,
    "Plasma Cmax (mg/L)" = plasma_cmax,
    "Plasma AUC0-24 (mg*h/L)" = plasma_auc24,
    "Milk Cavg0-24 (mg/L)" = milk_cavg24,
    "Milk:plasma AUC0-24" = milk_plasma_ratio,
    "Milk t1/2 (h)" = milk_thalf,
    "Milk C24 (mg/L)" = milk_c24,
    "Milk C48 (mg/L)" = milk_c48
  ) |>
  knitr::kable(
    digits = 4,
    caption = paste(
      "Typical-value (47.9 kg) predictions from each Table 2 vector against the",
      "paper's numeric statements. The published Cavg and ratio are the mean over",
      "the 15 lactating women's Bayesian posteriors, not typical values."
    )
  )
Typical-value (47.9 kg) predictions from each Table 2 vector against the paper’s numeric statements. The published Cavg and ratio are the mean over the 15 lactating women’s Bayesian posteriors, not typical values.
Parameter vector Plasma Cmax (mg/L) Plasma AUC0-24 (mg*h/L) Milk Cavg0-24 (mg/L) Milk:plasma AUC0-24 Milk t1/2 (h) Milk C24 (mg/L) Milk C48 (mg/L)
Table 2 mean 1.8768 8.8681 0.1163 0.3146 1.3105 2.74e-05 8.41e-11
Table 2 median 1.4454 10.3505 0.1233 0.2859 1.8536 9.40e-04 7.51e-08
Published NA NA 0.1850 0.3600 1.9000 4.00e-04 3.00e-07

# The Results-text milk half-life (1.90 h) and 24 h / 48 h concentrations for
# 'a lactating woman of average weight' match the MEDIAN vector, not the mean:
stopifnot(
  abs(mvm$milk_thalf[2] / 1.90 - 1) < 0.1,
  abs(mvm$milk_thalf[1] / 1.90 - 1) > 0.25
)

The two sets of paper numbers point in different directions:

  • The Results-text breast-milk figures (half-life 1.90 h; 0.0004 mg/L at 24 h; 3e-7 mg/L at 48 h) come from a single typical woman. The median vector reproduces the half-life to within 3% and gets C24 and C48 within about 2-4-fold. The mean vector is off by 30% on the half-life and by orders of magnitude at 48 h.
  • The population Monte Carlo simulation in Figure 7 (1,000 lactating women) is reproduced by the mean vector, as shown below. The median vector’s Ka of 0.395 1/h is 5-fold slower. Once log-normal variability is added, it puts the simulated median plasma peak near 1 mg/L, about half of Figure 7A’s. Pmetrics seeds its Monte Carlo from the population mean vector and covariance matrix by default.

The model file therefore ships the mean vector, because it is the one that reproduces the paper’s population-level simulation. That choice also matches the sibling Bustinduy_2016_praziquantel. The median-vector breast-milk numbers are recorded above for reference.

Virtual cohort

Subject-level data are not public. Three cohorts of 150 women are drawn with the Table 1 weight means and SDs, truncated to the roughly 36-63 kg range visible in Figure 5 (reject-and-redraw). Weight enters only through the mg/kg dose.

# set.seed() seeds R's RNG (the weights) but not rxode2's simulation RNG,
# whose streams depend on the solver thread count. Every assertion below holds
# for any cohort.
set.seed(20200820)
n_per_arm <- 150L

rtnorm <- function(n, mean, sd, lo, hi) {
  x <- stats::rnorm(n * 20, mean, sd)
  x <- x[x >= lo & x <= hi]
  stopifnot(length(x) >= n)
  x[seq_len(n)]
}

tgrid <- c(seq(0, 10, by = 0.1), seq(10.25, 24, by = 0.25))

make_cohort <- function(n, wt_mean, wt_sd, label, id_offset) {
  subj <- tibble::tibble(
    id = id_offset + seq_len(n),
    WT = rtnorm(n, wt_mean, wt_sd, 36, 63),
    treatment = label
  )
  dplyr::bind_rows(
    subj |> tidyr::crossing(time = c(0, 3)) |>
      dplyr::mutate(amt = 30 * WT, evid = 1L, cmt = "depot", dvid = NA_integer_),
    subj |> tidyr::crossing(time = tgrid) |>
      dplyr::mutate(amt = NA_real_, evid = 0L, cmt = "central", dvid = 1L)
  ) |>
    dplyr::arrange(id, time, dplyr::desc(evid))
}

events <- dplyr::bind_rows(
  make_cohort(n_per_arm, 47.6, 8.12, "Early pregnancy", 0L),
  make_cohort(n_per_arm, 51.5, 7.08, "Late pregnancy", n_per_arm),
  make_cohort(n_per_arm, 46.6, 7.40, "Postpartum", 2L * n_per_arm)
)

Simulation

sim <- rxode2::rxSolve(mod, events = events, keep = c("WT", "treatment")) |>
  as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
stopifnot(nrow(sim) > 0, !anyNA(sim$Cc), all(sim$Cc >= -1e-7), all(sim$Cmilk >= -1e-7))

Replicate published figures

# Corresponds to Figure 2 of Bustinduy 2020. The published figure shows the 15
# observed profiles per cohort; 15 simulated women per cohort are drawn here.
show_ids <- sim |>
  dplyr::distinct(id, treatment) |>
  dplyr::group_by(treatment) |>
  dplyr::slice_head(n = 15) |>
  dplyr::pull(id)

fig2 <- dplyr::bind_rows(
  sim |> dplyr::transmute(id, time, treatment, matrix = "Plasma", conc = Cc),
  sim |> dplyr::filter(treatment == "Postpartum") |>
    dplyr::transmute(id, time, treatment, matrix = "Breast milk", conc = Cmilk)
) |>
  dplyr::mutate(panel = paste(treatment, matrix, sep = ": "))

fig2_med <- fig2 |>
  dplyr::group_by(panel, time) |>
  dplyr::summarise(conc = stats::median(conc), .groups = "drop")

ggplot(dplyr::filter(fig2, id %in% show_ids), aes(time, conc)) +
  geom_line(aes(group = id), alpha = 0.3, colour = "grey40") +
  geom_line(data = fig2_med, colour = "#b2182b", linewidth = 1) +
  facet_wrap(~panel, ncol = 2) +
  labs(
    x = "Time after first dose (h)", y = "PZQ (mg/L)",
    caption = "Replicates Figure 2 of Bustinduy 2020 (simulated; red = median)."
  )
Median and individual plasma profiles by cohort, and milk in the postpartum cohort.

Median and individual plasma profiles by cohort, and milk in the postpartum cohort.

# Figure 7 of Bustinduy 2020: 1,000 simulated lactating women at the median
# weight of 47.9 kg; 5th/25th/50th/75th/95th centiles in plasma (A) and milk (B).
# 200 women are simulated here (the per-arm cohort cap).
ev7 <- dplyr::bind_rows(
  tidyr::crossing(id = 1:200, time = c(0, 3)) |>
    dplyr::mutate(amt = 30 * wt_ref, evid = 1L, cmt = "depot", dvid = NA_integer_),
  tidyr::crossing(id = 1:200, time = tgrid) |>
    dplyr::mutate(amt = NA_real_, evid = 0L, cmt = "central", dvid = 1L)
) |>
  dplyr::arrange(id, time, dplyr::desc(evid))
sim7 <- rxode2::rxSolve(mod, events = ev7) |> as.data.frame()

cent7 <- sim7 |>
  tidyr::pivot_longer(c(Cc, Cmilk), names_to = "matrix", values_to = "conc") |>
  dplyr::group_by(matrix, time) |>
  dplyr::summarise(
    p05 = stats::quantile(conc, 0.05), p25 = stats::quantile(conc, 0.25),
    p50 = stats::median(conc), p75 = stats::quantile(conc, 0.75),
    p95 = stats::quantile(conc, 0.95), .groups = "drop"
  ) |>
  dplyr::mutate(matrix = dplyr::recode(matrix, Cc = "A: plasma", Cmilk = "B: breast milk"))

ggplot(cent7, aes(time)) +
  geom_ribbon(aes(ymin = p05, ymax = p95), fill = "grey85") +
  geom_ribbon(aes(ymin = p25, ymax = p75), fill = "grey65") +
  geom_line(aes(y = p50), linewidth = 0.9) +
  facet_wrap(~matrix, ncol = 1) +
  labs(
    x = "Time (h)", y = "PZQ (mg/L)",
    caption = "Replicates Figure 7A-B of Bustinduy 2020 (5th-95th, 25th-75th centiles and median)."
  )
Monte Carlo centiles in 200 lactating women of 47.9 kg.

Monte Carlo centiles in 200 lactating women of 47.9 kg.


# Centiles at the second peak (t = 4.4 h), read from Figure 7A by the
# maintainers: about 0.65, 1.35, 1.85, 2.75 and 5.4 mg/L.
fig7_pub <- c(p05 = 0.65, p25 = 1.35, p50 = 1.85, p75 = 2.75, p95 = 5.4)
fig7_sim <- cent7 |>
  dplyr::filter(matrix == "A: plasma", abs(time - 4.4) < 1e-6) |>
  dplyr::select(p05:p95) |>
  unlist()
tibble::tibble(
  Centile = names(fig7_pub),
  `Figure 7A (digitised)` = fig7_pub,
  Simulated = round(fig7_sim, 2)
) |>
  knitr::kable(caption = "Plasma centiles at the second peak (4.4 h), mg/L.")
Plasma centiles at the second peak (4.4 h), mg/L.
Centile Figure 7A (digitised) Simulated
p05 0.65 0.49
p25 1.35 1.12
p50 1.85 1.62
p75 2.75 2.17
p95 5.40 3.32

# The simulated median sits within a factor of 1.6 of the digitised Figure 7A
# median. A mis-transcribed CL, V or dose unit moves it by a factor of several.
stopifnot(fig7_sim[["p50"]] > fig7_pub[["p50"]] / 1.6, fig7_sim[["p50"]] < fig7_pub[["p50"]] * 1.6)

The simulated centiles are narrower and lower than Figure 7A. That is expected for a log-normal model with independent etas: Pmetrics draws from the full NPAG covariance, including the r = 0.636 correlation between CL/F and V/F reported in the Results. The Figure 7C overlay puts most simulated women between 5 and 20 mgh/L in plasma and 2 and 10 mgh/L in milk. The cohort below falls in the same region.

PKNCA validation

dose_df <- events |>
  dplyr::filter(evid == 1) |>
  dplyr::select(id, time, amt, treatment)

# The ODE solver can undershoot zero by about its absolute tolerance on a
# decayed tail; PKNCA rejects negative concentrations (NaN AUC), so the tail is
# floored at zero. The minimum was asserted >= -1e-7 above.
conc_p <- sim |>
  dplyr::filter(!is.na(Cc)) |>
  dplyr::mutate(Cc = pmax(Cc, 0)) |>
  dplyr::select(id, time, Cc, treatment)
conc_m <- sim |>
  dplyr::filter(!is.na(Cmilk), treatment == "Postpartum") |>
  dplyr::mutate(Cmilk = pmax(Cmilk, 0)) |>
  dplyr::select(id, time, Cmilk, treatment)
stopifnot(all(tapply(conc_p$time, conc_p$id, min) == 0))

iv <- data.frame(start = 0, end = 24, cmax = TRUE, tmax = TRUE, auclast = TRUE)

nca_p <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  PKNCA::PKNCAconc(conc_p, Cc ~ time | treatment + id),
  PKNCA::PKNCAdose(dose_df, amt ~ time | treatment + id),
  intervals = iv
)) |> as.data.frame()

nca_m <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  PKNCA::PKNCAconc(conc_m, Cmilk ~ time | treatment + id),
  PKNCA::PKNCAdose(dplyr::filter(dose_df, treatment == "Postpartum"), amt ~ time | treatment + id),
  intervals = iv
)) |> as.data.frame()

stopifnot(
  !anyNA(nca_p$PPORRES[nca_p$PPTESTCD == "auclast"]),
  sum(nca_p$PPTESTCD == "auclast") == 3L * n_per_arm,
  !anyNA(nca_m$PPORRES[nca_m$PPTESTCD == "auclast"])
)

Comparison against published values

Figure 6D gives the posterior AUC0-24 by cohort as box plots. The maintainers digitised the medians as about 7.1 (early pregnancy), 11.8 (late pregnancy) and 11.2 (postpartum) mgh/L. The model has no cohort effect: the authors reported the early-pregnancy difference only as a post hoc comparison of Bayesian posteriors. The simulated cohorts therefore differ only through their weights. All three land near the pooled centre of about 9 mgh/L: above the early-pregnancy median and below the late-pregnancy and postpartum medians. Deviations of about 20-25% in either direction are the expected result of fitting one population to three cohorts whose posterior clearances differ by about 1.7-fold. They do not indicate a transcription error.

sim_med <- nca_p |>
  dplyr::filter(PPTESTCD %in% c("auclast", "cmax")) |>
  dplyr::group_by(treatment, PPTESTCD) |>
  dplyr::summarise(value = stats::median(PPORRES, na.rm = TRUE), .groups = "drop") |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = value) |>
  dplyr::select(treatment, auclast)

published <- tibble::tribble(
  ~treatment, ~auclast,
  "Early pregnancy", 7.1,
  "Late pregnancy", 11.8,
  "Postpartum", 11.2
)

cmp <- nlmixr2lib::ncaComparisonTable(
  simulated = sim_med,
  reference = published,
  by = "treatment",
  units = c(auclast = "mg*h/L"),
  tolerance_pct = 20
)
knitr::kable(
  cmp,
  digits = 3,
  caption = paste(
    "Simulated median AUC0-24 vs the digitised Figure 6D medians.",
    "* differs from the reference by more than 20%."
  )
)
Simulated median AUC0-24 vs the digitised Figure 6D medians. * differs from the reference by more than 20%.
NCA parameter treatment Reference Simulated % diff
AUClast (mg*h/L) Early pregnancy 7.1 8.87 +24.9%*
AUClast (mg*h/L) Late pregnancy 11.8 9.49 -19.6%
AUClast (mg*h/L) Postpartum 11.2 8.3 -25.9%*

# Pooled over all three cohorts, the simulated median AUC0-24 must sit near the
# pooled posterior centre (about 10 mg*h/L from Figure 6D). Centre-based gate.
pooled_auc <- stats::median(nca_p$PPORRES[nca_p$PPTESTCD == "auclast"])
stopifnot(pooled_auc > 10 / 1.5, pooled_auc < 10 * 1.5)

For breast milk the paper reports three posterior summaries over the 15 lactating women:

  • mean average concentration 0.185 mg/L (AUC0-24 / 24);
  • mean milk:plasma AUC0-24 ratio 0.36 (SD 0.13, range 0.19-0.55; printed as “AUCplasma:AUCbreast milk”, see Errata);
  • “approximately 30%” partitioning in the Monte Carlo simulation.
milk_auc <- nca_m |>
  dplyr::filter(PPTESTCD == "auclast") |>
  dplyr::select(id, auc_milk = PPORRES)
plasma_auc_pp <- nca_p |>
  dplyr::filter(PPTESTCD == "auclast", treatment == "Postpartum") |>
  dplyr::select(id, auc_plasma = PPORRES)
milk_tab <- dplyr::inner_join(milk_auc, plasma_auc_pp, by = "id") |>
  dplyr::mutate(ratio = auc_milk / auc_plasma)
stopifnot(nrow(milk_tab) == n_per_arm)

milk_sum <- tibble::tibble(
  Quantity = c(
    "Mean milk Cavg0-24 (mg/L)", "Mean milk:plasma AUC0-24 ratio",
    "Median milk:plasma AUC0-24 ratio"
  ),
  Published = c(0.185, 0.36, 0.30),
  Simulated = c(
    mean(milk_tab$auc_milk) / 24, mean(milk_tab$ratio),
    stats::median(milk_tab$ratio)
  )
)
knitr::kable(milk_sum, digits = 3, caption = "Breast-milk exposure, postpartum cohort.")
Breast-milk exposure, postpartum cohort.
Quantity Published Simulated
Mean milk Cavg0-24 (mg/L) 0.185 0.218
Mean milk:plasma AUC0-24 ratio 0.360 0.580
Median milk:plasma AUC0-24 ratio 0.300 0.348

# Centre-based gates: the milk:plasma partition is set by (Kcb/Kbc)(Vc/Vb)
# and is 0.315 at typical values. A mis-transcribed milk parameter moves it
# by a factor.
stopifnot(
  stats::median(milk_tab$ratio) > 0.2, stats::median(milk_tab$ratio) < 0.45,
  mean(milk_tab$auc_milk) / 24 > 0.185 / 2, mean(milk_tab$auc_milk) / 24 < 0.185 * 2
)
# Figure 7C: the bulk of simulated women lie at 5-20 mg*h/L in plasma and
# 2-10 mg*h/L in milk. The cohort medians must fall in that region.
stopifnot(
  stats::median(milk_tab$auc_plasma) > 5, stats::median(milk_tab$auc_plasma) < 20,
  stats::median(milk_tab$auc_milk) > 2, stats::median(milk_tab$auc_milk) < 10
)

The simulated median ratio agrees with the paper. The simulated mean ratio is higher, because the independent log-normal etas on Kcb, Kbc, Vc and Vb give the ratio a long right tail. The posterior ratios of the 15 women spanned only 0.19-0.55.

The paper estimates infant intake by multiplying the average milk concentration by 0.15 L/kg/day: 0.185 x 0.15 = 0.028 mg/kg/day. The Abstract and Discussion instead quote 0.037 mg/kg/day. Either figure is roughly 1000-fold below the therapeutic 40-60 mg/kg.

Assumptions and deviations

  • The Table 2 mean vector is used, not the median. See the mean-or-median section. The mean reproduces the population Monte Carlo (Figure 7) and the milk partitioning. The median reproduces the Results-text milk half-life and 24 h / 48 h concentrations for a single typical woman.
  • IIV is a log-normal approximation to a non-parametric distribution. The Table 2 CV% (linear scale, confirmed above) is carried as omega^2 = log(CV^2 + 1). Covariances are not published, so the etas are independent. The one reported correlation (posterior CL/F and V/F, r = 0.636) is therefore missing, which is why the simulated Figure 7 spread differs from the published one. The Ka CV of 214% gives a very wide log-normal (omega^2 = 1.72), and the simulated absorption phase is correspondingly heterogeneous.
  • No cohort (pregnancy-stage) effect. Posterior CL/F was higher in early pregnancy (P = 0.016) and AUC0-24 lower (P = 0.01). The authors reported this as a post hoc ANOVA on Bayesian posteriors and chose not to “further complicate the structural model”. No effect size is estimated inside the model, so none is encoded.
  • The milk compartment is present for every woman. The model was fitted jointly to all 45 women, with milk observed only in the lactating cohort. Structurally, the reversible milk exchange acts as an extra distribution space in every simulated subject, as in the authors’ model.
  • Weights are drawn from truncated normals with the Table 1 cohort means and SDs, over the roughly 36-63 kg range visible in Figure 5.
  • Residual unexplained variability is fixed(0) for both outputs.

Errata and source-reporting gaps

  • Methods equation (3) is printed XP(3) = Kcp x X(2) - Kcp x X(3). The return term must use Kpc, which is otherwise used only in eq. (2) and is an estimated Table 2 parameter.
  • Methods equation (4) is printed XP(4) = -Kbc * X(4) - Kcb * X(2). The inflow must be positive (+ Kcb * X(2)), mirroring the - Kcb * X(2) loss in eq. (2). As printed, the milk amount would go negative and mass would not be conserved. The partition-identity check above confirms the corrected form.
  • Output equation Y(1) = X(1) / Vc divides the gut amount. The plasma concentration is X(2) / Vc. Eq. (1) also prints “Bolas” for “Bolus”.
  • Milk:plasma ratio label. The Results call the 0.36 figure “AUCplasma:AUCbreast milk”, but milk exposure is lower than plasma exposure (Figure 7C). The value is the milk:plasma ratio.
  • Model compartment count. The paper calls its model “a standard 3-compartment PK model”, counting the gut as Pmetrics does. With the milk compartment, the fitted system has four states: two disposition compartments, a depot and a milk compartment.
  • Residual error is unreported. Neither the Pmetrics assay-error polynomial nor a gamma/lambda term is given. The only supplemental item is Figure S1 (individual fits).