Artemisinin (Sidhu 1998)
Source:vignettes/articles/Sidhu_1998_artemisinin.Rmd
Sidhu_1998_artemisinin.RmdModel and source
- Citation: Sidhu JS, Ashton M, Huong NV, Hai TN, Karlsson MO, Sy ND, Jonsson EN, Cong LD (1998). Artemisinin population pharmacokinetics in children and adults with uncomplicated falciparum malaria. British Journal of Clinical Pharmacology 45(4):347-354. doi:10.1046/j.1365-2125.1998.t01-1-00686.x.
- Article: https://doi.org/10.1046/j.1365-2125.1998.t01-1-00686.x
The package model can be loaded with:
mod_fn <- readModelDb("Sidhu_1998_artemisinin")
mod <- rxode2::rxode2(mod_fn())
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_cl_1, etaiov_cl_2, etaiov_vc_1, etaiov_vc_2
#> as a work-around try putting the mu-referenced expression on a simple linePopulation
Sidhu 1998 enrolled 54 Vietnamese patients with uncomplicated Plasmodium falciparum malaria at health stations of the Phu Rieng rubber plantation in Song Be Province, Vietnam (Sidhu 1998 Methods + Table 1). The cohort was recruited in a stratified manner to avoid over-representation of older children and younger adults:
- 23 paediatric subjects (16 M / 7 F), age 2-12 years (10 in the 2-7 y stratum, 13 in the 8-12 y stratum), weight 8-32 kg, baseline haemoglobin 102 g/L (range 82-136), initial parasitaemia 120 000 parasites/uL (range 55 000-144 000).
- 31 adult subjects (19 M / 12 F), age 16-45 years (19 in the 16-30 y stratum, 12 in the 31-45 y stratum), weight 34-56 kg, baseline haemoglobin 118 g/L (range 32-137), initial parasitaemia 118 000 parasites/uL (range 32 000-137 000).
Children were significantly more anaemic than adults (P = 0.004) but had comparable parasite clearance and fever subsidence times. Eighteen out of 19 adult male patients and one male in the paediatric group were smokers; no female subjects smoked. Necessary co-medications were restricted to paracetamol (500 mg/day in adults, weight-adjusted in children), diazepam (5 mg/day) and thiamine (100 mg/day).
A total of 140 plasma concentrations from 54 subjects entered the popPK fit: 107 capillary plasma samples on Day 1 of the regimen (2-3 per subject) and 33 samples on Day 5 (one per subject in 18 children and 15 adults).
The same information is available programmatically via the model’s
population metadata.
str(mod_fn()$population)
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_cl_1, etaiov_cl_2, etaiov_vc_1, etaiov_vc_2
#> as a work-around try putting the mu-referenced expression on a simple line
#> List of 12
#> $ species : chr "human"
#> $ n_subjects : int 54
#> $ n_studies : int 1
#> $ n_observations: int 140
#> $ age_range : chr "2-45 years"
#> $ weight_range : chr "8-56 kg"
#> $ sex_female_pct: num 35
#> $ race_ethnicity: Named num 100
#> ..- attr(*, "names")= chr "Asian"
#> $ disease_state : chr "uncomplicated Plasmodium falciparum malaria"
#> $ dose_range : chr "Oral artemisinin 10 mg/kg/day for 5 days. Paediatric subjects: 10 mg/kg single dose on Days 1 and 5 (morning), "| __truncated__
#> $ regions : chr "Vietnam (Phu Rieng rubber plantation, Song Be Province)"
#> $ notes : chr "Sidhu 1998 Table 1: 23 paediatric subjects (16 M / 7 F, age 2-12 y, weight 8-32 kg; 10 in the 2-7 y stratum and"| __truncated__Source trace
Every parameter and equation traces back to the publication; the
per-parameter origin is recorded as an in-file comment next to each
ini() entry in
inst/modeldb/specificDrugs/Sidhu_1998_artemisinin.R. The
table below collects them in one place for review.
| Equation / parameter | Value | Source location |
|---|---|---|
lcl = log(432) – adult CL/F (L/h) |
432 | Sidhu 1998 Table 2 (CL/F adults; rel. s.e. 19%) |
lvc = log(1600) – adult V/F (L) |
1600 | Sidhu 1998 Table 2 (V/F adults; rel. s.e. 28%) |
lka = log(1.7) – absorption rate (1/h) |
1.7 | Sidhu 1998 Table 2 (ka; rel. s.e. 25%); constrained ka > CL/V during fitting (Sidhu 1998 Results) |
lfdepot = fixed(log(1)) – F anchor Day 1 |
1.00 (fixed) | Sidhu 1998 Methods: F is identifiable only as a relative quantity; anchored at unity on Day 1 |
e_child_cl = log(14.4 / 432) – CHILD shift on log
CL/F |
-3.402 | Sidhu 1998 Table 2 (CL/F children = 14.4 L/h/kg; rel. s.e. 24%) |
e_child_vc = log(37.9 / 1600) – CHILD shift on log
V/F |
-3.741 | Sidhu 1998 Table 2 (V/F children = 37.9 L/kg; rel. s.e. 33%) |
e_occ2_fdepot = log(1 / 6.9) – Day-5 shift on log
F |
-1.932 | Sidhu 1998 Table 2 (delta_F_Day1->Day5 = 6.9; rel. s.e. 20%) |
etalcl + etalvc ~ c(0.1843, 0.3493, 0.7334) – IIV with
near-unity correlation |
45% / 104% CV, cor = 0.95 | Sidhu 1998 Table 2 (omega CL/F = 45%, omega V/F = 104%) + Results p. 350 (‘correlation between eta V/F and eta CL/F was found to be near unity’) |
etalka ~ 3.5316 – IIV ka |
576% CV | Sidhu 1998 Table 2 (omega ka = 576%; rel. s.e. 21%); retained per Results (estimating its variance was preferred to fixing) |
etaiov_cl_1 = etaiov_cl_2 ~ 0.2475 – IOV on log
CL/F |
53% CV (shared) | Sidhu 1998 Table 2 (pi CL/F = 53%; rel. s.e. 32%) |
etaiov_vc_1 = etaiov_vc_2 ~ 0.5536 – IOV on log
V/F |
86% CV (shared) | Sidhu 1998 Table 2 (pi V/F = 86%; rel. s.e. 36%) |
propSd = 0.47 – proportional residual error
(fraction) |
47% CV | Sidhu 1998 Table 2 (sigma = 47%; rel. s.e. 41%) |
Age-group switch
ln_cl_typ = lcl + CHILD * (e_child_cl + log(WT))
|
– | Sidhu 1998 Table 2 (separate CL/F and V/F for adults and children) + footnote b (children’s units adjusted for total body weight) |
Time-dependent F:
f(depot) = exp(lfdepot + e_occ2_fdepot * oc2)
|
– | Sidhu 1998 Methods ‘time-variance modelled both as systematic and random variability’ + Table 2 (delta_F_Day1->Day5 = 6.9) |
IOV multiplex:
iov_cl = oc1*etaiov_cl_1 + oc2*etaiov_cl_2,
iov_vc = oc1*etaiov_vc_1 + oc2*etaiov_vc_2
|
– | Sidhu 1998 Methods ‘P_ij = P_typical_ij * exp(eta_i + kappa_ij)’ |
1-compartment oral: d/dt(depot) = -ka * depot,
d/dt(central) = ka * depot - kel * central
|
– | Sidhu 1998 Results ‘A one-compartment model with first-order absorption … was found to best describe the plasma artemisinin concentration data’ |
Concentration scaling Cc = central / vc * 1000 (mg/L
-> ug/L) |
– | Sidhu 1998 Figure 1 (concentrations reported in ug/L; doses in mg, V/F in L) |
Virtual cohort
We build four virtual cohorts that mirror the published study design.
Each cohort is a single dose at t = 0 carrying the appropriate
OCC (1 = Day-1 occasion, 2 = Day-5 occasion); the fast
artemisinin elimination (t1/2 ~ 2 h) means the Day-5 dose can be
simulated independently of the Day-1 dose without modelling the
intervening Days 2-4 doses. Subject body weights are drawn from a
truncated normal centred on the published Sidhu 1998 Table 1 medians and
clipped to the published ranges. Subject IDs are disjoint across cohorts
so the cohorts can be bind_rows()-ed safely.
set.seed(19980427L)
n_per_arm <- 60L
adult_dose_mg <- 500 # 2 x 250 mg capsules per Sidhu 1998 Methods
child_dose_per_kg <- 10 # mg/kg per Sidhu 1998 Methods
make_subjects <- function(n, child_flag, id_offset) {
if (child_flag == 1L) {
# paediatric weights: Sidhu 1998 Table 1 median 20 kg (range 8-32)
wt <- round(pmin(pmax(rnorm(n, mean = 20, sd = 6), 8), 32), 1)
} else {
# adult weights: Sidhu 1998 Table 1 median 46.5 kg (range 34-56)
wt <- round(pmin(pmax(rnorm(n, mean = 46.5, sd = 6), 34), 56), 1)
}
data.frame(
id = id_offset + seq_len(n),
CHILD = child_flag,
WT = wt
)
}
subjects <- dplyr::bind_rows(
make_subjects(n_per_arm, 1L, id_offset = 0L) |>
dplyr::mutate(cohort = "Children, Day 1", OCC = 1L,
dose_mg = child_dose_per_kg * WT),
make_subjects(n_per_arm, 1L, id_offset = n_per_arm) |>
dplyr::mutate(cohort = "Children, Day 5", OCC = 2L,
dose_mg = child_dose_per_kg * WT),
make_subjects(n_per_arm, 0L, id_offset = 2L * n_per_arm) |>
dplyr::mutate(cohort = "Adults, Day 1", OCC = 1L,
dose_mg = adult_dose_mg),
make_subjects(n_per_arm, 0L, id_offset = 3L * n_per_arm) |>
dplyr::mutate(cohort = "Adults, Day 5", OCC = 2L,
dose_mg = adult_dose_mg)
)
obs_times <- c(0.25, 0.5, 1, 1.5, 2, 2.5, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24)
build_events <- function(subjects, obs_times) {
out <- vector("list", length = nrow(subjects))
for (i in seq_len(nrow(subjects))) {
s <- subjects[i, ]
dose_row <- data.frame(
id = s$id, time = 0, evid = 1L, amt = s$dose_mg, cmt = 1L,
cohort = s$cohort, CHILD = s$CHILD, OCC = s$OCC, WT = s$WT
)
obs_rows <- data.frame(
id = s$id, time = obs_times, evid = 0L, amt = 0, cmt = NA_integer_,
cohort = s$cohort, CHILD = s$CHILD, OCC = s$OCC, WT = s$WT
)
out[[i]] <- rbind(dose_row, obs_rows)
}
events <- dplyr::bind_rows(out)
events <- events[order(events$id, events$time, -events$evid), ]
events
}
events <- build_events(subjects, obs_times)
stopifnot(!anyDuplicated(unique(events[, c("id", "time", "evid")])))Simulation
sim <- rxode2::rxSolve(
mod,
events = events,
keep = c("cohort", "CHILD", "OCC", "WT")
) |>
as.data.frame()The typical-value (no-IIV, no-residual) traces give a clean deterministic backbone for each cohort using the median weight of the corresponding age group.
mod_typical <- rxode2::zeroRe(mod)
#> Warning: some etas defaulted to non-mu referenced, possible parsing error: etaiov_cl_1, etaiov_cl_2, etaiov_vc_1, etaiov_vc_2
#> as a work-around try putting the mu-referenced expression on a simple line
typical_subjects <- data.frame(
id = 1:4,
CHILD = c(1L, 1L, 0L, 0L),
OCC = c(1L, 2L, 1L, 2L),
WT = c(20, 20, 46.5, 46.5),
cohort = c("Children, Day 1", "Children, Day 5",
"Adults, Day 1", "Adults, Day 5"),
dose_mg = c(child_dose_per_kg * 20, child_dose_per_kg * 20,
adult_dose_mg, adult_dose_mg)
)
typical_events <- build_events(typical_subjects, obs_times)
sim_typical <- rxode2::rxSolve(
mod_typical,
events = typical_events,
keep = c("cohort", "CHILD", "OCC", "WT")
) |>
as.data.frame()
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalka', 'etaiov_cl_1', 'etaiov_cl_2', 'etaiov_vc_1', 'etaiov_vc_2'
#> Warning: multi-subject simulation without without 'omega'Replicate published figures
Figure 1 – individual artemisinin plasma concentrations
Sidhu 1998 Figure 1a shows individual plasma concentration-time profiles after a 10 mg/kg dose on Day 1, with adults (solid lines) and children (dashed lines). Figure 1b contrasts Day-1 and Day-5 concentrations at matched post-dose times for the sub-group sampled on both occasions, illustrating the time-dependent decrease in oral bioavailability. The plots below stack the four virtual cohorts on a log y-axis for visual comparison.
sim_typical |>
dplyr::filter(time > 0) |>
ggplot(aes(time, Cc, colour = cohort, linetype = cohort)) +
geom_line(linewidth = 0.9) +
scale_y_log10() +
scale_linetype_manual(values = c(
"Children, Day 1" = "dashed", "Children, Day 5" = "dashed",
"Adults, Day 1" = "solid", "Adults, Day 5" = "solid"
)) +
labs(x = "Time post-dose (h)",
y = "Artemisinin plasma concentration (ug/L)",
colour = NULL, linetype = NULL,
title = "Typical-value oral artemisinin profiles by cohort and occasion",
caption = paste(
"Replicates Sidhu 1998 Figure 1.",
"Day-5 concentrations are reduced by 1/6.9 = 14.5% relative to Day 1,",
"reflecting the published systematic decrease in oral bioavailability."
))
The stochastic VPC below shows the population spread that the published IIV / IOV variance components produce.
sim |>
dplyr::filter(time > 0, Cc > 0) |>
dplyr::group_by(time, cohort) |>
dplyr::summarise(
p05 = quantile(Cc, 0.05, na.rm = TRUE),
p50 = quantile(Cc, 0.50, na.rm = TRUE),
p95 = quantile(Cc, 0.95, na.rm = TRUE),
.groups = "drop"
) |>
ggplot(aes(time, p50, colour = cohort, fill = cohort)) +
geom_ribbon(aes(ymin = p05, ymax = p95), alpha = 0.2, colour = NA) +
geom_line(linewidth = 0.6) +
facet_wrap(~cohort) +
scale_y_log10() +
labs(x = "Time post-dose (h)",
y = "Artemisinin plasma concentration (ug/L)",
title = "Stochastic VPC of single-dose oral artemisinin by cohort",
caption = "Solid: median; ribbon: 5%-95% population interval (n = 60 per arm).") +
theme(legend.position = "none")
PKNCA validation
Single-dose, dense-sampling NCA per
references/pknca-recipes.md. The published reference values
are the typical population estimates (Sidhu 1998 Results: median
half-life 2.6 h in adults, 1.8 h in children; Table 2
delta_F_Day1->Day5 = 6.9), so the NCA validation here runs against
the typical-value simulation (zeroRe()-d model,
one typical subject per cohort). The full-IIV stochastic NCA is reported
below it for population-spread context; the high published omega ka =
576% CV produces a heavy-tailed distribution of individual ka values
that can occasionally fall below the typical kel and trigger flip-flop
terminal phases, inflating PKNCA’s individual half-life estimates
relative to the published typical value (this is documented in
Assumptions and deviations).
# Add a t = 0 Cc = 0 placeholder per subject so PKNCA's AUC interval can
# start at 0; the oral 1-compartment model has Cc(0) = 0 by construction
# (the depot compartment carries the dose before absorption starts).
typical_t0 <- typical_subjects |>
dplyr::transmute(id, time = 0, Cc = 0, cohort)
typical_sim_nca <- dplyr::bind_rows(
typical_t0,
sim_typical |>
dplyr::filter(!is.na(Cc), time > 0) |>
dplyr::select(id, time, Cc, cohort)
) |>
dplyr::arrange(id, time)
typical_dose_df <- typical_events |>
dplyr::filter(evid == 1) |>
dplyr::select(id, time, amt, cohort)
conc_t <- PKNCA::PKNCAconc(typical_sim_nca, Cc ~ time | cohort + id,
concu = "ug/L", timeu = "h")
dose_t <- PKNCA::PKNCAdose(typical_dose_df, amt ~ time | cohort + id,
doseu = "mg")
intervals <- data.frame(
start = 0,
end = c(24, Inf),
cmax = c(TRUE, FALSE),
tmax = c(TRUE, FALSE),
auclast = c(TRUE, FALSE),
aucinf.obs = c(FALSE, TRUE),
half.life = c(FALSE, TRUE)
)
nca_typical <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_t, dose_t,
intervals = intervals))
nca_typical_df <- as.data.frame(nca_typical$result) |>
dplyr::filter(PPTESTCD %in% c("cmax", "tmax", "auclast",
"aucinf.obs", "half.life")) |>
dplyr::select(cohort, PPTESTCD, value = PPORRES)
knitr::kable(nca_typical_df,
caption = "Typical-value NCA per cohort (zeroRe simulation; one typical subject per cohort).",
digits = 2)| cohort | PPTESTCD | value |
|---|---|---|
| Adults, Day 1 | auclast | 1148.45 |
| Adults, Day 1 | cmax | 218.78 |
| Adults, Day 1 | tmax | 1.50 |
| Adults, Day 1 | tmax | 1.50 |
| Adults, Day 1 | half.life | 2.58 |
| Adults, Day 1 | aucinf.obs | 1150.57 |
| Adults, Day 5 | auclast | 166.44 |
| Adults, Day 5 | cmax | 31.71 |
| Adults, Day 5 | tmax | 1.50 |
| Adults, Day 5 | tmax | 1.50 |
| Adults, Day 5 | half.life | 2.58 |
| Adults, Day 5 | aucinf.obs | 166.75 |
| Children, Day 1 | auclast | 688.56 |
| Children, Day 1 | cmax | 170.31 |
| Children, Day 1 | tmax | 1.00 |
| Children, Day 1 | tmax | 1.00 |
| Children, Day 1 | half.life | 1.83 |
| Children, Day 1 | aucinf.obs | 688.66 |
| Children, Day 5 | auclast | 99.79 |
| Children, Day 5 | cmax | 24.68 |
| Children, Day 5 | tmax | 1.00 |
| Children, Day 5 | tmax | 1.00 |
| Children, Day 5 | half.life | 1.83 |
| Children, Day 5 | aucinf.obs | 99.81 |
sim_t0 <- subjects |>
dplyr::transmute(id, time = 0, Cc = 0, cohort)
sim_nca <- dplyr::bind_rows(
sim_t0,
sim |>
dplyr::filter(!is.na(Cc), time > 0) |>
dplyr::select(id, time, Cc, cohort)
) |>
dplyr::arrange(id, time)
dose_df <- events |>
dplyr::filter(evid == 1) |>
dplyr::select(id, time, amt, cohort)
conc_obj <- PKNCA::PKNCAconc(sim_nca, Cc ~ time | cohort + id,
concu = "ug/L", timeu = "h")
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
dose_obj <- PKNCA::PKNCAdose(dose_df, amt ~ time | cohort + id,
doseu = "mg")
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj,
intervals = intervals))
#> Warning: Too few points for half-life calculation (min.hl.points=3 with only 2 points)
#> Negative concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning: Too few points for half-life calculation (min.hl.points=3 with only 2
#> points)
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning: Too few points for half-life calculation (min.hl.points=3 with only 0
#> points)
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning in assert_conc(conc = conc): Negative concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(data$conc): NaNs produced
#> Warning in assert_conc(conc, any_missing_conc = any_missing_conc): Negative
#> concentrations found
#> Warning in log(conc.2/conc.1): NaNs produced
#> Warning: Too few points for half-life calculation (min.hl.points=3 with only 0
#> points)
nca_df <- as.data.frame(nca_res$result)
nca_summary <- nca_df |>
dplyr::filter(PPTESTCD %in% c("cmax", "tmax", "auclast",
"aucinf.obs", "half.life")) |>
dplyr::group_by(cohort, PPTESTCD) |>
dplyr::summarise(
median = median(PPORRES, na.rm = TRUE),
p05 = quantile(PPORRES, 0.05, na.rm = TRUE),
p95 = quantile(PPORRES, 0.95, na.rm = TRUE),
.groups = "drop"
)
knitr::kable(nca_summary,
caption = "Stochastic NCA per cohort (median [5%-95%]).",
digits = 2)| cohort | PPTESTCD | median | p05 | p95 |
|---|---|---|---|---|
| Adults, Day 1 | aucinf.obs | 1093.35 | 408.60 | 3070.15 |
| Adults, Day 1 | auclast | 1000.94 | 350.25 | 2866.48 |
| Adults, Day 1 | cmax | 224.84 | 24.75 | 1137.94 |
| Adults, Day 1 | half.life | 2.63 | 0.61 | 17.74 |
| Adults, Day 1 | tmax | 1.00 | 0.25 | 6.20 |
| Adults, Day 5 | aucinf.obs | 165.47 | 61.16 | 429.04 |
| Adults, Day 5 | auclast | 148.82 | 60.29 | 419.03 |
| Adults, Day 5 | cmax | 21.29 | 4.55 | 75.67 |
| Adults, Day 5 | half.life | 3.35 | 1.03 | 28.61 |
| Adults, Day 5 | tmax | 1.00 | 0.25 | 6.10 |
| Children, Day 1 | aucinf.obs | 668.63 | 266.83 | 1812.12 |
| Children, Day 1 | auclast | 607.79 | 205.25 | 1542.91 |
| Children, Day 1 | cmax | 141.80 | 15.03 | 754.40 |
| Children, Day 1 | half.life | 2.57 | 0.51 | 14.22 |
| Children, Day 1 | tmax | 1.00 | 0.25 | 6.00 |
| Children, Day 5 | aucinf.obs | 100.25 | 34.57 | 300.19 |
| Children, Day 5 | auclast | 90.12 | 27.60 | 249.90 |
| Children, Day 5 | cmax | 16.37 | 1.85 | 109.40 |
| Children, Day 5 | half.life | 2.21 | 0.65 | 11.01 |
| Children, Day 5 | tmax | 1.00 | 0.25 | 5.05 |
Comparison against published NCA
Sidhu 1998 does not publish a side-by-side Cmax / AUC NCA table – the field-setting sparse-sampling design did not support per-subject NCA. The paper does publish median elimination half-lives by age group on Day 1 (the typical-value estimate), the individual-estimate range across the 54 subjects, and the systematic Day-5 / Day-1 decrease in bioavailability (delta_F_Day1->Day5 = 6.9 in Table 2). The simulated typical-value NCA is compared against the typical-value published half-life; the simulated AUC_Day1 / AUC_Day5 ratio is compared against the published 6.9-fold F change.
typical_t12 <- nca_typical_df |>
dplyr::filter(PPTESTCD == "half.life", grepl("Day 1$", cohort)) |>
dplyr::select(cohort, simulated_typical_h = value)
published_t12 <- dplyr::tribble(
~cohort, ~published_typical_h, ~published_range_lo_h, ~published_range_hi_h,
"Children, Day 1", 1.8, 0.8, 7.9,
"Adults, Day 1", 2.6, 1.0, 11.8
)
half_life_cmp <- dplyr::left_join(published_t12, typical_t12, by = "cohort")
knitr::kable(half_life_cmp,
caption = paste(
"Half-life comparison: Sidhu 1998 Results report 'population",
"estimates of artemisinin half-life on Day 1 were 2.6 h (range of",
"individual estimates: 1.0-11.8 h) in adults and 1.8 h (range:",
"0.8-7.9 h) in children'. The simulated typical-value half-life",
"uses the zeroRe()-d model and matches the published typical value",
"by construction (log(2) * V/F / CL/F): adults 0.693 * 1600/432 =",
"2.567 h; children 0.693 * 37.9/14.4 = 1.824 h."
),
digits = 2)| cohort | published_typical_h | published_range_lo_h | published_range_hi_h | simulated_typical_h |
|---|---|---|---|---|
| Children, Day 1 | 1.8 | 0.8 | 7.9 | 1.83 |
| Adults, Day 1 | 2.6 | 1.0 | 11.8 | 2.58 |
typical_auc <- nca_typical_df |>
dplyr::filter(PPTESTCD == "aucinf.obs") |>
dplyr::select(cohort, sim_auc = value)
day1_adult_auc <- typical_auc$sim_auc[typical_auc$cohort == "Adults, Day 1"]
day5_adult_auc <- typical_auc$sim_auc[typical_auc$cohort == "Adults, Day 5"]
day1_child_auc <- typical_auc$sim_auc[typical_auc$cohort == "Children, Day 1"]
day5_child_auc <- typical_auc$sim_auc[typical_auc$cohort == "Children, Day 5"]
f_change_cmp <- dplyr::tibble(
group = c("Adults", "Children"),
published_F_Day1_over_Day5 = 6.9,
simulated_AUC_Day1_over_Day5 = c(
day1_adult_auc / day5_adult_auc,
day1_child_auc / day5_child_auc
)
)
knitr::kable(f_change_cmp,
caption = paste(
"Time-dependent bioavailability check: published",
"delta_F_Day1->Day5 = 6.9 (Sidhu 1998 Table 2) vs simulated",
"typical-value AUC_Day1 / AUC_Day5 ratio. The two should match",
"exactly because F is the only Day-1 vs Day-5 difference in the",
"structural model."
),
digits = 2)| group | published_F_Day1_over_Day5 | simulated_AUC_Day1_over_Day5 |
|---|---|---|
| Adults | 6.9 | 6.9 |
| Children | 6.9 | 6.9 |
The simulated typical-value half-lives reproduce the published 2.6 h
(adults) and 1.8 h (children) point estimates exactly because they are
arithmetic consequences of CL/F and V/F:
t1/2_adults = log(2) * 1600 / 432 = 2.567 h;
t1/2_children = log(2) * 37.9 / 14.4 = 1.824 h. The
simulated AUC_Day1 / AUC_Day5 ratio recovers the published 6.9-fold
change for both age groups because the Day 1 -> Day 5 dose is
modelled as a multiplicative shift on the depot-compartment
bioavailability with no other Day-5-specific changes.
Assumptions and deviations
-
High published omega ka = 576% inflates stochastic
individual half-life NCA. The model retains the published Table
2 omega ka exactly as reported, including the very high (576%)
coefficient of variation. In simulation, a log-normal eta with this
variance generates a heavy-tailed individual ka distribution that
occasionally falls well below the typical kel (0.27 /h in adults),
producing flip-flop terminal kinetics in those individuals; PKNCA then
estimates the terminal slope from ka rather than kel and reports an
inflated apparent half-life. The simulated stochastic-NCA median
half-life for adults is therefore ~60% above the published 2.6 h typical
value, whereas the simulated typical-value half-life matches it exactly.
The Sidhu 1998 Results note that the authors tested fixing typical Ka
and found it had little effect on the estimates of the other parameters;
a downstream user who wants better-behaved stochastic NCA can edit the
model to fix
etalka ~ 0(or truncate ka draws to ka > kel) without changing CL/F or V/F. - IIV correlation between CL/F and V/F set to 0.95 (encoded as covariance 0.3493 in the omega block). Sidhu 1998 Results state “the correlation between eta V/F i and eta CL/F i was found to be near unity” but do not publish the numeric off-diagonal. A correlation of exactly 1.0 would make the omega matrix singular and is unestimable in NONMEM; the encoded 0.95 preserves the qualitative finding (CL/F and V/F move together at the subject level) while keeping the matrix positive definite. A user fitting this model to real data should re-estimate the correlation; the simulated VPC bands are insensitive to the exact value above ~0.9.
-
Variance scale interpretation. The published omega
and pi values are reported as percentage CV in Table 2 without an
explicit conversion footnote. We use the log-normal interpretation
omega^2 = log(CV^2 + 1)matching the convention used in the sibling artemisinin extractionBirgersson_2016_artemisinin. The alternative (linear) interpretationomega^2 = CV^2would give slightly larger variances for the high-CV terms (omega ka in particular) but does not change the central tendency of the simulation. -
IOV variance shared between occasions. Table 2
reports a single pi value per parameter (53% on CL/F, 86% on V/F) rather
than per-occasion values, implying the variance is the same on both
occasions (NONMEM
$OMEGA BLOCK(1) SAMEidiom). The encoded variance on occasion 2 isfix()-ed to the occasion-1 estimate, mirroring theAregbe_2012_alvespimycinandJonsson_2011_ethambutolconventions. -
Bioavailability anchored at unity on Day 1; Day-5 ratio is
the only F change. The paper does not separately estimate
absolute bioavailability (F_Day1 is unidentifiable from oral-only data)
– it estimates only the relative change
F_Day1 / F_Day5 = 6.9. The simulation therefore reports concentrations on a relative scale: the Day-1 values are the typical population trajectory and the Day-5 values are 1/6.9 of those. -
No covariate-effect modelling of gender or smoking.
Sidhu 1998 screened gender and smoking as potential descriptors of CL/F
and V/F and reported that adding them gave a 26-unit decrease in the
objective function, but the resulting THETA estimates carried 60-90%
relative standard errors and were biased by the confounding of male sex
with smoking in the adult cohort (Results p. 350). The published final
model and this implementation therefore exclude gender and smoking. The
6.7 (children) and 7.3 (adults) per-group estimates of
delta_F_Day1->Day5(and 6.7 / 7.4 for males / females) are reported in the text but were pooled to the single value 6.9 in the final published model; the package model uses 6.9. -
Days 2-4 dosing omitted. The published study
administered artemisinin twice daily on Days 2-4 between the Day-1 and
Day-5 morning doses; only Day-1 and Day-5 samples entered the popPK fit.
Because the population-typical half-life (2.6 h adults / 1.8 h children)
clears any residual drug well within the 12 h interval between any
single dose and the next, the Day-5 dose can be simulated independently
of the intervening doses without loss of accuracy. A user who wants to
inspect the steady-state concentration after the Days 2-4 doses can
append the intermediate dose rows at 24, 36, 48, … 84 h with
OCC = 1(the systematic F change kicks in only on Day 5, so the intermediate doses carry the Day-1 bioavailability per the paper’s narrative). - Capillary plasma (Sidhu 1998 Methods) treated as equivalent to venous plasma. The paper validated the capillary-vs-venous equivalence in a pilot study of healthy adults (Sidhu and Ashton 1997, Am J Trop Med Hyg 56:13-16) before the present cohort; the package model reports concentrations in ug/L with no further matrix correction.
- Virtual cohort size n = 60 per arm. The published study had n = 23 children + n = 31 adults; the virtual cohort uses 60 independent subjects per cohort to give tighter NCA percentiles. The simulated 5%-95% population intervals are therefore narrower than the published min-max individual ranges, which is the expected behaviour when the simulation pool is larger than the original study.