Skip to contents

Model and source

  • Citation: Fediuk DJ, Zhou S, Dawra VK, Sahasrabudhe V, Sweeney K (2021). Population Pharmacokinetic Model for Ertugliflozin in Healthy Subjects and Patients With Type 2 Diabetes Mellitus. Clinical Pharmacology in Drug Development 10(7):696-706. doi:10.1002/cpdd.885.
  • Description: Two-compartment population PK model for oral ertugliflozin in healthy adults and adults with type 2 diabetes mellitus, pooled across 15 phase 1-3 studies (Fediuk 2021; 2276 subjects, 13,691 concentrations). First-order absorption with a lag time and first-order elimination. Allometric body-weight scaling fixed at 0.75 on CL/F and Q/F and 1 on Vc/F and Vp/F (85 kg reference). Full-model covariate effects on CL/F (eGFR power term referenced to 90 mL/min/1.73 m^2 and capped at 120; T2DM, female sex, Black, Asian and other race), on Vc/F (age power term referenced to 65 years; female sex, Black, Asian and other race), and on absorption (fed and without-regard-to-food multipliers on ka and fractional decreases in relative bioavailability). IIV on CL/F only; log-scale additive residual error estimated separately for the phase 1 and the phase 2/3 studies.
  • Article: https://doi.org/10.1002/cpdd.885 (open access; PMC8359437)
  • Supplement: the Supplementary Appendix (Supporting Information file s001) holds the covariate parameterization (Equations 5-8), the study list (Table S1), the sampling schemes (Table S2) and the base-model building steps (Table S3).

Ertugliflozin is an oral sodium-glucose cotransporter 2 (SGLT2) inhibitor for type 2 diabetes mellitus (T2DM). Fediuk 2021 pooled 15 phase 1-3 studies and fitted a two-compartment model with a lag time and first-order absorption, then estimated every candidate covariate effect simultaneously (full model estimation) and reported that none was clinically relevant.

Population

The analysis data set held 13,691 ertugliflozin plasma concentrations from 2276 subjects in 9 phase 1, 2 phase 2 and 4 phase 3 studies (Table S1). Of the subjects, 2084 (91.6%) had T2DM and 192 (8.4%) were healthy. Age was 18-87 years (median 57), baseline body weight 42.6-197 kg (median 84.8), and 56.5% were male. By race 71.8% were White, 8.74% Black, 13.8% Asian and 5.62% other. MDRD eGFR had a median of 86.6 mL/min/1.73 m^2 (range 6.8-196); about 44% of subjects had normal renal function, 41% mild, 14% moderate and 1% severe renal impairment (Table 1). The phase 1 studies sampled intensively; the phase 2 and 3 studies sampled sparsely (Table S2). The phase 2 studies dosed with the morning meal, and the phase 3 studies did not record food status (dosing “without regard to food”).

The same information is available programmatically:

readModelDb("Fediuk_2021_ertugliflozin")()$population[c(
  "n_subjects", "n_studies", "age_range", "weight_range", "sex_female_pct"
)]
#> $n_subjects
#> [1] 2276
#> 
#> $n_studies
#> [1] 15
#> 
#> $age_range
#> [1] "18-87 years"
#> 
#> $weight_range
#> [1] "42.6-197 kg"
#> 
#> $sex_female_pct
#> [1] 43.5

Source trace

Every ini() value carries an in-file comment pointing to its source in inst/modeldb/specificDrugs/Fediuk_2021_ertugliflozin.R. The table collects them. All estimates are the final-model column of Table 2.

Equation / parameter Value Source location
lcl (CL/F) log(12.0) L/h Table 2
lvc (Vc/F) log(6.54) L Table 2
lvp (Vp/F) log(107) L Table 2
lq (Q/F) log(7.77) L/h Table 2
lka (ka, fasted) log(0.329) 1/h Table 2
ltlag (lag time) log(0.228) h Table 2
lfdepot (F1, fasted) fixed(log(1)) Table 2 (1.00 FIX)
e_wt_cl_q fixed(0.75) Table 2 (0.750 FIX on CL/F and Q/F)
e_wt_vc_vp fixed(1) Table 2 (1.00 FIX on Vc/F and Vp/F)
e_crcl_cl 0.455 Table 2, CL/F ‘Effect of eGFR’
e_dis_diab_cl 0.904 Table 2, CL/F ‘Effect of T2DM patient status’
e_sexf_cl 0.962 Table 2, CL/F ‘Effect of female sex’
e_race_black_cl / e_race_asian_cl / e_race_other_cl 0.985 / 1.08 / 0.992 Table 2, CL/F race rows
e_age_vc -0.243 Table 2, Vc/F ‘Effect of age’
e_sexf_vc 1.36 Table 2, Vc/F ‘Effect of female sex’
e_race_black_vc / e_race_asian_vc / e_race_other_vc 0.917 / 2.12 / 1.15 Table 2, Vc/F race rows
e_fed_ka / e_fed_missing_ka 0.726 / 0.663 Table 2, ka food rows
e_fed_fdepot / e_fed_missing_fdepot 0.0683 / 0.0809 Table 2, F1 food rows
etalcl 0.102 (variance) Table 2, omega^2 (CL/F); 32% CV in Results
expSdPh1 / expSdPh23 0.387 / 0.836 Table 2 residual rows; 38.7% and 83.6% in Results
Continuous covariates (cov/ref)^theta, refs 85 kg, 65 y, 90 mL/min/1.73 m^2 n/a Supplementary Appendix Equation 5
Categorical covariates theta^cov n/a Supplementary Appendix Equation 6
F1 = 1 - FED*theta - FED_MISSING*theta n/a Supplementary Appendix Equations 7-8
CL/F, Vc/F, Vp/F, Q/F, ka, F1 covariate model n/a Supplementary Appendix Equation 8
eGFR capped at 120 mL/min/1.73 m^2 n/a Methods, Covariate Evaluation
Cc ~ lnorm() (additive on log concentration) n/a Supplementary Appendix Equation 4
2-compartment ODEs, lag time, first-order absorption n/a Methods, PopPK Model

Typical-value covariate checks

The Results section translates the covariate effects into typical-value statements relative to a reference subject: a 65-year-old healthy White man weighing 85 kg with an eGFR of 90 mL/min/1.73 m^2, dosed fasted. Those statements depend on no random effect, so the packaged model must reproduce them to within the paper’s own rounding.

mod <- readModelDb("Fediuk_2021_ertugliflozin")
# Random effects set to zero; later chunks supply the CL/F random effect as
# a data column where between-subject variability is wanted.
mod_typical <- rxode2::zeroRe(mod)
#> ℹ parameter labels from comments will be replaced by 'label()'
#> Warning: No sigma parameters in the model

ref_subject <- tibble(
  WT = 85, AGE = 65, CRCL = 90, DIS_DIAB = 0, SEXF = 0,
  RACE_BLACK = 0, RACE_ASIAN = 0, RACE_OTHER = 0,
  FED = 0, FED_MISSING = 0, STUDY_PHASE2 = 0, STUDY_PHASE3 = 0
)

scenarios <- bind_rows(
  ref_subject |> mutate(scenario = "Reference"),
  ref_subject |> mutate(scenario = "WT 59.5 kg", WT = 59.5),
  ref_subject |> mutate(scenario = "WT 123 kg", WT = 123),
  ref_subject |> mutate(scenario = "eGFR 60", CRCL = 60),
  ref_subject |> mutate(scenario = "eGFR 45", CRCL = 45),
  ref_subject |> mutate(scenario = "eGFR 150 (capped at 120)", CRCL = 150),
  ref_subject |> mutate(scenario = "eGFR 120", CRCL = 120),
  ref_subject |> mutate(scenario = "T2DM", DIS_DIAB = 1),
  ref_subject |> mutate(scenario = "Female", SEXF = 1),
  ref_subject |> mutate(scenario = "Asian", RACE_ASIAN = 1),
  ref_subject |> mutate(scenario = "Fed", FED = 1),
  ref_subject |> mutate(scenario = "Without regard to food", FED_MISSING = 1)
) |>
  mutate(id = row_number())

# One 15 mg dose per scenario plus a dense grid on the central compartment;
# the algebraic outputs cl, vc, vp, q, ka and fdepot come back per row.
typ_events <- scenarios |>
  tidyr::crossing(time = c(0, seq(0.05, 96, by = 0.05))) |>
  mutate(
    evid = ifelse(time == 0, 1L, 0L),
    amt = ifelse(evid == 1L, 15, 0),
    cmt = ifelse(evid == 1L, "depot", "central")
  ) |>
  bind_rows(scenarios |> mutate(time = 0, evid = 0L, amt = 0, cmt = "central")) |>
  arrange(id, time, desc(evid))

typ <- rxode2::rxSolve(
  mod_typical, typ_events, returnType = "data.frame",
  keep = "scenario"
)
#> ℹ omega/sigma items treated as zero: 'etalcl'
#> Warning: multi-subject simulation without without 'omega'

typ_par <- typ |>
  group_by(scenario) |>
  summarise(
    cl = first(cl), vc = first(vc), vp = first(vp), q = first(q),
    ka = first(ka), fdepot = first(fdepot),
    cmax = max(Cc), tmax = time[which.max(Cc)],
    .groups = "drop"
  ) |>
  mutate(auc_tau_ss = 15 * fdepot / cl * 1000) # ng*h/mL, = AUCinf single dose

ref_row <- typ_par |> filter(scenario == "Reference")
typ_par <- typ_par |>
  mutate(
    cl_ratio = cl / ref_row$cl,
    auc_ratio = auc_tau_ss / ref_row$auc_tau_ss,
    vc_ratio = vc / ref_row$vc,
    ka_ratio = ka / ref_row$ka
  )

typ_par |>
  select(scenario, cl, vc, vp, q, ka, fdepot, cl_ratio, auc_ratio, vc_ratio) |>
  knitr::kable(digits = 3, caption = "Typical-value parameters by scenario.")
Typical-value parameters by scenario.
scenario cl vc vp q ka fdepot cl_ratio auc_ratio vc_ratio
Asian 12.960 13.865 107.000 7.770 0.329 1.000 1.080 0.926 2.120
Fed 12.000 6.540 107.000 7.770 0.239 0.932 1.000 0.932 1.000
Female 11.544 8.894 107.000 7.770 0.329 1.000 0.962 1.040 1.360
Reference 12.000 6.540 107.000 7.770 0.329 1.000 1.000 1.000 1.000
T2DM 10.848 6.540 107.000 7.770 0.329 1.000 0.904 1.106 1.000
WT 123 kg 15.832 9.464 154.835 10.251 0.329 1.000 1.319 0.758 1.447
WT 59.5 kg 9.183 4.578 74.900 5.946 0.329 1.000 0.765 1.307 0.700
Without regard to food 12.000 6.540 107.000 7.770 0.218 0.919 1.000 0.919 1.000
eGFR 120 13.678 6.540 107.000 7.770 0.329 1.000 1.140 0.877 1.000
eGFR 150 (capped at 120) 13.678 6.540 107.000 7.770 0.329 1.000 1.140 0.877 1.000
eGFR 45 8.754 6.540 107.000 7.770 0.329 1.000 0.730 1.371 1.000
eGFR 60 9.978 6.540 107.000 7.770 0.329 1.000 0.832 1.203 1.000

The paper’s weight statements (Results, Parameter Estimate Results) cover the 5th and 95th percentiles of observed body weight, 59.5 and 123 kg.

published_wt <- tribble(
  ~scenario,    ~parameter, ~published,
  "WT 59.5 kg", "cl",       9.18,
  "WT 123 kg",  "cl",       15.8,
  "WT 59.5 kg", "vc",       4.58,
  "WT 123 kg",  "vc",       9.46,
  "WT 59.5 kg", "vp",       75,
  "WT 123 kg",  "vp",       155,
  "WT 59.5 kg", "q",        5.95,
  "WT 123 kg",  "q",        10.3
)
wt_check <- published_wt |>
  left_join(
    typ_par |>
      select(scenario, cl, vc, vp, q) |>
      pivot_longer(-scenario, names_to = "parameter", values_to = "model"),
    by = c("scenario", "parameter")
  ) |>
  mutate(pct_diff = 100 * (model / published - 1))

wt_check |>
  rename(
    "Scenario" = scenario, "Parameter" = parameter,
    "Published" = published, "Model" = model, "% difference" = pct_diff
  ) |>
  knitr::kable(digits = 2, caption = "Body-weight extremes: model vs Results text.")
Body-weight extremes: model vs Results text.
Scenario Parameter Published Model % difference
WT 59.5 kg cl 9.18 9.18 0.04
WT 123 kg cl 15.80 15.83 0.20
WT 59.5 kg vc 4.58 4.58 -0.04
WT 123 kg vc 9.46 9.46 0.04
WT 59.5 kg vp 75.00 74.90 -0.13
WT 123 kg vp 155.00 154.84 -0.11
WT 59.5 kg q 5.95 5.95 -0.06
WT 123 kg q 10.30 10.25 -0.47

# Deterministic: the only error is the paper's 3-significant-figure rounding
# (largest 0.5%, Q at 123 kg: 10.25 vs 10.3).
stopifnot(all(abs(wt_check$pct_diff) < 1))

The AUC and volume ratios are the paper’s Figure 3 statements: AUCtau is 20% and 37% higher at an eGFR of 60 and 45 mL/min/1.73 m^2; 11% higher in T2DM, 4% higher in women and 7% lower in Asians; Vc/F is 36% higher in women and 112% higher in Asians. The food statements are Table 2 read as percent decreases: ka falls about 27% (fed) and 34% (without regard to food), and F1 about 7% and 8%.

published_ratio <- tribble(
  ~scenario,                ~quantity,   ~published,
  "eGFR 60",                "auc_ratio", 1.20,
  "eGFR 45",                "auc_ratio", 1.37,
  "T2DM",                   "auc_ratio", 1.11,
  "Female",                 "auc_ratio", 1.04,
  "Asian",                  "auc_ratio", 0.93,
  "Female",                 "vc_ratio",  1.36,
  "Asian",                  "vc_ratio",  2.12,
  "Fed",                    "ka_ratio",  0.73,
  "Without regard to food", "ka_ratio",  0.66,
  "Fed",                    "fdepot",    0.93,
  "Without regard to food", "fdepot",    0.92
)
ratio_check <- published_ratio |>
  left_join(
    typ_par |>
      select(scenario, auc_ratio, vc_ratio, ka_ratio, fdepot) |>
      pivot_longer(-scenario, names_to = "quantity", values_to = "model"),
    by = c("scenario", "quantity")
  ) |>
  mutate(abs_diff = model - published)

ratio_check |>
  rename(
    "Scenario" = scenario, "Quantity" = quantity,
    "Published" = published, "Model" = model, "Difference" = abs_diff
  ) |>
  knitr::kable(digits = 3, caption = "Covariate ratios: model vs Results text and Figure 3.")
Covariate ratios: model vs Results text and Figure 3.
Scenario Quantity Published Model Difference
eGFR 60 auc_ratio 1.20 1.203 0.003
eGFR 45 auc_ratio 1.37 1.371 0.001
T2DM auc_ratio 1.11 1.106 -0.004
Female auc_ratio 1.04 1.040 0.000
Asian auc_ratio 0.93 0.926 -0.004
Female vc_ratio 1.36 1.360 0.000
Asian vc_ratio 2.12 2.120 0.000
Fed ka_ratio 0.73 0.726 -0.004
Without regard to food ka_ratio 0.66 0.663 0.003
Fed fdepot 0.93 0.932 0.002
Without regard to food fdepot 0.92 0.919 -0.001

# Deterministic: published values are rounded to two decimals, so every
# difference must sit inside half a unit in the second place plus a margin.
stopifnot(all(abs(ratio_check$abs_diff) < 0.01))

# The eGFR cap: any value above 120 gives the eGFR-120 clearance.
stopifnot(
  isTRUE(all.equal(
    typ_par$cl[typ_par$scenario == "eGFR 150 (capped at 120)"],
    typ_par$cl[typ_par$scenario == "eGFR 120"]
  ))
)

The Results also report a mean elimination half-life of 15.3 hours for healthy subjects and 16.6 hours for patients with T2DM and normal renal function. The terminal half-life of the typical reference subject, and of the same subject with T2DM, follows in closed form from the two-compartment micro-constants.

terminal_half_life <- function(cl, vc, vp, q) {
  k10 <- cl / vc
  k12 <- q / vc
  k21 <- q / vp
  s <- k10 + k12 + k21
  beta <- (s - sqrt(s^2 - 4 * k10 * k21)) / 2
  log(2) / beta
}
hl <- typ_par |>
  filter(scenario %in% c("Reference", "T2DM")) |>
  mutate(
    half_life = terminal_half_life(cl, vc, vp, q),
    published = c(Reference = 15.3, T2DM = 16.6)[scenario],
    pct_diff = 100 * (half_life / published - 1)
  ) |>
  select(scenario, half_life, published, pct_diff)
hl |>
  rename(
    "Scenario" = scenario, "Model t1/2 (h)" = half_life,
    "Published mean t1/2 (h)" = published, "% difference" = pct_diff
  ) |>
  knitr::kable(digits = 2, caption = "Terminal half-life: typical subject vs published mean.")
Terminal half-life: typical subject vs published mean.
Scenario Model t1/2 (h) Published mean t1/2 (h) % difference
Reference 15.88 15.3 3.77
T2DM 16.56 16.6 -0.25

The typical-subject half-life is not the same statistic as the paper’s mean of individual half-lives (a mean over subjects of varying eGFR, weight, sex and race), so the agreement is expected to be close but not exact; the check below allows 10%.

stopifnot(all(abs(hl$pct_diff) < 10))

Virtual cohort

Original observed data are not publicly available. The virtual population approximates Table 1: body weight, age and eGFR drawn from truncated normal distributions with the published means and SDs, and sex, race and T2DM status drawn at the published proportions.

set.seed(20210701)
stopifnot(isTRUE(all.equal(unname(rxode2::rxode(mod)$omega[1, 1]), 0.102)))
#> ℹ parameter labels from comments will be replaced by 'label()'

rtnorm <- function(n, mean, sd, lo, hi) {
  x <- rnorm(n, mean, sd)
  bad <- x < lo | x > hi
  while (any(bad)) {
    x[bad] <- rnorm(sum(bad), mean, sd)
    bad <- x < lo | x > hi
  }
  x
}

make_cohort <- function(n, dose, id_offset = 0L) {
  race <- sample(
    c("White", "Black", "Asian", "Other"), n,
    replace = TRUE, prob = c(71.8, 8.74, 13.8, 5.62)
  )
  subj <- tibble(
    id = id_offset + seq_len(n),
    treatment = paste(dose, "mg QD"),
    WT = rtnorm(n, 86.9, 19.7, 42.6, 197),
    AGE = rtnorm(n, 55.7, 11.6, 18, 87),
    CRCL = rtnorm(n, 85.9, 24.3, 6.8, 196),
    SEXF = rbinom(n, 1, 0.435),
    RACE_BLACK = as.integer(race == "Black"),
    RACE_ASIAN = as.integer(race == "Asian"),
    RACE_OTHER = as.integer(race == "Other"),
    DIS_DIAB = 1L,
    FED = 0L, FED_MISSING = 0L,
    STUDY_PHASE2 = 0L, STUDY_PHASE3 = 0L,
    # CL/F random effect drawn in R (Table 2 omega^2 = 0.102) and supplied
    # as a data column to the zeroRe() model, so the cohort does not depend
    # on rxode2's per-thread random-number streams.
    etalcl = rnorm(n, 0, sqrt(0.102))
  )
  # Seven once-daily doses (steady state is reached well within a week for
  # a ~16 h half-life), then an intensively sampled day-7 interval and a
  # 72 h washout tail, as in the phase 1 multiple-dose studies (Table S2).
  doses <- subj |>
    tidyr::crossing(time = seq(0, 144, by = 24)) |>
    mutate(evid = 1L, amt = dose, cmt = "depot")
  obs_times <- c(0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 12, 24, seq(144, 216, by = 0.5))
  obs <- subj |>
    tidyr::crossing(time = sort(unique(obs_times))) |>
    mutate(evid = 0L, amt = 0, cmt = "central")
  bind_rows(doses, obs) |> arrange(id, time, desc(evid))
}

events <- bind_rows(
  make_cohort(200, dose = 5, id_offset = 0L),
  make_cohort(200, dose = 15, id_offset = 200L)
)
stopifnot(!anyDuplicated(unique(events[, c("id", "time", "evid")])))

Simulation

sim <- rxode2::rxSolve(
  mod_typical, events = events, keep = c("treatment"),
  returnType = "data.frame"
)
#> ℹ omega/sigma items treated as zero: 'etalcl'
#> Warning: multi-subject simulation without without 'omega'

Cc is the individual prediction (IPRED) given each subject’s covariates and drawn CL/F random effect; residual error is not added.

Replicate published figures

Figure 2 of Fediuk 2021 is a visual predictive check of dose-normalized concentration against time after dose, pooled across the 15 studies. The pooled observed data are not reproducible, so the analogous panel below shows the dose-normalized day-7 steady-state profile of the virtual T2DM cohort, with the median and 95% prediction interval.

sim |>
  filter(time >= 144, time <= 168) |>
  mutate(
    tad = time - 144,
    dn = Cc / ifelse(treatment == "5 mg QD", 5, 15)
  ) |>
  group_by(tad) |>
  summarise(
    Q025 = quantile(dn, 0.025),
    Q50 = median(dn),
    Q975 = quantile(dn, 0.975),
    .groups = "drop"
  ) |>
  ggplot(aes(tad, Q50)) +
  geom_ribbon(aes(ymin = Q025, ymax = Q975), alpha = 0.25) +
  geom_line() +
  scale_y_log10() +
  labs(
    x = "Time after dose (h)",
    y = "Dose-normalized ertugliflozin (ng/mL per mg)",
    title = "Steady-state dose-normalized profile, virtual T2DM cohort",
    caption = "Analogous to Figure 2B (log scale) of Fediuk 2021."
  )

PKNCA validation

Steady state, 5 and 15 mg once daily

ss_conc <- sim |>
  filter(!is.na(Cc), time >= 144) |>
  mutate(Cc = pmax(Cc, 0)) |>
  select(id, time, Cc, treatment)

ss_dose <- events |>
  filter(evid == 1) |>
  select(id, time, amt, treatment)

conc_obj <- PKNCA::PKNCAconc(ss_conc, Cc ~ time | treatment + id)
dose_obj <- PKNCA::PKNCAdose(ss_dose, amt ~ time | treatment + id)
# Day-7 dosing interval for exposure; the 72 h washout after the last dose
# for the terminal half-life.
intervals_ss <- data.frame(
  start = c(144, 144), end = c(168, 216),
  cmax = c(TRUE, FALSE), tmax = c(TRUE, FALSE),
  auclast = c(TRUE, FALSE), cav = c(TRUE, FALSE),
  half.life = c(FALSE, TRUE)
)
nca_ss <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  conc_obj, dose_obj,
  intervals = intervals_ss
))
# Select each metric from its own interval: requesting half.life on the
# washout interval also emits companion rows (e.g. tmax) for that interval,
# which a group_by(PPTESTCD) summary would otherwise pool silently.
nca_ss_ind <- as.data.frame(nca_ss$result) |>
  filter(
    (end == 168 & PPTESTCD %in% c("cmax", "tmax", "auclast", "cav")) |
      (end == 216 & PPTESTCD == "half.life")
  )
stopifnot(
  !anyDuplicated(nca_ss_ind[, c("id", "PPTESTCD")]),
  nrow(nca_ss_ind) == 5 * length(unique(ss_conc$id))
)
nca_ss_summary <- nca_ss_ind |>
  group_by(treatment, PPTESTCD) |>
  summarise(median = median(PPORRES, na.rm = TRUE), .groups = "drop") |>
  pivot_wider(names_from = PPTESTCD, values_from = median)

nca_ss_summary |>
  rename(
    "Regimen" = treatment,
    "Cmax (ng/mL)" = cmax,
    "Tmax (h)" = tmax,
    "AUCtau (ng*h/mL)" = auclast,
    "Cav (ng/mL)" = cav,
    "t1/2 (h)" = half.life
  ) |>
  knitr::kable(digits = 1, caption = "Median steady-state NCA of the virtual T2DM cohort.")
Median steady-state NCA of the virtual T2DM cohort.
Regimen AUCtau (ng*h/mL) Cav (ng/mL) Cmax (ng/mL) t1/2 (h) Tmax (h)
15 mg QD 1421.8 59.2 201.8 16.8 1.5
5 mg QD 478.8 20.0 69.6 16.8 1.5

The model is linear, so the exposure must be dose-proportional, and the median PKNCA half-life of this T2DM cohort (mixed renal function) should sit near the published 16.6 h for T2DM patients with normal renal function.

auc_by_dose <- setNames(nca_ss_summary$auclast, nca_ss_summary$treatment)
# Two independent 200-subject cohorts: the dose-normalized median AUC ratio
# is 1 in expectation; the between-cohort spread of the median is a few
# percent at 32% CV on CL/F.
stopifnot(
  abs(auc_by_dose[["15 mg QD"]] / (3 * auc_by_dose[["5 mg QD"]]) - 1) < 0.1,
  abs(median(nca_ss_summary$half.life) / 16.6 - 1) < 0.15
)

Food effect, single 15 mg dose

The Discussion compares the model’s food effects with a phase 1 food-effect study (study I-8, 15 mg single dose in healthy subjects), in which the fed state lowered Cmax by 29%, delayed median Tmax by 1 hour and lowered AUCinf by about 8%. The same subjects are simulated here under both prandial states by passing each subject’s CL/F random effect as a data column, so the fed-to-fasted ratios are paired within subject.

n_food <- 200
food_subj <- tibble(
  id = seq_len(n_food),
  WT = rtnorm(n_food, 75, 12, 50, 110),
  AGE = rtnorm(n_food, 35, 10, 18, 55),
  CRCL = rtnorm(n_food, 105, 15, 80, 196),
  SEXF = rbinom(n_food, 1, 0.3),
  RACE_BLACK = 0L, RACE_ASIAN = 0L, RACE_OTHER = 0L,
  DIS_DIAB = 0L, FED_MISSING = 0L, STUDY_PHASE2 = 0L, STUDY_PHASE3 = 0L,
  etalcl = rnorm(n_food, 0, sqrt(0.102))
)
food_events <- bind_rows(
  food_subj |> mutate(treatment = "Fasted", FED = 0L),
  food_subj |> mutate(treatment = "Fed", FED = 1L, id = id + n_food)
) |>
  tidyr::crossing(time = c(seq(0, 6, by = 0.25), 8, 10, 12, 16, 24, 36, 48, 72, 96)) |>
  mutate(evid = 0L, amt = 0, cmt = "central")
food_events <- bind_rows(
  food_events,
  food_events |>
    distinct(id, .keep_all = TRUE) |>
    mutate(time = 0, evid = 1L, amt = 15, cmt = "depot")
) |>
  arrange(id, time, desc(evid))

food_sim <- rxode2::rxSolve(
  mod_typical, food_events, keep = "treatment",
  returnType = "data.frame"
)
#> ℹ omega/sigma items treated as zero: 'etalcl'
#> Warning: multi-subject simulation without without 'omega'

food_conc <- food_sim |>
  filter(!is.na(Cc)) |>
  mutate(Cc = pmax(Cc, 0)) |>
  select(id, time, Cc, treatment)
food_dose <- food_events |>
  filter(evid == 1) |>
  select(id, time, amt, treatment)

nca_food <- PKNCA::pk.nca(PKNCA::PKNCAdata(
  PKNCA::PKNCAconc(food_conc, Cc ~ time | treatment + id),
  PKNCA::PKNCAdose(food_dose, amt ~ time | treatment + id),
  intervals = data.frame(start = 0, end = Inf, cmax = TRUE, tmax = TRUE, aucinf.obs = TRUE)
))

food_ind <- as.data.frame(nca_food$result) |>
  filter(PPTESTCD %in% c("cmax", "tmax", "aucinf.obs")) |>
  mutate(subject = ifelse(id > n_food, id - n_food, id)) |>
  select(subject, treatment, PPTESTCD, PPORRES) |>
  pivot_wider(names_from = treatment, values_from = PPORRES)

food_summary <- food_ind |>
  group_by(PPTESTCD) |>
  summarise(
    model = ifelse(
      first(PPTESTCD) == "tmax",
      median(Fed) - median(Fasted),
      exp(mean(log(Fed / Fasted)))
    ),
    .groups = "drop"
  ) |>
  mutate(
    published = c(aucinf.obs = 0.92, cmax = 0.71, tmax = 1)[PPTESTCD],
    statistic = ifelse(
      PPTESTCD == "tmax", "median Tmax difference (h)", "geometric mean ratio fed/fasted"
    )
  ) |>
  select(PPTESTCD, statistic, model, published)

food_summary |>
  rename(
    "NCA parameter" = PPTESTCD, "Statistic" = statistic,
    "Model" = model, "Published (study I-8)" = published
  ) |>
  knitr::kable(digits = 2, caption = "Food effect, 15 mg single dose: model vs Discussion.")
Food effect, 15 mg single dose: model vs Discussion.
NCA parameter Statistic Model Published (study I-8)
aucinf.obs geometric mean ratio fed/fasted 0.93 0.92
cmax geometric mean ratio fed/fasted 0.72 0.71
tmax median Tmax difference (h) 0.00 1.00
fe <- setNames(food_summary$model, food_summary$PPTESTCD)
stopifnot(
  # AUC ratio is exactly 1 - 0.0683 per subject (linear model, paired CL/F);
  # the published ~8% decrease is a separate NCA study, rounded.
  abs(fe[["aucinf.obs"]] - 0.9317) < 0.005,
  # Cmax: the typical reference subject gives 0.715 (0.05 h grid, above); the
  # paired cohort ratio depends only weakly on CL/F and weight. The Discussion
  # reports a 29% decrease (ratio 0.71).
  abs(fe[["cmax"]] - 0.71) < 0.05
)

The simulated AUCinf ratio is 0.93, in line with the published 8% decrease, and the Cmax ratio of 0.72 reproduces the 29% Cmax decrease of the dedicated food-effect study. The model’s median Tmax delay is 0 h on this 0.25 h grid (0.25 h for the typical subject on the 0.05 h grid above) against the observed median delay of 1 hour. The pooled model carries a single lag time for all prandial states and slows only ka with food, so it does not reproduce the full Tmax shift of a standardized food-effect crossover; Tmax is shown as a descriptive comparison and is not gated.

Assumptions and deviations

  • Residual error is on the SD scale. Table 2 labels the two residual rows only ‘residual error’ (0.387 and 0.836), while it labels the IIV row explicitly as a variance (omega^2 0.102). The Results report the residual errors as 38.7% and 83.6% and the IIV as 32%, i.e. the authors took the square root of the variance for the IIV but not for the residual rows. The residual values are therefore read as log-scale SDs and used directly in lnorm().
  • The phase 2 and phase 3 studies share one residual SD. The model selects it when either STUDY_PHASE2 or STUDY_PHASE3 is 1; set both to 0 (phase 1 residual) for a richly sampled profile.
  • “Without regard to food” is a missing-food-status stratum. The phase 3 studies did not document food intake, so the corresponding indicator is encoded as FED_MISSING, not as a meal type. Use it only to reproduce the phase 3 setting; simulate a defined fasted or fed state with FED_MISSING = 0.
  • eGFR cap. The authors set eGFR values above 120 mL/min/1.73 m^2 to 120 in the data set; model() applies the same cap, so users can supply uncapped eGFR.
  • Virtual cohort. Body weight, age and eGFR were drawn from truncated normal distributions with the Table 1 means, SDs and ranges, independently of each other; the paper reports a correlation of -0.531 between age and eGFR that the cohort does not reproduce. Every virtual subject in the steady-state cohort has T2DM. The food-effect cohort’s demographics (young healthy adults) are assumptions, since study I-8 demographics are not reported in the paper.
  • Base model not packaged. Table 2 also reports the final base model (no covariates beyond weight and food). Only the final model is packaged.
  • No erratum was found for this article (Europe PMC search, 2026-09-28).