Sitafloxacin (Paiboonvong 2025)
Source:vignettes/articles/Paiboonvong_2025_sitafloxacin.Rmd
Paiboonvong_2025_sitafloxacin.RmdModel and source
mod <- readModelDb("Paiboonvong_2025_sitafloxacin")
ui <- rxode2::rxode(mod)
#> ℹ parameter labels from comments will be replaced by 'label()'- Citation: Paiboonvong T, Montakantikul P, Panjasawatwong N, Singkham N, Punyawudho B. Population pharmacokinetics and pharmacodynamics of sitafloxacin in plasma and alveolar epithelial lining fluid of critically ill Thai patients with pneumonia. Pharmacol Res Perspect. 2025;13(2):e70081. doi:10.1002/prp2.70081. PMCID: PMC11930543. Final parameter estimates: Table 2. Model structure, the transit-absorption chain with KA = KTR, the ELF compartment and the age-on-F relationship: Results section 3.3 and Figure 1. Structural encoding including the placement of the unbound fraction inside the central-to-ELF micro-rate constant and the omega / sigma values on their estimation scales: Data S1 (Supporting Information), NONMEM control streams Run56 (estimation) and Run59 (simulation). Underlying clinical study, sampling design and bioanalysis: reference 7 of the paper. See also modellib(‘Wu_2025_sitafloxacin’) and modellib(‘Rodjun_2023_sitafloxacin’) for independent sitafloxacin popPK models in non-critically-ill populations.
- Description: Population PK model for oral sitafloxacin in plasma and pulmonary epithelial lining fluid (ELF) of 12 critically ill Thai patients with pneumonia, developed by Paiboonvong 2025 to drive Monte Carlo probability-of-target-attainment simulation against Streptococcus pneumoniae. Plasma disposition is one-compartment; absorption is a Savic transit chain of one transit compartment in which the absorption rate constant is constrained to equal the transit rate constant (ka = ktr = (ntr + 1) / mtt with ntr fixed to 1). An ELF compartment hangs off the central compartment as a peripheral compartment with a fixed physiological volume of 0.025 L and an inter-compartmental clearance q_elf. IMPORTANT: in the authors’ own control stream the central-to-ELF micro-rate constant carries the plasma unbound fraction, k_central_elf = q_elf * fu * pcelf / vc while k_elf_central = q_elf / v_elf, so the estimated partition coefficient pcelf = 0.772 is the ELF-to-UNBOUND-plasma ratio and the ELF-to-total-plasma concentration (and AUC) ratio the model actually produces is fu * pcelf = 0.63 * 0.772 = 0.486. Body weight is an a-priori allometric covariate on CL/F (exponent 0.75 fixed), V/F (exponent 1 fixed) and Q/F_ELF (exponent 0.75 fixed), all normalized to the cohort median 52 kg. Age is a LINEAR covariate on relative bioavailability, F = 1 + 0.0258 * (AGE - 57), which is only usable over the adult age range the authors simulated (30-70 years): the expression reaches zero at 18.2 years and is negative below it. All disposition parameters are apparent (/F): only oral data were analysed and F was fixed to 1 with its inter-individual variability estimated. Residual variability was additive on the natural-log concentration scale for both plasma and ELF, encoded here as lnorm().
- Article: https://doi.org/10.1002/prp2.70081
- Supporting Information (Data S1, NONMEM control streams Run56 and Run59): https://europepmc.org/article/MED/40128137
Population
Twelve critically ill Thai patients with pneumonia, admitted to an intensive care unit, each given a single 200 mg oral dose of sitafloxacin under fasting conditions (Methods 2.1). Six of the twelve were male. Median (IQR) age was 57 (40-65) years, body weight 52 (44-68) kg and Cockcroft-Gault creatinine clearance 68 (30-96) mL/min; median APACHE II score was 21 (18-33) and median serum albumin 2.0 (1.8-2.2) g/dL (Results 3.1). Two thirds of the cohort had mild-to-moderate renal impairment and one patient showed augmented renal clearance at 235 mL/min.
Plasma was sampled pre-dose and at 0.5, 1, 2, 3, 8 and 12 h; each patient additionally contributed one bronchoalveolar-lavage sample, randomly assigned to the 0.5-2, 3-4, 5-6 or 7-9 h window. The model was built on 83 plasma and 12 epithelial lining fluid (ELF) concentrations, one plasma sample below the 0.025 mg/L limit of quantification having been excluded (Results 3.3).
The same information is available programmatically from the model’s
population metadata:
str(ui$population)
#> List of 10
#> $ species : chr "human"
#> $ n_subjects : int 12
#> $ n_studies : int 1
#> $ age_median : chr "57 years (IQR 40-65)"
#> $ weight_median : chr "52 kg (IQR 44-68)"
#> $ sex_female_pct: num 50
#> $ disease_state : chr "Critically ill patients with pneumonia admitted to an intensive care unit. Median APACHE II score 21 (IQR 18-33"| __truncated__
#> $ dose_range : chr "Sitafloxacin 200 mg orally as a single dose under fasting conditions"
#> $ regions : chr "Thailand"
#> $ notes : chr "Baseline demographics: Results 3.1. The model was built on 83 plasma and 12 ELF concentrations; one plasma conc"| __truncated__Source trace
Every ini() entry in
inst/modeldb/specificDrugs/Paiboonvong_2025_sitafloxacin.R
carries an in-file comment naming its origin. They are collected here
for review. “Table 2” and “Results” refer to the main article; “Run59”
refers to the simulation control stream printed in Data S1 of the
Supporting Information, which carries the final parameter estimates
(Run56, the estimation stream, carries earlier initial values for two of
them).
| Equation / parameter | Value | Source location |
|---|---|---|
lcl (CL/F at 52 kg) |
7.03 L/h | Table 2, RSE 24.9%, SIR 95% CI 4.36-11.0; Run59
THETA(1)
|
lvc (V/F at 52 kg) |
116 L | Table 2, RSE 13.0%, SIR 95% CI 93.9-153; Run59
THETA(2)
|
lfdepot (F) |
1, fixed | Table 2 “1 fix”; Methods 2.2; Run59
THETA(3) (1) FIX
|
lmtt (MTT) |
1.48 h | Table 2, RSE 27.1%, SIR 95% CI 0.858-2.44; Run59
THETA(4)
|
lq_elf (Q/F ELF at 52 kg) |
0.0441 L/h | Table 2, RSE 36.5%, SIR 95% CI 0.0194-0.0812; Run59
THETA(5)
|
lv_elf (V/F ELF) |
0.025 L, fixed | Table 2 “0.025 fix”; Results 3.3 (lung physiological value, paper
reference 11); Run59 THETA(6) (0.025) FIX
|
lpcelf (partition coefficient) |
0.772 | Table 2, RSE 18.2%, SIR 95% CI 0.556-1.08; Run59
THETA(7)
|
fu (fraction unbound) |
0.63, fixed | Table 2 “0.63 fix”; Methods 2.4; used inside Run59
K24 = QELF*0.63*PC/VBD
|
e_age_fdepot (age on F) |
0.0258 /year | Table 2 “AGE on F 2.58”, RSE 7.64%; Results 3.3 “F = 1 + (Age - 57)
* 0.0258”; Run59 THETA(8)
|
e_wt_cl |
0.75, fixed | Methods 2.2; Run59
TVCLBD = THETA(1)*((WT/52)**0.75)
|
e_wt_vc |
1, fixed | Methods 2.2; Run59 TVVBD = THETA(2)*(WT/52)
|
e_wt_q_elf |
0.75, fixed | Run59 TVQELF = THETA(5)*((WT/52)**0.75) and its header
comment |
etalcl |
0.566 (87.2 %CV) | Run59 $OMEGA 1; Table 2 IIV CL/F 87.2 %CV |
etalfdepot |
0.0285 (17.0 %CV) | Run59 $OMEGA 3; Table 2 IIV F 17.0 %CV |
etalmtt |
0.626 (93.3 %CV) | Run59 $OMEGA 4; Table 2 IIV MTT 93.3 %CV |
etalpcelf |
0.0528 (23.3 %CV) | Run59 $OMEGA 7; Table 2 IIV PC 23.3 %CV |
expSd (plasma) |
sqrt(0.122) | Run59 $SIGMA 1; Table 2 sigma plasma 36.0 |
expSd_Celf (ELF) |
sqrt(0.249) | Run59 $SIGMA 2; Table 2 sigma ELF 53.2 |
ktr = (ntr + 1) / mtt, ntr = 1
|
n/a | Results 3.3 “one fixed transit absorption compartment, which set Ka
to be identical to Ktr”; Run59 NN = 1,
KTR = (NN+1)/MTT, K13 = KTR,
K32 = KTR
|
d/dt(depot), d/dt(transit1),
d/dt(central), d/dt(elf)
|
n/a | Run59 $DES DADT(1), DADT(3),
DADT(2), DADT(4); Figure 1 schematic |
k_central_elf = q_elf * fu * pcelf / vc |
n/a | Figure 1 arrow labelled (Q_ELF x F_u x PC)/V; Run59
K24 = QELF*0.63*PC/VBD
|
k_elf_central = q_elf / v_elf |
n/a | Figure 1 arrow labelled Q_ELF/V_ELF; Run59
K42 = QELF/VELF
|
Cc ~ lnorm(), Celf ~ lnorm()
|
n/a | Results 3.3 “additive residual error model on the logarithmic
scale”; Run59 $ERROR Y = IPRED + EPS(n) with
IPRED = LOG(IPRED)
|
The IIV column of Table 2 is printed as %CV while the control stream
carries log-scale variances; the two agree exactly under
CV = sqrt(exp(v) - 1), which is checked below. The same
identity converts the two sigma rows.
th <- setNames(ui$iniDf$est, ui$iniDf$name)
etas <- ui$iniDf |> dplyr::filter(!is.na(neta1), neta1 == neta2)
omega_chk <- tibble::tibble(
parameter = etas$name,
variance = etas$est,
cv_pct = 100 * sqrt(exp(etas$est) - 1),
published = c(87.2, 17.0, 93.3, 23.3)
)
sig_chk <- tibble::tibble(
parameter = c("expSd", "expSd_Celf"),
variance = c(th[["expSd"]], th[["expSd_Celf"]])^2,
cv_pct = 100 * sqrt(exp(c(th[["expSd"]], th[["expSd_Celf"]])^2) - 1),
published = c(36.0, 53.2)
)
scale_chk <- dplyr::bind_rows(omega_chk, sig_chk) |>
dplyr::mutate(abs_diff = abs(cv_pct - published))
knitr::kable(scale_chk, digits = 3,
caption = "Log-scale variances re-expressed as %CV against Table 2.")| parameter | variance | cv_pct | published | abs_diff |
|---|---|---|---|---|
| etalcl | 0.566 | 87.247 | 87.2 | 0.047 |
| etalfdepot | 0.028 | 17.003 | 17.0 | 0.003 |
| etalmtt | 0.626 | 93.280 | 93.3 | 0.020 |
| etalpcelf | 0.053 | 23.285 | 23.3 | 0.015 |
| expSd | 0.122 | 36.021 | 36.0 | 0.021 |
| expSd_Celf | 0.249 | 53.173 | 53.2 | 0.027 |
Structural verification
Two exact identities follow from the ODE system and hold for every subject, independently of the parameter values. They are the cheapest available test that the compartments are wired the way the control stream wires them.
- Because
elfreturns all of its drug tocentral(it is a true peripheral compartment, not a sink), total plasma exposure over one steady-state dosing interval is fixed by mass balance:AUC(0-24, plasma) = 24 h / tau * dose * F / CL. - Because the central-to-ELF micro-constant carries
fuwhile the ELF-to-central one does not, ELF equilibrates tofu * pcelftimes the total plasma concentration, soAUC(ELF) / AUC(plasma) = fu * pcelfexactly.
make_ss_events <- function(subj, dose, tau = 12, n_doses = 16L,
obs = seq(168, 192, by = 0.5)) {
dosing <- subj |>
dplyr::mutate(time = 0, amt = dose, evid = 1L, cmt = "depot",
dvid = NA_integer_, ii = tau, addl = n_doses - 1L)
# Observation rows address the ODE state `central` and select the endpoint
# with `dvid = 1L`. rxode2 returns BOTH observables (Cc and Celf) as columns
# on those rows; naming an algebraic observable in the compartment column
# instead would inject a new compartment slot and renumber the ODE states.
observing <- subj |>
tidyr::crossing(time = obs) |>
dplyr::mutate(amt = 0, evid = 0L, cmt = "central",
dvid = 1L, ii = 0, addl = 0L)
dplyr::bind_rows(dosing, observing) |>
dplyr::arrange(id, time, dplyr::desc(evid)) |>
as.data.frame()
}
trapz <- function(t, y) sum(diff(t) * (utils::head(y, -1) + utils::tail(y, -1)) / 2)
# Typical-value subjects spanning the age range the paper simulated.
typ_subj <- tibble::tibble(id = 1:5, WT = 52, AGE = c(30, 40, 50, 60, 70),
treatment = "100 mg q12h")
typ_sim <- rxode2::rxSolve(
rxode2::zeroRe(mod), events = make_ss_events(typ_subj, dose = 100),
keep = c("WT", "AGE"), useLinCmt = FALSE, returnType = "data.frame"
) |>
dplyr::filter(!is.na(Cc))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalfdepot', 'etalmtt', 'etalpcelf'
#> Warning: multi-subject simulation without without 'omega'
ident <- typ_sim |>
dplyr::group_by(id, WT, AGE) |>
dplyr::summarise(
auc_plasma = trapz(time, Cc),
auc_elf = trapz(time, Celf),
cl = dplyr::first(cl), fdepot = dplyr::first(fdepot),
pcelf = dplyr::first(pcelf), .groups = "drop"
) |>
dplyr::mutate(
auc_closed_form = 2 * 100 * fdepot / cl,
pct_diff_auc = 100 * (auc_plasma / auc_closed_form - 1),
ratio_elf = auc_elf / auc_plasma,
ratio_expected = th[["fu"]] * pcelf
)
ident |>
dplyr::select(AGE, auc_plasma, auc_closed_form, pct_diff_auc,
ratio_elf, ratio_expected) |>
dplyr::rename(
"Age (years)" = AGE,
"AUC0-24 plasma, solved" = auc_plasma,
"AUC0-24 plasma, closed form" = auc_closed_form,
"% difference" = pct_diff_auc,
"AUC(ELF)/AUC(plasma)" = ratio_elf,
"fu * pcelf" = ratio_expected
) |>
knitr::kable(digits = 5,
caption = "Steady-state mass-balance identities, typical-value solve.")| Age (years) | AUC0-24 plasma, solved | AUC0-24 plasma, closed form | % difference | AUC(ELF)/AUC(plasma) | fu * pcelf |
|---|---|---|---|---|---|
| 30 | 8.63123 | 8.63158 | -0.00409 | 0.48638 | 0.48636 |
| 40 | 15.97090 | 15.97155 | -0.00409 | 0.48638 | 0.48636 |
| 50 | 23.31057 | 23.31152 | -0.00409 | 0.48638 | 0.48636 |
| 60 | 30.65024 | 30.65149 | -0.00409 | 0.48638 | 0.48636 |
| 70 | 37.98991 | 37.99147 | -0.00409 | 0.48638 | 0.48636 |
# Both sides of each comparison use the SAME drawn parameters, so the residual
# is pure trapezoidal / solver error and a tight bound is correct here. The ELF
# ratio carries slightly more of it than the plasma AUC because the ELF profile
# lags plasma, so the trapezoid over a finite interval is not exactly
# proportional; the realised relative error is about 4e-5. Dropping `fu` from
# the micro-constant would move this ratio from 0.486 to 0.772, a 59% error.
stopifnot(
max(abs(ident$pct_diff_auc)) < 0.05,
max(abs(ident$ratio_elf / ident$ratio_expected - 1)) < 1e-3
)The second identity is the reason this vignette treats the published
partition coefficient carefully: the model’s
ELF-to-total-plasma exposure ratio is fu * pcelf =
0.63 * 0.772 = 0.486, not the 0.772 the abstract quotes. See Assumptions
and deviations.
Virtual cohort
Original observed data are not publicly available. Three virtual cohorts are used, each of 200 subjects (the per-arm cap for validation vignettes).
- Study cohort - a 200 mg single dose, with age and weight drawn to match the medians and IQRs of Results 3.1. Used for the Figure 3 visual predictive check.
- 100 mg q12h and 50 mg q12h - the two approved regimens, dosed to steady state. Ages cycle through the five values the paper simulated (30, 40, 50, 60 and 70 years) and weight is held at the 52 kg reference, which is what reproduces the published median exposures (see Assumptions).
# set.seed() seeds R's RNG, which is what draws the covariates below. It does
# NOT seed rxode2's simulation RNG, and rxode2's streams are partitioned per
# solver thread, so the etas drawn downstream differ between a 2-core CI runner
# and a 16-thread workstation. Every assertion below is therefore either
# deterministic (no cohort involved) or written with headroom for that spread.
set.seed(20250926)
n_arm <- 200L
# Study cohort: lognormal weight with median 52 kg and IQR 44-68 kg; normal age
# with median 57 y and IQR 40-65 y, truncated to 25-85 y so that the LINEAR
# age-on-F relationship stays positive (it reaches zero at 18.2 years).
study_subj <- tibble::tibble(
id = seq_len(n_arm),
WT = 52 * exp(stats::rnorm(n_arm, 0, log(68 / 44) / (2 * stats::qnorm(0.75)))),
AGE = pmin(85, pmax(25, stats::rnorm(n_arm, 57, 25 / (2 * stats::qnorm(0.75))))),
treatment = "200 mg single dose"
)
make_ss_subj <- function(label, id_offset) {
tibble::tibble(
id = id_offset + seq_len(n_arm),
WT = 52,
AGE = rep(c(30, 40, 50, 60, 70), length.out = n_arm),
treatment = label
)
}
ss_subj <- dplyr::bind_rows(
make_ss_subj("100 mg q12h", id_offset = 1000L),
make_ss_subj("50 mg q12h", id_offset = 2000L)
)
ss_events <- dplyr::bind_rows(
make_ss_events(dplyr::filter(ss_subj, treatment == "100 mg q12h"), dose = 100),
make_ss_events(dplyr::filter(ss_subj, treatment == "50 mg q12h"), dose = 50)
)
stopifnot(!anyDuplicated(ss_events[, c("id", "time", "evid")]))
# Study cohort event table: single dose, sampled over the observed 0-12 h window.
study_events <- dplyr::bind_rows(
study_subj |>
dplyr::mutate(time = 0, amt = 200, evid = 1L, cmt = "depot",
dvid = NA_integer_, ii = 0, addl = 0L),
study_subj |>
tidyr::crossing(time = seq(0, 12, by = 0.25)) |>
dplyr::mutate(amt = 0, evid = 0L, cmt = "central",
dvid = 1L, ii = 0, addl = 0L)
) |>
dplyr::arrange(id, time, dplyr::desc(evid)) |>
as.data.frame()
stopifnot(!anyDuplicated(study_events[, c("id", "time", "evid")]))Simulation
rxode2::rxSetSeed(20250926)
study_sim <- rxode2::rxSolve(
mod, events = study_events, keep = c("WT", "AGE", "treatment"),
useLinCmt = FALSE, returnType = "data.frame"
)
#> ℹ parameter labels from comments will be replaced by 'label()'
ss_sim <- rxode2::rxSolve(
mod, events = ss_events, keep = c("WT", "AGE", "treatment"),
useLinCmt = FALSE, returnType = "data.frame"
)
stopifnot(nrow(study_sim) > 0, nrow(ss_sim) > 0,
all(study_sim$Cc[!is.na(study_sim$Cc)] >= 0),
all(ss_sim$Celf[!is.na(ss_sim$Celf)] >= 0))Replicate published figures
vpc_dat <- study_sim |>
dplyr::filter(!is.na(Cc)) |>
tidyr::pivot_longer(c(Cc, Celf), names_to = "matrix", values_to = "conc") |>
dplyr::mutate(matrix = dplyr::recode(matrix,
Cc = "A. Plasma", Celf = "B. ELF")) |>
dplyr::group_by(matrix, time) |>
dplyr::summarise(
Q05 = stats::quantile(conc, 0.05),
Q50 = stats::quantile(conc, 0.50),
Q95 = stats::quantile(conc, 0.95),
.groups = "drop"
)
ggplot(vpc_dat, aes(time, Q50)) +
geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.25) +
geom_line(linewidth = 0.8) +
facet_wrap(~matrix) +
scale_y_log10() +
labs(x = "Time after dose (h)", y = "Sitafloxacin concentration (mg/L)",
caption = paste("Replicates Figure 3 of Paiboonvong 2025.",
"Line = median, band = 5th-95th percentile,",
"200 virtual subjects given 200 mg orally."))
#> Warning in scale_y_log10(): log-10 transformation introduced infinite values.
#> log-10 transformation introduced infinite values.
#> log-10 transformation introduced infinite values.
#> log-10 transformation introduced infinite values.
Replicates Figure 3 of Paiboonvong 2025: visual predictive check of plasma (A) and ELF (B) sitafloxacin concentrations after a single 200 mg oral dose.
Figure 3 of the paper plots concentrations on a ug/L axis. Its plasma panel shows a broad, flat peak: the median climbs to roughly 1000 ug/L (1 mg/L) between about 2 and 5 h and is still near 700-900 ug/L at 12 h, which is the delayed, prolonged absorption the Discussion attributes to gastrointestinal dysmotility in critical illness. Its ELF panel sits near 500-600 ug/L across the 1-9 h bronchoalveolar-lavage window, roughly half of plasma. Both features are reproduced above.
peak_chk <- study_sim |>
dplyr::filter(!is.na(Cc)) |>
dplyr::group_by(time) |>
dplyr::summarise(med_plasma = stats::median(Cc),
med_elf = stats::median(Celf), .groups = "drop")
tmax_med <- peak_chk$time[which.max(peak_chk$med_plasma)]
cmax_med <- max(peak_chk$med_plasma)
# ELF-to-plasma concentration ratio once the ELF compartment has equilibrated
# (its half-life of equilibration is about 0.3 h, so 4 h is well past it).
ratio_med <- stats::median(
study_sim$Celf[!is.na(study_sim$Cc) & study_sim$time >= 4] /
study_sim$Cc[!is.na(study_sim$Cc) & study_sim$time >= 4]
)
tibble::tibble(
quantity = c("Median Cmax (mg/L)", "Time of median peak (h)",
"Median Celf/Cc after 4 h"),
value = c(cmax_med, tmax_med, ratio_med)
) |>
dplyr::rename("Quantity" = quantity, "Value" = value) |>
knitr::kable(digits = 3,
caption = "Summary of the simulated single-dose median profile.")| Quantity | Value |
|---|---|
| Median Cmax (mg/L) | 1.102 |
| Time of median peak (h) | 4.000 |
| Median Celf/Cc after 4 h | 0.501 |
# Cohort-derived, so the bounds carry headroom for the per-thread RNG spread.
# The typical-value profile peaks at 1.44 mg/L at 3.6 h and the paper's own
# Figure 3 median peaks near 1 mg/L between 2 and 5 h; a mis-transcribed
# volume, dose or MTT moves these by a factor, not by a few percent.
stopifnot(cmax_med > 0.4, cmax_med < 3.0, tmax_med > 1, tmax_med < 9)
# Structural, and therefore tight: the equilibrated ratio is fu * pcelf with
# only the 23.3 %CV on pcelf around it. Dropping fu would put it at 0.772.
stopifnot(ratio_med > 0.42, ratio_med < 0.56)PKNCA validation
Non-compartmental analysis of the last steady-state dosing interval (168-192 h), run once over a stacked frame in which each matrix is carried as its own treatment level so that plasma and ELF appear in a single comparison table.
# Only `!is.na(Cc)` -- a `time > 0` or `Cc > 0` filter would drop rows PKNCA
# needs to anchor the interval.
ss_long <- ss_sim |>
dplyr::filter(!is.na(Cc)) |>
dplyr::select(id, time, treatment, Cc, Celf) |>
tidyr::pivot_longer(c(Cc, Celf), names_to = "matrix", values_to = "conc") |>
dplyr::mutate(
matrix = dplyr::recode(matrix, Cc = "plasma", Celf = "ELF"),
treatment = paste(treatment, matrix, sep = " - ")
) |>
dplyr::rename(Cc = conc) |>
dplyr::select(id, time, Cc, treatment) |>
dplyr::arrange(treatment, id, time)
conc_obj <- PKNCA::PKNCAconc(ss_long, Cc ~ time | treatment + id,
concu = "mg/L", timeu = "h")
dose_df <- ss_events |>
dplyr::filter(evid == 1) |>
dplyr::select(id, amt, treatment, ii, addl) |>
tidyr::crossing(dose_index = 0:15) |>
dplyr::mutate(time = dose_index * ii) |>
dplyr::filter(dose_index <= addl) |>
dplyr::select(id, time, amt, treatment) |>
tidyr::crossing(matrix = c("plasma", "ELF")) |>
dplyr::mutate(treatment = paste(treatment, matrix, sep = " - ")) |>
dplyr::select(id, time, amt, treatment)
dose_obj <- PKNCA::PKNCAdose(dose_df, amt ~ time | treatment + id, doseu = "mg")
intervals <- data.frame(
start = 168, end = 192,
cmax = TRUE, tmax = TRUE, cmin = TRUE, auclast = TRUE, cav = TRUE
)
nca_res <- PKNCA::pk.nca(PKNCA::PKNCAdata(conc_obj, dose_obj,
intervals = intervals))
knitr::kable(summary(nca_res),
caption = "Steady-state NCA over the 168-192 h dosing interval.")| Interval Start | Interval End | treatment | N | AUClast (h*mg/L) | Cmax (mg/L) | Cmin (mg/L) | Tmax (h) | Cav (mg/L) |
|---|---|---|---|---|---|---|---|---|
| 168 | 192 | 100 mg q12h - ELF | 200 | 9.44 [110] | 0.496 [92.8] | 0.269 [173] | 15.5 [1.50, 20.0] | 0.393 [110] |
| 168 | 192 | 100 mg q12h - plasma | 200 | 18.5 [105] | 0.996 [86.8] | 0.506 [175] | 15.0 [0.500, 19.5] | 0.772 [105] |
| 168 | 192 | 50 mg q12h - ELF | 200 | 5.12 [126] | 0.265 [108] | 0.150 [184] | 15.5 [2.00, 20.0] | 0.213 [126] |
| 168 | 192 | 50 mg q12h - plasma | 200 | 10.4 [116] | 0.552 [97.3] | 0.294 [178] | 15.0 [1.00, 19.5] | 0.435 [116] |
Comparison against published NCA
Results 3.4 reports the median steady-state 24 h AUC in plasma and ELF for both regimens. Note that the paper labels these values “fAUC” but, as shown under Assumptions and deviations, they are the model’s total plasma AUC and the ELF AUC with no further unbound-fraction multiplication.
published <- tibble::tribble(
~treatment, ~auclast,
"100 mg q12h - plasma", 21.02,
"100 mg q12h - ELF", 10.17,
"50 mg q12h - plasma", 10.42,
"50 mg q12h - ELF", 5.12
)
cmp <- nlmixr2lib::ncaComparisonTable(
simulated = nca_res,
reference = published,
by = "treatment",
units = c(auclast = "mg.h/L"),
tolerance_pct = 20
)
knitr::kable(cmp, caption = paste(
"Simulated (200 subjects per arm) vs. published median steady-state AUC0-24.",
"* differs from the reference by more than 20%."
), align = c("l", "l", "r", "r", "r"))| NCA parameter | treatment | Reference | Simulated | % diff |
|---|---|---|---|---|
| AUClast (mg.h/L) | 100 mg q12h - plasma | 21 | 18.1 | -13.7% |
| AUClast (mg.h/L) | 100 mg q12h - ELF | 10.2 | 9.18 | -9.7% |
| AUClast (mg.h/L) | 50 mg q12h - plasma | 10.4 | 11.3 | +8.8% |
| AUClast (mg.h/L) | 50 mg q12h - ELF | 5.12 | 5.37 | +4.9% |
# The simulated side is a 200-subject median of a 5-component age mixture whose
# log-scale spread is about 0.93, giving a median standard error near 8%. The
# bound below is roughly 3.5 of those and still goes red on any mis-transcribed
# clearance, dose or unit, which move the median by tens of percent. Realised
# values on one 16-thread run were +0.3 / -4.3 / -3.2 / -5.5 percent; do NOT
# tighten to those, they are one draw. The deterministic reproduction in the
# next section is the tight gate.
cmp_pct <- suppressWarnings(as.numeric(gsub("[^0-9.eE+-]", "", cmp$`% diff`)))
stopifnot(max(abs(cmp_pct), na.rm = TRUE) < 30)Deterministic reproduction of the published exposures
The Monte Carlo medians of Results 3.4 can be reproduced exactly,
without any simulation, because the steady-state 24 h plasma AUC of
subject i is 24/tau * dose * F_i / CL_i, which is
log-normal within each of the five simulated age groups. The population
median is then the median of an equally weighted five-component
log-normal mixture, obtained below by root-finding on the mixture CDF.
Every input is read out of the packaged model, so the check goes red if
any shipped value drifts.
cl_typ <- exp(th[["lcl"]])
fu_typ <- th[["fu"]]
pc_typ <- exp(th[["lpcelf"]])
age_slope <- th[["e_age_fdepot"]]
var_of <- function(nm) etas$est[match(nm, etas$name)]
ages <- c(30, 40, 50, 60, 70)
f_age <- 1 + age_slope * (ages - 57)
# AUC = 24/tau * dose * F / CL varies with eta_F and eta_CL; the ELF AUC picks
# up eta_PC as well.
sd_plasma <- sqrt(var_of("etalfdepot") + var_of("etalcl"))
sd_elf <- sqrt(var_of("etalfdepot") + var_of("etalcl") + var_of("etalpcelf"))
mixture_median <- function(mu, sdlog) {
stats::uniroot(function(x) mean(stats::pnorm((x - mu) / sdlog)) - 0.5,
c(-20, 20))$root |> exp()
}
predicted <- function(dose_per_24h, matrix) {
scale <- if (matrix == "plasma") 1 else fu_typ * pc_typ
sdlog <- if (matrix == "plasma") sd_plasma else sd_elf
mixture_median(log(dose_per_24h * f_age / cl_typ) + log(scale), sdlog)
}
exposure_chk <- tibble::tribble(
~regimen, ~matrix, ~dose24, ~published,
"100 mg q12h", "plasma", 200, 21.02,
"100 mg q12h", "ELF", 200, 10.17,
"50 mg q12h", "plasma", 100, 10.42,
"50 mg q12h", "ELF", 100, 5.12
) |>
dplyr::rowwise() |>
dplyr::mutate(predicted = predicted(dose24, matrix)) |>
dplyr::ungroup() |>
dplyr::mutate(pct_diff = 100 * (predicted / published - 1))
exposure_chk |>
dplyr::select(-dose24) |>
dplyr::rename(
"Regimen" = regimen,
"Matrix" = matrix,
"Published median (mg.h/L)" = published,
"Model median (mg.h/L)" = predicted,
"% difference" = pct_diff
) |>
knitr::kable(digits = c(0, 0, 2, 3, 2), caption = paste(
"Published median steady-state AUC0-24 (Results 3.4) against the exact",
"population median implied by the packaged parameters. No simulation is",
"involved, so this comparison is identical on every machine."
))| Regimen | Matrix | Published median (mg.h/L) | Model median (mg.h/L) | % difference |
|---|---|---|---|---|
| 100 mg q12h | plasma | 21.02 | 21.067 | 0.22 |
| 100 mg q12h | ELF | 10.17 | 10.230 | 0.59 |
| 50 mg q12h | plasma | 10.42 | 10.533 | 1.09 |
| 50 mg q12h | ELF | 5.12 | 5.115 | -0.10 |
# Deterministic; the realised maximum is about 1.1%. 3% still admits the
# paper's own rounding while going red on any parameter drift.
stopifnot(max(abs(exposure_chk$pct_diff)) < 3)All four published medians are reproduced to within about 1%. That is
the strongest available evidence that the parameter values, the age
covariate, the allometric reference weight and the placement of
fu inside the ELF micro-constant have all been transcribed
correctly: getting any one of them wrong moves at least one of these
four numbers by tens of percent.
Reproduction of the target-attainment table
Table 3 reports the probability of attaining
fAUC/MIC > 30. The same log-normal mixture gives each
cell in closed form.
pta <- function(dose24, mic, matrix) {
scale <- if (matrix == "plasma") 1 else fu_typ * pc_typ
sdlog <- if (matrix == "plasma") sd_plasma else sd_elf
mu <- log(dose24 * f_age / cl_typ) + log(scale)
100 * mean(1 - stats::pnorm((log(30 * mic) - mu) / sdlog))
}
pta_chk <- tidyr::expand_grid(
regimen = c("100 mg q12h", "50 mg q12h"),
matrix = c("plasma", "ELF"),
mic = c(0.032, 0.0625, 0.125, 0.25)
) |>
dplyr::mutate(
dose24 = ifelse(regimen == "100 mg q12h", 200, 100),
published = c(100.00, 100.00, 94.60, 85.72,
99.18, 95.36, 79.44, 62.50,
100.00, 96.34, 81.42, 63.48,
95.34, 84.84, 55.70, 33.70)
) |>
dplyr::rowwise() |>
dplyr::mutate(model = pta(dose24, mic, matrix)) |>
dplyr::ungroup() |>
dplyr::mutate(
abs_diff = abs(model - published),
# Both MIC = 0.125 cells of each matrix are recorded deviations; see below.
deviation = mic == 0.125
)
pta_chk |>
dplyr::select(regimen, matrix, mic, published, model, abs_diff, deviation) |>
dplyr::rename(
"Regimen" = regimen,
"Matrix" = matrix,
"MIC (mg/L)" = mic,
"Published %PTA" = published,
"Model %PTA" = model,
"Absolute difference (pp)" = abs_diff,
"Recorded deviation" = deviation
) |>
knitr::kable(digits = c(0, 0, 4, 2, 2, 2, 0),
caption = "Table 3 of Paiboonvong 2025 against the model.")| Regimen | Matrix | MIC (mg/L) | Published %PTA | Model %PTA | Absolute difference (pp) | Recorded deviation |
|---|---|---|---|---|---|---|
| 100 mg q12h | plasma | 0.0320 | 100.00 | 99.95 | 0.05 | FALSE |
| 100 mg q12h | plasma | 0.0625 | 100.00 | 99.45 | 0.55 | FALSE |
| 100 mg q12h | plasma | 0.1250 | 94.60 | 96.33 | 1.73 | TRUE |
| 100 mg q12h | plasma | 0.2500 | 85.72 | 85.73 | 0.01 | FALSE |
| 100 mg q12h | ELF | 0.0320 | 99.18 | 99.21 | 0.03 | FALSE |
| 100 mg q12h | ELF | 0.0625 | 95.36 | 95.67 | 0.31 | FALSE |
| 100 mg q12h | ELF | 0.1250 | 79.44 | 84.43 | 4.99 | TRUE |
| 100 mg q12h | ELF | 0.2500 | 62.50 | 62.42 | 0.08 | FALSE |
| 50 mg q12h | plasma | 0.0320 | 100.00 | 99.41 | 0.59 | FALSE |
| 50 mg q12h | plasma | 0.0625 | 96.34 | 96.33 | 0.01 | FALSE |
| 50 mg q12h | plasma | 0.1250 | 81.42 | 85.73 | 4.31 | TRUE |
| 50 mg q12h | plasma | 0.2500 | 63.48 | 63.89 | 0.41 | FALSE |
| 50 mg q12h | ELF | 0.0320 | 95.34 | 95.44 | 0.10 | FALSE |
| 50 mg q12h | ELF | 0.0625 | 84.84 | 84.43 | 0.41 | FALSE |
| 50 mg q12h | ELF | 0.1250 | 55.70 | 62.42 | 6.72 | TRUE |
| 50 mg q12h | ELF | 0.2500 | 33.70 | 34.53 | 0.83 | FALSE |
# Deterministic. Realised maximum over the gated cells is 0.83 pp.
stopifnot(max(pta_chk$abs_diff[!pta_chk$deviation]) < 2)Twelve of the sixteen cells reproduce to within 0.9 percentage
points. The four that do not are exactly the four
MIC = 0.125 mg/L cells, and Table 3 is internally
inconsistent on those cells regardless of any model: because the
attainment probability for fAUC/MIC > 30 depends only on
the ratio of dose to MIC, halving the dose and halving the MIC must give
the identical value.
pairs_chk <- tibble::tribble(
~matrix, ~cell_a, ~a, ~cell_b, ~b,
"plasma", "100 mg q12h, MIC 0.125", 94.60, "50 mg q12h, MIC 0.0625", 96.34,
"plasma", "100 mg q12h, MIC 0.25", 85.72, "50 mg q12h, MIC 0.125", 81.42,
"ELF", "100 mg q12h, MIC 0.125", 79.44, "50 mg q12h, MIC 0.0625", 84.84,
"ELF", "100 mg q12h, MIC 0.25", 62.50, "50 mg q12h, MIC 0.125", 55.70
) |>
dplyr::mutate(published_gap = abs(a - b))
pairs_chk |>
dplyr::rename(
"Matrix" = matrix,
"Cell A" = cell_a, "%PTA A" = a,
"Cell B (identical dose/MIC)" = cell_b, "%PTA B" = b,
"Published gap (pp)" = published_gap
) |>
knitr::kable(digits = 2, caption = paste(
"Pairs of Table 3 cells that carry the same dose-to-MIC ratio and must",
"therefore be equal. Every discrepant pair involves a MIC = 0.125 cell."
))| Matrix | Cell A | %PTA A | Cell B (identical dose/MIC) | %PTA B | Published gap (pp) |
|---|---|---|---|---|---|
| plasma | 100 mg q12h, MIC 0.125 | 94.60 | 50 mg q12h, MIC 0.0625 | 96.34 | 1.74 |
| plasma | 100 mg q12h, MIC 0.25 | 85.72 | 50 mg q12h, MIC 0.125 | 81.42 | 4.30 |
| ELF | 100 mg q12h, MIC 0.125 | 79.44 | 50 mg q12h, MIC 0.0625 | 84.84 | 5.40 |
| ELF | 100 mg q12h, MIC 0.25 | 62.50 | 50 mg q12h, MIC 0.125 | 55.70 | 6.80 |
# Every published pair whose gap is large contains a MIC = 0.125 cell; the one
# pair that contains none of them (not shown, MIC 0.0625 vs 0.032 has no
# matching partner) cannot be formed from the four tabulated MICs. This
# assertion pins the observation that the discrepancy is confined to MIC 0.125.
stopifnot(all(grepl("0.125", paste(pairs_chk$cell_a, pairs_chk$cell_b))))Assumptions and deviations
-
The unbound fraction sits inside the ELF distribution rate,
so the published partition coefficient is not the ELF:total-plasma
ratio. The abstract states that “the partition coefficient
which relates drug exposure in ELF to drug exposure in plasma was
estimated to be 0.77”, and the Discussion adds that “approximately 80%
of the plasma concentration of sitafloxacin can be diffused into ELF”.
The structural diagram in Figure 1 of the paper labels the
central-to-ELF arrow
(Q_ELF x F_u x PC)/Vagainst a plainQ_ELF/V_ELFin the return direction, and the control stream codes the same thing asK24 = QELF*0.63*PC/VBDagainstK42 = QELF/VELF, so the concentration ratio the model actually produces isfu * PC= 0.63 * 0.772 = 0.486. The model file reproduces the control stream verbatim; the two prose statements should be read as the ELF-to-unbound-plasma ratio. The published median exposures corroborate the code rather than the prose: the reported ELF-to-plasma AUC ratios are 10.17/21.02 = 0.484 and 5.12/10.42 = 0.491, both at 0.486 and neither at 0.772. -
The values Results 3.4 labels “fAUC” are not multiplied by
the unbound fraction on the plasma side. Reproducing them
requires taking the total plasma AUC (Deterministic reproduction
section, agreement within 1%); multiplying by
fu = 0.63would put the plasma value at 13.3 rather than the published 21.02. The ELF values do carryfu, but only through theK24micro-constant described above, not as a second multiplication. The same reading reproduces twelve of the sixteen%PTAcells of Table 3 to within 0.9 percentage points. -
Recorded deviation: the four MIC = 0.125 mg/L cells of Table
3. The model gives 96.33 / 85.73 (plasma, 100 and 50 mg q12h)
and 84.43 / 62.42 (ELF) where the paper reports 94.60 / 81.42 and 79.44
/ 55.70. As shown above, Table 3’s own MIC = 0.125 entries contradict
its MIC = 0.0625 and MIC = 0.25 entries under the exact dose-to-MIC
scaling that the target
fAUC/MIC > 30implies, so the discrepancy is in the published row rather than in the transcription. These cells are excluded from the assertion and left visible in the table. - Simulation covariate distributions. Methods 2.4 states the ages simulated (30, 40, 50, 60 and 70 years) but not the body weights. Holding weight at the 52 kg allometric reference reproduces all four published median exposures to within about 1%, so that is what the steady-state cohorts use. The study cohort’s weight and age distributions were constructed to match the medians and IQRs of Results 3.1; the paper does not publish the underlying distributions.
-
The age-on-F relationship must not be extrapolated below
adulthood. It is linear rather than exponential, so
F = 1 + 0.0258 * (AGE - 57)reaches zero at 18.2 years and is negative below it. The virtual cohorts here are truncated at 25 years. The model file records the same caution in theAGEcovariate metadata. -
No inter-individual variability on V/F, Q/F ELF or V/F
ELF. The paper states that the data did not support estimating
them (Results 3.3 and the Discussion’s limitations paragraph) and the
control stream fixes the corresponding
$OMEGAelements to 0. They are therefore omitted from the model rather than carried as zero-variance etas. - Final estimates come from the simulation control stream. Data S1 contains two streams. Run56 (estimation) carries earlier initial values for the ELF inter-compartmental clearance (0.0714 vs 0.0441), the partition coefficient (0.733 vs 0.772), clearance (7.01 vs 7.03), central volume (115 vs 116), mean transit time (1.49 vs 1.48) and the ELF residual variance (0.274 vs 0.249). Run59 (simulation) carries values that match Table 2 exactly, and it is Run59 that the model file follows.
-
Residual variability is encoded as
lnorm(). The paper’s “additive residual error model on the logarithmic scale” is a log-normal residual on the linear concentration scale;Cc ~ lnorm(expSd)is nlmixr2’s spelling of the control stream’sY = LOG(IPRED) + EPS(1). -
Related sitafloxacin models.
modellib("Wu_2025_sitafloxacin")andmodellib("Rodjun_2023_sitafloxacin")package independent sitafloxacin popPK models fitted in non-critically-ill populations; neither carries an ELF compartment.