8  Multiple-Dose and Steady-State

8.1 Multiple-dose NCA concepts

At steady state, the dosing interval τ (tau) is the key unit of analysis. Instead of analyzing from 0 to ∞, you analyze within one dosing interval [0, τ]. Additional parameters describe how concentrations fluctuate over the interval. Two companion chapters complete the steady-state picture: Superposition predicts steady-state profiles (and accumulation ratios) from single-dose data, and Time to Steady State estimates when steady state is reached.

Term Meaning
τ (tau) Dosing interval (h)
Ctrough Concentration at end of τ (= concentration at time = end of interval)
Cmin Minimum observed concentration within τ
Cmax Peak concentration within τ
Cav Average concentration over τ = AUCτ / τ
Peak-trough ratio (PTR) Cmax / Ctrough
Degree of fluctuation (Cmax − Cmin) / Cav × 100%
Swing (Cmax − Cmin) / Cmin × 100%

8.2 Synthetic steady-state dataset

We simulate steady-state oral theophylline to demonstrate all parameters. The key is that start and end define a single dosing interval.

set.seed(42)

# Theoph-like PK: Ka=1.5/h, ke=0.08/h, V=30L, dose=400mg
ka <- 1.5; ke <- 0.08; V <- 30; dose <- 400; tau <- 12

# One-compartment oral steady-state: C(t) = [dose/V * ka/(ka-ke)] * [e^(-ke*t)/(1-e^(-ke*tau)) - e^(-ka*t)/(1-e^(-ka*tau))]
C_ss <- function(t, ka, ke, V, dose, tau) {
  A <- (dose / V) * (ka / (ka - ke))
  A * (exp(-ke * t) / (1 - exp(-ke * tau)) - exp(-ka * t) / (1 - exp(-ka * tau)))
}

timepoints <- c(0, 0.5, 1, 1.5, 2, 3, 4, 6, 8, 10, 12)
n_subjects <- 4

d_conc_ss <- expand.grid(Subject = factor(1:n_subjects), time = timepoints) |>
  mutate(
    # Add ~20% log-normal inter-individual variability (IIV) on the elimination rate constant (ke)
    ke_i  = ke * exp(rnorm(n(), 0, 0.2)),
    conc  = mapply(C_ss, time, ka, ke_i, V, dose, tau) + rnorm(n(), 0, 0.05),
    conc  = pmax(conc, 0)
  ) |>
  select(Subject, time, conc)

d_dose_ss <- data.frame(
  Subject = factor(1:n_subjects),
  dose    = dose,
  time    = 0,
  route   = "extravascular"
)

ggplot(d_conc_ss, aes(x = time, y = conc, group = Subject, colour = Subject)) +
  geom_line() + geom_point() +
  labs(title = "Simulated steady-state oral profiles (one dosing interval)",
       x = "Time within interval (h)", y = "Concentration (mg/L)") +
  theme_minimal()


8.3 Basic steady-state NCA

For a single dosing interval, set end = tau. Parameters like cav, ctrough, ptr, deg.fluc, and swing all require a finite interval end.

o_conc_ss <- PKNCAconc(d_conc_ss, conc ~ time | Subject)
o_dose_ss <- PKNCAdose(d_dose_ss, dose ~ time | Subject, route = "extravascular")
Found column named route, using it for the attribute of the same name.
ss_intervals <- data.frame(
  start    = 0,
  end      = tau,
  auclast  = TRUE,
  cmax     = TRUE,
  tmax     = TRUE,
  ctrough  = TRUE,
  cstart   = TRUE,
  cav      = TRUE,
  ptr      = TRUE,
  deg.fluc = TRUE,
  swing    = TRUE
)

o_data_ss <- PKNCAdata(o_conc_ss, o_dose_ss, intervals = ss_intervals)
o_nca_ss  <- pk.nca(o_data_ss)

as.data.frame(o_nca_ss) |>
  filter(PPTESTCD %in% c("auclast", "cmax", "ctrough", "cstart", "cav",
                          "ptr", "deg.fluc", "swing")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(Subject)
# A tibble: 4 × 9
  Subject auclast  cmax   cav ctrough cstart   ptr deg.fluc swing
  <fct>     <dbl> <dbl> <dbl>   <dbl>  <dbl> <dbl>    <dbl> <dbl>
1 1          156.  23.1  13.0    8.15   5.59  2.84     135.  314.
2 2          196.  28.4  16.3    9.75  10.3   2.91     114.  191.
3 3          179.  27.4  14.9    6.90   7.78  3.97     138.  297.
4 4          174.  22.5  14.5   10.8    7.27  2.08     105.  210.

8.4 Parameter reference

8.4.1 Cstart and Ctrough

cstart is the concentration at the start of the interval (time = start). For steady state this equals the pre-dose trough. ctrough is the concentration at the end of the interval (time = end). For steady state, cstartctrough.

as.data.frame(o_nca_ss) |>
  filter(PPTESTCD %in% c("cstart", "ctrough")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  arrange(Subject, PPTESTCD)
# A tibble: 8 × 3
  Subject PPTESTCD PPORRES
  <fct>   <chr>      <dbl>
1 1       cstart      5.59
2 1       ctrough     8.15
3 2       cstart     10.3 
4 2       ctrough     9.75
5 3       cstart      7.78
6 3       ctrough     6.90
7 4       cstart      7.27
8 4       ctrough    10.8 

8.4.2 Cav — average concentration

cav = AUCτ / τ. Useful for computing clearance at steady state (CL/F = dose / (AUCτ)).

as.data.frame(o_nca_ss) |>
  filter(PPTESTCD %in% c("cav", "auclast")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  mutate(cav_check = auclast / tau) |>
  arrange(Subject)
# A tibble: 4 × 4
  Subject auclast   cav cav_check
  <fct>     <dbl> <dbl>     <dbl>
1 1          156.  13.0      13.0
2 2          196.  16.3      16.3
3 3          179.  14.9      14.9
4 4          174.  14.5      14.5

8.4.3 PTR, degree of fluctuation, swing

These describe how much concentrations vary within a dosing interval — important for characterizing exposure variability and clinical relevance.

Parameter Formula
ptr Cmax / Ctrough
deg.fluc (Cmax − Cmin) / Cav × 100%
swing (Cmax − Cmin) / Cmin × 100%
as.data.frame(o_nca_ss) |>
  filter(PPTESTCD %in% c("ptr", "deg.fluc", "swing")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(Subject)
# A tibble: 4 × 4
  Subject   ptr deg.fluc swing
  <fct>   <dbl>    <dbl> <dbl>
1 1        2.84     135.  314.
2 2        2.91     114.  191.
3 3        3.97     138.  297.
4 4        2.08     105.  210.

8.4.4 Ceoi — concentration at end of infusion

ceoi is the concentration at the end of infusion (time = dose time + duration). It is meaningful only for infusion dosing with a nonzero duration; for bolus or oral dosing (duration 0) it is generally not meaningful — it simply returns the concentration at the dose time.

# 100 mg infused over 1 h into V = 30 L with CL = 5 L/h (plateau = rate/CL = 20 mg/L)
ke_inf <- 5 / 30
t_inf  <- c(0, 0.5, 1, 1.5, 2, 4, 8, 12)
c_inf  <- ifelse(t_inf <= 1, 20 * (1 - exp(-ke_inf * t_inf)),
                 20 * (1 - exp(-ke_inf)) * exp(-ke_inf * (t_inf - 1)))
d_inf      <- data.frame(Subject = 1, time = t_inf, conc = round(c_inf, 3))
d_dose_inf <- data.frame(Subject = 1, dose = 100, time = 0, duration = 1)

o_conc_inf <- PKNCAconc(d_inf, conc ~ time | Subject)
o_dose_inf <- PKNCAdose(d_dose_inf, dose ~ time | Subject,
                        route = "intravascular", duration = "duration")
o_nca_inf  <- pk.nca(PKNCAdata(o_conc_inf, o_dose_inf,
                               intervals = data.frame(start = 0, end = 12,
                                                      ceoi = TRUE, cmax = TRUE)))

as.data.frame(o_nca_inf) |>
  filter(PPTESTCD %in% c("ceoi", "cmax")) |>
  select(PPTESTCD, PPORRES)
# A tibble: 2 × 2
  PPTESTCD PPORRES
  <chr>      <dbl>
1 cmax        3.07
2 ceoi        3.07

Here ceoi equals cmax because an infusion peaks exactly when the pump stops. After bolus or oral dosing there is no infusion end, so request cmax/tmax instead.


8.5 Multiple dosing occasions in one dataset

When data span multiple dose occasions, define one interval row per occasion per group. PKNCA applies each interval independently. For the full interval-specification rules, see the vignette Selection of Calculation Intervals.

# Two dosing intervals: 0-12h and 12-24h
# Drop the duplicate t=12 row before shifting; PKNCA uses a boundary sample for both adjacent intervals
d_conc_occ1 <- d_conc_ss |> filter(time < 12)   # first interval: 0 to <12
d_conc_occ2 <- d_conc_ss |> mutate(time = time + 12)  # second interval: 12 to 24

d_conc_2dose <- bind_rows(d_conc_occ1, d_conc_occ2)

# Two dose events
d_dose_2dose <- bind_rows(
  d_dose_ss |> mutate(time = 0),
  d_dose_ss |> mutate(time = 12)
)

o_conc_2d <- PKNCAconc(d_conc_2dose, conc ~ time | Subject)
o_dose_2d <- PKNCAdose(d_dose_2dose, dose ~ time | Subject, route = "extravascular")
Found column named route, using it for the attribute of the same name.
# One interval per occasion
two_dose_intervals <- data.frame(
  start    = c(0, 12),
  end      = c(12, 24),
  auclast  = TRUE,
  cmax     = TRUE,
  ctrough  = TRUE,
  deg.fluc = TRUE
)

o_nca_2d <- pk.nca(PKNCAdata(o_conc_2d, o_dose_2d, intervals = two_dose_intervals))

as.data.frame(o_nca_2d) |>
  filter(PPTESTCD %in% c("auclast", "cmax", "ctrough")) |>
  select(Subject, start, end, PPTESTCD, PPORRES) |>
  arrange(Subject, start, PPTESTCD)
# A tibble: 24 × 5
   Subject start   end PPTESTCD PPORRES
   <fct>   <dbl> <dbl> <chr>      <dbl>
 1 1           0    12 auclast   152.  
 2 1           0    12 cmax       23.1 
 3 1           0    12 ctrough     5.59
 4 1          12    24 auclast   156.  
 5 1          12    24 cmax       23.1 
 6 1          12    24 ctrough    NA   
 7 2           0    12 auclast   197.  
 8 2           0    12 cmax       28.4 
 9 2           0    12 ctrough    10.3 
10 2          12    24 auclast   196.  
# ℹ 14 more rows

8.6 Finding tau automatically: find.tau()

When the dosing interval is not known a priori, find.tau() estimates it from a numeric vector of dose times. If the doses are equally spaced it returns that spacing; otherwise it searches the candidate intervals in tau.choices for the repeating interval most consistent with the dose times. Sentinel values flag special cases: 0 when all doses occur at a single time, -1 when no candidate interval repeats, and NA when tau cannot be determined.

# Dose times for one subject: 0, 12, 24, 36, 48, 60 h
dose_times <- c(0, 12, 24, 36, 48, 60)
find.tau(dose_times)
[1] 12

Detect tau from the two-occasion dataset above:

# find.tau() expects one subject's dose times — filter before calling
tau_detected <- find.tau(d_dose_2dose$time[d_dose_2dose$Subject == 1])
cat("Detected tau:", tau_detected, "h\n")
Detected tau: 12 h

8.7 Multiple-dose Vss (vss.md.*)

vss.md.obs and vss.md.pred are estimates of the volume of distribution at steady state using the multiple-dose MRT formula:

\[V_{ss} = CL \times MRT_{md}\]

where mrt.md accounts for the accumulation over repeated dosing.

Parameter Meaning
mrt.md.obs Multiple-dose MRT (observed Clast)
mrt.md.pred Multiple-dose MRT (predicted Clast)
vss.md.obs Vss from mrt.md.obs and cl.last
vss.md.pred Vss from mrt.md.pred and cl.last

Note: As of the version this book was built against, vss.md.* may fail to resolve its MRT inputs from the interval specification (the calculation errors rather than returning a value). If you need Vss at steady state, compute it manually from cl.* and mrt.md.* results.


pkgdown reference: PKNCAconc() · PKNCAdose() · PKNCAdata() · pk.nca() · find.tau()