Dexamethasone and betamethasone IM and oral (Krzyzanski 2021)
Source:vignettes/articles/Krzyzanski_2021_corticosteroids.Rmd
Krzyzanski_2021_corticosteroids.RmdModel and source
Krzyzanski et al. (2021) fitted the dexamethasone (DEX) and betamethasone (BET) data independently, each with its own two-compartment model, so the paper contributes two model files that share this vignette:
-
Krzyzanski_2021_dexamethasone: Two-compartment population PK model for dexamethasone after single 6 mg doses of dexamethasone phosphate given intramuscularly or orally to healthy nonpregnant Indian women, with separate first-order absorption from an IM and an oral depot. Clearances and volumes are apparent (divided by the IM bioavailability FIM); oral bioavailability is relative to IM (Fr = FPO / FIM). No covariates. -
Krzyzanski_2021_betamethasone: Two-compartment population PK model for betamethasone after single 6 mg doses of betamethasone phosphate given intramuscularly or orally, or of a 1:1 betamethasone phosphate/acetate IM suspension (Celestone), to healthy nonpregnant Indian women. Three parallel first-order absorption depots: IM phosphate, oral phosphate, and a slow IM acetate depot (flip-flop terminal phase). Clearances and volumes are apparent (divided by the IM bioavailability FIM); oral and acetate bioavailabilities are relative to IM phosphate. No covariates. - Citation: Krzyzanski W, Milad MA, Jobe AH, Peppard T, Bies RR, Jusko WJ. Population pharmacokinetic modeling of intramuscular and oral dexamethasone and betamethasone in Indian women. J Pharmacokinet Pharmacodyn. 2021;48(2):261-272. doi:10.1007/s10928-020-09730-z
- Article (open access): https://doi.org/10.1007/s10928-020-09730-z
- Supplement (NONMEM control stream for BET; Tables S1-S2): Electronic Supplementary Material 1 and 2 of the article.
Population
Forty-eight healthy, nonpregnant Indian women (ages 22-39 years, body weight 47.0-68.7 kg, mean 56.8 kg, BMI 20.6-25.0 kg/m^2) took part in an open-label, randomized, two-period partial crossover study (NCT03668860; Krzyzanski 2021 Tables 1-2). Each woman received two of five single-dose treatments, each delivering 6 mg of the free-alcohol steroid:
| Treatment | Route | Formulation | n |
|---|---|---|---|
| A (DEX-P IM) | IM | dexamethasone phosphate solution | 12 |
| B (BET-P IM) | IM | betamethasone phosphate solution | 12 |
| C (BET-PA IM) | IM | 3 mg betamethasone phosphate + 3 mg betamethasone acetate suspension (Celestone) | 24 |
| D (DEX-P PO) | PO | 0.5 mg dexamethasone phosphate tablets | 24 |
| E (BET-P PO) | PO | 0.5 mg betamethasone phosphate tablets | 24 |
Plasma was sampled to 96 h after each dose. The DEX model was fitted
to 578 concentrations (103 below the 0.1 ng/mL LLOQ) from the 36 women
who received DEX; the BET model to 949 concentrations (19 below LLOQ)
from all 48 women. The homogeneous population precluded a covariate
analysis, so neither model carries covariates. The same information is
available programmatically via
readModelDb("Krzyzanski_2021_dexamethasone")()$population.
Source trace
Model equations are Krzyzanski 2021 Eqs 1-7 (structure), Eq 10
(log-normal IIV) and Eq 11 (additive residual error on log
concentrations, i.e. lnorm). The supplement’s BET NONMEM
control stream confirms the structure: five compartments (IM, plasma,
peripheral, PO, IM acetate), F4 = FR,
F5 = FRA, the IM phosphate depot as the bioavailability
reference, and Y = LOG(A(2)/V) + EPS(1).
| Parameter | DEX | BET | Source location |
|---|---|---|---|
lcl CL/FIM (L/h) |
9.29 | 5.95 | Tables 3 / 4; BET supplement $THETA 1
|
lvc Vp/FIM (L) |
51.3 | 67.5 | Tables 3 / 4; $THETA 2
|
lq CLD/FIM (L/h) |
0.538 | 0.173 | Tables 3 / 4; $THETA 8
|
lvp VT/FIM (L) |
5.06 | 4.94 | Tables 3 / 4; $THETA 9
|
lka_im kaIM (1/h) |
0.460 | 0.971 | Tables 3 / 4; $THETA 3
|
lka_oral kaPO (1/h) |
0.936 | 1.21 | Tables 3 / 4; $THETA 5
|
lka_im_acetate kaIMa (1/h) |
– | 0.00638 | Table 4; $THETA 4
|
lfdepot_oral Fr = FPO/FIM |
1.04 | 0.935 | Tables 3 / 4, Eq 7; $THETA 6
|
lfdepot_im_acetate Fra = FIMa/FIM |
– | 0.819 | Table 4, Eq 7; $THETA 7
|
etalcl omega^2 |
0.0265 | 0.0210 | Tables 3 / 4 |
etalvc omega^2 |
0 (fixed; omitted) | 0.0188 | Tables 3 / 4 |
cov(etalcl, etalvc) |
– | 0.0155 | Table 4; $OMEGA BLOCK(2)
|
etalka_im omega^2 |
0.0633 | 0.0441 | Tables 3 / 4 |
etalka_oral omega^2 |
0.395 | 0.241 | Tables 3 / 4 |
etalka_im_acetate omega^2 |
– | 0.147 | Table 4 |
etalfdepot_oral omega^2 |
– | 0.0182 | Table 4 |
etalfdepot_im_acetate omega^2 |
– | 0.00773 | Table 4 |
expSd = sqrt(sigma^2) |
sqrt(0.0455) | sqrt(0.0211) | Tables 3 / 4; $SIGMA
|
The steady-state volumes quoted in the abstract are reproduced from the table values: Vss/FIM = 51.3 + 5.06 = 56.4 L (DEX) and 67.5 + 4.94 = 72.4 L (BET).
dex_ini <- rxode2::rxode(readModelDb("Krzyzanski_2021_dexamethasone"))$iniDf
#> ℹ parameter labels from comments will be replaced by 'label()'
bet_ini <- rxode2::rxode(readModelDb("Krzyzanski_2021_betamethasone"))$iniDf
#> ℹ parameter labels from comments will be replaced by 'label()'
theta <- function(ini, nm) exp(ini$est[ini$name == nm])
vss <- c(
DEX = theta(dex_ini, "lvc") + theta(dex_ini, "lvp"),
BET = theta(bet_ini, "lvc") + theta(bet_ini, "lvp")
)
vss
#> DEX BET
#> 56.36 72.44
# Abstract: Vss/FIM = 56.4 L (DEX) and 72.4 L (BET).
stopifnot(abs(vss - c(56.4, 72.4)) < 0.05)Dosing the models
Both models use numbered parallel absorption depots. Doses are in mg
of the free-alcohol steroid and Cc is in ng/mL.
| Compartment | Route / formulation | Bioavailability |
|---|---|---|
depot1 |
IM phosphate (DEX-P or BET-P) | 1 (reference, FIM) |
depot2 |
oral phosphate tablets | Fr |
depot3 |
IM acetate (BET model only) | Fra |
A Celestone (BET-PA) injection is split between two depots: half of
the dose as phosphate into depot1 and half as acetate into
depot3.
Virtual cohort and simulation of the five treatments
The observed data are not public. Each treatment is simulated in 200 virtual women over the 96 h sampling window. No covariates are needed.
rxode2::rxSetSeed(20210103)
n_arm <- 200L
obs_times <- sort(unique(c(seq(0, 6, by = 0.25), 7:96)))
# One cohort as a self-contained event table. `id_offset` keeps subject IDs
# disjoint across arms so rxSolve cannot merge subjects.
make_arm <- function(n, treatment, cmts, amts, dose_times = 0, id_offset = 0L) {
ids <- id_offset + seq_len(n)
doses <- tidyr::expand_grid(
id = ids,
time = dose_times,
dose_row = seq_along(cmts)
) |>
mutate(
evid = 1L,
cmt = cmts[dose_row],
amt = amts[dose_row]
) |>
select(-dose_row)
obs <- tidyr::expand_grid(id = ids, time = obs_times) |>
mutate(evid = 0L, cmt = "central", amt = 0)
bind_rows(doses, obs) |>
mutate(treatment = treatment) |>
arrange(id, time, desc(evid))
}
events_dex <- bind_rows(
make_arm(n_arm, "DEX-P IM", "depot1", 6, id_offset = 0L),
make_arm(n_arm, "DEX-P PO", "depot2", 6, id_offset = n_arm)
)
events_bet <- bind_rows(
make_arm(n_arm, "BET-P IM", "depot1", 6, id_offset = 0L),
make_arm(n_arm, "BET-P PO", "depot2", 6, id_offset = n_arm),
make_arm(n_arm, "BET-PA IM", c("depot1", "depot3"), c(3, 3), id_offset = 2L * n_arm)
)
stopifnot(
!anyDuplicated(unique(events_dex[, c("id", "time", "evid", "cmt")])),
!anyDuplicated(unique(events_bet[, c("id", "time", "evid", "cmt")]))
)
mod_dex <- readModelDb("Krzyzanski_2021_dexamethasone")
mod_bet <- readModelDb("Krzyzanski_2021_betamethasone")
sim_dex <- rxode2::rxSolve(mod_dex, events = events_dex, keep = "treatment") |>
as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
sim_bet <- rxode2::rxSolve(mod_bet, events = events_bet, keep = "treatment") |>
as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
sim_all <- bind_rows(
sim_dex |> mutate(id = id, drug = "DEX"),
sim_bet |> mutate(id = id + 10000L, drug = "BET")
) |>
mutate(treatment = factor(
treatment,
levels = c("DEX-P IM", "DEX-P PO", "BET-P IM", "BET-P PO", "BET-PA IM")
))Replicate Figures 2-6
# Replicates the model-predicted bands of the VPCs in Figures 3-6 of
# Krzyzanski 2021 (median and 5th-95th percentiles by treatment); Figure 2 is
# the matching observed-data spaghetti plot.
sim_all |>
filter(time > 0) |>
group_by(treatment, time) |>
summarise(
Q05 = quantile(Cc, 0.05),
Q50 = quantile(Cc, 0.50),
Q95 = quantile(Cc, 0.95),
.groups = "drop"
) |>
ggplot(aes(time, Q50)) +
geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.25) +
geom_line() +
geom_hline(yintercept = 0.1, linetype = "dotted") +
facet_wrap(~treatment) +
scale_y_log10() +
scale_x_continuous(breaks = seq(0, 96, by = 24)) +
labs(
x = "Time after dose (h)", y = "Plasma concentration (ng/mL)",
caption = paste(
"Median and 5th-95th percentiles of 200 simulated women per treatment;",
"dotted line = LLOQ 0.1 ng/mL. Compare Figures 2-6 of Krzyzanski 2021."
)
)
PKNCA validation against Supplementary Table S1
Supplementary Table S1 lists the mean (SD) over individual model predictions of Cmax, tmax and terminal half-life for each treatment, where the paper took the half-life from the 95 and 96 h points only. PKNCA computes Cmax, tmax and the half-life with its automatic terminal-phase selection. The paper’s two-point half-life is computed separately below.
sim_nca <- sim_all |>
filter(!is.na(Cc)) |>
select(id, time, Cc, treatment)
# Guarantee a time-zero row per subject (pre-dose Cc = 0 for extravascular).
sim_nca <- bind_rows(
sim_nca,
sim_nca |> distinct(id, treatment) |> mutate(time = 0, Cc = 0)
) |>
distinct(id, treatment, time, .keep_all = TRUE) |>
arrange(treatment, id, time)
dose_df <- bind_rows(
events_dex |> mutate(id = id),
events_bet |> mutate(id = id + 10000L)
) |>
filter(evid == 1) |>
group_by(id, time, treatment) |>
summarise(amt = sum(amt), .groups = "drop")
conc_obj <- PKNCA::PKNCAconc(sim_nca, Cc ~ time | treatment + id,
concu = "ng/mL", timeu = "h")
dose_obj <- PKNCA::PKNCAdose(dose_df, amt ~ time | treatment + id,
doseu = "mg")
intervals <- data.frame(
start = 0, end = 96,
cmax = TRUE, tmax = TRUE, half.life = TRUE
)
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals))
# Table S1 reports means of individual values, so the simulated side is
# aggregated with the mean as well (ncaComparisonTable() defaults to median).
sim_mean <- as.data.frame(nca_res$result) |>
filter(PPTESTCD %in% c("cmax", "tmax", "half.life")) |>
group_by(treatment, PPTESTCD) |>
summarise(PPORRES = mean(PPORRES, na.rm = TRUE), .groups = "drop")
published_s1 <- tibble::tribble(
~treatment, ~cmax, ~tmax, ~half.life,
"DEX-P IM", 62.5, 3.3, 7.5,
"DEX-P PO", 78.9, 2.2, 7.6,
"BET-P IM", 66.9, 2.8, 14.9,
"BET-P PO", 65.9, 2.6, 18.7,
"BET-PA IM", 35.8, 2.9, 77.6
)
cmp <- nlmixr2lib::ncaComparisonTable(
simulated = sim_mean,
reference = published_s1,
by = "treatment",
units = c(cmax = "ng/mL", tmax = "h", half.life = "h"),
tolerance_pct = 20
)
knitr::kable(
cmp,
caption = "Simulated (mean of 200 per treatment) vs. Krzyzanski 2021 Table S1. * differs by >20%."
)| NCA parameter | treatment | Reference | Simulated | % diff |
|---|---|---|---|---|
| Cmax (ng/mL) | DEX-P IM | 62.5 | 63 | +0.9% |
| Cmax (ng/mL) | DEX-P PO | 78.9 | 79.5 | +0.8% |
| Cmax (ng/mL) | BET-P IM | 66.9 | 70.9 | +5.9% |
| Cmax (ng/mL) | BET-P PO | 65.9 | 68.1 | +3.4% |
| Cmax (ng/mL) | BET-PA IM | 35.8 | 35.8 | -0.1% |
| Tmax (h) | DEX-P IM | 3.3 | 3.29 | -0.3% |
| Tmax (h) | DEX-P PO | 2.2 | 2.33 | +6.0% |
| Tmax (h) | BET-P IM | 2.8 | 2.72 | -2.8% |
| Tmax (h) | BET-P PO | 2.6 | 2.52 | -3.1% |
| Tmax (h) | BET-PA IM | 2.9 | 2.83 | -2.5% |
| t½ (h) | DEX-P IM | 7.5 | 7.26 | -3.2% |
| t½ (h) | DEX-P PO | 7.6 | 7.24 | -4.7% |
| t½ (h) | BET-P IM | 14.9 | 16.6 | +11.6% |
| t½ (h) | BET-P PO | 18.7 | 16.8 | -10.3% |
| t½ (h) | BET-PA IM | 77.6 | 88.4 | +13.9% |
chk <- sim_mean |>
filter(PPTESTCD == "cmax") |>
inner_join(published_s1, by = "treatment") |>
mutate(pct_diff = 100 * (PPORRES - cmax) / cmax)
chk
#> # A tibble: 5 × 7
#> treatment PPTESTCD PPORRES cmax tmax half.life pct_diff
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 BET-P IM cmax 70.9 66.9 2.8 14.9 5.93
#> 2 BET-P PO cmax 68.1 65.9 2.6 18.7 3.38
#> 3 BET-PA IM cmax 35.8 35.8 2.9 77.6 -0.0677
#> 4 DEX-P IM cmax 63.0 62.5 3.3 7.5 0.859
#> 5 DEX-P PO cmax 79.5 78.9 2.2 7.6 0.763
# Mean Cmax over 200 subjects is a centre statistic. A mis-transcribed
# clearance, volume, ka or bioavailability moves it by tens of percent.
stopifnot(all(abs(chk$pct_diff) < 12))The Table S1 half-life used only the 95 and 96 h points. Reproducing that definition:
hl_95_96 <- sim_all |>
filter(time %in% c(95, 96)) |>
select(treatment, id, time, Cc) |>
tidyr::pivot_wider(names_from = time, values_from = Cc, names_prefix = "C") |>
mutate(thalf = log(2) / log(C95 / C96)) |>
group_by(treatment) |>
summarise(mean_thalf = mean(thalf), sd_thalf = sd(thalf), .groups = "drop") |>
inner_join(published_s1 |> select(treatment, published = half.life), by = "treatment")
hl_95_96 |>
dplyr::rename(
"Treatment" = treatment,
"Simulated mean t1/2 (h)" = mean_thalf,
"Simulated SD (h)" = sd_thalf,
"Table S1 mean t1/2 (h)" = published
) |>
knitr::kable(digits = 1)| Treatment | Simulated mean t1/2 (h) | Simulated SD (h) | Table S1 mean t1/2 (h) |
|---|---|---|---|
| DEX-P IM | 7.4 | 0.3 | 7.5 |
| DEX-P PO | 7.3 | 0.2 | 7.6 |
| BET-P IM | 17.1 | 1.0 | 14.9 |
| BET-P PO | 17.2 | 0.9 | 18.7 |
| BET-PA IM | 90.6 | 24.7 | 77.6 |
For DEX-P IM and PO, BET-P IM and BET-P PO the two-point half-life matches the paper to within about 15%. The BET-PA IM value is longer than Table S1 reports (77.6 h). With the typical parameters, the 95-96 h slope still mixes the 17 h disposition phase and the 109 h acetate absorption phase, and the simulated mean lies between them. Table S1 averages the post-hoc estimates of the 24 women who received BET-PA, whereas this is a fresh simulation from the population estimates. See Assumptions and deviations.
Mean residence time (Eq 9)
The paper defines MRT = (Vss/FIM)/(CL/FIM) + 1/ka, with the effective ka for BET-PA combining the two IM depots weighted by their bioavailabilities.
mrt <- sim_all |>
filter(time == 0) |>
distinct(id, .keep_all = TRUE) |>
mutate(
ka_eff = case_when(
treatment %in% c("DEX-P IM", "BET-P IM") ~ ka_im,
treatment %in% c("DEX-P PO", "BET-P PO") ~ ka_oral,
treatment == "BET-PA IM" ~ (1 + fra) / (1 / ka_im + fra / ka_im_acetate)
),
mrt = (vc + vp) / cl + 1 / ka_eff
) |>
group_by(treatment) |>
summarise(mean_mrt = mean(mrt), sd_mrt = sd(mrt), .groups = "drop") |>
mutate(published = c(7.81, 6.91, 12.1, 12.5, 96.5)[match(
treatment, c("DEX-P IM", "DEX-P PO", "BET-P IM", "BET-P PO", "BET-PA IM")
)])
mrt |>
dplyr::rename(
"Treatment" = treatment,
"Simulated mean MRT (h)" = mean_mrt,
"Simulated SD (h)" = sd_mrt,
"Table S1 mean MRT (h)" = published
) |>
knitr::kable(digits = 2)| Treatment | Simulated mean MRT (h) | Simulated SD (h) | Table S1 mean MRT (h) |
|---|---|---|---|
| DEX-P IM | 8.38 | 1.15 | 7.81 |
| DEX-P PO | 7.51 | 1.42 | 6.91 |
| BET-P IM | 13.36 | 1.19 | 12.10 |
| BET-P PO | 12.98 | 1.09 | 12.50 |
| BET-PA IM | 86.98 | 30.19 | 96.50 |
The simulated mean MRTs are within about 10% of Table S1.
Mass balance
With the random effects removed, AUC(0-inf) x CL/FIM must equal the bioavailable dose. This check does not depend on random draws.
typ_dex <- rxode2::zeroRe(mod_dex)
#> ℹ parameter labels from comments will be replaced by 'label()'
typ_bet <- rxode2::zeroRe(mod_bet)
#> ℹ parameter labels from comments will be replaced by 'label()'
long_grid <- c(seq(0, 24, by = 0.1), seq(25, 4000, by = 1))
mb_events <- function(cmts, amts) {
bind_rows(
tibble(id = 1L, time = 0, evid = 1L, cmt = cmts, amt = amts),
tibble(id = 1L, time = long_grid, evid = 0L, cmt = "central", amt = 0)
) |>
arrange(time, desc(evid))
}
trap <- function(s) sum(diff(s$time) * (head(s$Cc, -1) + tail(s$Cc, -1)) / 2)
mb <- tibble::tribble(
~case, ~model, ~cmts, ~amts, ~expected,
"DEX-P IM", "dex", "depot1", 6, 6 / 9.29,
"DEX-P PO", "dex", "depot2", 6, 6 * 1.04 / 9.29,
"BET-P IM", "bet", "depot1", 6, 6 / 5.95,
"BET-P PO", "bet", "depot2", 6, 6 * 0.935 / 5.95,
"BET-PA IM", "bet", c("depot1", "depot3"), c(3, 3), (3 + 3 * 0.819) / 5.95
)
mb$simulated <- vapply(seq_len(nrow(mb)), function(i) {
m <- if (mb$model[i] == "dex") typ_dex else typ_bet
s <- as.data.frame(rxode2::rxSolve(m, mb_events(mb$cmts[[i]], mb$amts[[i]])))
# ng*h/mL = ug*h/L; convert to mg*h/L to compare with dose (mg) / CL (L/h)
trap(s) / 1000
}, numeric(1))
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalka_im', 'etalka_oral'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalka_im', 'etalka_oral'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalka_im', 'etalka_im_acetate', 'etalka_oral', 'etalfdepot_oral', 'etalfdepot_im_acetate'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalka_im', 'etalka_im_acetate', 'etalka_oral', 'etalfdepot_oral', 'etalfdepot_im_acetate'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc', 'etalka_im', 'etalka_im_acetate', 'etalka_oral', 'etalfdepot_oral', 'etalfdepot_im_acetate'
mb$pct_diff <- 100 * (mb$simulated - mb$expected) / mb$expected
mb |>
select(case, expected, simulated, pct_diff) |>
dplyr::rename(
"Treatment" = case,
"F x Dose / CL (mg*h/L)" = expected,
"Simulated AUC0-4000 (mg*h/L)" = simulated,
"% diff" = pct_diff
) |>
knitr::kable(digits = 3)| Treatment | F x Dose / CL (mg*h/L) | Simulated AUC0-4000 (mg*h/L) | % diff |
|---|---|---|---|
| DEX-P IM | 0.646 | 0.646 | -0.001 |
| DEX-P PO | 0.672 | 0.672 | -0.010 |
| BET-P IM | 1.008 | 1.008 | 0.001 |
| BET-P PO | 0.943 | 0.943 | -0.001 |
| BET-PA IM | 0.917 | 0.917 | 0.000 |
Antenatal regimens: Figure 8 and Supplementary Table S2
The paper simulated three WHO antenatal corticosteroid regimens in 200 women each: DEX-P 6 mg IM every 12 h x 4, BET-P 12 mg IM every 24 h x 2, and BET-PA (Celestone) 12 mg IM every 24 h x 2. Table S2 reports the median and 5th-95th percentiles of Cmax, Ctrough and AUC0-48 and AUC0-72.
obs_times <- sort(unique(c(seq(0, 72, by = 0.25))))
events_reg_dex <- make_arm(n_arm, "DEX 6 mg IM 4xBID", "depot1", 6,
dose_times = c(0, 12, 24, 36), id_offset = 0L)
events_reg_bet <- bind_rows(
make_arm(n_arm, "BET 12 mg IM 2xQD", "depot1", 12,
dose_times = c(0, 24), id_offset = 0L),
make_arm(n_arm, "CEL 12 mg IM 2xQD", c("depot1", "depot3"), c(6, 6),
dose_times = c(0, 24), id_offset = n_arm)
)
sim_reg <- bind_rows(
rxode2::rxSolve(mod_dex, events = events_reg_dex, keep = "treatment") |>
as.data.frame() |> mutate(id = id),
rxode2::rxSolve(mod_bet, events = events_reg_bet, keep = "treatment") |>
as.data.frame() |> mutate(id = id + 10000L)
)
# Replicates Figure 8 of Krzyzanski 2021.
sim_reg |>
group_by(treatment, time) |>
summarise(
Q05 = quantile(Cc, 0.05), Q50 = median(Cc), Q95 = quantile(Cc, 0.95),
.groups = "drop"
) |>
ggplot(aes(time, Q50, colour = treatment, fill = treatment)) +
geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.2, colour = NA) +
geom_line() +
scale_x_continuous(breaks = seq(0, 72, by = 12)) +
labs(
x = "Time (h)", y = "Plasma concentration (ng/mL)", colour = NULL, fill = NULL,
caption = "Median and 5th-95th percentiles, 200 women per regimen. Replicates Figure 8 of Krzyzanski 2021."
) +
theme(legend.position = "bottom")
reg_nca <- bind_rows(
sim_reg |> filter(!is.na(Cc)) |> select(id, time, Cc, treatment),
sim_reg |> distinct(id, treatment) |> mutate(time = 0, Cc = 0)
) |>
distinct(id, treatment, time, .keep_all = TRUE) |>
arrange(treatment, id, time)
reg_dose <- bind_rows(
events_reg_dex |> mutate(id = id),
events_reg_bet |> mutate(id = id + 10000L)
) |>
filter(evid == 1) |>
group_by(id, time, treatment) |>
summarise(amt = sum(amt), .groups = "drop")
reg_intervals <- data.frame(
start = c(0, 0), end = c(48, 72),
cmax = c(FALSE, TRUE), auclast = c(TRUE, TRUE)
)
reg_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(
PKNCA::PKNCAconc(reg_nca, Cc ~ time | treatment + id, concu = "ng/mL", timeu = "h"),
PKNCA::PKNCAdose(reg_dose, amt ~ time | treatment + id, doseu = "mg"),
intervals = reg_intervals
))
reg_sim_long <- as.data.frame(reg_res$result) |>
filter(PPTESTCD %in% c("cmax", "auclast")) |>
mutate(window = paste0("0-", end, " h")) |>
group_by(treatment, window, PPTESTCD) |>
summarise(PPORRES = median(PPORRES), .groups = "drop")
published_s2 <- tibble::tribble(
~treatment, ~window, ~cmax, ~auclast,
"DEX 6 mg IM 4xBID", "0-48 h", NA, 2366,
"DEX 6 mg IM 4xBID", "0-72 h", 71.6, 2521,
"BET 12 mg IM 2xQD", "0-48 h", NA, 3594,
"BET 12 mg IM 2xQD", "0-72 h", 144.2, 3861,
"CEL 12 mg IM 2xQD", "0-48 h", NA, 2021,
"CEL 12 mg IM 2xQD", "0-72 h", 77.4, 2342
)
cmp_reg <- nlmixr2lib::ncaComparisonTable(
simulated = reg_sim_long,
reference = published_s2,
by = c("treatment", "window"),
units = c(cmax = "ng/mL", auclast = "ng*h/mL"),
tolerance_pct = 20
)
knitr::kable(
cmp_reg,
caption = "Simulated median vs. Krzyzanski 2021 Table S2 (median of 200). * differs by >20%."
)| NCA parameter | treatment | window | Reference | Simulated | % diff |
|---|---|---|---|---|---|
| Cmax (ng/mL) | DEX 6 mg IM 4xBID | 0-72 h | 71.6 | 78 | +8.9% |
| Cmax (ng/mL) | BET 12 mg IM 2xQD | 0-72 h | 144 | 157 | +9.0% |
| Cmax (ng/mL) | CEL 12 mg IM 2xQD | 0-72 h | 77.4 | 84.9 | +9.7% |
| AUClast (ng*h/mL) | DEX 6 mg IM 4xBID | 0-48 h | 2370 | 2400 | +1.5% |
| AUClast (ng*h/mL) | DEX 6 mg IM 4xBID | 0-72 h | 2520 | 2560 | +1.6% |
| AUClast (ng*h/mL) | BET 12 mg IM 2xQD | 0-48 h | 3590 | 3690 | +2.8% |
| AUClast (ng*h/mL) | BET 12 mg IM 2xQD | 0-72 h | 3860 | 3980 | +3.2% |
| AUClast (ng*h/mL) | CEL 12 mg IM 2xQD | 0-48 h | 2020 | 2120 | +5.0% |
| AUClast (ng*h/mL) | CEL 12 mg IM 2xQD | 0-72 h | 2340 | 2460 | +5.0% |
chk_reg <- reg_sim_long |>
filter(PPTESTCD == "auclast") |>
inner_join(published_s2, by = c("treatment", "window")) |>
mutate(pct_diff = 100 * (PPORRES - auclast) / auclast)
chk_reg
#> # A tibble: 6 × 7
#> treatment window PPTESTCD PPORRES cmax auclast pct_diff
#> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 BET 12 mg IM 2xQD 0-48 h auclast 3694. NA 3594 2.78
#> 2 BET 12 mg IM 2xQD 0-72 h auclast 3984. 144. 3861 3.18
#> 3 CEL 12 mg IM 2xQD 0-48 h auclast 2123. NA 2021 5.04
#> 4 CEL 12 mg IM 2xQD 0-72 h auclast 2459. 77.4 2342 5.01
#> 5 DEX 6 mg IM 4xBID 0-48 h auclast 2402. NA 2366 1.54
#> 6 DEX 6 mg IM 4xBID 0-72 h auclast 2562. 71.6 2521 1.62
# Median AUC over 200 subjects is a centre statistic driven by CL/FIM and the
# relative bioavailabilities; a transcription error moves it by >10%.
stopifnot(all(abs(chk_reg$pct_diff) < 10))Trough concentrations (Table S2 Ctrough; the paper does not state the time at which the trough was taken):
published_ctrough <- c(
"BET 12 mg IM 2xQD" = 26.4, "CEL 12 mg IM 2xQD" = 17.1, "DEX 6 mg IM 4xBID" = 33.1
)
sim_reg |>
filter(time %in% c(12, 24, 36, 48)) |>
group_by(treatment, time) |>
summarise(median_Cc = median(Cc), .groups = "drop") |>
tidyr::pivot_wider(names_from = time, values_from = median_Cc, names_prefix = "t = ") |>
mutate(`Table S2 Ctrough` = unname(published_ctrough[as.character(treatment)])) |>
dplyr::rename("Regimen" = treatment) |>
knitr::kable(digits = 1, caption = "Simulated median Cc (ng/mL) at candidate trough times vs. Table S2 Ctrough.")| Regimen | t = 12 | t = 24 | t = 36 | t = 48 | Table S2 Ctrough |
|---|---|---|---|---|---|
| BET 12 mg IM 2xQD | 66.7 | 23.2 | 75.3 | 26.4 | 26.4 |
| CEL 12 mg IM 2xQD | 37.5 | 16.4 | 46.0 | 22.4 | 17.1 |
| DEX 6 mg IM 4xBID | 20.6 | 23.7 | 24.3 | 24.5 | 33.1 |
The BET-P regimen’s Table S2 trough (26.4 ng/mL) matches the simulated median at 48 h. The CEL trough (17.1 ng/mL) is closest to the simulated median at 24 h. No candidate trough time reproduces the DEX value (33.1 ng/mL), which is higher than the simulated median at any of 12, 24, 36 or 48 h. Because the trough time is not stated, Ctrough is reported here but not asserted.
The simulated median AUCs agree with Table S2 to within 5%, and so do their 5th-95th percentiles (for example, DEX AUC0-72 about 2000-3300 ng*h/mL simulated vs. 1948-3272 reported). The simulated median Cmax is about 9% above Table S2 for all three regimens. Table S2’s Cmax percentile ranges are also much narrower than the simulated ones: 69.6-74.0 ng/mL reported for DEX vs. about 63-96 ng/mL simulated, even though the AUC ranges agree.
Assumptions and deviations
- Two model files, one vignette. The paper fitted DEX and BET independently (Methods, “Parameter estimation and simulations”). Each is a separate model file.
-
Apparent parameters. Without IV data, FIM is not
identifiable. All clearances and volumes are divided by FIM and the IM
phosphate depot is the bioavailability reference (FIM = 1).
FrandFraare relative bioavailabilities (Eq 7), so values above 1 are allowed (DEXFr= 1.04). - DEX IIV on Vp/FIM. Table 3 reports this variance as 0, fixed after it was estimated near zero. It is omitted from the model rather than written as a zero-variance eta, which would make OMEGA singular for simulation.
- IIV on Fra. The Results text says the Fra variance “warranted setting this parameter at 0” but that it “was kept as a model parameter”. Table 4 and the supplement control stream both carry it at 0.00773, so it is kept.
-
Supplement comment typo. In the supplement’s
$OMEGAblock, the variance 0.0182 is commented;6. FRA. It is the Fr (ETA(6)) variance, as Table 4 confirms. -
Residual error. Eq 11 and the control stream model
log concentrations with an additive error, encoded as
lnorm(expSd)withexpSd= sqrt(sigma^2). The paper handled BLQ data with Beal M3. To reproduce that when fitting, supplyCENS/LIMITcolumns in the data; this does not affect simulation. - MRT for BET-PA. Eq 9 prints the effective BET-PA absorption rate as (Fr + Fra)/(Fr/kaIM + Fra/kaIMa), using the oral relative bioavailability Fr. In BET-PA, both depots are intramuscular and the phosphate depot has bioavailability 1 by construction, so this vignette uses (1 + Fra)/(1/kaIM + Fra/kaIMa). With the typical values the two forms give MRTs of 83 h and 86 h. Either is below the Table S1 mean of 96.5 (SD 33.2) h, which averages post-hoc individual estimates.
- Individual vs. population descriptors. Table S1 summarises post-hoc (individual) predictions for the women actually studied. The comparisons above use fresh simulations from the population estimates, so small differences are expected. The largest difference is the BET-PA two-point half-life: the simulation gives about 90 h against 77.6 h. This is recorded, not tuned.
- Table S2 Cmax and Ctrough. The trough sampling time is not stated, and the DEX value is not reproduced at any candidate time (see above). The simulated median Cmax is about 9% above Table S2 for every regimen, and Table S2’s Cmax 5th-95th percentile ranges are much narrower than a simulation with the published IIV produces, although the AUC percentiles agree. How the paper summarised Cmax and Ctrough could not be established. The regimen gate therefore uses median AUCs only.
- No correction notice for this article was listed in Europe PMC (PMID 33389521) as of 2026-09-27.