Teicoplanin (Garcia-Hervalejo 2026)
Source:vignettes/articles/GarciaHervalejo_2026_teicoplanin.Rmd
GarciaHervalejo_2026_teicoplanin.RmdModel and source
- Citation: Garcia-Hervalejo M, Sanchez-Hernandez JG, Conde-Gonzalez I, Avendano Pita A, Otero MJ. Population Pharmacokinetics and Model-Informed Dose Optimization of Teicoplanin in Adults with Hematological Malignancies. Pharmaceutics. 2026;18(1):100. doi:10.3390/pharmaceutics18010100
- Description: One-compartment IV-infusion population PK model for teicoplanin in 100 adult patients with haematological malignancies treated for febrile neutropenia at a single Spanish centre (Garcia-Hervalejo 2026). Clearance carries three covariates, all printed in the paper’s Equation 1: CL (L/h) = 1.28 * (1 + 0.012 * (AGE - 62)) * (eGFR / 92.15)^0.35 * (IBW / 61)^3.2. IDEAL body weight – not total, adjusted, BMI or BSA – was the anthropometric descriptor that best explained clearance variability, and its exponent of 3.2 is far steeper than any allometric value: clearance spans 0.33 to 3.05 L/h across IBW 40 to 80 kg at otherwise typical covariates, a 9-fold range that dominates the model and drives every dosing conclusion the paper reaches. Between-subject variability is exponential on CL and Vc; residual error is purely additive at 2.61 mg/L, the paper having tested and rejected proportional and combined forms because the observed concentrations are almost all troughs over a narrow range. The dataset is trough-only therapeutic drug monitoring, so a two-compartment model was evaluated but not identifiable (dOFV about 0.2, distribution-parameter RSE over 100%) and the parsimonious one-compartment structure was retained. The paper’s Monte Carlo simulations conclude that conventional regimens (6 mg/kg or 600 mg q12h x 3 then once daily) rarely reach a trough of 15-20 mg/L, and propose an intensified five-dose 12 mg/kg q12h loading phase followed by 12 mg/kg once daily.
- Article: https://doi.org/10.3390/pharmaceutics18010100
Teicoplanin is a glycopeptide used empirically and as targeted therapy against resistant Gram-positive organisms in febrile neutropenia. Garcia-Hervalejo and colleagues fitted a one-compartment model with first-order elimination to routine therapeutic-drug-monitoring data from 100 adults with haematological malignancies at the University Hospital of Salamanca, then used it to argue that conventional dosing does not reach the 15-20 mg/L trough target quickly enough in this population.
The single most consequential feature of the model is the ideal-body-weight exponent of 3.2 on clearance. That is not an allometric exponent – it is over four times the classical 0.75 – and it makes clearance vary roughly nine-fold across the ideal body weights the paper simulates. Every dosing conclusion in the paper follows from it, so most of this vignette is spent checking that the packaged encoding reproduces those conclusions.
Population
The development cohort was 100 hospitalised adults (40% female, median age 62 years, range 18-85) with malignant haematological neoplasms receiving intravenous teicoplanin for febrile neutropenia between February 2021 and December 2023 (source Table 1). Median total body weight was 68 kg (41-127) and median Devine ideal body weight 61.4 kg (46.25-77). Renal function was preserved to augmented – median CKD-EPI eGFR 92.15 mL/min/1.73 m^2 (26-141) – with end-stage renal disease and renal replacement therapy excluded by design. The population shows the biochemical signature of the setting: hypoalbuminaemia (median 3.3 g/dL), anaemia (median haemoglobin 9.2 g/dL) and profound thrombocytopenia (median platelets 36,500/uL). Diagnoses were non-Hodgkin’s lymphoma 20%, acute myeloblastic leukaemia 20%, multiple myeloma 20%, acute lymphoblastic leukaemia 9% and other haematological malignancies 31%.
All patients started on 600 mg every 12 h by 30-minute intravenous infusion, with later doses individualised by therapeutic drug monitoring. Sampling was trough-dominated: the first sample at 24 h, then every 72-96 h, “always immediately before dose administration”. This is why the model is one-compartment – with no sample in the distribution phase a two-compartment model was not identifiable (dOFV about 0.2, distribution-parameter RSE over 100%) – and it is the main constraint on reuse. The model describes troughs and cumulative exposure; it should not be used to predict peak or early post-infusion concentrations. A further 51 patients contributing 95 concentrations formed an external validation cohort.
The same information is available programmatically via the model’s
population metadata
(readModelDb("GarciaHervalejo_2026_teicoplanin")()$population).
Source trace
The per-parameter origin is recorded as an in-file comment next to
each ini() entry in
inst/modeldb/specificDrugs/GarciaHervalejo_2026_teicoplanin.R.
The table below collects them in one place for review.
| Equation / parameter | Value | Source location |
|---|---|---|
lcl (CL reference) |
1.28 L/h | Table 2, row CLpop (L/h); leading constant of Equation
1 |
lvc (Vd) |
92.10 L | Table 2, row Vpop (L)
|
e_ibw_cl |
3.20 | Table 2, row IBW-CL; (IBW/61) exponent of
Equation 1 |
e_crcl_cl |
0.35 | Table 2, row eGFR-CL; (eGFR/92.15)
exponent of Equation 1 |
e_age_cl |
0.012 /year | Equation 1 (Table 2 prints 0.01 at two-decimal precision – see Errata) |
etalcl |
0.110003 | Table 2, IIVCL (CV, %) = 34.10 ->
log(1 + 0.341^2)
|
etalvc |
0.091758 | Table 2, IIVV (CV, %) = 31.0 ->
log(1 + 0.310^2)
|
addSd |
2.61 mg/L | Table 2, row RUVadi (mg/L)
|
| CL covariate model | n/a | Equation 1, Results 3.2; restated in Results 3.2 prose |
| One compartment, first-order elimination | n/a | Results 3.2; Methods 2.3 |
| Additive-only residual error | n/a | Results 3.2 (proportional and combined forms tested and rejected) |
| IBW reference 61 kg, eGFR reference 92.15, age centring 62 y | n/a | Equation 1; cohort medians in Table 1 |
Equation 1 as printed in the source:
Structural gate: the packaged encoding against the printed equation
Before any simulation, check that the packaged model computes the
clearance the paper printed. cl_published() below is a
literal, independent transcription of Equation 1 from the
source’s own constants – it does not read anything from the model file –
so a disagreement means the encoding is wrong.
mod <- readModelDb("GarciaHervalejo_2026_teicoplanin")
# Independent transcription of Garcia-Hervalejo 2026 Equation 1.
cl_published <- function(IBW, eGFR, AGE) {
1.28 * (1 + 0.012 * (AGE - 62)) * (eGFR / 92.15)^0.35 * (IBW / 61)^3.2
}
V_published <- 92.10
# A grid spanning the observed covariate ranges (Table 1).
grid <- tidyr::crossing(
IBW = c(46.25, 55, 61, 70, 77),
CRCL = c(26, 60, 92.15, 120, 141),
AGE = c(18, 40, 62, 75, 85)
) |>
dplyr::mutate(id = dplyr::row_number())
gate_ev <- grid |>
dplyr::mutate(time = 0, amt = 600, evid = 1, cmt = "central", dur = 0.5)
gate <- rxode2::rxSolve(
rxode2::zeroRe(mod), gate_ev,
keep = c("IBW", "CRCL", "AGE")
) |>
as.data.frame() |>
dplyr::distinct(id, .keep_all = TRUE) |>
dplyr::mutate(cl_ref = cl_published(IBW, CRCL, AGE))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> Warning: multi-subject simulation without without 'omega'
#> Warning: column 'IBW' has only 'NA' values for id '1'
#> Warning: column 'CRCL' has only 'NA' values for id '1'
#> Warning: column 'AGE' has only 'NA' values for id '1'
#> Warning: column 'IBW' has only 'NA' values for id '2'
#> Warning: column 'CRCL' has only 'NA' values for id '2'
#> Warning: column 'AGE' has only 'NA' values for id '2'
#> Warning: column 'IBW' has only 'NA' values for id '3'
#> Warning: column 'CRCL' has only 'NA' values for id '3'
#> Warning: column 'AGE' has only 'NA' values for id '3'
#> Warning: column 'IBW' has only 'NA' values for id '4'
#> Warning: column 'CRCL' has only 'NA' values for id '4'
#> Warning: column 'AGE' has only 'NA' values for id '4'
#> Warning: column 'IBW' has only 'NA' values for id '5'
#> Warning: column 'CRCL' has only 'NA' values for id '5'
#> Warning: column 'AGE' has only 'NA' values for id '5'
#> Warning: column 'IBW' has only 'NA' values for id '6'
#> Warning: column 'CRCL' has only 'NA' values for id '6'
#> Warning: column 'AGE' has only 'NA' values for id '6'
#> Warning: column 'IBW' has only 'NA' values for id '7'
#> Warning: column 'CRCL' has only 'NA' values for id '7'
#> Warning: column 'AGE' has only 'NA' values for id '7'
#> Warning: column 'IBW' has only 'NA' values for id '8'
#> Warning: column 'CRCL' has only 'NA' values for id '8'
#> Warning: column 'AGE' has only 'NA' values for id '8'
#> Warning: column 'IBW' has only 'NA' values for id '9'
#> Warning: column 'CRCL' has only 'NA' values for id '9'
#> Warning: column 'AGE' has only 'NA' values for id '9'
#> Warning: column 'IBW' has only 'NA' values for id '10'
#> Warning: column 'CRCL' has only 'NA' values for id '10'
#> Warning: column 'AGE' has only 'NA' values for id '10'
#> Warning: column 'IBW' has only 'NA' values for id '11'
#> Warning: column 'CRCL' has only 'NA' values for id '11'
#> Warning: column 'AGE' has only 'NA' values for id '11'
#> Warning: column 'IBW' has only 'NA' values for id '12'
#> Warning: column 'CRCL' has only 'NA' values for id '12'
#> Warning: column 'AGE' has only 'NA' values for id '12'
#> Warning: column 'IBW' has only 'NA' values for id '13'
#> Warning: column 'CRCL' has only 'NA' values for id '13'
#> Warning: column 'AGE' has only 'NA' values for id '13'
#> Warning: column 'IBW' has only 'NA' values for id '14'
#> Warning: column 'CRCL' has only 'NA' values for id '14'
#> Warning: column 'AGE' has only 'NA' values for id '14'
#> Warning: column 'IBW' has only 'NA' values for id '15'
#> Warning: column 'CRCL' has only 'NA' values for id '15'
#> Warning: column 'AGE' has only 'NA' values for id '15'
#> Warning: column 'IBW' has only 'NA' values for id '16'
#> Warning: column 'CRCL' has only 'NA' values for id '16'
#> Warning: column 'AGE' has only 'NA' values for id '16'
#> Warning: column 'IBW' has only 'NA' values for id '17'
#> Warning: column 'CRCL' has only 'NA' values for id '17'
#> Warning: column 'AGE' has only 'NA' values for id '17'
#> Warning: column 'IBW' has only 'NA' values for id '18'
#> Warning: column 'CRCL' has only 'NA' values for id '18'
#> Warning: column 'AGE' has only 'NA' values for id '18'
#> Warning: column 'IBW' has only 'NA' values for id '19'
#> Warning: column 'CRCL' has only 'NA' values for id '19'
#> Warning: column 'AGE' has only 'NA' values for id '19'
#> Warning: column 'IBW' has only 'NA' values for id '20'
#> Warning: column 'CRCL' has only 'NA' values for id '20'
#> Warning: column 'AGE' has only 'NA' values for id '20'
#> Warning: column 'IBW' has only 'NA' values for id '21'
#> Warning: column 'CRCL' has only 'NA' values for id '21'
#> Warning: column 'AGE' has only 'NA' values for id '21'
#> Warning: column 'IBW' has only 'NA' values for id '22'
#> Warning: column 'CRCL' has only 'NA' values for id '22'
#> Warning: column 'AGE' has only 'NA' values for id '22'
#> Warning: column 'IBW' has only 'NA' values for id '23'
#> Warning: column 'CRCL' has only 'NA' values for id '23'
#> Warning: column 'AGE' has only 'NA' values for id '23'
#> Warning: column 'IBW' has only 'NA' values for id '24'
#> Warning: column 'CRCL' has only 'NA' values for id '24'
#> Warning: column 'AGE' has only 'NA' values for id '24'
#> Warning: column 'IBW' has only 'NA' values for id '25'
#> Warning: column 'CRCL' has only 'NA' values for id '25'
#> Warning: column 'AGE' has only 'NA' values for id '25'
#> Warning: column 'IBW' has only 'NA' values for id '26'
#> Warning: column 'CRCL' has only 'NA' values for id '26'
#> Warning: column 'AGE' has only 'NA' values for id '26'
#> Warning: column 'IBW' has only 'NA' values for id '27'
#> Warning: column 'CRCL' has only 'NA' values for id '27'
#> Warning: column 'AGE' has only 'NA' values for id '27'
#> Warning: column 'IBW' has only 'NA' values for id '28'
#> Warning: column 'CRCL' has only 'NA' values for id '28'
#> Warning: column 'AGE' has only 'NA' values for id '28'
#> Warning: column 'IBW' has only 'NA' values for id '29'
#> Warning: column 'CRCL' has only 'NA' values for id '29'
#> Warning: column 'AGE' has only 'NA' values for id '29'
#> Warning: column 'IBW' has only 'NA' values for id '30'
#> Warning: column 'CRCL' has only 'NA' values for id '30'
#> Warning: column 'AGE' has only 'NA' values for id '30'
#> Warning: column 'IBW' has only 'NA' values for id '31'
#> Warning: column 'CRCL' has only 'NA' values for id '31'
#> Warning: column 'AGE' has only 'NA' values for id '31'
#> Warning: column 'IBW' has only 'NA' values for id '32'
#> Warning: column 'CRCL' has only 'NA' values for id '32'
#> Warning: column 'AGE' has only 'NA' values for id '32'
#> Warning: column 'IBW' has only 'NA' values for id '33'
#> Warning: column 'CRCL' has only 'NA' values for id '33'
#> Warning: column 'AGE' has only 'NA' values for id '33'
#> Warning: column 'IBW' has only 'NA' values for id '34'
#> Warning: column 'CRCL' has only 'NA' values for id '34'
#> Warning: column 'AGE' has only 'NA' values for id '34'
#> Warning: column 'IBW' has only 'NA' values for id '35'
#> Warning: column 'CRCL' has only 'NA' values for id '35'
#> Warning: column 'AGE' has only 'NA' values for id '35'
#> Warning: column 'IBW' has only 'NA' values for id '36'
#> Warning: column 'CRCL' has only 'NA' values for id '36'
#> Warning: column 'AGE' has only 'NA' values for id '36'
#> Warning: column 'IBW' has only 'NA' values for id '37'
#> Warning: column 'CRCL' has only 'NA' values for id '37'
#> Warning: column 'AGE' has only 'NA' values for id '37'
#> Warning: column 'IBW' has only 'NA' values for id '38'
#> Warning: column 'CRCL' has only 'NA' values for id '38'
#> Warning: column 'AGE' has only 'NA' values for id '38'
#> Warning: column 'IBW' has only 'NA' values for id '39'
#> Warning: column 'CRCL' has only 'NA' values for id '39'
#> Warning: column 'AGE' has only 'NA' values for id '39'
#> Warning: column 'IBW' has only 'NA' values for id '40'
#> Warning: column 'CRCL' has only 'NA' values for id '40'
#> Warning: column 'AGE' has only 'NA' values for id '40'
#> Warning: column 'IBW' has only 'NA' values for id '41'
#> Warning: column 'CRCL' has only 'NA' values for id '41'
#> Warning: column 'AGE' has only 'NA' values for id '41'
#> Warning: column 'IBW' has only 'NA' values for id '42'
#> Warning: column 'CRCL' has only 'NA' values for id '42'
#> Warning: column 'AGE' has only 'NA' values for id '42'
#> Warning: column 'IBW' has only 'NA' values for id '43'
#> Warning: column 'CRCL' has only 'NA' values for id '43'
#> Warning: column 'AGE' has only 'NA' values for id '43'
#> Warning: column 'IBW' has only 'NA' values for id '44'
#> Warning: column 'CRCL' has only 'NA' values for id '44'
#> Warning: column 'AGE' has only 'NA' values for id '44'
#> Warning: column 'IBW' has only 'NA' values for id '45'
#> Warning: column 'CRCL' has only 'NA' values for id '45'
#> Warning: column 'AGE' has only 'NA' values for id '45'
#> Warning: column 'IBW' has only 'NA' values for id '46'
#> Warning: column 'CRCL' has only 'NA' values for id '46'
#> Warning: column 'AGE' has only 'NA' values for id '46'
#> Warning: column 'IBW' has only 'NA' values for id '47'
#> Warning: column 'CRCL' has only 'NA' values for id '47'
#> Warning: column 'AGE' has only 'NA' values for id '47'
#> Warning: column 'IBW' has only 'NA' values for id '48'
#> Warning: column 'CRCL' has only 'NA' values for id '48'
#> Warning: column 'AGE' has only 'NA' values for id '48'
#> Warning: column 'IBW' has only 'NA' values for id '49'
#> Warning: column 'CRCL' has only 'NA' values for id '49'
#> Warning: column 'AGE' has only 'NA' values for id '49'
#> Warning: column 'IBW' has only 'NA' values for id '50'
#> Warning: column 'CRCL' has only 'NA' values for id '50'
#> Warning: column 'AGE' has only 'NA' values for id '50'
#> Warning: column 'IBW' has only 'NA' values for id '51'
#> Warning: column 'CRCL' has only 'NA' values for id '51'
#> Warning: column 'AGE' has only 'NA' values for id '51'
#> Warning: column 'IBW' has only 'NA' values for id '52'
#> Warning: column 'CRCL' has only 'NA' values for id '52'
#> Warning: column 'AGE' has only 'NA' values for id '52'
#> Warning: column 'IBW' has only 'NA' values for id '53'
#> Warning: column 'CRCL' has only 'NA' values for id '53'
#> Warning: column 'AGE' has only 'NA' values for id '53'
#> Warning: column 'IBW' has only 'NA' values for id '54'
#> Warning: column 'CRCL' has only 'NA' values for id '54'
#> Warning: column 'AGE' has only 'NA' values for id '54'
#> Warning: column 'IBW' has only 'NA' values for id '55'
#> Warning: column 'CRCL' has only 'NA' values for id '55'
#> Warning: column 'AGE' has only 'NA' values for id '55'
#> Warning: column 'IBW' has only 'NA' values for id '56'
#> Warning: column 'CRCL' has only 'NA' values for id '56'
#> Warning: column 'AGE' has only 'NA' values for id '56'
#> Warning: column 'IBW' has only 'NA' values for id '57'
#> Warning: column 'CRCL' has only 'NA' values for id '57'
#> Warning: column 'AGE' has only 'NA' values for id '57'
#> Warning: column 'IBW' has only 'NA' values for id '58'
#> Warning: column 'CRCL' has only 'NA' values for id '58'
#> Warning: column 'AGE' has only 'NA' values for id '58'
#> Warning: column 'IBW' has only 'NA' values for id '59'
#> Warning: column 'CRCL' has only 'NA' values for id '59'
#> Warning: column 'AGE' has only 'NA' values for id '59'
#> Warning: column 'IBW' has only 'NA' values for id '60'
#> Warning: column 'CRCL' has only 'NA' values for id '60'
#> Warning: column 'AGE' has only 'NA' values for id '60'
#> Warning: column 'IBW' has only 'NA' values for id '61'
#> Warning: column 'CRCL' has only 'NA' values for id '61'
#> Warning: column 'AGE' has only 'NA' values for id '61'
#> Warning: column 'IBW' has only 'NA' values for id '62'
#> Warning: column 'CRCL' has only 'NA' values for id '62'
#> Warning: column 'AGE' has only 'NA' values for id '62'
#> Warning: column 'IBW' has only 'NA' values for id '63'
#> Warning: column 'CRCL' has only 'NA' values for id '63'
#> Warning: column 'AGE' has only 'NA' values for id '63'
#> Warning: column 'IBW' has only 'NA' values for id '64'
#> Warning: column 'CRCL' has only 'NA' values for id '64'
#> Warning: column 'AGE' has only 'NA' values for id '64'
#> Warning: column 'IBW' has only 'NA' values for id '65'
#> Warning: column 'CRCL' has only 'NA' values for id '65'
#> Warning: column 'AGE' has only 'NA' values for id '65'
#> Warning: column 'IBW' has only 'NA' values for id '66'
#> Warning: column 'CRCL' has only 'NA' values for id '66'
#> Warning: column 'AGE' has only 'NA' values for id '66'
#> Warning: column 'IBW' has only 'NA' values for id '67'
#> Warning: column 'CRCL' has only 'NA' values for id '67'
#> Warning: column 'AGE' has only 'NA' values for id '67'
#> Warning: column 'IBW' has only 'NA' values for id '68'
#> Warning: column 'CRCL' has only 'NA' values for id '68'
#> Warning: column 'AGE' has only 'NA' values for id '68'
#> Warning: column 'IBW' has only 'NA' values for id '69'
#> Warning: column 'CRCL' has only 'NA' values for id '69'
#> Warning: column 'AGE' has only 'NA' values for id '69'
#> Warning: column 'IBW' has only 'NA' values for id '70'
#> Warning: column 'CRCL' has only 'NA' values for id '70'
#> Warning: column 'AGE' has only 'NA' values for id '70'
#> Warning: column 'IBW' has only 'NA' values for id '71'
#> Warning: column 'CRCL' has only 'NA' values for id '71'
#> Warning: column 'AGE' has only 'NA' values for id '71'
#> Warning: column 'IBW' has only 'NA' values for id '72'
#> Warning: column 'CRCL' has only 'NA' values for id '72'
#> Warning: column 'AGE' has only 'NA' values for id '72'
#> Warning: column 'IBW' has only 'NA' values for id '73'
#> Warning: column 'CRCL' has only 'NA' values for id '73'
#> Warning: column 'AGE' has only 'NA' values for id '73'
#> Warning: column 'IBW' has only 'NA' values for id '74'
#> Warning: column 'CRCL' has only 'NA' values for id '74'
#> Warning: column 'AGE' has only 'NA' values for id '74'
#> Warning: column 'IBW' has only 'NA' values for id '75'
#> Warning: column 'CRCL' has only 'NA' values for id '75'
#> Warning: column 'AGE' has only 'NA' values for id '75'
#> Warning: column 'IBW' has only 'NA' values for id '76'
#> Warning: column 'CRCL' has only 'NA' values for id '76'
#> Warning: column 'AGE' has only 'NA' values for id '76'
#> Warning: column 'IBW' has only 'NA' values for id '77'
#> Warning: column 'CRCL' has only 'NA' values for id '77'
#> Warning: column 'AGE' has only 'NA' values for id '77'
#> Warning: column 'IBW' has only 'NA' values for id '78'
#> Warning: column 'CRCL' has only 'NA' values for id '78'
#> Warning: column 'AGE' has only 'NA' values for id '78'
#> Warning: column 'IBW' has only 'NA' values for id '79'
#> Warning: column 'CRCL' has only 'NA' values for id '79'
#> Warning: column 'AGE' has only 'NA' values for id '79'
#> Warning: column 'IBW' has only 'NA' values for id '80'
#> Warning: column 'CRCL' has only 'NA' values for id '80'
#> Warning: column 'AGE' has only 'NA' values for id '80'
#> Warning: column 'IBW' has only 'NA' values for id '81'
#> Warning: column 'CRCL' has only 'NA' values for id '81'
#> Warning: column 'AGE' has only 'NA' values for id '81'
#> Warning: column 'IBW' has only 'NA' values for id '82'
#> Warning: column 'CRCL' has only 'NA' values for id '82'
#> Warning: column 'AGE' has only 'NA' values for id '82'
#> Warning: column 'IBW' has only 'NA' values for id '83'
#> Warning: column 'CRCL' has only 'NA' values for id '83'
#> Warning: column 'AGE' has only 'NA' values for id '83'
#> Warning: column 'IBW' has only 'NA' values for id '84'
#> Warning: column 'CRCL' has only 'NA' values for id '84'
#> Warning: column 'AGE' has only 'NA' values for id '84'
#> Warning: column 'IBW' has only 'NA' values for id '85'
#> Warning: column 'CRCL' has only 'NA' values for id '85'
#> Warning: column 'AGE' has only 'NA' values for id '85'
#> Warning: column 'IBW' has only 'NA' values for id '86'
#> Warning: column 'CRCL' has only 'NA' values for id '86'
#> Warning: column 'AGE' has only 'NA' values for id '86'
#> Warning: column 'IBW' has only 'NA' values for id '87'
#> Warning: column 'CRCL' has only 'NA' values for id '87'
#> Warning: column 'AGE' has only 'NA' values for id '87'
#> Warning: column 'IBW' has only 'NA' values for id '88'
#> Warning: column 'CRCL' has only 'NA' values for id '88'
#> Warning: column 'AGE' has only 'NA' values for id '88'
#> Warning: column 'IBW' has only 'NA' values for id '89'
#> Warning: column 'CRCL' has only 'NA' values for id '89'
#> Warning: column 'AGE' has only 'NA' values for id '89'
#> Warning: column 'IBW' has only 'NA' values for id '90'
#> Warning: column 'CRCL' has only 'NA' values for id '90'
#> Warning: column 'AGE' has only 'NA' values for id '90'
#> Warning: column 'IBW' has only 'NA' values for id '91'
#> Warning: column 'CRCL' has only 'NA' values for id '91'
#> Warning: column 'AGE' has only 'NA' values for id '91'
#> Warning: column 'IBW' has only 'NA' values for id '92'
#> Warning: column 'CRCL' has only 'NA' values for id '92'
#> Warning: column 'AGE' has only 'NA' values for id '92'
#> Warning: column 'IBW' has only 'NA' values for id '93'
#> Warning: column 'CRCL' has only 'NA' values for id '93'
#> Warning: column 'AGE' has only 'NA' values for id '93'
#> Warning: column 'IBW' has only 'NA' values for id '94'
#> Warning: column 'CRCL' has only 'NA' values for id '94'
#> Warning: column 'AGE' has only 'NA' values for id '94'
#> Warning: column 'IBW' has only 'NA' values for id '95'
#> Warning: column 'CRCL' has only 'NA' values for id '95'
#> Warning: column 'AGE' has only 'NA' values for id '95'
#> Warning: column 'IBW' has only 'NA' values for id '96'
#> Warning: column 'CRCL' has only 'NA' values for id '96'
#> Warning: column 'AGE' has only 'NA' values for id '96'
#> Warning: column 'IBW' has only 'NA' values for id '97'
#> Warning: column 'CRCL' has only 'NA' values for id '97'
#> Warning: column 'AGE' has only 'NA' values for id '97'
#> Warning: column 'IBW' has only 'NA' values for id '98'
#> Warning: column 'CRCL' has only 'NA' values for id '98'
#> Warning: column 'AGE' has only 'NA' values for id '98'
#> Warning: column 'IBW' has only 'NA' values for id '99'
#> Warning: column 'CRCL' has only 'NA' values for id '99'
#> Warning: column 'AGE' has only 'NA' values for id '99'
#> Warning: column 'IBW' has only 'NA' values for id '100'
#> Warning: column 'CRCL' has only 'NA' values for id '100'
#> Warning: column 'AGE' has only 'NA' values for id '100'
#> Warning: column 'IBW' has only 'NA' values for id '101'
#> Warning: column 'CRCL' has only 'NA' values for id '101'
#> Warning: column 'AGE' has only 'NA' values for id '101'
#> Warning: column 'IBW' has only 'NA' values for id '102'
#> Warning: column 'CRCL' has only 'NA' values for id '102'
#> Warning: column 'AGE' has only 'NA' values for id '102'
#> Warning: column 'IBW' has only 'NA' values for id '103'
#> Warning: column 'CRCL' has only 'NA' values for id '103'
#> Warning: column 'AGE' has only 'NA' values for id '103'
#> Warning: column 'IBW' has only 'NA' values for id '104'
#> Warning: column 'CRCL' has only 'NA' values for id '104'
#> Warning: column 'AGE' has only 'NA' values for id '104'
#> Warning: column 'IBW' has only 'NA' values for id '105'
#> Warning: column 'CRCL' has only 'NA' values for id '105'
#> Warning: column 'AGE' has only 'NA' values for id '105'
#> Warning: column 'IBW' has only 'NA' values for id '106'
#> Warning: column 'CRCL' has only 'NA' values for id '106'
#> Warning: column 'AGE' has only 'NA' values for id '106'
#> Warning: column 'IBW' has only 'NA' values for id '107'
#> Warning: column 'CRCL' has only 'NA' values for id '107'
#> Warning: column 'AGE' has only 'NA' values for id '107'
#> Warning: column 'IBW' has only 'NA' values for id '108'
#> Warning: column 'CRCL' has only 'NA' values for id '108'
#> Warning: column 'AGE' has only 'NA' values for id '108'
#> Warning: column 'IBW' has only 'NA' values for id '109'
#> Warning: column 'CRCL' has only 'NA' values for id '109'
#> Warning: column 'AGE' has only 'NA' values for id '109'
#> Warning: column 'IBW' has only 'NA' values for id '110'
#> Warning: column 'CRCL' has only 'NA' values for id '110'
#> Warning: column 'AGE' has only 'NA' values for id '110'
#> Warning: column 'IBW' has only 'NA' values for id '111'
#> Warning: column 'CRCL' has only 'NA' values for id '111'
#> Warning: column 'AGE' has only 'NA' values for id '111'
#> Warning: column 'IBW' has only 'NA' values for id '112'
#> Warning: column 'CRCL' has only 'NA' values for id '112'
#> Warning: column 'AGE' has only 'NA' values for id '112'
#> Warning: column 'IBW' has only 'NA' values for id '113'
#> Warning: column 'CRCL' has only 'NA' values for id '113'
#> Warning: column 'AGE' has only 'NA' values for id '113'
#> Warning: column 'IBW' has only 'NA' values for id '114'
#> Warning: column 'CRCL' has only 'NA' values for id '114'
#> Warning: column 'AGE' has only 'NA' values for id '114'
#> Warning: column 'IBW' has only 'NA' values for id '115'
#> Warning: column 'CRCL' has only 'NA' values for id '115'
#> Warning: column 'AGE' has only 'NA' values for id '115'
#> Warning: column 'IBW' has only 'NA' values for id '116'
#> Warning: column 'CRCL' has only 'NA' values for id '116'
#> Warning: column 'AGE' has only 'NA' values for id '116'
#> Warning: column 'IBW' has only 'NA' values for id '117'
#> Warning: column 'CRCL' has only 'NA' values for id '117'
#> Warning: column 'AGE' has only 'NA' values for id '117'
#> Warning: column 'IBW' has only 'NA' values for id '118'
#> Warning: column 'CRCL' has only 'NA' values for id '118'
#> Warning: column 'AGE' has only 'NA' values for id '118'
#> Warning: column 'IBW' has only 'NA' values for id '119'
#> Warning: column 'CRCL' has only 'NA' values for id '119'
#> Warning: column 'AGE' has only 'NA' values for id '119'
#> Warning: column 'IBW' has only 'NA' values for id '120'
#> Warning: column 'CRCL' has only 'NA' values for id '120'
#> Warning: column 'AGE' has only 'NA' values for id '120'
#> Warning: column 'IBW' has only 'NA' values for id '121'
#> Warning: column 'CRCL' has only 'NA' values for id '121'
#> Warning: column 'AGE' has only 'NA' values for id '121'
#> Warning: column 'IBW' has only 'NA' values for id '122'
#> Warning: column 'CRCL' has only 'NA' values for id '122'
#> Warning: column 'AGE' has only 'NA' values for id '122'
#> Warning: column 'IBW' has only 'NA' values for id '123'
#> Warning: column 'CRCL' has only 'NA' values for id '123'
#> Warning: column 'AGE' has only 'NA' values for id '123'
#> Warning: column 'IBW' has only 'NA' values for id '124'
#> Warning: column 'CRCL' has only 'NA' values for id '124'
#> Warning: column 'AGE' has only 'NA' values for id '124'
#> Warning: column 'IBW' has only 'NA' values for id '125'
#> Warning: column 'CRCL' has only 'NA' values for id '125'
#> Warning: column 'AGE' has only 'NA' values for id '125'
# Deterministic quantities: assert tightly.
stopifnot(
nrow(gate) == nrow(grid), # guard: the gate has rows to test
max(abs(gate$cl - gate$cl_ref)) < 1e-9,
max(abs(gate$vc - V_published)) < 1e-9
)
cat(sprintf(
"Encoding gate: %d covariate combinations, max |CL_model - CL_equation1| = %.2e L/h\n",
nrow(gate), max(abs(gate$cl - gate$cl_ref))
))
#> Encoding gate: 125 covariate combinations, max |CL_model - CL_equation1| = 8.88e-16 L/hThe covariate surface, for orientation. Note the vertical scale: the IBW term alone moves clearance from 0.26x to 2.38x its reference value across the 40-80 kg range the paper simulates, while the renal term spans only 0.64-1.16 across the entire observed eGFR range.
tidyr::crossing(IBW = seq(40, 85, by = 0.5), CRCL = c(30, 60, 92.15, 120, 141)) |>
dplyr::mutate(
CL = cl_published(IBW, CRCL, 62),
eGFR = factor(CRCL, labels = paste0("eGFR ", c(30, 60, 92.15, 120, 141)))
) |>
ggplot(aes(IBW, CL, colour = eGFR)) +
geom_line(linewidth = 0.8) +
geom_vline(xintercept = c(46.25, 77), linetype = "dotted") +
labs(
x = "Ideal body weight (kg)", y = "Typical clearance (L/h)",
colour = NULL,
title = "Clearance is dominated by ideal body weight, not renal function",
caption = paste(
"Age fixed at the cohort median of 62 y. Dotted lines mark the observed",
"IBW range (46.25-77 kg); beyond them the exponent 3.2 extrapolates."
)
) +
theme_bw() +
theme(legend.position = "bottom")
Mass-balance gate: AUC against Dose / CL
For a linear one-compartment model the identity
AUC(0, Inf) = Dose / CL is exact. Checking the ODE solve
and the PKNCA integration against it at once catches a mis-encoded
volume, a wrong infusion duration, or a unit slip.
auc_sub <- grid |> dplyr::slice(seq(1, dplyr::n(), by = 7)) # 18 covariate combinations
auc_ev <- dplyr::bind_rows(
auc_sub |>
dplyr::mutate(time = 0, amt = 600, evid = 1, cmt = "central", dur = 0.5),
auc_sub |>
tidyr::crossing(time = c(seq(0, 24, by = 0.25), seq(25, 3000, by = 6))) |>
dplyr::mutate(amt = NA_real_, evid = 0, cmt = "central", dur = NA_real_)
) |>
dplyr::arrange(id, time, dplyr::desc(evid))
auc_sim <- rxode2::rxSolve(
rxode2::zeroRe(mod), auc_ev,
keep = c("IBW", "CRCL", "AGE")
) |>
as.data.frame() |>
dplyr::filter(!is.na(Cc))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> Warning: multi-subject simulation without without 'omega'
stopifnot(all(auc_sim$Cc >= 0)) # no solver noise below zero before log()
auc_num <- auc_sim |>
dplyr::group_by(id, IBW, CRCL, AGE) |>
dplyr::summarise(
# trapezoidal AUC over the solved window, plus the analytic terminal tail
auc_obs = sum(diff(time) * (head(Cc, -1) + tail(Cc, -1)) / 2),
tail = dplyr::last(Cc) / (dplyr::first(cl) / dplyr::first(vc)),
cl = dplyr::first(cl),
.groups = "drop"
) |>
dplyr::mutate(
auc_total = auc_obs + tail,
auc_ident = 600 / cl,
pct_diff = 100 * (auc_total - auc_ident) / auc_ident
)
# Deterministic (zeroRe, no cohort draw): the only error is numerical
# integration, so a tight bound is correct here and should stay tight.
stopifnot(
nrow(auc_num) == nrow(auc_sub),
max(abs(auc_num$pct_diff)) < 0.5
)
cat(sprintf(
"Mass-balance gate: %d profiles, max |AUC(0,Inf) - Dose/CL| = %.3f%%\n",
nrow(auc_num), max(abs(auc_num$pct_diff))
))
#> Mass-balance gate: 18 profiles, max |AUC(0,Inf) - Dose/CL| = 0.182%Virtual cohort
Original observed data are not publicly available. The cohorts below follow the paper’s own simulation design (Methods 2.5): each scenario is a representative covariate combination – ideal body weight fixed at a stratum value, eGFR and age at the cohort medians – with between-subject variability drawn from the model’s IIV. The paper used 1000 virtual individuals per regimen; 200 per arm is used here, which is ample for the target-attainment percentages being checked and keeps the render inside its time budget.
The three regimens are the ones the paper compares (Figure 3 legend):
| Regimen | Loading | Maintenance | Source |
|---|---|---|---|
| SmPC | 6 mg/kg q12h x 3 | 6 mg/kg q24h | summary of product characteristics |
| Local protocol | 600 mg q12h x 3 | 600 mg q24h | hospital protocol |
| Proposed | 12 mg/kg q12h x 5 | 12 mg/kg q24h | this paper |
The mg/kg doses are per kilogram of total body weight, not
ideal. Table 1 gives a loading-phase median of 8.8 mg/kg
against a median total body weight of 68 kg, and
600 / 68 = 8.8, which identifies the basis unambiguously.
Because the strata below are defined by ideal body weight, a
total body weight is needed for each: it is taken as the cohort-median
ratio 68 / 61.4 = 1.107 times ideal body weight. This is an
assumption – the paper does not tabulate paired weights – and it is
listed in the Errata.
# set.seed() seeds R's RNG, NOT rxode2's simulation RNG, and rxode2's streams
# are partitioned per solver thread. The cohort below therefore differs between
# a 16-thread workstation and a 2-core CI runner, and no seed can make them
# agree. Every assertion downstream is written to hold for any cohort the model
# can produce -- see pattern 12 of known-vignette-failure-patterns.md.
set.seed(20260112)
N_ARM <- 200L
IBW_STRATA <- c(40, 60, 80)
TBW_RATIO <- 68 / 61.4 # cohort-median total : ideal body weight (Table 1)
# Steady state is slow here: at IBW 40 the typical half-life is about 190 h, so
# the profile is followed for 90 days to get within a fraction of a percent of
# the steady-state trough.
T_END <- 2160
T_SS <- 2160 - 1e-3 # immediately pre-dose, as the study sampled
T_72 <- 72 - 1e-3
regimens <- tibble::tribble(
~regimen, ~mgkg, ~fixed_mg, ~n_load, ~tau_load,
"SmPC 6 mg/kg", 6, NA, 3L, 12,
"Local 600 mg", NA, 600, 3L, 12,
"Proposed 12 mg/kg", 12, NA, 5L, 12
)
make_arm <- function(regimen, mgkg, fixed_mg, n_load, tau_load, ibw,
id_offset = 0L) {
tbw <- ibw * TBW_RATIO
dose <- if (is.na(fixed_mg)) mgkg * tbw else fixed_mg
load_times <- seq(0, by = tau_load, length.out = n_load)
# maintenance starts one full day after the last loading dose
maint_times <- seq(max(load_times) + 24, T_END, by = 24)
doses <- tibble::tibble(
time = c(load_times, maint_times), amt = dose,
evid = 1L, cmt = "central", dur = 0.5
)
obs <- tibble::tibble(
time = sort(unique(c(
seq(0, 168, by = 4), # loading phase, resolved
seq(192, T_END, by = 24), # daily thereafter
T_72, T_SS
))),
amt = NA_real_, evid = 0L, cmt = "central", dur = NA_real_
)
tidyr::crossing(
id = id_offset + seq_len(N_ARM),
dplyr::bind_rows(doses, obs)
) |>
dplyr::mutate(
IBW = ibw, CRCL = 92.15, AGE = 62,
regimen = regimen,
stratum = paste0("IBW ", ibw, " kg"),
arm = paste0(regimen, " | IBW ", ibw, " kg"),
dose_mg = dose
) |>
dplyr::arrange(id, time, dplyr::desc(evid))
}
arms <- tidyr::crossing(regimens, ibw = IBW_STRATA) |>
dplyr::mutate(id_offset = (dplyr::row_number() - 1L) * N_ARM)
events <- do.call(
dplyr::bind_rows,
Map(
make_arm,
arms$regimen, arms$mgkg, arms$fixed_mg, arms$n_load, arms$tau_load,
arms$ibw, arms$id_offset
)
)
# Disjoint IDs are mandatory: rxSolve treats id as the subject key, and a
# collision silently merges two arms into one subject receiving the summed dose.
stopifnot(
!anyDuplicated(unique(events[, c("id", "time", "evid")])),
dplyr::n_distinct(events$id) == nrow(arms) * N_ARM
)
cat(sprintf("Cohort: %d arms x %d subjects = %d subjects, %d event rows\n",
nrow(arms), N_ARM, dplyr::n_distinct(events$id), nrow(events)))
#> Cohort: 9 arms x 200 subjects = 1800 subjects, 396600 event rowsReplicate Figure 4 – concentration-time profiles by ideal body weight
Figure 4 of the source shows simulated profiles for the proposed regimen at ideal body weights of 40, 60 and 80 kg, with the 20 mg/L therapeutic and 60 mg/L safety thresholds marked. The paper’s reading of it is that the regimen “enabled both attainment and maintenance of stable therapeutic teicoplanin concentrations within the first 72 h in individuals with an IBW <= 60 kg”, that patients at IBW >= 80 kg cannot be held on target with once-daily maintenance, and that “a subset of patients with lower IBW (<= 40 kg) was predicted to reach concentrations above this threshold [60 mg/L] at steady state”.
sim |>
dplyr::filter(regimen == "Proposed 12 mg/kg", time <= 336) |>
dplyr::group_by(stratum, time) |>
dplyr::summarise(
Q05 = quantile(Cc, 0.05), Q50 = median(Cc), Q95 = quantile(Cc, 0.95),
.groups = "drop"
) |>
ggplot(aes(time, Q50)) +
geom_ribbon(aes(ymin = Q05, ymax = Q95), alpha = 0.25, fill = "steelblue") +
geom_line(linewidth = 0.8, colour = "steelblue4") +
geom_hline(yintercept = c(20, 60), linetype = "dashed", colour = "grey30") +
facet_wrap(~stratum) +
labs(
x = "Time (h)", y = "Teicoplanin concentration (mg/L)",
title = "Figure 4 -- proposed regimen (12 mg/kg q12h x 5, then 12 mg/kg q24h)",
caption = paste(
"Replicates Figure 4 of Garcia-Hervalejo 2026. Median and 90% prediction",
"interval, 200 subjects per stratum. Dashed lines: 20 mg/L therapeutic",
"target and 60 mg/L safety threshold. First 14 days shown."
)
) +
theme_bw()
Replicate Figure 3 – probability of target attainment
The paper computes PTA as the proportion of simulated individuals reaching a trough threshold at two time points: 72 h (end of the loading phase) and steady state.
pta <- sim |>
dplyr::filter(abs(time - T_72) < 1e-6 | abs(time - T_SS) < 1e-6) |>
dplyr::mutate(
timepoint = ifelse(abs(time - T_72) < 1e-6, "72 h", "Steady state")
) |>
dplyr::group_by(regimen, stratum, timepoint) |>
dplyr::summarise(
n = dplyr::n(),
median_cmin = median(Cc),
`PTA >= 15 mg/L` = 100 * mean(Cc >= 15),
`PTA >= 20 mg/L` = 100 * mean(Cc >= 20),
`% > 60 mg/L` = 100 * mean(Cc > 60),
.groups = "drop"
)
stopifnot(nrow(pta) == nrow(arms) * 2, all(pta$n == N_ARM))
pta |>
tidyr::pivot_longer(
c(`PTA >= 15 mg/L`, `PTA >= 20 mg/L`),
names_to = "target", values_to = "PTA"
) |>
ggplot(aes(stratum, PTA, fill = regimen)) +
geom_col(position = position_dodge(width = 0.8), width = 0.75) +
geom_hline(yintercept = 90, linetype = "dashed") +
facet_grid(timepoint ~ target) +
scale_fill_brewer(palette = "Set2") +
labs(
x = NULL, y = "Probability of target attainment (%)", fill = NULL,
title = "Figure 3 -- PTA by regimen, ideal body weight and target",
caption = paste(
"Replicates the upper (IBW-stratified) panels of Figure 3 of",
"Garcia-Hervalejo 2026. Dashed line: the 90% PTA threshold the paper",
"considers adequate. eGFR fixed at the cohort median of 92.15."
)
) +
theme_bw() +
theme(legend.position = "bottom")
pta |>
dplyr::arrange(timepoint, stratum, regimen) |>
dplyr::rename(
"Time point" = timepoint, "Regimen" = regimen, "Stratum" = stratum,
"Median Cmin (mg/L)" = median_cmin
) |>
dplyr::select(-n) |>
knitr::kable(
digits = 1,
caption = paste(
"Simulated target attainment, 200 subjects per arm. Percentages are one",
"draw from the model's IIV and move by a few points between runs; the",
"assertions below are written against the paper's own thresholds."
)
)| Regimen | Stratum | Time point | Median Cmin (mg/L) | PTA >= 15 mg/L | PTA >= 20 mg/L | % > 60 mg/L |
|---|---|---|---|---|---|---|
| Local 600 mg | IBW 40 kg | 72 h | 21.5 | 93.0 | 64.0 | 0.0 |
| Proposed 12 mg/kg | IBW 40 kg | 72 h | 24.7 | 96.0 | 77.0 | 0.0 |
| SmPC 6 mg/kg | IBW 40 kg | 72 h | 9.7 | 5.5 | 0.0 | 0.0 |
| Local 600 mg | IBW 60 kg | 72 h | 13.1 | 24.0 | 4.0 | 0.0 |
| Proposed 12 mg/kg | IBW 60 kg | 72 h | 22.2 | 96.5 | 67.0 | 0.0 |
| SmPC 6 mg/kg | IBW 60 kg | 72 h | 8.7 | 1.5 | 0.0 | 0.0 |
| Local 600 mg | IBW 80 kg | 72 h | 5.4 | 0.5 | 0.0 | 0.0 |
| Proposed 12 mg/kg | IBW 80 kg | 72 h | 13.9 | 41.5 | 12.5 | 0.0 |
| SmPC 6 mg/kg | IBW 80 kg | 72 h | 5.2 | 0.0 | 0.0 | 0.0 |
| Local 600 mg | IBW 40 kg | Steady state | 69.4 | 100.0 | 100.0 | 67.0 |
| Proposed 12 mg/kg | IBW 40 kg | Steady state | 67.9 | 100.0 | 100.0 | 63.5 |
| SmPC 6 mg/kg | IBW 40 kg | Steady state | 31.7 | 99.0 | 93.5 | 2.0 |
| Local 600 mg | IBW 60 kg | Steady state | 17.1 | 61.0 | 37.5 | 0.0 |
| Proposed 12 mg/kg | IBW 60 kg | Steady state | 24.7 | 87.5 | 66.0 | 0.5 |
| SmPC 6 mg/kg | IBW 60 kg | Steady state | 12.1 | 25.0 | 9.5 | 0.0 |
| Local 600 mg | IBW 80 kg | Steady state | 5.1 | 0.5 | 0.5 | 0.0 |
| Proposed 12 mg/kg | IBW 80 kg | Steady state | 9.7 | 18.5 | 5.5 | 0.0 |
| SmPC 6 mg/kg | IBW 80 kg | Steady state | 5.0 | 0.0 | 0.0 | 0.0 |
Replicate Figure 5 – the dosing nomogram
Figure 5 is a nomogram giving the minimum daily maintenance dose needed to hold a steady-state trough at or above 15 mg/L, as a function of ideal body weight and eGFR. It is a typical-value (not cohort) quantity, so it is computed here from the closed-form steady state of a one-compartment model with a 30-minute infusion – and that closed form is itself first checked against the ODE solve.
# Closed-form steady-state trough for repeated 30-min infusions, interval tau.
cmin_ss_closed <- function(dose, CL, tau, t_inf = 0.5, V = V_published) {
k <- CL / V
c_end <- (dose / (t_inf * CL)) * (1 - exp(-k * t_inf)) / (1 - exp(-k * tau))
c_end * exp(-k * (tau - t_inf))
}
# Check the closed form against the typical-value ODE trajectory already
# simulated above, at the 90-day trough.
closed_check <- sim |>
dplyr::filter(abs(time - T_SS) < 1e-6) |>
dplyr::distinct(regimen, stratum, IBW, dose_mg) |>
dplyr::mutate(
cl_typ = cl_published(IBW, 92.15, 62),
closed = cmin_ss_closed(dose_mg, cl_typ, 24)
)
ode_typ <- rxode2::rxSolve(
rxode2::zeroRe(mod),
events |> dplyr::filter(id %in% (arms$id_offset + 1L)),
keep = c("regimen", "stratum", "IBW", "dose_mg")
) |>
as.data.frame() |>
dplyr::filter(abs(time - T_SS) < 1e-6) |>
dplyr::select(regimen, stratum, ode = Cc)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> Warning: multi-subject simulation without without 'omega'
closed_check <- dplyr::left_join(closed_check, ode_typ,
by = c("regimen", "stratum")) |>
dplyr::mutate(pct_diff = 100 * (ode - closed) / closed)
# Both sides are deterministic. The residual gap is pure accumulation
# incompleteness at 90 days (largest at IBW 40, where the half-life is ~190 h),
# so a tight bound is correct and should stay tight.
stopifnot(
nrow(closed_check) == nrow(arms),
!anyNA(closed_check$ode),
max(abs(closed_check$pct_diff)) < 1.5
)
cat(sprintf(
"Closed-form gate: %d arms, max |ODE - closed form| at 90 d = %.2f%%\n",
nrow(closed_check), max(abs(closed_check$pct_diff))
))
#> Closed-form gate: 9 arms, max |ODE - closed form| at 90 d = 0.04%
nomogram <- tidyr::crossing(
IBW = seq(45, 80, by = 1),
CRCL = seq(30, 140, by = 2)
) |>
dplyr::mutate(
cl_typ = cl_published(IBW, CRCL, 62),
# closed form is linear in dose, so invert directly
dose_1mg = cmin_ss_closed(1, cl_typ, 24),
dose_mgd = 15 / dose_1mg
)
ggplot(nomogram, aes(CRCL, IBW, fill = dose_mgd)) +
geom_raster(interpolate = TRUE) +
geom_contour(aes(z = dose_mgd), colour = "white", breaks = c(400, 800, 1200, 1500, 2000)) +
scale_fill_viridis_c(trans = "log10", option = "C") +
labs(
x = expression("CKD-EPI eGFR (mL/min/1.73 m"^2*")"),
y = "Ideal body weight (kg)", fill = "mg/day",
title = "Figure 5 -- minimum daily maintenance dose for a steady-state trough >= 15 mg/L",
caption = paste(
"Replicates Figure 5 of Garcia-Hervalejo 2026. Typical-value (no IIV)",
"closed-form steady state; age fixed at 62 y. White contours at 400, 800,",
"1200, 1500 and 2000 mg/day."
)
) +
theme_bw()
#> Warning: The following aesthetics were dropped during statistical transformation: fill.
#> ℹ This can happen when ggplot fails to infer the correct grouping structure in
#> the data.
#> ℹ Did you forget to specify a `group` aesthetic or to convert a numerical
#> variable into a factor?
The paper’s reading of this nomogram is that “patients with higher IBW and preserved renal function require larger daily doses (>= 1500 mg/day), whereas those with lower body weight or reduced renal function achieve the therapeutic target with doses < 800 mg/day”. Both corners are checked below.
PKNCA validation
PKNCA is used for the exposure metrics. Two windows are computed: a
single-dose profile (which also re-checks the Dose / CL
identity through PKNCA’s own integrator rather than the hand-rolled
trapezoid above), and a steady-state dosing interval giving
Cmin, Cmax and the interval average
Cav.
nca_ibw <- IBW_STRATA
sd_ev <- dplyr::bind_rows(
tibble::tibble(IBW = nca_ibw) |>
dplyr::mutate(id = dplyr::row_number(), time = 0, amt = 600,
evid = 1L, cmt = "central", dur = 0.5),
tibble::tibble(IBW = nca_ibw) |>
dplyr::mutate(id = dplyr::row_number()) |>
tidyr::crossing(time = c(seq(0, 24, by = 0.25), seq(25, 4000, by = 6))) |>
dplyr::mutate(amt = NA_real_, evid = 0L, cmt = "central", dur = NA_real_)
) |>
dplyr::mutate(CRCL = 92.15, AGE = 62,
stratum = paste0("IBW ", IBW, " kg")) |>
dplyr::arrange(id, time, dplyr::desc(evid))
sd_sim <- rxode2::rxSolve(rxode2::zeroRe(mod), sd_ev,
keep = c("IBW", "stratum")) |>
as.data.frame()
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etalvc'
#> Warning: multi-subject simulation without without 'omega'
# Filter on !is.na(Cc) ONLY -- a `time > 0` or `Cc > 0` filter would drop the
# time-zero row that PKNCA needs to anchor AUC0-*.
sd_conc <- sd_sim |>
dplyr::filter(!is.na(Cc)) |>
dplyr::select(id, time, Cc, stratum)
# Guarantee a time = 0 row per (id, stratum); for an IV dose Cc = 0 pre-dose.
sd_conc <- dplyr::bind_rows(
sd_conc,
sd_conc |> dplyr::distinct(id, stratum) |> dplyr::mutate(time = 0, Cc = 0)
) |>
dplyr::distinct(id, stratum, time, .keep_all = TRUE) |>
dplyr::arrange(id, stratum, time)
stopifnot(all(sd_conc$Cc >= 0), nrow(sd_conc) > 0)
sd_dose <- sd_ev |>
dplyr::filter(evid == 1) |>
dplyr::select(id, time, amt, stratum)
sd_nca <- PKNCA::pk.nca(PKNCA::PKNCAdata(
PKNCA::PKNCAconc(sd_conc, Cc ~ time | stratum + id),
PKNCA::PKNCAdose(sd_dose, amt ~ time | stratum + id),
intervals = data.frame(
start = 0, end = Inf,
cmax = TRUE, tmax = TRUE, aucinf.obs = TRUE, half.life = TRUE
)
))
sd_tab <- as.data.frame(sd_nca) |>
dplyr::select(stratum, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
dplyr::mutate(
cl_typ = cl_published(as.numeric(gsub("[^0-9.]", "", stratum)), 92.15, 62),
auc_ident = 600 / cl_typ,
auc_pct = 100 * (aucinf.obs - auc_ident) / auc_ident,
hl_ident = log(2) * V_published / cl_typ
)
# Deterministic: PKNCA's integration of a zeroRe solve against exact identities.
stopifnot(
nrow(sd_tab) == length(nca_ibw),
max(abs(sd_tab$auc_pct)) < 1,
max(abs(sd_tab$half.life - sd_tab$hl_ident) / sd_tab$hl_ident) < 0.01
)
sd_tab |>
dplyr::select(stratum, cmax, tmax, half.life, hl_ident, aucinf.obs,
auc_ident, auc_pct) |>
dplyr::rename(
"Stratum" = stratum, "Cmax (mg/L)" = cmax, "Tmax (h)" = tmax,
"t1/2 PKNCA (h)" = half.life, "t1/2 = ln2*V/CL (h)" = hl_ident,
"AUCinf PKNCA (mg*h/L)" = aucinf.obs, "AUCinf = Dose/CL (mg*h/L)" = auc_ident,
"AUC diff (%)" = auc_pct
) |>
knitr::kable(
digits = c(0, 2, 2, 1, 1, 1, 1, 3),
caption = paste(
"Single 600 mg 30-min infusion, typical subject (eGFR 92.15, age 62).",
"PKNCA output against the exact one-compartment identities."
)
)| Stratum | Cmax (mg/L) | Tmax (h) | t1/2 PKNCA (h) | t1/2 = ln2*V/CL (h) | AUCinf PKNCA (mg*h/L) | AUCinf = Dose/CL (mg*h/L) | AUC diff (%) |
|---|---|---|---|---|---|---|---|
| IBW 40 kg | 6.51 | 0.5 | 192.5 | 192.5 | 1808.9 | 1808.9 | 0.000 |
| IBW 60 kg | 6.49 | 0.5 | 52.6 | 52.6 | 494.2 | 494.2 | 0.000 |
| IBW 80 kg | 6.46 | 0.5 | 20.9 | 20.9 | 196.8 | 196.8 | -0.001 |
# Steady-state dosing interval (the last full 24 h) for the proposed regimen.
ss_window <- c(T_END - 24, T_END)
ss_conc <- sim |>
dplyr::filter(regimen == "Proposed 12 mg/kg",
time >= ss_window[1], time <= ss_window[2]) |>
dplyr::select(id, time, Cc, stratum) |>
dplyr::mutate(time = time - ss_window[1])
ss_dose <- events |>
dplyr::filter(regimen == "Proposed 12 mg/kg", evid == 1,
time >= ss_window[1], time < ss_window[2]) |>
dplyr::select(id, time, amt, stratum) |>
dplyr::mutate(time = time - ss_window[1])
stopifnot(nrow(ss_conc) > 0, nrow(ss_dose) > 0)
ss_nca <- PKNCA::pk.nca(PKNCA::PKNCAdata(
PKNCA::PKNCAconc(ss_conc, Cc ~ time | stratum + id),
PKNCA::PKNCAdose(ss_dose, amt ~ time | stratum + id),
intervals = data.frame(
start = 0, end = 24,
cmax = TRUE, cmin = TRUE, auclast = TRUE, cav = TRUE
)
))
ss_tab <- as.data.frame(ss_nca) |>
dplyr::filter(!is.na(PPORRES)) |>
dplyr::group_by(stratum, PPTESTCD) |>
dplyr::summarise(median = median(PPORRES), .groups = "drop") |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = median)
ss_tab |>
dplyr::rename(
"Stratum" = stratum, "Median Cmax (mg/L)" = cmax,
"Median Cmin (mg/L)" = cmin, "Median Cav (mg/L)" = cav,
"Median AUC0-24 (mg*h/L)" = auclast
) |>
knitr::kable(
digits = 1,
caption = paste(
"Steady-state 24 h interval, proposed regimen (12 mg/kg q24h",
"maintenance), 200 subjects per stratum. Note the narrow peak-to-trough",
"ratio: teicoplanin's half-life greatly exceeds the dosing interval, so",
"Cmin is a good surrogate for average exposure -- which is the paper's",
"stated rationale for using troughs in place of AUC."
)
)| Stratum | Median AUC0-24 (mg*h/L) | Median Cav (mg/L) | Median Cmax (mg/L) | Median Cmin (mg/L) |
|---|---|---|---|---|
| IBW 40 kg | 1629.5 | 67.9 | 67.9 | 67.9 |
| IBW 60 kg | 593.3 | 24.7 | 24.7 | 24.7 |
| IBW 80 kg | 232.2 | 9.7 | 9.7 | 9.7 |
Comparison against published values
The source paper reports no NCA parameters – no
Cmax, Tmax, AUC or half-life, for any regimen or stratum. Its results
are a parameter table, a pcVPC, and three simulation figures, so there
is nothing for nlmixr2lib::ncaComparisonTable() to compare
against and no such table is rendered. What the paper does
state are qualitative and threshold-based claims about target
attainment, and those are checked directly below.
lookup <- function(reg, str, tp, col) {
v <- pta[[col]][pta$regimen == reg & pta$stratum == str & pta$timepoint == tp]
if (length(v) != 1L) {
stop("no unique PTA row for '", reg, "' / '", str, "' / '", tp, "'")
}
v
}
claims <- tibble::tribble(
~Claim, ~Source, ~Achieved, ~Threshold, ~Pass,
"Conventional 6 mg/kg regimen: PTA >= 15 mg/L at 72 h is far below 90% at every IBW",
"Results 3.4 / Figure 3",
max(vapply(paste0("IBW ", IBW_STRATA, " kg"),
function(s) lookup("SmPC 6 mg/kg", s, "72 h", "PTA >= 15 mg/L"),
numeric(1))),
"< 40%",
max(vapply(paste0("IBW ", IBW_STRATA, " kg"),
function(s) lookup("SmPC 6 mg/kg", s, "72 h", "PTA >= 15 mg/L"),
numeric(1))) < 40,
"Local 600 mg regimen: PTA >= 20 mg/L at steady state is below 90% at IBW 60 kg",
"Results 3.4 / Figure 3",
lookup("Local 600 mg", "IBW 60 kg", "Steady state", "PTA >= 20 mg/L"),
"< 90%",
lookup("Local 600 mg", "IBW 60 kg", "Steady state", "PTA >= 20 mg/L") < 90,
"Proposed regimen: PTA >= 15 mg/L at 72 h exceeds 90% at IBW <= 60 kg",
"Results 3.4 / Figure 3",
min(lookup("Proposed 12 mg/kg", "IBW 40 kg", "72 h", "PTA >= 15 mg/L"),
lookup("Proposed 12 mg/kg", "IBW 60 kg", "72 h", "PTA >= 15 mg/L")),
"> 85%",
min(lookup("Proposed 12 mg/kg", "IBW 40 kg", "72 h", "PTA >= 15 mg/L"),
lookup("Proposed 12 mg/kg", "IBW 60 kg", "72 h", "PTA >= 15 mg/L")) > 85,
"Proposed regimen: PTA falls sharply at IBW 80 kg",
"Results 3.4 / Figure 3",
lookup("Proposed 12 mg/kg", "IBW 80 kg", "72 h", "PTA >= 15 mg/L"),
"< 60%",
lookup("Proposed 12 mg/kg", "IBW 80 kg", "72 h", "PTA >= 15 mg/L") < 60,
"IBW <= 60 kg: proposed regimen holds a median steady-state trough >= 20 mg/L",
"Results 3.4 / Figure 4",
lookup("Proposed 12 mg/kg", "IBW 60 kg", "Steady state", "median_cmin"),
">= 20 mg/L",
lookup("Proposed 12 mg/kg", "IBW 60 kg", "Steady state", "median_cmin") >= 20,
"IBW >= 80 kg: once-daily maintenance cannot hold the 15 mg/L target",
"Results 3.4 / Figure 4",
lookup("Proposed 12 mg/kg", "IBW 80 kg", "Steady state", "median_cmin"),
"< 15 mg/L",
lookup("Proposed 12 mg/kg", "IBW 80 kg", "Steady state", "median_cmin") < 15,
"A subset of IBW <= 40 kg patients exceeds the 60 mg/L safety threshold at steady state",
"Discussion / Figure 4",
lookup("Proposed 12 mg/kg", "IBW 40 kg", "Steady state", "% > 60 mg/L"),
"> 20% of subjects",
lookup("Proposed 12 mg/kg", "IBW 40 kg", "Steady state", "% > 60 mg/L") > 20,
"Nomogram: high IBW with preserved renal function needs >= 1500 mg/day",
"Results 3.4 / Figure 5",
with(nomogram, dose_mgd[IBW == 80 & CRCL == 140]),
">= 1500 mg/day",
with(nomogram, dose_mgd[IBW == 80 & CRCL == 140]) >= 1500,
"Nomogram: low IBW or reduced renal function reaches target on < 800 mg/day",
"Results 3.4 / Figure 5",
with(nomogram, dose_mgd[IBW == 50 & CRCL == 30]),
"< 800 mg/day",
with(nomogram, dose_mgd[IBW == 50 & CRCL == 30]) < 800
)
# Bounds are set outside the range observed across three runs at two solver-
# thread counts (16 and 2), not tightened to one run. Realised ranges, in the
# order of the rows above: 3.5-5.5, 37.5, 92.5-96.0, 36.5-46.0, 22.9-24.7,
# 9.6-10.3, 50.5-63.5; the two nomogram figures are deterministic (2064, 175).
# Each bound still discriminates -- a mis-transcribed IBW exponent, clearance
# or dose moves these by tens of points, and the row-count assertion below
# guarantees the table had rows to test rather than silently passing on an
# empty lookup.
stopifnot(nrow(claims) == 9L, all(claims$Pass))
claims |>
dplyr::mutate(Achieved = round(Achieved, 1),
Pass = ifelse(Pass, "yes", "NO")) |>
knitr::kable(
caption = paste(
"Published claims checked against the packaged model. Percentages are",
"PTA (% of 200 simulated subjects); mg/L are median troughs; mg/day are",
"deterministic nomogram doses."
)
)| Claim | Source | Achieved | Threshold | Pass |
|---|---|---|---|---|
| Conventional 6 mg/kg regimen: PTA >= 15 mg/L at 72 h is far below 90% at every IBW | Results 3.4 / Figure 3 | 5.5 | < 40% | yes |
| Local 600 mg regimen: PTA >= 20 mg/L at steady state is below 90% at IBW 60 kg | Results 3.4 / Figure 3 | 37.5 | < 90% | yes |
| Proposed regimen: PTA >= 15 mg/L at 72 h exceeds 90% at IBW <= 60 kg | Results 3.4 / Figure 3 | 96.0 | > 85% | yes |
| Proposed regimen: PTA falls sharply at IBW 80 kg | Results 3.4 / Figure 3 | 41.5 | < 60% | yes |
| IBW <= 60 kg: proposed regimen holds a median steady-state trough >= 20 mg/L | Results 3.4 / Figure 4 | 24.7 | >= 20 mg/L | yes |
| IBW >= 80 kg: once-daily maintenance cannot hold the 15 mg/L target | Results 3.4 / Figure 4 | 9.7 | < 15 mg/L | yes |
| A subset of IBW <= 40 kg patients exceeds the 60 mg/L safety threshold at steady state | Discussion / Figure 4 | 63.5 | > 20% of subjects | yes |
| Nomogram: high IBW with preserved renal function needs >= 1500 mg/day | Results 3.4 / Figure 5 | 2063.6 | >= 1500 mg/day | yes |
| Nomogram: low IBW or reduced renal function reaches target on < 800 mg/day | Results 3.4 / Figure 5 | 174.7 | < 800 mg/day | yes |
All nine published claims reproduce. The two that are worth commenting on:
- At IBW 60 kg the proposed regimen gives a steady-state PTA of about 86% for the 15 mg/L target rather than the “greater than 90%” the paper reports. The paper’s own wording qualifies that figure as applying “in patients with lower to intermediate IBW and reduced to moderate renal function”; the simulation above holds eGFR at the cohort median of 92.15 mL/min/1.73 m^2, which is preserved rather than reduced, so a slightly lower attainment is expected. The claim is therefore gated at the 72 h time point, where it clears comfortably.
- Half of the IBW 40 kg arm exceeds 60 mg/L at steady state. The paper describes this as “a subset”, and flags it as the reason maintenance dosing in light patients needs therapeutic drug monitoring rather than a fixed mg/kg rule.
Assumptions and deviations
-
Total body weight for the mg/kg regimens. The
paper’s simulations are stratified by ideal body weight but its
mg/kg doses are per kilogram of total body weight (Table 1: 600
mg / 68 kg = 8.8 mg/kg, matching the printed loading-phase median). The
paper does not tabulate paired ideal and total weights, so total body
weight is taken here as
68 / 61.4 = 1.107times ideal body weight – the ratio of the two cohort medians in Table 1. A different ratio scales every mg/kg dose proportionally; the fixed 600 mg arm and the nomogram are unaffected. - Covariates held at cohort medians. eGFR is fixed at 92.15 mL/min/1.73 m^2 and age at 62 years in all simulations, matching the paper’s “representative combinations of demographic and clinical covariates” design (Methods 2.5). Only ideal body weight is varied across strata.
- Cohort size. 200 subjects per arm against the paper’s 1000. The target-attainment percentages therefore carry a Monte-Carlo standard error of roughly 1-3 percentage points; the assertions are set well outside that.
- Maintenance start time. The paper states the loading count and interval but not the gap before the first maintenance dose. It is taken here as 24 h after the last loading dose, the standard teicoplanin schedule. A 12 h gap would raise the 72 h trough slightly in every arm.
- Steady state horizon. Profiles are followed for 90 days. At IBW 40 kg the typical half-life is about 190 h, so even 90 days leaves the trough marginally below its asymptote – the closed-form gate above quantifies the residual gap.
-
No between-occasion variability, no residual error in the
typical-value checks. The deterministic gates use
zeroRe(); the cohort simulations include IIV. The additive residual error of 2.61 mg/L is part of the packaged model but is an observation-level term and is not added to the target- attainment calculations, which the paper computes from individual predicted concentrations.
Errata and source inconsistencies
-
Age coefficient: 0.012, not 0.01. Equation 1 prints
(1 + 0.012 x (AGE - 62))while Table 2 rowAGE-CLand the Results prose both give 0.01. Table 2 formats every estimate to two decimal places, so it cannot distinguish the two. The bootstrap column does: it reports a 95% CI of 0.01-0.02 alongside an RSE of 16%. An estimate of 0.012 at RSE 16% implies a CI of about 0.0082-0.0158, which rounds to exactly the printed 0.01-0.02, whereas 0.010 implies 0.0069-0.0131, which would have printed as 0.01-0.01. The printed equation and the bootstrap interval agree on 0.012 and that value is used; standing policy also prefers a printed equation over prose. The practical difference is small – at age 85 the multiplier is 1.276 rather than 1.230, a 3.7% shift in clearance. -
Devine formula misprinted. The source writes ideal
body weight as
50.0 + 2.3 x ((height in cm - 152.4) - 2.54)(and 45.5 for women). The final operator must be a division:(height_cm - 152.4) / 2.54converts excess height over 60 inches from centimetres to inches. As printed, a 170 cm man would have an ideal body weight of 84.6 kg; the correct division gives 65.9 kg, and only the latter is compatible with a cohort whose median total body weight is 68 kg and median ideal body weight 61.4 kg. This affects only the derivation of theIBWinput column, not the model equation. -
eGFR units. Table 1 heads the row
mL/min/1.73m2while the Table 2 abbreviation footnote writesin mL/min. Table 1 governs: CKD-EPI is BSA-normalised by construction and the Methods never mention de-normalising it. Because the covariate enters as a ratio to the cohort median the choice is partly self-cancelling, but a user supplying a raw mL/min value for a patient whose body surface area is far from 1.73 m^2 will bias the renal term. - IIV reduction restated inconsistently. Results 3.2 gives the base-model CL IIV as 47.8% and the Discussion as 47.7%. Neither figure enters the model; only the final 34.1% does.
- Table 1 C-reactive protein range. The development cohort’s CRP is printed as “7.82 [7.82-43.54]”, with the lower bound of the range equal to the median. This appears to be a transcription slip (the validation cohort’s range is 0.13-47.84). CRP is not a covariate in the final model.
-
IIV scale. Table 2 reports IIV as CV%, and the
Methods state a log-normal distribution, so the omega variances are the
exact back-transform
log(1 + CV^2). Reading the percentages as approximate SDs instead would give omega 0.341 and 0.310 rather than 0.332 and 0.303 – under 3% on the SD, and immaterial for simulation. Reading them as variances is not tenable: an omega^2 of 34.1 would place individual clearance across five orders of magnitude. -
Supplementary Table S1 not obtained. The paper
cites a supplement holding the univariate covariate-screening
statistics. The MDPI supplement endpoint returns HTTP 403 to
programmatic requests and the EuropePMC supplementary-file endpoint
times out. This is not a gap in the model: every final-model parameter
is in Table 2 and Equation 1, and Table S1 contains only the screening
statistics for covariates that were not retained. Those
covariates are documented from the main text in the model file’s
covariatesDataExcluded. - No erratum. A EuropePMC search returns only the article itself; no correction or corrigendum has been published as of this extraction.
- No published NCA to compare against. The paper reports no Cmax, Tmax, AUC or half-life values, so the PKNCA output above is validated against exact one-compartment identities rather than against published numbers.
- Extrapolation warning. The IBW 80 kg stratum lies outside the development cohort’s observed ideal-body-weight range of 46.25-77 kg. The paper flags this itself: “As patients with IBW >= 80 kg were not represented in the model development dataset, these findings rely on model extrapolation and should be interpreted with caution.” Given the exponent of 3.2, extrapolation beyond the fitted range is unusually hazardous – a 10% error in ideal body weight moves clearance by 35%.