Glibenclamide glucose response PK/PD (Rambiritch 2016)
Source:vignettes/articles/Rambiritch_2016_glibenclamide_glucose_response.Rmd
Rambiritch_2016_glibenclamide_glucose_response.RmdModel and source
Rambiritch 2016 fitted six pharmacodynamic models to one glibenclamide dose-escalation study: three glucose endpoints, each driven either by the daily dose or by the average steady-state plasma concentration (Cpss). All six are reported as final, with full parameter tables, so each is a separate model file and all six share this vignette.
| Model | Driver | Endpoint | Model file |
|---|---|---|---|
| 1 | Dose | Fasting blood glucose (FBG) | Rambiritch_2016_glibenclamide_dose_fpg |
| 2 | Cpss | Fasting blood glucose | Rambiritch_2016_glibenclamide_cpss_fpg |
| 3 | Dose | Mean glucose over the sampling day | Rambiritch_2016_glibenclamide_dose_meanglucose |
| 4 | Cpss | Mean glucose over the sampling day | Rambiritch_2016_glibenclamide_cpss_meanglucose |
| 5 | Dose | Full glucose-time profile | Rambiritch_2016_glibenclamide_dose_glucoseprofile |
| 6 | Cpss | Full glucose-time profile | Rambiritch_2016_glibenclamide_cpss_glucoseprofile |
The companion population PK paper on the same cohort is a separate
model, Rambiritch_2016_glibenclamide; its CL/F (2.16 L/h)
converts dose to Cpss in Models 2, 4 and 6.
stems <- c(
m1 = "Rambiritch_2016_glibenclamide_dose_fpg",
m2 = "Rambiritch_2016_glibenclamide_cpss_fpg",
m3 = "Rambiritch_2016_glibenclamide_dose_meanglucose",
m4 = "Rambiritch_2016_glibenclamide_cpss_meanglucose",
m5 = "Rambiritch_2016_glibenclamide_dose_glucoseprofile",
m6 = "Rambiritch_2016_glibenclamide_cpss_glucoseprofile"
)
uis <- lapply(stems, function(s) rxode2::rxode(readModelDb(s)))
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
#> ℹ parameter labels from comments will be replaced by 'label()'
outputs <- c(
m1 = "fpg", m2 = "fpg", m3 = "glucose_mean", m4 = "glucose_mean",
m5 = "glucose", m6 = "glucose"
)- Citation: Rambiritch V, Naidoo P, Pillai G. Glibenclamide population pharmacokinetic/pharmacodynamic modeling in South African type 2 diabetic subjects. Clin Pharmacol. 2016;8:141-150. doi:10.2147/CPAA.S102674. Parameter values are the ‘Dose-fasting glucose model’ column of Table 1; the structural equation is Equation 1.
- Article: https://doi.org/10.2147/CPAA.S102674
The structure is an inhibitory Emax model on a drug-free baseline:
- Models 1, 3, 5 (Equation 1):
Effect = E0 * (1 - DOSE * Emax / (DOSE + ED50)) - Models 2, 4, 6 (Equations 2-3):
Effect = E0 * (1 - Cpss * Emax / (Cpss + EC50)), withCpss = DOSE / (CL/F * 24) - Models 5 and 6 replace the constant
E0with a two-harmonic Fourier series with an 8-hour period (Equation 4), which captures the glucose excursions after breakfast (time 0) and lunch (about 4 hours later):E0(t) = A0 + A1 cos(2 pi t/8) + B1 sin(2 pi t/8) + A2 cos(4 pi t/8) + B2 sin(4 pi t/8).
Population
Twenty-four adults with poorly controlled type 2 diabetes in KwaZulu-Natal, South Africa, were escalated through glibenclamide 0, 2.5, 5, 10 and 20 mg once daily at 2-week intervals. Glucose was measured by glucometer over an 8-hour sampling day on each dose level (fasting sample at time 0, breakfast, lunch about 4 hours later). Subject 16 stopped at 5 mg/day because of hypoglycaemia and subjects 20, 21 and 24 had incomplete data, but all 24 were in the PK/PD data set. The paper does not reprint demographics; the companion PK paper’s Table 1 describes the same cohort: age 39-73 years (mean 54), weight 42.0-107.8 kg (mean 71.1), 20 female and 2 male among the 22 characterised at entry, mean fasting blood glucose 15.4 mmol/L and mean HbA1c 12.2%.
Source trace
Every ini() value carries an in-file comment naming its
table and column. In summary:
| Element | Source |
|---|---|
| Inhibitory Emax on dose | Equation 1 |
| Inhibitory Emax on Cpss | Equation 2 |
Cpss = DOSE / (CL/F x 24), CL/F = 2.16 L/h |
Equation 3; footnotes of Tables 1-3 (companion PK paper, its Table 3) |
| Fourier baseline, period 8 h | Equation 4 and the text below it |
| Exponential BSV on A0 / E0 (and Emax, ED50, EC50) | Equation 5 |
| Additive BSV on A1, B1, A2, B2 with one shared variance | Equation 6 and the text below it |
| Proportional residual error | Equation 7 |
| Models 1-2 parameters | Table 1 |
| Models 3-4 parameters | Table 2 |
| Models 5-6 parameters | Table 3 |
| BSV on CL/F in Models 2, 4, 6 (33.91% CV) | Companion PK paper, Table 3 |
param_tab <- bind_rows(lapply(names(uis), function(m) {
ini <- uis[[m]]$iniDf
data.frame(
model = m,
parameter = ini$name,
estimate = signif(ifelse(grepl("^l", ini$name) & is.na(ini$neta1),
exp(ini$est), ini$est), 4),
fixed = ini$fix
)
}))
param_tab |>
dplyr::rename(
"Model" = model, "Parameter" = parameter,
"Estimate (back-transformed for log-scale thetas; variance for etas)" = estimate,
"Fixed" = fixed
) |>
knitr::kable()| Model | Parameter | Estimate (back-transformed for log-scale thetas; variance for etas) | Fixed |
|---|---|---|---|
| m1 | le0 | 14.1000000 | FALSE |
| m1 | lemax | 0.3700000 | FALSE |
| m1 | led50 | 4.5600000 | FALSE |
| m1 | propSd | 0.1500000 | FALSE |
| m1 | etale0 | 0.0843900 | FALSE |
| m1 | etalemax | 0.2370000 | FALSE |
| m1 | etaled50 | 0.0000000 | FALSE |
| m2 | lcl | 2.1600000 | TRUE |
| m2 | le0 | 14.3000000 | FALSE |
| m2 | lemax | 0.3900000 | FALSE |
| m2 | lec50 | 85.2000000 | FALSE |
| m2 | propSd | 0.1400000 | FALSE |
| m2 | etalcl | 0.1150000 | TRUE |
| m2 | etale0 | 0.0750800 | FALSE |
| m2 | etalemax | 0.3080000 | FALSE |
| m2 | etalec50 | 0.0000002 | FALSE |
| m3 | le0 | 16.7000000 | FALSE |
| m3 | lemax | 0.3400000 | FALSE |
| m3 | led50 | 1.8500000 | FALSE |
| m3 | propSd | 0.0875000 | FALSE |
| m3 | etale0 | 0.0732800 | FALSE |
| m3 | etalemax | 0.2600000 | FALSE |
| m3 | etaled50 | 1.4500000 | FALSE |
| m4 | lcl | 2.1600000 | TRUE |
| m4 | le0 | 16.7000000 | FALSE |
| m4 | lemax | 0.3400000 | FALSE |
| m4 | lec50 | 36.0000000 | FALSE |
| m4 | propSd | 0.0892000 | FALSE |
| m4 | etalcl | 0.1150000 | TRUE |
| m4 | etale0 | 0.0725800 | FALSE |
| m4 | etalemax | 0.2590000 | FALSE |
| m4 | etalec50 | 1.1700000 | FALSE |
| m5 | le0 | 15.9000000 | FALSE |
| m5 | e0_glucose_fourier_cos1 | 0.9500000 | FALSE |
| m5 | e0_glucose_fourier_sin1 | 1.4500000 | FALSE |
| m5 | e0_glucose_fourier_cos2 | -1.9500000 | FALSE |
| m5 | e0_glucose_fourier_sin2 | 2.6300000 | FALSE |
| m5 | e0_glucose_fourier_period | 8.0000000 | TRUE |
| m5 | lemax | 0.3400000 | FALSE |
| m5 | led50 | 2.2100000 | FALSE |
| m5 | propSd | 0.1300000 | FALSE |
| m5 | etale0 | 0.0824300 | FALSE |
| m5 | etae0_glucose_fourier_cos1 | 1.4600000 | FALSE |
| m5 | etae0_glucose_fourier_sin1 | 1.4600000 | FALSE |
| m5 | etae0_glucose_fourier_cos2 | 1.4600000 | FALSE |
| m5 | etae0_glucose_fourier_sin2 | 1.4600000 | FALSE |
| m5 | etalemax | 0.0033060 | FALSE |
| m5 | etaled50 | 0.0228900 | FALSE |
| m6 | lcl | 2.1600000 | TRUE |
| m6 | le0 | 15.4000000 | FALSE |
| m6 | e0_glucose_fourier_cos1 | 0.9700000 | FALSE |
| m6 | e0_glucose_fourier_sin1 | 1.4500000 | FALSE |
| m6 | e0_glucose_fourier_cos2 | -1.8600000 | FALSE |
| m6 | e0_glucose_fourier_sin2 | 2.5700000 | FALSE |
| m6 | e0_glucose_fourier_period | 8.0000000 | TRUE |
| m6 | lemax | 0.3100000 | FALSE |
| m6 | lec50 | 43.6000000 | FALSE |
| m6 | propSd | 0.1300000 | FALSE |
| m6 | etalcl | 0.1150000 | TRUE |
| m6 | etale0 | 0.1070000 | FALSE |
| m6 | etae0_glucose_fourier_cos1 | 0.0049700 | FALSE |
| m6 | etae0_glucose_fourier_sin1 | 0.0049700 | FALSE |
| m6 | etae0_glucose_fourier_cos2 | 0.0049700 | FALSE |
| m6 | etae0_glucose_fourier_sin2 | 0.0049700 | FALSE |
| m6 | etalemax | 0.2060000 | FALSE |
| m6 | etalec50 | 4.8800000 | FALSE |
Deterministic checks against the paper
These use the typical-value parameters only, so every comparison is exact up to the rounding of the printed numbers.
typical <- function(m, dose, time = 0) {
ev <- expand.grid(time = time, DOSE = dose)
ev$id <- seq_len(nrow(ev))
ev$evid <- 0
sim <- rxode2::rxSolve(rxode2::zeroRe(uis[[m]]), ev, returnType = "data.frame")
stopifnot(nrow(sim) == nrow(ev))
data.frame(DOSE = ev$DOSE, time = ev$time, value = sim[[outputs[[m]]]])
}Derived ED50 of the Cpss models (unit check on EC50)
Each table footnote prints
derived ED50 = EC50 x CL/f x 24. With CL/F = 2.16 L/h the
multiplier is 51.84 L, so the printed derived ED50s (4.41, 1.87 and 2.26
mg) hold only if EC50 is in ug/L (ng/mL), not the mg/L the
table headers print. The models encode EC50 in ng/mL; solving each Cpss
model at the paper’s derived ED50 must give exactly half of the maximal
inhibition.
derived <- data.frame(
model = c("m2", "m4", "m6"),
ec50_ngml = c(85.20, 36.00, 43.60),
printed_ed50_mg = c(4.41, 1.87, 2.26)
)
derived$computed_ed50_mg <- derived$ec50_ngml * 2.16 * 24 / 1000
derived$frac_of_emax_at_printed_ed50 <- vapply(seq_len(nrow(derived)), function(i) {
m <- derived$model[i]
e0 <- typical(m, 0)$value
eff <- typical(m, derived$printed_ed50_mg[i])$value
emax <- exp(uis[[m]]$iniDf$est[uis[[m]]$iniDf$name == "lemax"])
(1 - eff / e0) / emax
}, numeric(1))
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etalec50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etalec50'
derived |>
dplyr::rename(
"Model" = model, "EC50 (ng/mL)" = ec50_ngml,
"Printed derived ED50 (mg)" = printed_ed50_mg,
"EC50 x 51.84 L (mg)" = computed_ed50_mg,
"Inhibition at printed ED50 / Emax" = frac_of_emax_at_printed_ed50
) |>
knitr::kable(digits = 3)| Model | EC50 (ng/mL) | Printed derived ED50 (mg) | EC50 x 51.84 L (mg) | Inhibition at printed ED50 / Emax |
|---|---|---|---|---|
| m2 | 85.2 | 4.41 | 4.417 | 0.500 |
| m4 | 36.0 | 1.87 | 1.866 | 0.501 |
| m6 | 43.6 | 2.26 | 2.260 | 0.500 |
Maximal effect: FBG from 15.4 to 10.6 and 9.7 mmol/L
The Discussion translates the range of Emax (0.37 for Model 1 to 0.31
for Model 6) into a reduction of FBG from the cohort mean of 15.4 mmol/L
to 9.7 and 10.6 mmol/L. At a dose far above every ED50 the models reach
E0 * (1 - Emax).
frac_remaining <- vapply(c("m1", "m6"), function(m) {
typical(m, 1e6)$value / typical(m, 0)$value
}, numeric(1))
#> ℹ omega/sigma items treated as zero: 'etale0', 'etalemax', 'etaled50'
#> ℹ omega/sigma items treated as zero: 'etale0', 'etalemax', 'etaled50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etalec50'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etalec50'
fbg_max <- unname(15.4 * frac_remaining)
data.frame(
model = c("Model 1", "Model 6"),
row.names = NULL,
predicted = round(fbg_max, 2),
paper = c(9.7, 10.6)
) |>
dplyr::rename("Model" = model, "FBG at maximal effect (mmol/L)" = predicted,
"Paper (mmol/L)" = paper) |>
knitr::kable()| Model | FBG at maximal effect (mmol/L) | Paper (mmol/L) |
|---|---|---|
| Model 1 | 9.70 | 9.7 |
| Model 6 | 10.63 | 10.6 |
Fourier baseline of Models 5 and 6
The placebo glucose profile has two peaks, one after breakfast and
one after lunch about 4 hours later. Because every term of the series
completes a whole number of cycles in 8 hours, the series averages to
A0 over the sampling day – a check that the period and
harmonics are encoded as printed.
grid <- seq(0, 8, by = 0.01)
fourier <- bind_rows(lapply(c("m5", "m6"), function(m) {
cbind(model = m, typical(m, 0, grid))
}))
#> ℹ omega/sigma items treated as zero: 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etaled50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etalec50'
#> Warning: multi-subject simulation without without 'omega'
peaks <- fourier |>
group_by(model) |>
summarise(
peak1_h = time[time < 4][which.max(value[time < 4])],
peak2_h = time[time >= 4][which.max(value[time >= 4])],
day_mean = mean(value[-length(value)]),
a0 = exp(uis[[model[1]]]$iniDf$est[uis[[model[1]]]$iniDf$name == "le0"])
)
peaks |>
dplyr::rename("Model" = model, "First peak (h)" = peak1_h,
"Second peak (h)" = peak2_h, "Mean over 0-8 h (mmol/L)" = day_mean,
"A0 (mmol/L)" = a0) |>
knitr::kable(digits = 2)| Model | First peak (h) | Second peak (h) | Mean over 0-8 h (mmol/L) | A0 (mmol/L) |
|---|---|---|---|---|
| m5 | 1.39 | 5.43 | 15.9 | 15.9 |
| m6 | 1.38 | 5.42 | 15.4 | 15.4 |
stopifnot(
nrow(peaks) == 2,
# paper: peaks at about 2 and 6 h, i.e. about 4 h apart
all(peaks$peak1_h > 1 & peaks$peak1_h < 2.5),
all(peaks$peak2_h > 5 & peaks$peak2_h < 6.5),
all(abs(peaks$peak2_h - peaks$peak1_h - 4) < 0.5),
# whole-cycle average equals A0 to grid precision
all(abs(peaks$day_mean - peaks$a0) < 1e-3)
)The model’s peaks sit at about 1.4 and 5.4 hours. The paper’s “about 2 and 6 hours” describes the observed data on its sampling grid (samples at 1.5 and 2 h after breakfast and at 5.5 and 6 h), so the agreement is within one sampling interval.
Typical dose-response across the six models
doses <- c(0, 2.5, 5, 10, 20)
dr <- bind_rows(lapply(names(uis), function(m) {
t0 <- if (m %in% c("m5", "m6")) grid else 0
typical(m, doses, t0) |>
group_by(DOSE) |>
summarise(value = mean(value), .groups = "drop") |>
mutate(model = m, pct_of_baseline = 100 * value / value[DOSE == 0])
}))
#> ℹ omega/sigma items treated as zero: 'etale0', 'etalemax', 'etaled50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etale0', 'etalemax', 'etaled50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etaled50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etae0_glucose_fourier_cos1', 'etae0_glucose_fourier_sin1', 'etae0_glucose_fourier_cos2', 'etae0_glucose_fourier_sin2', 'etalemax', 'etalec50'
#> Warning: multi-subject simulation without without 'omega'
dr |>
select(model, DOSE, pct_of_baseline) |>
tidyr::pivot_wider(names_from = DOSE, values_from = pct_of_baseline) |>
dplyr::rename("Model" = model) |>
knitr::kable(digits = 1, caption = "Typical glucose as % of the drug-free value, by daily dose (mg). For Models 5-6 the value is the 0-8 h average.")| Model | 0 | 2.5 | 5 | 10 | 20 |
|---|---|---|---|---|---|
| m1 | 100 | 86.9 | 80.6 | 74.6 | 69.9 |
| m2 | 100 | 85.9 | 79.3 | 72.9 | 68.1 |
| m3 | 100 | 80.5 | 75.2 | 71.3 | 68.9 |
| m4 | 100 | 80.5 | 75.2 | 71.3 | 68.9 |
| m5 | 100 | 82.0 | 76.4 | 72.2 | 69.4 |
| m6 | 100 | 83.7 | 78.7 | 74.7 | 72.1 |
gain <- dr |>
group_by(model) |>
summarise(
first5 = 100 - pct_of_baseline[DOSE == 5],
extra = pct_of_baseline[DOSE == 5] - pct_of_baseline[DOSE == 20]
)
stopifnot(nrow(gain) == 6, all(gain$extra < gain$first5))Across the six models, the first 5 mg/day lowers typical glucose by 19-25 percentage points of baseline, and going on from 5 to 20 mg/day lowers it by only a further 6-11 points. This matches the paper’s conclusion that doses above 5 mg/day add little.
Virtual cohort and simulation
Each simulated subject is escalated through the five dose levels exactly as in the study, with one sampling day at steady state on each level (days 14, 28, 42, 56 and 70). The profile models are sampled at the paper’s times (0, 0.5, 1, 1.5, 2, 4, 4.5, 5, 5.5, 6 and 7 hours); the fasting and mean-glucose models have one value per sampling day. 200 subjects per model.
rxode2::rxSetSeed(20160926)
n_sub <- 200
samp_h <- c(0, 0.5, 1, 1.5, 2, 4, 4.5, 5, 5.5, 6, 7)
make_events <- function(hours) {
expand.grid(h = hours, level = seq_along(doses), id = seq_len(n_sub)) |>
mutate(
DOSE = doses[level],
time = (14 * level) * 24 + h,
evid = 0
) |>
arrange(id, time)
}
sims <- bind_rows(lapply(names(uis), function(m) {
ev <- make_events(if (m %in% c("m5", "m6")) samp_h else 0)
s <- rxode2::rxSolve(uis[[m]], ev, returnType = "data.frame")
stopifnot(nrow(s) == nrow(ev))
data.frame(
model = m, id = s$id, DOSE = ev$DOSE, h = ev$h,
ipred = s[[outputs[[m]]]], dv = s$sim
)
}))Replicates Figures 1-3: glucose by dose level
Figures 1-3 of Rambiritch 2016 show per-subject observed and predicted FBG (Models 1 and 2) and mean glucose (Model 4) across the dose levels. The simulated median and 5th-95th percentiles are below.
sims |>
filter(model %in% c("m1", "m2", "m3", "m4")) |>
group_by(model, DOSE) |>
summarise(
lo = quantile(dv, 0.05), med = median(dv), hi = quantile(dv, 0.95),
.groups = "drop"
) |>
mutate(model = factor(model, labels = c(
"1: dose -> FBG", "2: Cpss -> FBG",
"3: dose -> mean glucose", "4: Cpss -> mean glucose"
))) |>
ggplot(aes(DOSE, med)) +
geom_ribbon(aes(ymin = lo, ymax = hi), alpha = 0.2) +
geom_line() +
geom_point() +
facet_wrap(~model) +
labs(x = "Glibenclamide dose (mg/day)", y = "Glucose (mmol/L)",
caption = "Median and 5th-95th percentiles of 200 simulated subjects.")
Replicates Figure 4: full glucose profile by dose level
Figure 4 shows each subject’s glucose profile over the sampling day, one column per dose level. The simulated median and 5th-95th percentiles for Model 6 (the model plotted in Figure 4) and Model 5 are below.
sims |>
filter(model %in% c("m5", "m6")) |>
group_by(model, DOSE, h) |>
summarise(
lo = quantile(dv, 0.05), med = median(dv), hi = quantile(dv, 0.95),
.groups = "drop"
) |>
mutate(
model = factor(model, labels = c("5: dose", "6: Cpss")),
dose_lab = factor(paste(DOSE, "mg/day"), levels = paste(doses, "mg/day"))
) |>
ggplot(aes(h, med)) +
geom_ribbon(aes(ymin = lo, ymax = hi), alpha = 0.2) +
geom_line() +
facet_grid(model ~ dose_lab) +
labs(x = "Time from fasting sample (h)", y = "Glucose (mmol/L)",
caption = "Median and 5th-95th percentiles of 200 simulated subjects.")
Cohort checks
Two checks tie the simulated cohort to the typical-value model.
First, the simulated median at the drug-free level should sit on E0: the
etas are centred on zero and the proportional residual error is
symmetric. Second, for the dose models (1 and 3) each subject’s
fractional glucose remaining at 20 mg/day, 1 - f * Emax_i
with f = 20 / (20 + ED50), is monotone in that subject’s
Emax, so its median is exactly the typical value. (The cohort median of
glucose itself at high dose sits below the typical value – by about 6%
at 20 mg/day in Model 1 even with 20 000 subjects – because the long
tail of Emax skews the product; that is why the drug effect is checked
per subject.)
typ_tab <- bind_rows(lapply(c("m1", "m2", "m3", "m4"), function(m) {
cbind(model = m, typical(m, doses))
})) |>
select(model, DOSE, typical = value)
#> ℹ omega/sigma items treated as zero: 'etale0', 'etalemax', 'etaled50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etale0', 'etalemax', 'etaled50'
#> Warning: multi-subject simulation without without 'omega'
#> ℹ omega/sigma items treated as zero: 'etalcl', 'etale0', 'etalemax', 'etalec50'
#> Warning: multi-subject simulation without without 'omega'
base_chk <- sims |>
filter(model %in% c("m1", "m2", "m3", "m4"), DOSE == 0) |>
group_by(model) |>
summarise(sim_median = median(dv), .groups = "drop") |>
left_join(filter(typ_tab, DOSE == 0), by = "model") |>
mutate(ratio = sim_median / typical)
inh_chk <- sims |>
filter(model %in% c("m1", "m3"), DOSE %in% c(0, 20)) |>
group_by(model, id) |>
summarise(frac = ipred[DOSE == 20] / ipred[DOSE == 0], .groups = "drop") |>
group_by(model) |>
summarise(sim_median = median(frac), .groups = "drop") |>
left_join(
typ_tab |>
group_by(model) |>
summarise(typical = typical[DOSE == 20] / typical[DOSE == 0]),
by = "model"
) |>
mutate(ratio = sim_median / typical)
stopifnot(nrow(base_chk) == 4, nrow(inh_chk) == 2,
!anyNA(base_chk$ratio), !anyNA(inh_chk$ratio))
bind_rows(
mutate(base_chk, check = "Glucose at 0 mg/day (mmol/L)"),
mutate(inh_chk, check = "Fraction remaining at 20 mg/day")
) |>
select(check, model, sim_median, typical, ratio) |>
dplyr::rename("Check" = check, "Model" = model,
"Simulated median" = sim_median, "Typical value" = typical,
"Ratio" = ratio) |>
knitr::kable(digits = 3)| Check | Model | Simulated median | Typical value | Ratio |
|---|---|---|---|---|
| Glucose at 0 mg/day (mmol/L) | m1 | 13.144 | 14.100 | 0.932 |
| Glucose at 0 mg/day (mmol/L) | m2 | 14.527 | 14.300 | 1.016 |
| Glucose at 0 mg/day (mmol/L) | m3 | 17.210 | 16.700 | 1.031 |
| Glucose at 0 mg/day (mmol/L) | m4 | 15.970 | 16.700 | 0.956 |
| Fraction remaining at 20 mg/day | m1 | 0.678 | 0.699 | 0.971 |
| Fraction remaining at 20 mg/day | m3 | 0.721 | 0.689 | 1.047 |
# Over ten 200-subject cohorts the largest deviations were 0.068 (baseline)
# and 0.055 (inhibition); the standard error of a 200-subject median is about
# 0.026 for each. A 15% error in E0 or a 10% error in the inhibition fails.
stopifnot(
all(abs(base_chk$ratio - 1) < 0.12),
all(abs(inh_chk$ratio - 1) < 0.10)
)
neg_pct <- 100 * mean(sims$ipred < 0)With exponential BSV on Emax (up to 55% CV), a small share of
simulated subjects draws Emax > 1 and so a negative
predicted glucose at the highest doses: 0.05% of simulated predictions
here. This is a property of the published model, not of the encoding;
see the assumptions below.
PKNCA
Not applicable. None of the six models has a PK layer or a
concentration-time course: the driver is the daily dose, or a
steady-state concentration computed algebraically from it, and each
endpoint is an algebraic function of that driver (and, for Models 5 and
6, of time within the sampling day). The deterministic checks above take
the place of an NCA comparison; the companion PK model
Rambiritch_2016_glibenclamide carries the NCA validation
for the glibenclamide concentrations.
Assumptions and deviations
-
EC50 units. Tables 1-3 head the EC50 row
mg/L. The paper’s own derived-ED50 footnotes reproduce only if EC50 is in ug/L (ng/mL), consistently across Models 2, 4 and 6, and only then does a 5 mg/day dose (Cpss about 96 ng/mL) sit near EC50, as the paper’s conclusions require. The models use ng/mL. -
BSV conversion. Every BSV is taken as
omega^2 = (CV/100)^2. For the Fourier coefficients this is the only reading consistent with the paper: their BSV is additive (Equation 6) with one variance shared by all four coefficients, and the table prints one identical number on all four rows (120.83 in Model 5, 7.05 in Model 6). That number is thereforesqrt(omega^2) x 100of the additive eta, i.e. an SD of 1.2083 and 0.0705 mmol/L. A CV reading would give four different SDs, contradicting the shared variance. - Fourier etas. “The omega matrix for all the coefficients was constrained to the same value, ie, the eta values were drawn from the same distribution” is encoded as four independent etas with equal variance, not one eta shared by all four coefficients.
- Form of the BSV on Emax, ED50 and EC50. Equation 5 states the exponential form for the baseline only; the same form is assumed for Emax, ED50 and EC50, which are positive and reported as %CV. An exponential Emax can exceed 1 in a small share of subjects, giving a negative predicted glucose at high doses.
- Near-zero BSV. ED50 in Model 1 (0.01% CV) and EC50 in Model 2 (0.04% CV) are kept as the tiny variances printed rather than dropped.
- Residual error. The glucose data were log-transformed and fitted with an additive error on the log scale; Equation 7 prints the proportional form, which is used here. The residual SD is the printed %CV (for example 15% for Model 1); the printed variance (0.02) is the same value rounded.
- CL/F in the Cpss models. The paper computed each subject’s Cpss from their individual CL/F from the companion PK model, which is not published per subject. The models use the population CL/F of 2.16 L/h with that model’s BSV of 33.91% CV, held fixed, so simulated Cpss carries the between-subject PK variability the Cpss driver was introduced to separate from PD variability. When fitting real data with known individual CL/F, the user may replace the random effect with the subject’s own value.
-
Time in Models 5 and 6.
tis hours from the fasting sample at the start of the sampling day. A continuous time axis across days stays in phase because 24 hours is three 8-hour periods, provided each day’s sampling starts at the same clock time; the series describes the 0-8 h window only. - Demographics. Taken from the companion PK paper’s Table 1 (same study), which characterises 22 of the 24 subjects.
- Units. Doses are in mg of glibenclamide and the outputs are glucose in mmol/L, so the dose and output units are deliberately of different dimensions.