Piperacillin + tazobactam in ESKD on haemodialysis (Kong 2025)
Source:vignettes/articles/Kong_2025_piperacillin_tazobactam.Rmd
Kong_2025_piperacillin_tazobactam.RmdModel and source
Kong et al. 2025 extended their previously published general-purpose piperacillin/tazobactam population PK model to patients with end-stage kidney disease (ESKD) receiving thrice-weekly high-flux intermittent haemodialysis. The structure is four compartments – a central and a peripheral compartment for each drug, with first-order elimination from the two central compartments and no interconversion – and the two analytes are fitted simultaneously via the NONMEM L2 data item.
What makes this an unusual extraction is how little of the model was estimated from the new data. Every structural parameter, the clearance maturation and decline functions, the serum-creatinine effect and all five inter-individual variances are fixed to the general-purpose parent model. Only four things were estimated from the 20-patient ESKD cohort:
- the residual endogenous clearance fraction
theta_ESKD, with residual diuresis as a covariate, - the piperacillin dialyser extraction ratio,
- the tazobactam dialyser extraction ratio, as a three-level function of vascular access type, and
- the residual error (proportional + additive per drug, plus a cross-drug residual correlation).
The packaged model uses piperacillin as the unsuffixed parent
(canonical compartments central / peripheral1,
parameters lcl / lvc / lq /
lvp, residuals propSd / addSd)
and tazobactam as the _taz sibling-drug suffix
(central_taz / peripheral1_taz,
lcl_taz / lvc_taz / lq_taz /
lvp_taz, propSd_taz / addSd_taz),
exactly paralleling Soto_2014_ampicillin_sulbactam and its
_sbt suffix.
- Citation: Kong D, Koomen JV, Vanommeslaeghe F, Delanghe S, Van Biesen W, Colin PJ, Eloot S. A Population Pharmacokinetic Analysis for Piperacillin/Tazobactam in Patients with End-Stage Kidney Disease Undergoing Intermittent Haemodialysis: Extension of a General-Purpose Model. Clin Pharmacokinet. 2025. doi:10.1007/s40262-025-01527-y. Structural parameters, maturation / decline functions, serum-creatinine effect and inter-individual variances are fixed to the general-purpose parent model: Kong D, et al. A pooled pharmacokinetic analysis for piperacillin/tazobactam across different patient populations: from premature infants to the elderly. Clin Pharmacokinet. 2025;64(1):107-126. The parent-model values used here are transcribed from the final NONMEM control stream reproduced verbatim in the Kong 2025 Electronic Supplementary Material, so nothing is imported from the parent publication itself.
- Article: https://doi.org/10.1007/s40262-025-01527-y
mod <- readModelDb("Kong_2025_piperacillin_tazobactam")
ui <- rxode2::rxode(mod)
ui$state
#> [1] "central" "peripheral1" "central_taz" "peripheral1_taz"Population
pop <- readModelDb("Kong_2025_piperacillin_tazobactam")()$population
data.frame(field = names(pop), value = vapply(pop, paste, character(1), collapse = " ")) |>
dplyr::rename("Field" = field, "Value" = value) |>
knitr::kable()| Field | Value | |
|---|---|---|
| species | species | human |
| n_subjects | n_subjects | 20 |
| n_studies | n_studies | 1 |
| age_range | age_range | 20-84 years (median 71.5) |
| age_median | age_median | 71.5 years |
| weight_range | weight_range | 27.0-93.8 kg (median 66.3) |
| weight_median | weight_median | 66.3 kg |
| sex_female_pct | sex_female_pct | 35 |
| disease_state | disease_state | End-stage kidney disease on thrice-weekly high-flux intermittent haemodialysis, with a documented or presumed infection requiring piperacillin/tazobactam. Residual diuresis present in 11/20 (55%) and absent in 9/20 (45%). Patients under 18 years, with known beta-lactam / beta-lactamase-inhibitor hypersensitivity, or pregnant were excluded (Kong 2025 Methods 2.1). |
| renal_function | renal_function | End-stage kidney disease; the model sets the serum-creatinine correction F_SCR to 1 for these patients (Eq. 2) because serum creatinine is itself removed by the dialyser, and instead estimates a residual endogenous clearance fraction theta_ESKD. Endogenous clearance was estimated to be 78.6% lower (95% CI 66.3-86.4%) without residual diuresis and 63.0% lower (95% CI 49.5-73.0%) with residual diuresis, relative to a patient with normal kidney function (Kong 2025 Results 3.4). |
| dose_range | dose_range | Piperacillin 0.60-4.00 g (median 3.08) with tazobactam 0.075-0.50 g (median 0.385) per dose, administered intravenously in the clinical fixed 8:1 ratio; infusion duration 0.25-24.50 h (median 2.85). Regimens were chosen by the treating physician, not by protocol. The actual delivered dose was corrected for drug remaining in the used infusion lines (Kong 2025 Methods 2.1, Table 1). |
| dialysis | dialysis | Thrice-weekly high-flux intermittent haemodialysis; session duration 89.4-333.4 min (median 240.0), dialyser blood flow rate 160-350 mL/min (median 225), priming volume 83-115 mL (median 115). Modality 14 (74%) haemodialysis, 1 (5%) pre-dilution and 4 (21%) post-dilution haemodiafiltration. Vascular access 6 (32%) tunnelled dialysis catheter, 4 (21%) single-needle and 9 (47%) double-needle arteriovenous fistula. Membranes were polysulfone (n = 14), polyacrylonitrile (3), polyamix (1), cellulosic triacetate (1) and polyarylethersulfone (1), surface area 1.3-2 m^2. One patient had no intradialytic sampling, so the dialysis characteristics are reported for n = 19 (Kong 2025 Results 3.1, Table 1). |
| regions | regions | Belgium (single centre, Ghent University Hospital) |
| notes | notes | Monocentric prospective observational study, ClinicalTrials.gov NCT03909698, completed 31 March 2022. 195 blood samples were collected with both piperacillin and tazobactam quantified in each; 4 piperacillin (2.1%) and 3 tazobactam (1.5%) concentrations were excluded for bioanalytical anomalies or missing sample-type information. Intradialytic samples were drawn SIMULTANEOUSLY from the arterial line entering the dialyser and the venous line exiting it, which is what makes the extraction ratio identifiable. Lower limits of quantification 0.5 mg/L (piperacillin) and 0.25 mg/L (tazobactam). Estimation used NONMEM 7.5 FOCE-I (Kong 2025 Methods 2.1-2.2, 2.6, Results 3.1-3.2). The Monte Carlo simulations of Table 4 and Figs. 3-4 drew 1000 virtual patients, sampling sex from the observed 35% female / 65% male split and continuous covariates from normal distributions truncated to the observed range; sex itself is not a covariate in the model. |
Twenty adults with end-stage kidney disease on thrice-weekly high-flux intermittent haemodialysis were enrolled at Ghent University Hospital (NCT03909698). Median age was 71.5 years (range 20-84), 13/20 (65%) were male, median body weight was 66.3 kg (27.0-93.8), and 11/20 (55%) had residual diuresis (Kong 2025 Table 1). Dialysis characteristics are reported for the 19 patients who had intradialytic sampling: session duration median 240 min (89.4-333.4), dialyser blood flow rate median 225 mL/min (160-350), vascular access 6 (32%) tunnelled dialysis catheter (TDC), 4 (21%) single-needle arteriovenous fistula (AVF 1N) and 9 (47%) double-needle arteriovenous fistula (AVF 2N).
The study design is what makes the dialyser extraction ratio identifiable: during each dialysis session blood was sampled simultaneously from the arterial line entering the dialyser and the venous line exiting it, so the fraction removed in one pass is observed directly rather than inferred from a concentration decline.
Source trace
Every ini() value and every non-trivial
model() equation, with the source location it came from.
The Electronic Supplementary Material (ESM) of Kong 2025 reproduces the
complete final NONMEM control stream ($MODEL,
$PK, $DES, $ERROR,
$THETA, $OMEGA, $SIGMA), so the
implemented values are available verbatim in addition to the rounded
Table 3 estimates. Where the two differ by rounding, the ESM code value
is used because it is the value that was actually fitted; the Table 3
equivalent is recorded alongside.
| Model element | Source location | Value / form |
|---|---|---|
lvc, lcl, lvp,
lq
|
ESM $THETA(1)-(4) FIX; Table 3 piperacillin column |
2.34 / 2.36 / 2.45 / 2.72 (log L, log L/h) |
lvc_taz, lcl_taz, lvp_taz,
lq_taz
|
ESM $THETA(5)-(8) FIX; Table 3 tazobactam column |
2.35 / 2.26 / 2.62 / 2.82 |
e_wt_cl, e_wt_vc, e_wt_q
|
Methods 2.3 (fixed West-Brown-Enquist exponents); ESM
$PK FSIZE**0.75, * FSIZE,
(V2SZ**0.75)
|
0.75 / 1.00 / 0.75, all fixed |
lhill_mat, ltmat50
|
ESM $THETA(9)-(10) FIX; Table 3 gamma_1 =
3.35, MAT50 = 54.2 weeks |
1.21 / 0.0358 |
lhill_dec, ltdec50,
ltdec50_taz
|
ESM $THETA(11)-(13) FIX; Table 3 gamma_2 =
1.92, DEC50 = 89.1 y (pip) / 61.6 y (taz) |
0.653 / 4.49 / 4.12 |
le_creat_cl |
ESM $THETA(14) FIX; Table 3 theta_SCR =
0.346 dL/mg |
-1.06 |
lf_eskd, lf_eskd_diur
|
ESM $THETA(15)-(16) estimated; Table 3
theta_ESKD 0.214 / 0.370 |
log(0.214) / log(0.370) |
logitedia |
ESM $THETA(17) estimated; Table 3 ER piperacillin
64.0% |
logit(0.640) |
logitedia_taz, ..._avf1n,
..._avf2n
|
ESM $THETA(18)-(20) estimated; Table 3 ER of TDC / AVF
1N / AVF 2N 80.1% / 73.9% / 73.5% |
logit(0.801) / logit(0.739) / logit(0.735) |
propSd, addSd, propSd_taz,
addSd_taz
|
ESM $THETA(21)-(24) estimated; Table 3
SD_prop / SD_add
|
0.412 / 0.588 mg/L / 0.423 / 0.214 mg/L |
etalvc, etalcl, etalcl_taz,
etalvp, etalq
|
ESM $OMEGA all FIX; Table 3 IIV rows, footnote b
sqrt(exp(omega) - 1) x 100%
|
0.167 / 0.171 / 0.159 / 0.548 / 0.358 |
creat_std equation |
Eq. 1 (and its repeat as Eq. 3); ESM $PK
STCR
|
exp[1.42 - (1.17 + 0.203 ln(PMA/100)) / sqrt(PMA/100)],
PMA in years |
f_scr piecewise |
Eq. 2 |
exp(-theta_SCR (SCR - SCR_std)) off dialysis,
1 on intermittent haemodialysis |
cl assembly |
Eq. 4; ESM $PK TVCL1 /
TVCL3
|
CL_normal x F_SCR x F_ESKD x F_MAT x F_DEC x F_SIZE x exp(eta_CL) |
f_eskd diuresis gate |
Table 2 model 11 (dOFV -12.37); ESM $PK
IF(UVOL0123.NE.0)
|
two typical values selected by residual diuresis |
cl_dialysis |
Eq. 5; ESM $PK
CL_DIA_PIP = ER_PIP*FLOW*IND_DIA
|
ER x blood flow rate (L/h) x session-active indicator |
edia logit form |
Eq. 6; ESM $PK
ER = EXP(LGT)/(1+EXP(LGT))
|
e^theta / (1 + e^theta) |
| ODE system | ESM $DES
|
two independent 2-compartment systems, dialyser clearance out of each central compartment |
Cc_post |
ESM $ERROR CPOST = CPRE * (1-ER)
|
post-dialyser (outlet) concentration |
| Residual error form | ESM $ERROR
PSD = SQRT(IPRED^2*PPROP^2 + PADD^2) with
$SIGMA 1 FIX
|
combined proportional + additive (nlmixr2
combined2) |
PAGE / 12 |
canonical PAGE is months; ESM $PK
RPMA = 35+(40/52) works in years |
unit conversion, not a fitted value |
BFR * 60 / 1000 |
ESM $PK FLOW = IFLOW*60/1000
|
mL/min to L/h |
| Reference weight 70 kg | Methods 2.3 | FSIZE = WT/70 |
URINE_VOL_24H > 100 |
NOT from Kong 2025 – see Assumptions | canonical anuria cutoff (100 mL/24h) standing in for the paper’s binary flag |
Published estimates reproduce from the packaged
ini()
The strongest check available for a model whose parameters are almost
all fixed is an enumerating one: back-transform every ini()
value and require it to equal the published Table 3 entry. This covers
all 14 fixed structural / maturation / decline / creatinine parameters,
the 5 fixed variances, the 6 estimated typical values and the 4
residual-error terms.
th <- ui$theta
om <- diag(ui$omega)
back <- tibble::tribble(
~Parameter, ~Published, ~Recovered,
"theta_V1 pip (L/70 kg)", 10.4, exp(th[["lvc"]]),
"theta_CL pip (L/h/70 kg)", 10.6, exp(th[["lcl"]]),
"theta_V2 pip (L/70 kg)", 11.6, exp(th[["lvp"]]),
"theta_Q2 pip (L/h/70 kg)", 15.2, exp(th[["lq"]]),
"theta_V1 taz (L/70 kg)", 10.5, exp(th[["lvc_taz"]]),
"theta_CL taz (L/h/70 kg)", 9.58, exp(th[["lcl_taz"]]),
"theta_V2 taz (L/70 kg)", 13.7, exp(th[["lvp_taz"]]),
"theta_Q2 taz (L/h/70 kg)", 16.8, exp(th[["lq_taz"]]),
"gamma_1 (maturation)", 3.35, exp(th[["lhill_mat"]]),
"gamma_2 (decline)", 1.92, exp(th[["lhill_dec"]]),
"DEC50 pip (years)", 89.1, exp(th[["ltdec50"]]),
"DEC50 taz (years)", 61.6, exp(th[["ltdec50_taz"]]),
"theta_SCR (dL/mg)", 0.346, exp(th[["le_creat_cl"]]),
"theta_ESKD no diuresis", 0.214, exp(th[["lf_eskd"]]),
"theta_ESKD with diuresis", 0.370, exp(th[["lf_eskd_diur"]]),
"ER pip (%)", 64.0, 100 * plogis(th[["logitedia"]]),
"ER taz TDC (%)", 80.1, 100 * plogis(th[["logitedia_taz"]]),
"ER taz AVF 1N (%)", 73.9, 100 * plogis(th[["logitedia_taz_avf1n"]]),
"ER taz AVF 2N (%)", 73.5, 100 * plogis(th[["logitedia_taz_avf2n"]]),
"SD_prop pip (%)", 41.2, 100 * th[["propSd"]],
"SD_add pip (mg/L)", 0.588, th[["addSd"]],
"SD_prop taz (%)", 42.3, 100 * th[["propSd_taz"]],
"SD_add taz (mg/L)", 0.214, th[["addSd_taz"]],
"IIV V1 (%)", 42.6, 100 * sqrt(exp(om[["etalvc"]]) - 1),
"IIV CL pip (%)", 43.2, 100 * sqrt(exp(om[["etalcl"]]) - 1),
"IIV CL taz (%)", 41.5, 100 * sqrt(exp(om[["etalcl_taz"]]) - 1),
"IIV V2 (%)", 85.4, 100 * sqrt(exp(om[["etalvp"]]) - 1),
"IIV Q2 (%)", 65.6, 100 * sqrt(exp(om[["etalq"]]) - 1)
)
back$`% diff` <- round(100 * (back$Recovered / back$Published - 1), 2)
back |>
dplyr::mutate(Recovered = signif(Recovered, 4)) |>
knitr::kable(caption = "Kong 2025 Table 3 recovered from the packaged ini() block.")| Parameter | Published | Recovered | % diff |
|---|---|---|---|
| theta_V1 pip (L/70 kg) | 10.400 | 10.3800 | -0.18 |
| theta_CL pip (L/h/70 kg) | 10.600 | 10.5900 | -0.09 |
| theta_V2 pip (L/70 kg) | 11.600 | 11.5900 | -0.10 |
| theta_Q2 pip (L/h/70 kg) | 15.200 | 15.1800 | -0.13 |
| theta_V1 taz (L/70 kg) | 10.500 | 10.4900 | -0.14 |
| theta_CL taz (L/h/70 kg) | 9.580 | 9.5830 | 0.03 |
| theta_V2 taz (L/70 kg) | 13.700 | 13.7400 | 0.26 |
| theta_Q2 taz (L/h/70 kg) | 16.800 | 16.7800 | -0.14 |
| gamma_1 (maturation) | 3.350 | 3.3530 | 0.10 |
| gamma_2 (decline) | 1.920 | 1.9210 | 0.07 |
| DEC50 pip (years) | 89.100 | 89.1200 | 0.02 |
| DEC50 taz (years) | 61.600 | 61.5600 | -0.07 |
| theta_SCR (dL/mg) | 0.346 | 0.3465 | 0.13 |
| theta_ESKD no diuresis | 0.214 | 0.2140 | 0.00 |
| theta_ESKD with diuresis | 0.370 | 0.3700 | 0.00 |
| ER pip (%) | 64.000 | 64.0000 | 0.00 |
| ER taz TDC (%) | 80.100 | 80.1000 | 0.00 |
| ER taz AVF 1N (%) | 73.900 | 73.9000 | 0.00 |
| ER taz AVF 2N (%) | 73.500 | 73.5000 | 0.00 |
| SD_prop pip (%) | 41.200 | 41.2000 | 0.00 |
| SD_add pip (mg/L) | 0.588 | 0.5880 | 0.00 |
| SD_prop taz (%) | 42.300 | 42.3000 | 0.00 |
| SD_add taz (mg/L) | 0.214 | 0.2140 | 0.00 |
| IIV V1 (%) | 42.600 | 42.6300 | 0.08 |
| IIV CL pip (%) | 43.200 | 43.1800 | -0.04 |
| IIV CL taz (%) | 41.500 | 41.5100 | 0.03 |
| IIV V2 (%) | 85.400 | 85.4300 | 0.03 |
| IIV Q2 (%) | 65.600 | 65.6100 | 0.02 |
# Every entry must agree with the published value to the printed precision.
stopifnot(all(abs(back$`% diff`) < 0.6))MAT50 is the one entry not gated above. Table 3 prints
54.2 weeks while the ESM code carries $THETA(10) = 0.0358
on the log-year scale, which back-transforms to 1.0364 years = 54.1
weeks. The ~0.2% difference is a weeks-per-year rounding convention, and
in an all-adult cohort the maturation ratio
F_MAT / F_MAT_ref is 1 to within 1e-09 either way, so the
discrepancy cannot affect any prediction here.
Closed-form structural gates
Before any cohort simulation, the individual equations are checked
against their published forms with exact identities. rxode2
returns every derived model() variable as a column, so each
factor of Eq. 4 and each piece of Eq. 5/6 can be read off a single
solve.
HD_DUR <- 4 # h; Methods 2.5 assumes 4-hour sessions
# Days 1, 3 and 5 of each week, with each session starting 4 h after a
# maintenance dose. Kong 2025 does not report where the session falls inside
# the dosing interval; this placement is the only one that keeps the whole
# 4-hour session inside a single 12-hour interval, so no dose is infused into a
# running dialyser and the supplementary post-dialysis dose never collides with
# a maintenance dose. See Assumptions.
HD_OFF <- c(4, 52, 100)
WEEKS <- 2
HD_START <- sort(as.numeric(outer(HD_OFF, (seq_len(WEEKS) - 1) * 168, "+")))
TEND <- WEEKS * 168
WIN <- c(TEND - 168, TEND) # metrics are taken over the final week
T_INF <- 0.5 # h; 30-minute infusion per the SmPC
RPMA_M <- (35 + 40 / 52) * 12 # reference postmenstrual age, months
hd_active <- function(tm) {
as.integer(rowSums(outer(tm, HD_START, function(a, b) a >= b & a < b + HD_DUR)) > 0)
}
# Serum creatinine standardised for postmenstrual age (Kong 2025 Eq. 1).
creat_std_fun <- function(pma_years) {
exp(1.42 - (1.17 + 0.203 * log(pma_years / 100)) / sqrt(pma_years / 100))
}
# One reference patient, probed on and off dialysis, for each access type and
# each diuresis status. All covariates are at their reference values so every
# factor of Eq. 4 other than F_ESKD collapses to exactly 1.
probe <- expand.grid(
hd_on = c(0L, 1L),
diuresis = c(0, 500),
access = c("TDC", "AVF 1N", "AVF 2N"),
stringsAsFactors = FALSE
) |>
dplyr::mutate(
id = dplyr::row_number(),
VASCACC_AVF1N = as.integer(access == "AVF 1N"),
VASCACC_AVF2N = as.integer(access == "AVF 2N")
)
probe_ev <- probe |>
dplyr::rowwise() |>
dplyr::reframe(
id, access, hd_on, diuresis, VASCACC_AVF1N, VASCACC_AVF2N,
time = c(0, 1),
cmt = c("central", "central"),
amt = c(4000, 0),
rate = c(4000 / T_INF, 0),
evid = c(1L, 0L),
dvid = c(NA_integer_, 1L)
) |>
dplyr::mutate(
RRT_HEMODIAL_STATUS = 1L,
RRT_HEMODIAL_ACTIVE = hd_on,
BFR = 225,
WT = 70,
PAGE = RPMA_M,
CREAT = 0,
URINE_VOL_24H = diuresis
) |>
as.data.frame()
probe_sim <- rxode2::rxSolve(
rxode2::zeroRe(ui), probe_ev,
returnType = "data.frame", addDosing = FALSE
)
#> ℹ omega/sigma items treated as zero: 'etalvc', 'etalcl', 'etalcl_taz', 'etalvp', 'etalq'
#> Warning: multi-subject simulation without without 'omega'
probe_sim <- merge(probe_sim, probe[, c("id", "access")], by = "id")
# Eq. 2: F_SCR is exactly 1 for every patient on intermittent haemodialysis.
stopifnot(isTRUE(all.equal(unique(probe_sim$f_scr), 1)))
# Eq. 4 F_ESKD: exactly the two published typical values, selected by diuresis.
f_eskd_by_diur <- tapply(probe_sim$f_eskd, probe_sim$URINE_VOL_24H > 100, unique)
stopifnot(
isTRUE(all.equal(as.numeric(f_eskd_by_diur[["FALSE"]]), 0.214, tolerance = 1e-3)),
isTRUE(all.equal(as.numeric(f_eskd_by_diur[["TRUE"]]), 0.370, tolerance = 1e-3))
)
# The maturation and decline ratios are exactly 1 at the 35-year reference, so
# the whole Eq. 4 product collapses to CL_normal x F_ESKD. This single identity
# exercises F_SIZE, F_MAT, F_DEC, F_SCR and F_ESKD simultaneously.
stopifnot(
isTRUE(all.equal(unique(probe_sim$fmat / probe_sim$fmat_ref), 1)),
isTRUE(all.equal(unique(probe_sim$fdec / probe_sim$fdec_ref), 1)),
isTRUE(all.equal(unique(probe_sim$fdec_taz / probe_sim$fdec_taz_ref), 1))
)
ref_cl <- probe_sim[probe_sim$URINE_VOL_24H == 0, ]
stopifnot(
isTRUE(all.equal(unique(ref_cl$cl), exp(ui$theta[["lcl"]]) * 0.214, tolerance = 1e-3)),
isTRUE(all.equal(unique(ref_cl$cl_taz), exp(ui$theta[["lcl_taz"]]) * 0.214, tolerance = 1e-3))
)
# Eq. 1: the standardised creatinine at the reference age, and the fact that a
# patient off dialysis whose CREAT equals CREAT_std also has F_SCR == 1.
stopifnot(isTRUE(all.equal(
unique(probe_sim$creat_std), creat_std_fun(RPMA_M / 12), tolerance = 1e-9
)))
cat(sprintf("CREAT_std at the 35.769-year reference: %.4f mg/dL\n",
creat_std_fun(RPMA_M / 12)))
#> CREAT_std at the 35.769-year reference: 0.8292 mg/dL
# Eq. 6: the three tazobactam extraction ratios by access type, and the single
# piperacillin one. This is the Kong 2025 Table 3 ER block and, read across the
# access levels, the effect that ESM Table S4 tabulates.
er_tbl <- probe_sim |>
dplyr::distinct(access, edia, edia_taz) |>
dplyr::arrange(match(access, c("TDC", "AVF 1N", "AVF 2N"))) |>
dplyr::mutate(
`ER piperacillin (%)` = round(100 * edia, 1),
`ER tazobactam (%)` = round(100 * edia_taz, 1),
`Published ER taz (%)` = c(80.1, 73.9, 73.5)
) |>
dplyr::select("Vascular access" = access, `ER piperacillin (%)`,
`ER tazobactam (%)`, `Published ER taz (%)`)
knitr::kable(er_tbl, caption = "Dialyser extraction ratios by vascular access type (Kong 2025 Table 3).")| Vascular access | ER piperacillin (%) | ER tazobactam (%) | Published ER taz (%) |
|---|---|---|---|
| TDC | 64 | 80.1 | 80.1 |
| AVF 1N | 64 | 73.9 | 73.9 |
| AVF 2N | 64 | 73.5 | 73.5 |
stopifnot(isTRUE(all.equal(er_tbl$`ER tazobactam (%)`, er_tbl$`Published ER taz (%)`)))
stopifnot(isTRUE(all.equal(unique(round(100 * probe_sim$edia, 1)), 64.0)))
# Eq. 5: CL_DIA = ER x blood flow rate (L/h), gated on by the session
# indicator. Exact to machine precision, and exactly zero between sessions.
on <- probe_sim[probe_sim$RRT_HEMODIAL_ACTIVE == 1L, ]
off <- probe_sim[probe_sim$RRT_HEMODIAL_ACTIVE == 0L, ]
stopifnot(
isTRUE(all.equal(on$cl_dialysis, on$edia * 225 * 60 / 1000)),
isTRUE(all.equal(on$cl_dialysis_taz, on$edia_taz * 225 * 60 / 1000)),
all(off$cl_dialysis == 0), all(off$cl_dialysis_taz == 0)
)
cat(sprintf("At BFR = 225 mL/min: CL_dia pip = %.2f L/h, CL_dia taz = %.2f L/h\n",
unique(on$cl_dialysis), max(on$cl_dialysis_taz)))
#> At BFR = 225 mL/min: CL_dia pip = 8.64 L/h, CL_dia taz = 10.81 L/h
# ESM $ERROR: the post-dialyser (outlet) concentration loses the extracted
# fraction during a session and equals the inlet concentration otherwise.
stopifnot(
isTRUE(all.equal(on$Cc_post, on$Cc * (1 - on$edia))),
isTRUE(all.equal(on$Cc_post_taz, on$Cc_taz * (1 - on$edia_taz))),
isTRUE(all.equal(off$Cc_post, off$Cc)),
isTRUE(all.equal(off$Cc_post_taz, off$Cc_taz))
)Virtual cohort
The source paper generated 1000 template virtual patients, drawing categorical covariates from the observed cohort proportions and continuous covariates from normal distributions truncated to the observed minimum and maximum (Methods 2.5). Table 1 reports medians and ranges rather than means and standard deviations, so the cohort below uses the published median as the distribution mean and one quarter of the observed range as the standard deviation. Cohort size is capped at 150 per arm, which is ample for the group-level means that Table 4 reports.
N_ARM <- 150
rtnorm <- function(n, mean, sd, lo, hi) {
x <- stats::rnorm(n, mean, sd)
bad <- x < lo | x > hi
while (any(bad)) {
x[bad] <- stats::rnorm(sum(bad), mean, sd)
bad <- x < lo | x > hi
}
x
}
make_arm <- function(n, diuresis, eskd = 1L,
pip_maint = 4000, taz_maint = 500,
pip_post = 2000, taz_post = 250, tau = 12) {
maint <- seq(0, TEND - tau, by = tau)
post <- if (eskd == 1L) HD_START + HD_DUR else numeric(0)
obs <- sort(unique(c(
seq(WIN[1], WIN[2], by = 0.5),
HD_START, HD_START + HD_DUR,
maint + T_INF, post + T_INF
)))
obs <- obs[obs >= WIN[1] & obs <= WIN[2]]
acc <- sample(c(0L, 1L, 2L), n, replace = TRUE, prob = c(0.32, 0.21, 0.47))
cov <- data.frame(
id = seq_len(n),
WT = rtnorm(n, 66.3, (93.8 - 27.0) / 4, 27.0, 93.8),
agey = rtnorm(n, 71.5, (84 - 20) / 4, 20, 84),
BFR = if (eskd == 1L) rtnorm(n, 225, (350 - 160) / 4, 160, 350) else 0,
VASCACC_AVF1N = as.integer(acc == 1L),
VASCACC_AVF2N = as.integer(acc == 2L)
)
one <- dplyr::bind_rows(
data.frame(time = maint, cmt = "central", amt = pip_maint,
rate = pip_maint / T_INF, evid = 1L, dvid = NA_integer_),
data.frame(time = maint, cmt = "central_taz", amt = taz_maint,
rate = taz_maint / T_INF, evid = 1L, dvid = NA_integer_),
if (length(post)) {
data.frame(time = post, cmt = "central", amt = pip_post,
rate = pip_post / T_INF, evid = 1L, dvid = NA_integer_)
},
if (length(post)) {
data.frame(time = post, cmt = "central_taz", amt = taz_post,
rate = taz_post / T_INF, evid = 1L, dvid = NA_integer_)
},
data.frame(time = obs, cmt = "central", amt = 0, rate = 0,
evid = 0L, dvid = 1L)
)
d <- do.call(rbind, lapply(seq_len(n), function(i) {
x <- one
x$id <- i
x
}))
d <- merge(d, cov, by = "id")
d <- d[order(d$id, d$time, -d$evid), ]
d$RRT_HEMODIAL_STATUS <- eskd
d$RRT_HEMODIAL_ACTIVE <- if (eskd == 1L) hd_active(d$time) else 0L
d$PAGE <- (d$agey + 40 / 52) * 12
d$URINE_VOL_24H <- if (diuresis) 500 else 0
# Off dialysis the serum-creatinine correction is active; setting CREAT to
# its age-standardised value makes F_SCR = 1, i.e. the Eq. 4 definition of
# CL_normal ("a 35-year-old, 70 kg patient, with a standardised serum
# creatinine concentration"). See Assumptions.
d$CREAT <- if (eskd == 1L) 0 else creat_std_fun(d$PAGE / 12)
d$agey <- NULL
d
}
set.seed(20250820)
ev_nodiur <- make_arm(N_ARM, diuresis = FALSE)
set.seed(20250820)
ev_diur <- make_arm(N_ARM, diuresis = TRUE)
# The seed is reset before each arm so the two cohorts share covariates AND
# random effects; the arms then differ only in theta_ESKD, which is exactly the
# contrast Table 4 reports.
set.seed(4242)
sim_nodiur <- rxode2::rxSolve(ui, ev_nodiur, returnType = "data.frame",
addDosing = FALSE)
set.seed(4242)
sim_diur <- rxode2::rxSolve(ui, ev_diur, returnType = "data.frame",
addDosing = FALSE)
sim_all <- dplyr::bind_rows(
dplyr::mutate(sim_nodiur, treatment = "ESKD, no residual diuresis"),
dplyr::mutate(sim_diur, treatment = "ESKD, residual diuresis")
)
nrow(sim_all)
#> [1] 101100Replicate Kong 2025 Figure 3
Simulated steady-state concentration-versus-time profiles over the third treatment week for the two ESKD arms, with the dialysis sessions shaded. This replicates Figure 3 of Kong 2025 (and ESM Figure S8) for the EMA regimen of 4 g/0.5 g every 12 h plus 2 g/0.25 g after each session.
hd_bands <- data.frame(
xmin = HD_START[HD_START >= WIN[1]],
xmax = HD_START[HD_START >= WIN[1]] + HD_DUR
)
prof <- sim_all |>
dplyr::select(id, time, treatment, Cc, Cc_taz) |>
tidyr::pivot_longer(c(Cc, Cc_taz), names_to = "analyte", values_to = "conc") |>
dplyr::mutate(analyte = ifelse(analyte == "Cc", "Piperacillin", "Tazobactam")) |>
dplyr::group_by(treatment, analyte, time) |>
dplyr::summarise(
mean = mean(conc),
lo = stats::quantile(conc, 0.025),
hi = stats::quantile(conc, 0.975),
.groups = "drop"
)
ggplot(prof, aes(time - WIN[1], mean, colour = treatment, fill = treatment)) +
geom_rect(data = hd_bands, inherit.aes = FALSE,
aes(xmin = xmin - WIN[1], xmax = xmax - WIN[1], ymin = 0, ymax = Inf),
fill = "grey80", alpha = 0.5) +
geom_ribbon(aes(ymin = lo, ymax = hi), alpha = 0.2, colour = NA) +
geom_line(linewidth = 0.6) +
facet_wrap(~analyte, ncol = 1, scales = "free_y") +
scale_x_continuous(breaks = seq(0, 168, by = 24)) +
labs(x = "Time in the third treatment week (h)",
y = "Concentration (mg/L)", colour = NULL, fill = NULL,
caption = "Grey bands are the 4-hour dialysis sessions. Replicates Figure 3 of Kong 2025.") +
theme_bw() +
theme(legend.position = "top")
PKNCA validation
Kong 2025 reports, per patient group and drug, the
mean, peak and trough
steady-state concentrations (Table 4). Two of those map directly onto
PKNCA parameters over the weekly steady-state interval: the peak is
cmax, and the mean is auclast / 168 – which is
exactly the definition the paper itself gives for the tazobactam mean
concentration in Eq. 9 (Cm = AUC_ss,24h / 24 h). The
overall minimum is cmin.
nca_conc <- sim_all |>
dplyr::filter(!is.na(Cc)) |>
dplyr::select(id, time, treatment, Cc, Cc_taz) |>
tidyr::pivot_longer(c(Cc, Cc_taz), names_to = "analyte", values_to = "conc") |>
dplyr::mutate(
analyte = ifelse(analyte == "Cc", "Piperacillin", "Tazobactam"),
group = paste(treatment, analyte, sep = " | ")
) |>
dplyr::select(id, time, group, conc)
nca_dose <- dplyr::bind_rows(
dplyr::mutate(ev_nodiur, treatment = "ESKD, no residual diuresis"),
dplyr::mutate(ev_diur, treatment = "ESKD, residual diuresis")
) |>
dplyr::filter(evid == 1L) |>
dplyr::mutate(
analyte = ifelse(cmt == "central", "Piperacillin", "Tazobactam"),
group = paste(treatment, analyte, sep = " | ")
) |>
dplyr::filter(time >= WIN[1], time <= WIN[2]) |>
dplyr::select(id, time, amt, group)
conc_obj <- PKNCA::PKNCAconc(nca_conc, conc ~ time | group + id,
concu = "ug/mL", timeu = "h")
dose_obj <- PKNCA::PKNCAdose(nca_dose, amt ~ time | group + id, doseu = "mg")
intervals <- data.frame(
start = WIN[1],
end = WIN[2],
cmax = TRUE,
tmax = TRUE,
cmin = TRUE,
auclast = TRUE
)
nca_res <- PKNCA::pk.nca(
PKNCA::PKNCAdata(conc_obj, dose_obj, intervals = intervals)
)
nca_wide <- nca_res$result |>
dplyr::filter(PPTESTCD %in% c("cmax", "cmin", "auclast", "tmax")) |>
dplyr::select(group, id, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
dplyr::mutate(cavg = auclast / diff(WIN))
nca_wide |>
dplyr::group_by(group) |>
dplyr::summarise(
n = dplyr::n(),
cavg = mean(cavg), cmax = mean(cmax), cmin = mean(cmin),
auclast = mean(auclast),
.groups = "drop"
) |>
dplyr::mutate(dplyr::across(c(cavg, cmax, cmin, auclast), ~ signif(.x, 4))) |>
dplyr::rename(
"Group | analyte" = group, "N" = n,
"Mean conc (mg/L)" = cavg, "Peak (mg/L)" = cmax,
"Overall minimum (mg/L)" = cmin, "AUC 0-168 h (mg*h/L)" = auclast
) |>
knitr::kable(caption = "PKNCA over the weekly steady-state interval.")| Group | analyte | N | Mean conc (mg/L) | Peak (mg/L) | Overall minimum (mg/L) | AUC 0-168 h (mg*h/L) |
|---|---|---|---|---|---|
| ESKD, no residual diuresis | Piperacillin | 150 | 205.30 | 468.50 | 43.910 | 34490 |
| ESKD, no residual diuresis | Tazobactam | 150 | 30.10 | 65.99 | 6.658 | 5057 |
| ESKD, residual diuresis | Piperacillin | 150 | 131.20 | 379.70 | 24.690 | 22050 |
| ESKD, residual diuresis | Tazobactam | 150 | 19.91 | 50.82 | 4.043 | 3344 |
Comparison against the published Table 4
sim_summary <- nca_wide |>
dplyr::group_by(group) |>
dplyr::summarise(cavg = mean(cavg), cmax = mean(cmax), .groups = "drop") |>
tidyr::pivot_longer(c(cavg, cmax), names_to = "PPTESTCD", values_to = "PPORRES")
published <- data.frame(
group = c("ESKD, no residual diuresis | Piperacillin",
"ESKD, no residual diuresis | Tazobactam",
"ESKD, residual diuresis | Piperacillin",
"ESKD, residual diuresis | Tazobactam"),
cavg = c(195.2, 28.3, 121.0, 18.4),
cmax = c(452.0, 56.3, 422.0, 51.9)
)
nca_cmp <- nlmixr2lib::ncaComparisonTable(
sim_summary, published,
by = "group",
units = c(cavg = "mg/L", cmax = "mg/L")
)
#> Warning: ncaParamLabel(): unknown PKNCA code(s) returned as-is: 'cavg'
knitr::kable(nca_cmp, caption = "Kong 2025 Table 4, EMA regimen: mean and peak steady-state concentrations.")| NCA parameter | group | Reference | Simulated | % diff |
|---|---|---|---|---|
| Cmax (mg/L) | ESKD, no residual diuresis | Piperacillin | 452 | 469 | +3.7% |
| Cmax (mg/L) | ESKD, no residual diuresis | Tazobactam | 56.3 | 66 | +17.2% |
| Cmax (mg/L) | ESKD, residual diuresis | Piperacillin | 422 | 380 | -10.0% |
| Cmax (mg/L) | ESKD, residual diuresis | Tazobactam | 51.9 | 50.8 | -2.1% |
| cavg (mg/L) | ESKD, no residual diuresis | Piperacillin | 195 | 205 | +5.2% |
| cavg (mg/L) | ESKD, no residual diuresis | Tazobactam | 28.3 | 30.1 | +6.4% |
| cavg (mg/L) | ESKD, residual diuresis | Piperacillin | 121 | 131 | +8.5% |
| cavg (mg/L) | ESKD, residual diuresis | Tazobactam | 18.4 | 19.9 | +8.2% |
attr(nca_cmp, "footnote")
#> NULL
# Table 4 splits the trough into a "dialysis off" and a "during dialysis"
# value. Neither is a PKNCA parameter, so they are computed directly from the
# simulated profiles using the session mask. The during-dialysis mask is
# inclusive of the session end, which is where the nadir sits.
troughs <- sim_all |>
dplyr::mutate(
on_hd = rowSums(outer(time, HD_START, function(a, b) a >= b & a <= b + HD_DUR)) > 0
) |>
tidyr::pivot_longer(c(Cc, Cc_taz), names_to = "analyte", values_to = "conc") |>
dplyr::mutate(analyte = ifelse(analyte == "Cc", "Piperacillin", "Tazobactam")) |>
dplyr::group_by(treatment, analyte, id) |>
dplyr::summarise(
trough_off = min(conc[!on_hd]),
trough_on = min(conc[on_hd]),
.groups = "drop"
) |>
dplyr::group_by(treatment, analyte) |>
dplyr::summarise(
trough_off = mean(trough_off), trough_on = mean(trough_on),
.groups = "drop"
)
published_troughs <- data.frame(
treatment = rep(c("ESKD, no residual diuresis", "ESKD, residual diuresis"), each = 2),
analyte = rep(c("Piperacillin", "Tazobactam"), 2),
pub_off = c(119.8, 18.5, 47.5, 9.8),
pub_on = c(25.7, 4.1, 10.5, 1.9)
)
troughs |>
dplyr::left_join(published_troughs, by = c("treatment", "analyte")) |>
dplyr::mutate(
diff_off = round(100 * (trough_off / pub_off - 1), 1),
diff_on = round(100 * (trough_on / pub_on - 1), 1),
dplyr::across(c(trough_off, trough_on), ~ signif(.x, 4))
) |>
dplyr::rename(
"Patient group" = treatment, "Analyte" = analyte,
"Sim trough, dialysis off" = trough_off, "Sim trough, during dialysis" = trough_on,
"Published, dialysis off" = pub_off, "Published, during dialysis" = pub_on,
"% diff off" = diff_off, "% diff on" = diff_on
) |>
knitr::kable(caption = "Kong 2025 Table 4 trough concentrations (mg/L).")| Patient group | Analyte | Sim trough, dialysis off | Sim trough, during dialysis | Published, dialysis off | Published, during dialysis | % diff off | % diff on |
|---|---|---|---|---|---|---|---|
| ESKD, no residual diuresis | Piperacillin | 103.900 | 43.910 | 119.8 | 25.7 | -13.3 | 70.9 |
| ESKD, no residual diuresis | Tazobactam | 15.260 | 6.658 | 18.5 | 4.1 | -17.5 | 62.4 |
| ESKD, residual diuresis | Piperacillin | 57.060 | 24.700 | 47.5 | 10.5 | 20.1 | 135.3 |
| ESKD, residual diuresis | Tazobactam | 9.711 | 4.043 | 9.8 | 1.9 | -0.9 | 112.8 |
The mean concentrations, the peaks and the dialysis-off troughs reproduce the published values within the 20% tolerance. The during-dialysis trough runs substantially high, and the reason is a simulation-design detail the paper does not report rather than a model defect: Kong 2025 states the dialysis schedule (thrice weekly, 4-hour sessions, Methods 2.5) but not where each session falls within the 12-hour dosing interval, and that placement is exactly what sets the nadir. Session placements from 0 to 8 h after a maintenance dose were examined while preparing this vignette and moved the during-dialysis trough by more than a factor of two while leaving the mean, the peak and the dialysis-off trough essentially unchanged. The published values imply a session that ends at or very near a pre-dose time; that placement makes the supplementary post-dialysis dose coincide with a maintenance dose, which in turn inflates the peak, so no single placement reproduces all four columns. The metric is reported here for completeness and is not gated.
# The schedule-robust Table 4 columns must agree within 20%. Recomputed from
# the raw frames because ncaComparisonTable() returns formatted strings.
gate <- published |>
tidyr::pivot_longer(c(cavg, cmax), names_to = "PPTESTCD", values_to = "Reference") |>
dplyr::inner_join(sim_summary, by = c("group", "PPTESTCD")) |>
dplyr::mutate(pct = 100 * (PPORRES / Reference - 1))
print(as.data.frame(dplyr::mutate(gate, dplyr::across(where(is.numeric), ~ round(.x, 2)))))
#> group PPTESTCD Reference PPORRES pct
#> 1 ESKD, no residual diuresis | Piperacillin cavg 195.2 205.32 5.18
#> 2 ESKD, no residual diuresis | Piperacillin cmax 452.0 468.51 3.65
#> 3 ESKD, no residual diuresis | Tazobactam cavg 28.3 30.10 6.36
#> 4 ESKD, no residual diuresis | Tazobactam cmax 56.3 65.99 17.21
#> 5 ESKD, residual diuresis | Piperacillin cavg 121.0 131.25 8.47
#> 6 ESKD, residual diuresis | Piperacillin cmax 422.0 379.65 -10.03
#> 7 ESKD, residual diuresis | Tazobactam cavg 18.4 19.91 8.19
#> 8 ESKD, residual diuresis | Tazobactam cmax 51.9 50.82 -2.08
stopifnot(nrow(gate) == 8L, all(abs(gate$pct) < 20))
trough_gate <- troughs |>
dplyr::left_join(published_troughs, by = c("treatment", "analyte")) |>
dplyr::mutate(pct_off = 100 * (trough_off / pub_off - 1))
stopifnot(all(abs(trough_gate$pct_off) < 25))What the model is responsible for is that the dialyser
removes drug at exactly ER x BFR, and that the ODE system
conserves mass while it does so. That is checked below with an identity
that does not depend on the schedule at all. Over any session interval
containing no dose, the change in total body amount must equal the
integral of total clearance times the plasma concentration:
A_total(t1) - A_total(t0) = -integral of (CL + CL_DIA) x Cc dt
typ_ev <- make_arm(1, diuresis = FALSE)
# Refine the grid inside the last session so the trapezoidal integral of the
# steep intradialytic decline is accurate.
s_last <- max(HD_START)
fine <- seq(s_last, s_last + HD_DUR, by = 0.01)
extra <- typ_ev[rep(which(typ_ev$evid == 0L)[1], length(fine)), ]
extra$time <- fine
extra$RRT_HEMODIAL_ACTIVE <- hd_active(fine)
typ_ev <- rbind(typ_ev, extra)
typ_ev <- typ_ev[order(typ_ev$time, -typ_ev$evid), ]
typ <- rxode2::rxSolve(rxode2::zeroRe(ui), typ_ev,
returnType = "data.frame", addDosing = FALSE)
#> ℹ omega/sigma items treated as zero: 'etalvc', 'etalcl', 'etalcl_taz', 'etalvp', 'etalq'
sess <- typ[typ$time >= s_last & typ$time <= s_last + HD_DUR, ]
sess <- sess[order(sess$time), ]
sess <- sess[!duplicated(sess$time), ]
stopifnot(nrow(sess) > 100)
trap <- function(tm, v) sum(diff(tm) * (utils::head(v, -1) + utils::tail(v, -1)) / 2)
mb <- lapply(
list(
Piperacillin = list(a = sess$central + sess$peripheral1,
cl = sess$cl + sess$cl_dialysis,
cdia = sess$cl_dialysis, conc = sess$Cc),
Tazobactam = list(a = sess$central_taz + sess$peripheral1_taz,
cl = sess$cl_taz + sess$cl_dialysis_taz,
cdia = sess$cl_dialysis_taz, conc = sess$Cc_taz)
),
function(z) {
lost_state <- utils::head(z$a, 1) - utils::tail(z$a, 1)
lost_flux <- trap(sess$time, z$cl * z$conc)
data.frame(
`Amount lost from the body (mg)` = signif(lost_state, 5),
`Integral of CL x Cc dt (mg)` = signif(lost_flux, 5),
`Relative discrepancy` = signif(lost_flux / lost_state - 1, 3),
`Dialyser share of elimination` = signif(
trap(sess$time, z$cdia * z$conc) / lost_flux, 3),
check.names = FALSE
)
}
)
mb <- cbind(Analyte = names(mb), do.call(rbind, mb))
rownames(mb) <- NULL
knitr::kable(mb, caption = "Mass balance across one 4-hour dialysis session, typical patient.")| Analyte | Amount lost from the body (mg) | Integral of CL x Cc dt (mg) | Relative discrepancy | Dialyser share of elimination |
|---|---|---|---|---|
| Piperacillin | 3728.80 | 3727.20 | -0.000429 | 0.826 |
| Tazobactam | 618.24 | 617.96 | -0.000445 | 0.881 |
# Mass balance must close to better than 0.1% on this grid.
stopifnot(all(abs(mb$`Relative discrepancy`) < 1e-3))
# The dialyser dominates elimination while a session runs: CL_DIA is 8.6 L/h
# (piperacillin) against an endogenous CL of ~1.5 L/h, so it must account for
# the large majority of drug removed during the session.
stopifnot(all(mb$`Dialyser share of elimination` > 0.75))
entry <- sess[which.min(sess$time), ]
exitc <- sess[which.max(sess$time), ]
data.frame(
Analyte = c("Piperacillin", "Tazobactam"),
`Session entry (mg/L)` = signif(c(entry$Cc, entry$Cc_taz), 4),
`Session exit (mg/L)` = signif(c(exitc$Cc, exitc$Cc_taz), 4),
`Decline factor` = round(c(entry$Cc / exitc$Cc, entry$Cc_taz / exitc$Cc_taz), 2),
`Published trough ratio (off / on)` = round(c(119.8 / 25.7, 18.5 / 4.1), 2),
check.names = FALSE
) |>
knitr::kable(caption = "Fractional decline across one session under the assumed schedule (descriptive, not gated).")| Analyte | Session entry (mg/L) | Session exit (mg/L) | Decline factor | Published trough ratio (off / on) |
|---|---|---|---|---|
| Piperacillin | 161.0 | 29.950 | 5.37 | 4.66 |
| Tazobactam | 24.1 | 4.125 | 5.84 | 4.51 |
Assumptions and deviations
-
Cross-drug residual correlation not implemented.
Kong 2025 estimates
rho = 95.0%(95% CI 91.4-97.1) between the piperacillin and tazobactam residuals, encoded in the ESM asTRE = (EPS(1)*RHO + EPS(2)*SQRT(1-RHO*RHO)) * TSD.rxode2/nlmixr2have no cross-endpoint residual correlation for a combined proportional-plus-additive error model, so the two residuals are declared independently and the correlation is recorded here. This is the same treatmentSoto_2014_ampicillin_sulbactamapplies to its ownrho = 0.946. Consequence: simulated observations (sim) will show independent piperacillin and tazobactam residual deviations where the source model would produce highly concordant ones. Individual predictions (Cc,Cc_taz), and therefore every comparison in this vignette, are unaffected. -
Residual diuresis is reconstructed as a volume
threshold. Kong 2025 reports residual diuresis only as a binary
present/absent status (11/20 present) and gates it in the ESM as
IF(UVOL0123.NE.0); no urine volumes are published. The canonical covariate columnURINE_VOL_24Hcarries a volume in mL/24h, so the model uses the register’s anuria cutoff,URINE_VOL_24H > 100, following theHuppe_2023_fosfomycinprecedent. The 100 mL/24h threshold is not a Kong 2025 value. Any positive value above 100 mL/24h reproduces the paper’s “residual diuresis present” branch, and any value at or below 100 (including 0) reproduces the “absent” branch. -
RRT_HEMODIAL_STATUSis an inferred column. Every subject in the fitted dataset had end-stage kidney disease, so the source data carries no such indicator; the ESM appliesCLCKDunconditionally and encodes “F_SCR = 1” via aCR = 0sentinel. The packaged model instead implements the published Eq. 2 gate explicitly onRRT_HEMODIAL_STATUSso that setting it to 0 recovers the general-purpose parent model for a patient with normal kidney function – which is exactly what Kong 2025 does to produce the normal-kidney-function comparator arm of Table 4 and Figures 3-4. -
The normal-kidney-function comparator arm is not reproduced
numerically. Kong 2025 built that arm by drawing serum
creatinine from a normal distribution and accepting only samples with a
Cockcroft-Gault eGFR above 40 mL/min. The distribution parameters come
from the general-purpose parent cohort and are not reported here, so the
arm cannot be reconstructed from this paper alone. The model supports it
(set
RRT_HEMODIAL_STATUS = 0,RRT_HEMODIAL_ACTIVE = 0and supplyCREAT), and the cohort builder above setsCREATto its age-standardised Eq. 1 value in that case, which makesF_SCR = 1and so reproducesCL_normalas Eq. 4 defines it. The Table 4 comparison is restricted to the two ESKD arms, which are the arms this paper parameterises. - Cohort covariate distributions are assumed. Table 1 reports medians and ranges, not the means and standard deviations the simulation section says were used. The published median is used as the mean and one quarter of the observed range as the standard deviation, truncated to the observed minimum and maximum. Age in particular is left-skewed (median 71.5, range 20-84), so using the median as the mean makes the virtual cohort slightly older – and therefore slightly lower-clearance – than the paper’s. This is the most likely source of the residual few-percent positive bias in the mean and trough comparisons above.
-
Dialysis session timing within the dosing interval is
assumed, and the during-dialysis trough is not reproducible.
Methods 2.5 gives the schedule (thrice weekly, 4-hour sessions) but not
where each session falls relative to the dosing interval. Sessions here
begin 4 h after a maintenance dose, the only placement that keeps a
whole 4-hour session inside a single 12-hour interval – so no dose is
infused into a running dialyser and the supplementary post-dialysis dose
never collides with a maintenance dose. Placements from 0 to 8 h after
the dose were examined: the weekly mean, the peak and the dialysis-off
trough moved by only a few percent, but the during-dialysis trough moved
by more than a factor of two. The published during-dialysis troughs
imply a session ending at or very near a pre-dose time, and that
placement makes the supplementary dose coincide with a maintenance dose
and inflates the peak past its published value, so no placement
reproduces all four Table 4 columns at once. The metric is reported but
not gated; what is gated instead is the schedule-independent mass
balance across a session and the exact
CL_DIA = ER x BFRidentity. - Infusion duration is assumed to be 30 minutes. Kong 2025 simulated “current dosing recommendations (i.e. infusion rate and dose)” from the EMA SmPC and the FDA label but does not print the infusion duration; the SmPC 30-minute infusion is used. The study cohort itself received much longer infusions (median 2.85 h, range 0.25-24.50), so this assumption affects the peak concentration more than any other metric.
-
MAT50uses the ESM code value. Table 3 prints 54.2 weeks; the ESM$THETA(10) = 0.0358back-transforms to 1.0364 years (54.1 weeks). The code value is used because it is what was fitted. Immaterial here: the maturation ratio is 1 to within machine precision across an all-adult cohort. -
The dialyser priming volume is computed but unused in the
source model. The ESM
$PKblock computesV_DIA = KNVOL/1000but never references it in$DESor$ERROR– a remnant of a dialyser-compartment parameterisation the authors report they did not adopt. It is recorded incovariatesDataExcludedrather thancovariateData, alongside dialysis modality, which was screened as a covariate on the extraction ratio and not retained. - Vascular access affects tazobactam only. The analogous effect on the piperacillin extraction ratio was added in forward inclusion (dOFV -8.02, p = 0.0183) but removed in backward elimination (dOFV +11.26, p < 0.001), so piperacillin carries a single 64.0% extraction ratio. Kong 2025 judged the tazobactam access-type differences statistically significant but “not expected to be clinically relevant”.
-
Single-needle blood flow correction. For
single-needle arteriovenous fistula sessions Kong 2025 supplied a
time-averaged blood flow rate to correct for recirculation
rather than the nominal pump setting (Methods 2.4). A user reproducing
the analysis should apply the same correction to
BFRwhenVASCACC_AVF1N = 1; the virtual cohort above uses the nominal distribution for all access types. -
New canonical names introduced by this extraction.
tazis registered as a sibling-drug suffix ininst/references/compartment-names.md(parallel to the existingsbtentry), andVASCACC_AVF1N/VASCACC_AVF2Nare registered ininst/references/covariate-columns.mdas a decomposed binary pair with tunnelled dialysis catheter as the implicit reference. The parameter nameslf_eskd,logitedia,tdec50andhill_decfollow established in-library conventions (logiteh/ehfor an extraction ratio,tmat50/hill_matfor the maturation sigmoid) but are not themselves register entries;inst/references/parameter-names.mdgates covariate and compartment names, not parameter names.