Amodiaquine and piperaquine in pregnancy (Ding 2024)
Source:vignettes/articles/Ding_2024_antimalarials_pregnancy.Rmd
Ding_2024_antimalarials_pregnancy.RmdModel and source
- Citation: Ding J, Hoglund RM, Tagbor H, Tinto H, Valea I, Mwapasa V, Kalilani-Phiri L, Van Geertruyden J-P, Nambozi M, Mulenga M, Hachizovu S, Ravinetto R, D’Alessandro U, Tarning J (2024). Population pharmacokinetics of amodiaquine and piperaquine in African pregnant women with uncomplicated Plasmodium falciparum infections. CPT: Pharmacometrics & Systems Pharmacology 13(11):1893-1903. doi:10.1002/psp4.13211.
- Article: https://doi.org/10.1002/psp4.13211
- Europe PMC: https://europepmc.org/article/MED/39498866 (PMC11578137; open access, CC-BY).
- Trial: PREGACT, ClinicalTrials.gov NCT00852423.
The paper reports two independently fitted population PK models from two randomised arms of the same phase 3 trial, so the package ships two model files and this one vignette covering both:
-
Ding_2024_amodiaquine– joint parent-metabolite model for amodiaquine and desethylamodiaquine (artesunate-amodiaquine arm, n = 771). -
Ding_2024_piperaquine– three-compartment model for piperaquine (dihydroartemisinin-piperaquine arm, n = 755).
mod_aq_fn <- readModelDb("Ding_2024_amodiaquine")
mod_pq_fn <- readModelDb("Ding_2024_piperaquine")
mod_aq <- rxode2::rxode2(mod_aq_fn())
mod_pq <- rxode2::rxode2(mod_pq_fn())The supporting information (Figures S1-S6: model structure diagrams, basic goodness-of-fit plots and prediction-corrected VPCs) was not on disk during this extraction. Every value used below comes from the main article text, Table 1, Table 2 and Table 3, which together are complete for both models.
Population
Both arms are drawn from PREGACT, a non-inferiority, multi-centre, randomised, open-label phase 3 trial of four artemisinin-based combination therapies conducted between June 2010 and August 2013 in Burkina Faso (two sites), Ghana (three sites), Malawi (one site) and Zambia (one site). Eligible women were in the second or third trimester of pregnancy with an acute uncomplicated P. falciparum mono-infection. Women who vomited after dosing were excluded from the PK analysis (13 of 784 in the artesunate-amodiaquine arm, 8 of 763 in the dihydroartemisinin-piperaquine arm). Sampling was sparse: a single venous sample on day 7 from every woman plus additional samples at other clinical visits when possible, which is why all absorption parameters were fixed to literature values.
Baseline demographics reproduce Ding 2024 Table 1.
tibble::tribble(
~Characteristic, ~Amodiaquine, ~Piperaquine,
"Number of patients", "771", "755",
"Daily dose (mg/kg, salt)", "12.8 (6.8-19.1)", "17.8 (8.3-27.4)",
"Age (years)", "22 (15-43)", "20 (15-43)",
"Bodyweight (kg)", "55 (37-104)", "54 (35-115)",
"Height (cm)", "158 (132-179)", "155 (138-178)",
"Gestational age (weeks)", "24 (13-36)", "24 (16-36)",
"Trimester 2, n (%)", "581 (75.7)", "519 (69.1)",
"Parasitaemia at enrolment (parasites/uL)", "560 (0-82,292)", "680 (5-355,400)"
) |>
knitr::kable(caption = "Ding 2024 Table 1. Median (min-max) unless stated.")| Characteristic | Amodiaquine | Piperaquine |
|---|---|---|
| Number of patients | 771 | 755 |
| Daily dose (mg/kg, salt) | 12.8 (6.8-19.1) | 17.8 (8.3-27.4) |
| Age (years) | 22 (15-43) | 20 (15-43) |
| Bodyweight (kg) | 55 (37-104) | 54 (35-115) |
| Height (cm) | 158 (132-179) | 155 (138-178) |
| Gestational age (weeks) | 24 (13-36) | 24 (16-36) |
| Trimester 2, n (%) | 581 (75.7) | 519 (69.1) |
| Parasitaemia at enrolment (parasites/uL) | 560 (0-82,292) | 680 (5-355,400) |
Source trace
Amodiaquine and desethylamodiaquine (Ding 2024 Table 2)
| Equation / parameter | Value | Source location |
|---|---|---|
lka = fixed(log(0.589)) |
0.589 1/h, fixed | Table 2, “Ka (1/h) – 0.589 fixed” |
lmtt = fixed(log(0.236)) |
0.236 h, fixed | Table 2, “MTT (h) – 0.236 fixed” |
| Number of transit compartments | 2, fixed | Table 2, “Number of transit compartment – 2 fixed” |
lcl = log(6780) |
6780 L/h (%RSE 4.9) | Table 2, “CL/F AQ (L/h)” |
lvc = log(272000) |
272,000 L (%RSE 8.9) | Table 2, “V_C/F AQ (L)” |
lcl_deaq = log(38.3) |
38.3 L/h (%RSE 9.6) | Table 2, “CL/F DEAQ (L/h)” |
lvc_deaq = log(861) |
861 L (%RSE 15.7) | Table 2, “V_C/F DEAQ (L)” |
lq_deaq = log(81.7) |
81.7 L/h (%RSE 7.0) | Table 2, “Q/F DEAQ (L/h)” |
lvp_deaq = log(13200) |
13,200 L (%RSE 13.4) | Table 2, “V_p/F DEAQ (L)” |
lfdepot = fixed(log(1)) |
100%, fixed | Table 2, “F AQ (%) – 100 fixed”; Methods, “Population PK analysis” |
e_wt_cl = fixed(0.75) |
0.75, fixed | Methods, “Covariates model”, Equation 2 |
e_wt_vc = fixed(1.00) |
1, fixed | Methods, “Covariates model”, Equation 3 |
e_ega_f = 0.0128 |
1.28%/week (%RSE 25.0) | Table 2, “Gestational age on F AQ (%)”; footnote form
[1 + (theta x (GA - 24))]
|
etalfdepot ~ 0.0895079 |
30.6% CV | Table 2, IIV column; footnote
CV = 100 x (e^variance - 1)^(1/2)
|
etalcl_deaq ~ 0.0380749 |
19.7% CV | Table 2, IIV column |
etalvc_deaq ~ 1.6491393 |
205% CV | Table 2, IIV column |
propSd = sqrt(0.267) |
RUV 0.267 (variance) | Table 2, “RUV” (AQ); footnote “RUV is the residual error variance” |
propSd_deaq = sqrt(0.122) |
RUV 0.122 (variance) | Table 2, “RUV” (DEAQ) |
| Allometric scaling, reference 70 kg | – | Equations 2 and 3; Table 2 footnote “estimates are given for a ‘typical’ pregnant women weighting 70 kg” |
| Molar conversion AQ -> DEAQ | 327.81 / 355.85 = 0.9212 | Methods, “complete bioconversion”; factor not printed (see Assumptions) |
Piperaquine (Ding 2024 Table 3)
| Equation / parameter | Value | Source location |
|---|---|---|
lmtt = fixed(log(2.11)) |
2.11 h, fixed | Table 3, “MTT (h) – 2.11 fixed” |
| Number of transit compartments | 2, fixed | Table 3, “Number of transit compartments – 2 fixed” |
lcl = log(69.9) |
69.9 L/h (%RSE 5.1) | Table 3, “CL/F (L/h)” |
lvc = log(4240) |
4240 L (%RSE 36.0) | Table 3, “V_C/F (L)” |
lq = log(265) |
265 L/h (%RSE 39.9) | Table 3, “Q_1/F (L/h)” |
lvp = log(3880) |
3880 L (%RSE 28.7) | Table 3, “V_p1/F (L)” |
lq2 = log(103) |
103 L/h (%RSE 12.8) | Table 3, “Q_2/F (L/h)” |
lvp2 = log(22900) |
22,900 L (%RSE 7.9) | Table 3, “V_p2/F (L)” |
lfdepot = fixed(log(1)) |
100%, fixed | Table 3, “F (%) – 100 fixed” |
e_wt_cl = fixed(0.75) |
0.75, fixed | Methods, “Covariates model”, Equation 2 |
e_wt_vc = fixed(1.00) |
1, fixed | Methods, “Covariates model”, Equation 3 |
e_para_f = -0.119 |
-11.9% per log10 (%RSE 19.3) | Table 3, “Baseline parasites count on F (%)”; footnote form
[1 + theta x (log(parasitemia) - 2.83)]
|
e_doseocc_f = fixed(0.237) |
23.7%, fixed | Table 3, “Dose occasion on F (%) – 23.7 fixed”; Methods cites Hoglund 2017 |
etalfdepot ~ 0.1081761 |
33.8% CV | Table 3, IIV column |
etalvc ~ 0.8128839 |
112% CV | Table 3, IIV column |
propSd = sqrt(0.222) |
RUV 0.222 (variance) | Table 3, “RUV” |
| Allometric scaling, reference 70 kg | – | Equations 2 and 3; Table 3 footnote “estimates are given for a ‘typical’ pregnant women weighted 70 kg” |
Dosing regimens
Both regimens are 3 consecutive daily doses under direct observation (Methods, “Drug regimen”). Doses below are converted to base, which is the amount unit of the model compartments.
dose_aq_mg <- 2 * 270 # 2 tablets x 270 mg amodiaquine base = 540 mg/day
dose_pq_mg <- 3 * 171 # 3 tablets x 171 mg piperaquine base = 513 mg/day
c(amodiaquine_base_mg_per_day = dose_aq_mg, piperaquine_base_mg_per_day = dose_pq_mg)
#> amodiaquine_base_mg_per_day piperaquine_base_mg_per_day
#> 540 513Typical-value replication of the published secondary parameters
This is the primary quantitative gate. Ding 2024 reports model-derived secondary parameters (terminal half-life, AUC from zero to infinity, and the day-7 concentration) in Tables 2 and 3, computed from the empirical Bayes post-hoc estimates and summarised as the cohort median. Simulating the typical individual at each arm’s median body weight and median covariate values should therefore land close to those published medians.
# Dense early sampling for the absorption / distribution phase, then a daily
# grid long enough to characterise the terminal phase of desethylamodiaquine
# (t1/2 ~ 14 days) and piperaquine (t1/2 ~ 17 days).
obs_times <- sort(unique(c(
seq(0, 12, by = 0.25),
seq(12, 72, by = 1),
seq(72, 240, by = 6),
seq(240, 24 * 150, by = 24)
)))
# Amodiaquine arm. The model has two endpoints (Cc, Cc_deaq), so observation
# rows carry dvid = 1 and rxode2 returns both observables as columns; dose
# rows use the ODE state name "depot".
make_events_aq <- function(id, wt, ega) {
dplyr::bind_rows(
data.frame(id = id, time = c(0, 24, 48), evid = 1L, amt = dose_aq_mg,
cmt = "depot", dvid = NA_integer_),
data.frame(id = id, time = obs_times, evid = 0L, amt = NA_real_,
cmt = NA_character_, dvid = 1L)
) |>
dplyr::mutate(WT = wt, EGA = ega) |>
dplyr::arrange(time, dplyr::desc(evid))
}
# Piperaquine arm. Single endpoint, so observation rows point at the ODE
# state "central". OCC increments across the three daily doses.
make_events_pq <- function(id, wt, para) {
dplyr::bind_rows(
data.frame(id = id, time = c(0, 24, 48), evid = 1L, amt = dose_pq_mg,
cmt = "depot", OCC = c(1, 2, 3)),
data.frame(id = id, time = obs_times, evid = 0L, amt = NA_real_,
cmt = "central",
OCC = ifelse(obs_times < 24, 1, ifelse(obs_times < 48, 2, 3)))
) |>
dplyr::mutate(WT = wt, PARA = para) |>
dplyr::arrange(time, dplyr::desc(evid))
}useLinCmt = FALSE is required on every
rxSolve() call for the amodiaquine model: rxode2’s
automatic ODE-to-linCmt conversion corrupts the dvid-to-compartment
mapping for multi-output models of this shape.
sim_aq_typ <- rxode2::rxSolve(
rxode2::zeroRe(mod_aq), make_events_aq(1L, wt = 55, ega = 24),
keep = c("WT", "EGA"), useLinCmt = FALSE
) |> as.data.frame()
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
sim_pq_typ <- rxode2::rxSolve(
rxode2::zeroRe(mod_pq), make_events_pq(1L, wt = 54, para = 680),
keep = c("WT", "PARA")
) |> as.data.frame()
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalvc'
# Terminal half-life and AUC(0-inf) by log-linear regression over a window
# well past the last dose, with the standard tail extrapolation Clast/lambda_z.
#
# `tail_to` bounds the regression window at the top. It matters: the shared
# observation grid runs to 150 days so that piperaquine's ~17-day terminal
# phase is captured, but amodiaquine (t1/2 ~26 h) has decayed to numerical
# noise long before then. Regressing over the full tail would fit that noise
# and return a badly inflated half-life. The relative floor drops points that
# have underflowed toward zero for the same reason.
terminal_summary <- function(time, conc, tail_from, tail_to = Inf) {
keep <- time >= tail_from & time <= tail_to & conc > max(conc) * 1e-8
stopifnot(sum(keep) >= 5)
lz <- -stats::coef(stats::lm(log(conc[keep]) ~ time[keep]))[[2]]
auc_obs <- sum(diff(time) * (utils::head(conc, -1) + utils::tail(conc, -1)) / 2)
c(half_life = log(2) / lz, auc_inf = auc_obs + utils::tail(conc, 1) / lz)
}
sec_aq <- terminal_summary(sim_aq_typ$time, sim_aq_typ$Cc,
tail_from = 72, tail_to = 240)
sec_deaq <- terminal_summary(sim_aq_typ$time, sim_aq_typ$Cc_deaq, tail_from = 24 * 60)
sec_pq <- terminal_summary(sim_pq_typ$time, sim_pq_typ$Cc, tail_from = 24 * 80)
day7 <- function(df, col) df[[col]][which.min(abs(df$time - 168))]
secondary <- tibble::tibble(
Parameter = c("t1/2 amodiaquine (h)", "AUC0-inf amodiaquine (h*ng/mL)",
"t1/2 desethylamodiaquine (day)", "AUC0-inf desethylamodiaquine (h*ug/mL)",
"Day 7 desethylamodiaquine (ng/mL)",
"t1/2 piperaquine (day)", "AUC0-inf piperaquine (h*ug/mL)",
"Day 7 piperaquine (ng/mL)"),
Published = c(26.3, 283, 14.1, 45.2, 63.3, 17.0, 31.6, 39.0),
`Published 95% CI` = c("24.7-28.6", "182-432", "12.8-16.9", "27.5-78.3",
"38.4-107", "15.8-18.6", "20.6-50.0", "25.1-65.0"),
Simulated = c(sec_aq[["half_life"]], sec_aq[["auc_inf"]],
sec_deaq[["half_life"]] / 24, sec_deaq[["auc_inf"]] / 1000,
day7(sim_aq_typ, "Cc_deaq"),
sec_pq[["half_life"]] / 24, sec_pq[["auc_inf"]] / 1000,
day7(sim_pq_typ, "Cc"))
) |>
dplyr::mutate(
`% diff` = 100 * (Simulated - Published) / Published,
Simulated = round(Simulated, 2),
`% diff` = round(`% diff`, 1)
)
knitr::kable(
secondary,
caption = paste("Typical-value replication of Ding 2024 Table 2 and Table 3",
"secondary parameters, at each arm's median body weight",
"(55 kg amodiaquine, 54 kg piperaquine), gestational age 24",
"weeks and baseline parasitaemia 680 parasites/uL.")
)| Parameter | Published | Published 95% CI | Simulated | % diff |
|---|---|---|---|---|
| t1/2 amodiaquine (h) | 26.3 | 24.7-28.6 | 26.18 | -0.5 |
| AUC0-inf amodiaquine (h*ng/mL) | 283.0 | 182-432 | 286.52 | 1.2 |
| t1/2 desethylamodiaquine (day) | 14.1 | 12.8-16.9 | 14.19 | 0.6 |
| AUC0-inf desethylamodiaquine (h*ug/mL) | 45.2 | 27.5-78.3 | 46.70 | 3.3 |
| Day 7 desethylamodiaquine (ng/mL) | 63.3 | 38.4-107 | 63.99 | 1.1 |
| t1/2 piperaquine (day) | 17.0 | 15.8-18.6 | 16.94 | -0.4 |
| AUC0-inf piperaquine (h*ug/mL) | 31.6 | 20.6-50.0 | 33.09 | 4.7 |
| Day 7 piperaquine (ng/mL) | 39.0 | 25.1-65.0 | 39.83 | 2.1 |
# Structural gate: every published secondary parameter is reproduced by the
# typical individual. A mis-transcribed clearance, a wrong reference weight,
# a dropped molar conversion or a dropped dose-occasion escalation each move
# one or more of these by >= 10%, so a 6% envelope is a real check while
# leaving room for the published values being cohort medians of post-hoc
# empirical Bayes estimates rather than exact typical-value predictions.
stopifnot(
max(abs(secondary$`% diff`)) < 6,
abs(stats::median(secondary$`% diff`)) < 3
)All eight published secondary parameters are reproduced within 5%. Three of the encoding decisions documented under “Assumptions and deviations” below are load-bearing for this agreement and are each individually falsified by this table:
- using a 70 kg allometric reference rather than the cohort median weight moves the piperaquine AUC to 27.2 (-14%);
- dropping the molar conversion on the amodiaquine-to-desethylamodiaquine flux moves the desethylamodiaquine AUC and day-7 concentration up by about 12%;
- dropping the fixed dose-occasion escalation on piperaquine bioavailability moves the piperaquine AUC to 26.8 (-15%).
Concentration-time profiles
prof <- dplyr::bind_rows(
sim_aq_typ |> dplyr::transmute(time, conc = Cc, Analyte = "Amodiaquine"),
sim_aq_typ |> dplyr::transmute(time, conc = Cc_deaq, Analyte = "Desethylamodiaquine"),
sim_pq_typ |> dplyr::transmute(time, conc = Cc, Analyte = "Piperaquine")
) |>
dplyr::filter(time <= 24 * 42, conc > 0)
ggplot2::ggplot(prof, ggplot2::aes(time / 24, conc, colour = Analyte)) +
ggplot2::geom_line(linewidth = 0.7) +
ggplot2::scale_y_log10() +
ggplot2::geom_vline(xintercept = 7, linetype = "dashed", colour = "grey40") +
ggplot2::labs(
x = "Time (days)", y = "Plasma concentration (ng/mL)",
caption = paste("Typical-value profiles for the three-day regimens.",
"Dashed line marks day 7, the trial's universal sampling day.",
"Compare the observed ranges in Ding 2024 Figures 1 and 2.")
) +
ggplot2::theme_bw()
Gestational-age effect on desethylamodiaquine exposure (Ding 2024 Figure 3)
Gestational age enters only the relative bioavailability of
amodiaquine, as F = 1 + 0.0128 * (GA - 24). Because
F scales the whole input, both AUC and the day-7
concentration should scale exactly linearly with that factor. Ding 2024
Results reports F of 90% at 16 weeks and 115% at 36 weeks
relative to the 24-week reference, and the abstract reports a predicted
desethylamodiaquine AUC 2.8%-32.2% higher in pregnant women at 16-36
weeks than in non-pregnant women.
ega_grid <- c(16, 20, 24, 28, 32, 36)
sim_ega <- lapply(seq_along(ega_grid), function(i) {
rxode2::rxSolve(
rxode2::zeroRe(mod_aq),
make_events_aq(as.integer(i), wt = 70, ega = ega_grid[i]),
keep = c("WT", "EGA"), useLinCmt = FALSE
) |>
as.data.frame() |>
dplyr::mutate(EGA = ega_grid[i])
}) |>
dplyr::bind_rows()
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalcl_deaq', 'etalvc_deaq'
ega_summary <- sim_ega |>
dplyr::group_by(EGA) |>
dplyr::summarise(
auc_deaq = terminal_summary(time, Cc_deaq, tail_from = 24 * 60)[["auc_inf"]] / 1000,
day7_deaq = Cc_deaq[which.min(abs(time - 168))],
.groups = "drop"
) |>
dplyr::mutate(
`F relative to 24 weeks` = 1 + 0.0128 * (EGA - 24),
`AUC ratio vs 24 weeks` = auc_deaq / auc_deaq[EGA == 24]
)
ega_summary |>
dplyr::rename(
"Gestational age (weeks)" = EGA,
"AUC0-inf DEAQ (h*ug/mL)" = auc_deaq,
"Day 7 DEAQ (ng/mL)" = day7_deaq
) |>
knitr::kable(digits = 3, caption = paste(
"Replicates Ding 2024 Figure 3: predicted desethylamodiaquine AUC and",
"day-7 concentration by gestational age for a typical 70 kg pregnant",
"woman."))| Gestational age (weeks) | AUC0-inf DEAQ (h*ug/mL) | Day 7 DEAQ (ng/mL) | F relative to 24 weeks | AUC ratio vs 24 weeks |
|---|---|---|---|---|
| 16 | 34.984 | 47.389 | 0.898 | 0.898 |
| 20 | 36.980 | 50.092 | 0.949 | 0.949 |
| 24 | 38.975 | 52.795 | 1.000 | 1.000 |
| 28 | 40.971 | 55.498 | 1.051 | 1.051 |
| 32 | 42.967 | 58.201 | 1.102 | 1.102 |
| 36 | 44.962 | 60.904 | 1.154 | 1.154 |
# Exact check: the covariate is a pure multiplier on F, so the AUC ratio must
# equal the F ratio to numerical precision. Both sides come from the same
# drawn parameters, so a tight bound is correct here.
stopifnot(
max(abs(ega_summary$`AUC ratio vs 24 weeks` -
ega_summary$`F relative to 24 weeks`)) < 1e-6,
# Ding 2024 Results: F = 90% at 16 weeks and 115% at 36 weeks.
abs(ega_summary$`F relative to 24 weeks`[ega_summary$EGA == 16] - 0.90) < 0.005,
abs(ega_summary$`F relative to 24 weeks`[ega_summary$EGA == 36] - 1.15) < 0.005
)
ggplot2::ggplot(ega_summary, ggplot2::aes(EGA, day7_deaq)) +
ggplot2::geom_line(colour = "steelblue") +
ggplot2::geom_point(colour = "steelblue", size = 2) +
ggplot2::labs(
x = "Gestational age (weeks)",
y = "Day 7 desethylamodiaquine (ng/mL)",
caption = "Replicates the day-7 panel of Ding 2024 Figure 3 (typical 70 kg woman)."
) +
ggplot2::theme_bw()
Piperaquine covariate effects (Ding 2024 Figure 4 and Table 3)
Neither gestational age nor trimester was retained for piperaquine. The two covariates that do act on relative bioavailability are baseline parasitaemia and the literature-fixed dose-occasion escalation.
# One log10 unit apart, anchored exactly on the model's centring constant.
# Note the anchor is 10^2.83 = 676.1 parasites/uL, not the Table 1 median of
# 680: the paper centres the covariate on the rounded log10 value 2.83, and
# the Results simulations use "a baseline parasitemia of 676 parasites/uL"
# for exactly this reason. Anchoring here makes F exactly 1 at the reference.
para_ref <- 10^2.83
para_grid <- para_ref * 10^(-1:2)
pq_para <- lapply(seq_along(para_grid), function(i) {
rxode2::rxSolve(
rxode2::zeroRe(mod_pq),
make_events_pq(as.integer(i), wt = 70, para = para_grid[i]),
keep = c("WT", "PARA")
) |>
as.data.frame() |>
dplyr::mutate(PARA = para_grid[i])
}) |>
dplyr::bind_rows() |>
dplyr::group_by(PARA) |>
dplyr::summarise(
auc_pq = terminal_summary(time, Cc, tail_from = 24 * 80)[["auc_inf"]] / 1000,
half_life = terminal_summary(time, Cc, tail_from = 24 * 80)[["half_life"]] / 24,
day7_pq = Cc[which.min(abs(time - 168))],
.groups = "drop"
) |>
dplyr::mutate(
`log10 parasitaemia` = log10(PARA),
`F relative to reference` = 1 - 0.119 * (log10(PARA) - 2.83),
`AUC ratio vs reference` = auc_pq / auc_pq[PARA == para_ref]
)
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalvc'
#> ℹ omega/sigma items treated as zero: 'etalfdepot', 'etalvc'
pq_para |>
dplyr::rename(
"Baseline parasitaemia (per uL)" = PARA,
"AUC0-inf (h*ug/mL)" = auc_pq,
"t1/2 (day)" = half_life,
"Day 7 (ng/mL)" = day7_pq
) |>
knitr::kable(digits = 3, caption = paste(
"Baseline-parasitaemia effect on piperaquine exposure for a typical 70 kg",
"pregnant woman. Ding 2024 Table 3: -11.9% relative bioavailability per",
"log10 unit above the centring value of 10^2.83 = 676 parasites/uL",
"(the Table 1 cohort median is 680)."))| Baseline parasitaemia (per uL) | AUC0-inf (h*ug/mL) | t1/2 (day) | Day 7 (ng/mL) | log10 parasitaemia | F relative to reference | AUC ratio vs reference |
|---|---|---|---|---|---|---|
| 67.608 | 30.488 | 18.071 | 36.351 | 1.83 | 1.119 | 1.119 |
| 676.083 | 27.245 | 18.071 | 32.485 | 2.83 | 1.000 | 1.000 |
| 6760.830 | 24.003 | 18.071 | 28.619 | 3.83 | 0.881 | 0.881 |
| 67608.298 | 20.761 | 18.071 | 24.754 | 4.83 | 0.762 | 0.762 |
# Pure multiplier on F again, so the AUC ratio must track the F ratio exactly.
# Both sides come from the same drawn parameters and the grid is anchored on
# the model's own centring constant, so a tight bound is correct here.
stopifnot(
max(abs(pq_para$`AUC ratio vs reference` -
pq_para$`F relative to reference`)) < 1e-6,
# One log10 step must change F by exactly the tabulated -11.9%.
max(abs(diff(pq_para$`F relative to reference`) + 0.119)) < 1e-9,
# Terminal half-life must be untouched by a bioavailability covariate.
diff(range(pq_para$half_life)) < 1e-3
)Stochastic cohort and PKNCA validation
A virtual cohort per arm, drawn from each model’s own
omega, with body weight, gestational age and baseline
parasitaemia sampled to match the Table 1 marginal distributions.
Cohorts are capped at 150 subjects per arm.
set.seed(20240101)
n_sub <- 150L
# Log-normal weight and parasitaemia distributions tuned to the Table 1
# medians; see Assumptions for why these are approximations.
cohort_aq <- tibble::tibble(
id = seq_len(n_sub),
WT = pmin(pmax(stats::rlnorm(n_sub, log(55), 0.20), 37), 104),
EGA = pmin(pmax(round(stats::rnorm(n_sub, 24, 5)), 13), 36)
)
cohort_pq <- tibble::tibble(
id = seq_len(n_sub),
WT = pmin(pmax(stats::rlnorm(n_sub, log(54), 0.20), 35), 115),
PARA = pmin(pmax(stats::rlnorm(n_sub, log(680), 1.6), 5), 355400)
)
set.seed(20240102)
ev_aq <- dplyr::bind_rows(lapply(
seq_len(nrow(cohort_aq)),
\(i) make_events_aq(cohort_aq$id[i], cohort_aq$WT[i], cohort_aq$EGA[i])
))
sim_aq <- rxode2::rxSolve(mod_aq, ev_aq, keep = c("WT", "EGA"),
useLinCmt = FALSE) |>
as.data.frame()
set.seed(20240103)
ev_pq <- dplyr::bind_rows(lapply(
seq_len(nrow(cohort_pq)),
\(i) make_events_pq(cohort_pq$id[i], cohort_pq$WT[i], cohort_pq$PARA[i])
))
sim_pq <- rxode2::rxSolve(mod_pq, ev_pq, keep = c("WT", "PARA")) |>
as.data.frame()
# One long concentration frame, with the analyte as the treatment grouping
# variable so PKNCA rolls its summaries up per analyte. The filter is
# !is.na(Cc) only, so the time-zero record is retained.
#
# Amodiaquine gets its own 240 h window. The shared grid runs to 150 days so
# that piperaquine's ~17-day terminal phase is characterised, but amodiaquine
# (t1/2 ~26 h) has decayed below any plausible quantification limit -- and
# into the solver's noise floor -- within a few days. Feeding PKNCA the full
# record makes its automatic lambda-z search fit that noise: half-life comes
# back at 67 h instead of 26 h and aucinf.obs fails outright for 145 of the
# 150 subjects. Truncating at 240 h (about 9 half-lives) is also the
# realistic analytical window: 79.2% of the trial's amodiaquine samples were
# below the LLOQ (Ding 2024 Results).
aq_nca_window_h <- 240
conc_all <- dplyr::bind_rows(
sim_aq |>
dplyr::filter(time <= aq_nca_window_h) |>
dplyr::transmute(id, time, Cc = Cc, treatment = "Amodiaquine"),
sim_aq |> dplyr::transmute(id, time, Cc = Cc_deaq, treatment = "Desethylamodiaquine"),
sim_pq |> dplyr::transmute(id, time, Cc = Cc, treatment = "Piperaquine")
) |>
dplyr::filter(!is.na(Cc))
stopifnot(sum(conc_all$time == 0) == 3 * n_sub) # time-zero record present
dose_all <- dplyr::bind_rows(
tidyr::expand_grid(id = cohort_aq$id, time = c(0, 24, 48),
amt = dose_aq_mg, treatment = c("Amodiaquine", "Desethylamodiaquine")),
tidyr::expand_grid(id = cohort_pq$id, time = c(0, 24, 48),
amt = dose_pq_mg, treatment = "Piperaquine")
)
conc_obj <- PKNCA::PKNCAconc(as.data.frame(conc_all), Cc ~ time | treatment + id,
concu = "ng/mL", timeu = "h")
dose_obj <- PKNCA::PKNCAdose(as.data.frame(dose_all), amt ~ time | treatment + id,
doseu = "mg")
intervals <- data.frame(
start = 0, end = Inf,
cmax = TRUE, tmax = TRUE, aucinf.obs = TRUE, half.life = TRUE, clast.obs = TRUE
)
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals))
nca_tbl <- as.data.frame(nca_res$result) |>
dplyr::filter(PPTESTCD %in% c("cmax", "tmax", "aucinf.obs", "half.life")) |>
dplyr::group_by(treatment, PPTESTCD) |>
dplyr::summarise(
Median = stats::median(PPORRES, na.rm = TRUE),
`P5` = stats::quantile(PPORRES, 0.05, na.rm = TRUE),
`P95` = stats::quantile(PPORRES, 0.95, na.rm = TRUE),
.groups = "drop"
)
nca_tbl |>
dplyr::rename("Analyte" = treatment, "NCA parameter" = PPTESTCD) |>
knitr::kable(digits = 2, caption = paste(
"PKNCA non-compartmental analysis of the simulated cohorts",
"(n = 150 per arm). Concentrations in ng/mL, times in h."))| Analyte | NCA parameter | Median | P5 | P95 |
|---|---|---|---|---|
| Amodiaquine | aucinf.obs | 286.71 | 167.89 | 511.74 |
| Amodiaquine | cmax | 4.04 | 2.33 | 7.42 |
| Amodiaquine | half.life | 25.93 | 24.07 | 28.66 |
| Amodiaquine | tmax | 53.00 | 53.00 | 53.00 |
| Desethylamodiaquine | aucinf.obs | 45802.31 | 25940.54 | 83768.97 |
| Desethylamodiaquine | cmax | 187.19 | 97.40 | 385.67 |
| Desethylamodiaquine | half.life | 345.52 | 266.84 | 476.39 |
| Desethylamodiaquine | tmax | 60.50 | 54.00 | 84.00 |
| Piperaquine | aucinf.obs | 34600.56 | 16871.12 | 58007.27 |
| Piperaquine | cmax | 230.08 | 89.86 | 609.95 |
| Piperaquine | half.life | 408.41 | 363.82 | 495.61 |
| Piperaquine | tmax | 52.00 | 51.00 | 53.00 |
Comparison against the published values
Ding 2024 reports AUC0-inf and terminal half-life for all three analytes, and the day-7 concentration for desethylamodiaquine and piperaquine. Cmax and Tmax are not reported by the paper and are therefore shown above but not compared here.
sim_for_cmp <- nca_tbl |>
dplyr::filter(PPTESTCD %in% c("aucinf.obs", "half.life")) |>
dplyr::transmute(treatment, PPTESTCD, PPORRES = Median)
ref_for_cmp <- tibble::tribble(
~treatment, ~aucinf.obs, ~half.life,
"Amodiaquine", 283, 26.3,
"Desethylamodiaquine", 45200, 14.1 * 24,
"Piperaquine", 31600, 17.0 * 24
)
cmp <- nlmixr2lib::ncaComparisonTable(
simulated = as.data.frame(sim_for_cmp),
reference = as.data.frame(ref_for_cmp),
by = "treatment",
units = c(aucinf.obs = "h*ng/mL", half.life = "h"),
tolerance_pct = 20
)
knitr::kable(cmp, digits = 2, caption = paste(
"Simulated (cohort median) versus Ding 2024 published values.",
attr(cmp, "footnote")))| NCA parameter | treatment | Reference | Simulated | % diff |
|---|---|---|---|---|
| AUC0-∞ (obs) (h*ng/mL) | Amodiaquine | 283 | 287 | +1.3% |
| AUC0-∞ (obs) (h*ng/mL) | Desethylamodiaquine | 45200 | 45800 | +1.3% |
| AUC0-∞ (obs) (h*ng/mL) | Piperaquine | 31600 | 34600 | +9.5% |
| t½ (h) | Amodiaquine | 26.3 | 25.9 | -1.4% |
| t½ (h) | Desethylamodiaquine | 338 | 346 | +2.1% |
| t½ (h) | Piperaquine | 408 | 408 | +0.1% |
pct <- suppressWarnings(as.numeric(gsub("[^0-9.eE+-]", "", cmp$`% diff`)))
pct <- pct[is.finite(pct)]
stopifnot(
length(pct) == 6,
# Every subject must yield a computable AUC and half-life. This is the
# check that catches a mis-specified NCA window: before amodiaquine was
# given its own 240 h window, aucinf.obs was NA for 145 of 150 subjects
# and the group median was computed from the surviving 5.
!any(is.na(nca_tbl$Median)),
# Cohort-level check: assert on the centre and a robust envelope, never on
# the extreme of a random cohort (see the repo note on cohort assertions).
abs(stats::median(pct)) < 5,
stats::quantile(abs(pct), 0.9) < 12
)All six comparisons agree with the published values, and the simulated 5th-95th percentile ranges bracket the published 95% intervals (for example, amodiaquine AUC0-inf 168-512 against a published 182-432).
The cohort medians sit slightly further from the published values
than the typical-value table above, which is expected: the published
secondary parameters are medians of post-hoc empirical Bayes estimates
from a cohort whose etas are heavily shrunk (70-74% shrinkage on the
desethylamodiaquine disposition etas, 73% on piperaquine
Vc/F), whereas this cohort draws unshrunk etas from the
full omega. The very large Vc/F variability
(205% CV for desethylamodiaquine, 112% for piperaquine) widens the
half-life distribution substantially without moving AUC, which depends
only on clearance and bioavailability. The piperaquine AUC is the
largest discrepancy because it additionally integrates over the
simulated baseline parasitaemia distribution, which is an approximation
of a covariate the paper reports only as a median and range.
Assumptions and deviations
Allometric reference weight of 70 kg. Equations 2 and 3 write the normalising constant generically as
BW_median, but the footnotes of both Table 2 and Table 3 state that the population estimates are given for a typical pregnant woman weighing 70 kg, the Discussion compares the piperaquineCL/Fof 69.9 L/h directly against a literature value “for a typical 70-kg adult patient”, and the in-silico simulations use 70 kg hypothetical patients. 70 kg is therefore encoded. The typical-value table above shows a cohort-median reference (55 / 54 kg) would miss the published AUCs by 12-14%.Molar conversion on the amodiaquine-to-desethylamodiaquine flux. The Methods state complete bioconversion but do not print a conversion factor. The model applies
MW_DEAQ / MW_AQ = 327.81 / 355.85 = 0.9212, matching the sibling WWARN modelAli_2018_amodiaquine, which is reference 29 of this paper and states the correction explicitly. The paper’s own numbers support it: the publishedAUC_DEAQ / AUC_AQratio of 45,200 / 283 = 159.7 back-solves a factor of 0.902 (this ratio cancels the dose and the bioavailability entirely), against 0.921 for the molar reading and 1.0 for a mass-for-mass reading.Amodiaquine absorption chain:
ktr = 2 / MTTwithkaas a separate final step. Table 2 fixes bothKa = 0.589 1/handMTT = 0.236 hwith 2 transit compartments. These two numbers cannot both belong to a pure Savic chain, in whichkaandktrare the same rate (that would requireka = 3 / 0.236 = 12.7, not 0.589). SinceMTT(0.236 h) is shorter than the mean time of the singlekastep (1/0.589 = 1.70 h),MTTprovably excludes thekastep – a mean transit time cannot be smaller than one of the mean times it sums.MTTtherefore spans only the two transit compartments, givingktr = 2 / MTT, withkagoverning the final transfer into the central compartment and a mean absorption time of 1.93 h. The siblingAli_2018_amodiaquinemodel, which fixes the identicalKa/MTTpair, instead usesktr = 3 / MTTand leaveskaunused; that reading discards a reported parameter. No published quantity discriminates between the two – AUC, both half-lives and the day-7 concentration are all insensitive to the absorption structure, and the paper reports no amodiaquine Cmax or Tmax – so this choice is visible only in the unreported amodiaquine peak.Piperaquine absorption chain:
ktr = 3 / MTT. No separatekais reported for piperaquine, soka = ktrand the standard Savic convention applies over the three transitionsdepot -> transit1 -> transit2 -> central. This matchesHoglund_2017_piperaquine, which is reference 28 of this paper and the source of the fixedMTT = 2.11 h.Residual error reported as a variance. The Table 2 and Table 3 footnotes state “RUV is the residual error variance”, so the tabulated
0.267/0.122/0.222are variances and the model encodespropSd = sqrt(RUV). The Methods describe an additive error on log-transformed concentrations, which is equivalent to a proportional error in linear concentration space. This is the same convention as the siblingHoglund_2017_piperaquine.Base of the parasitaemia logarithm. The Table 3 footnote writes
[1 + theta x (log(parasitemia) - 2.83)]without stating the base. It is base 10: the cohort median is 680 parasites/uL andlog10(680) = 2.83, and the Results simulations use “a baseline parasitemia of 676 parasites/uL”, which is10^2.83 = 676.1.max(PARA, 1)gating is applied because the Table 1 minimum is 0 parasites/uL.Text-versus-table discrepancies, resolved in favour of the final-estimates tables. The Results text gives an 11.6% decrease in piperaquine bioavailability per log unit of parasitaemia while Table 3 gives -11.9%; the abstract gives a 1.25%/week gestational-age effect while both the Results text and Table 2 give 1.28%/week. The Table 2 and Table 3 values are used.
Gestational age is not extrapolable to the non-pregnant anchor. The covariate register defines
EGA = 0as the non-pregnant reference, but this model’s linear form would giveF = 0.69there. The cohort range is 13-36 weeks and the relationship is only supported inside it; no non-pregnant women were enrolled.Virtual-cohort covariate distributions are approximations. Table 1 reports only medians and ranges, not distributional shapes or correlations. Weight and parasitaemia are drawn as truncated log-normals and gestational age as a truncated normal, each matched to the published median and clipped to the published range. Weight and gestational age are drawn independently, which the paper gives no basis to correlate.
deaqmetabolite suffix registered.central_deaq,peripheral1_deaqandpropSd_deaqfollow the standard<canonical>_<metab>pattern and match the already-shippedAli_2018_amodiaquine, which models the identical parent-metabolite pair. The suffix was not previously in the canonical register, so both files raisedcheckModelConventions()warnings;deaqis registered ininst/references/compartment-names.mdalongside this extraction, which clears the warnings on both.Supplementary figures not used. Figures S1-S6 (structure diagrams, goodness-of-fit plots, prediction-corrected VPCs) were not on disk. They contain no parameter values; Tables 1-3 in the main article are complete for both models.