9  Multiple-Dose and Steady-State

9.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. Companion chapters complete the steady-state picture: Automatic Intervals and Dosing Regimens shows the intervals PKNCA generates from the dose times, Superposition predicts steady-state profiles from single-dose data, Time to Steady State estimates when steady state is reached, and Secondary Parameters computes accumulation ratios.

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%

9.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

# ~20% log-normal inter-individual variability (IIV) on the elimination rate
# constant: one ke_i per subject
d_ke_ss <- data.frame(Subject = factor(1:n_subjects),
                      ke_i    = ke * exp(rnorm(n_subjects, 0, 0.2)))

d_conc_ss <- expand.grid(Subject = factor(1:n_subjects), time = timepoints) |>
  left_join(d_ke_ss, by = "Subject") |>
  mutate(
    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
)

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()


9.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")

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          126.  15.5  10.5    5.59   5.68  2.78     94.7 178. 
2 2          186.  20.3  15.5   10.3   10.3   1.96     64.5  96.8
3 3          154.  17.8  12.9    7.78   7.90  2.28     77.7 128. 
4 4          146.  17.2  12.2    7.27   7.19  2.37     82.4 140. 

9.4 Parameter reference

9.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). At steady state the two are equal apart from measurement error; here they differ only by the simulated residual noise.

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.68
2 1       ctrough     5.59
3 2       cstart     10.3 
4 2       ctrough    10.3 
5 3       cstart      7.90
6 3       ctrough     7.78
7 4       cstart      7.19
8 4       ctrough     7.27

9.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          126.  10.5      10.5
2 2          186.  15.5      15.5
3 3          154.  12.9      12.9
4 4          146.  12.2      12.2

9.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.78     94.7 178. 
2 2        1.96     64.5  96.8
3 3        2.28     77.7 128. 
4 4        2.37     82.4 140. 

9.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.


9.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")

# One interval per occasion
two_dose_intervals <- data.frame(
  start    = c(0, 12),
  end      = c(12, 24),
  auclast  = TRUE,
  cmax     = 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", "deg.fluc")) |>
  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    126. 
 2 1           0    12 cmax        15.5
 3 1           0    12 deg.fluc    93.7
 4 1          12    24 auclast    126. 
 5 1          12    24 cmax        15.5
 6 1          12    24 deg.fluc    94.7
 7 2           0    12 auclast    186. 
 8 2           0    12 cmax        20.3
 9 2           0    12 deg.fluc    64.5
10 2          12    24 auclast    186. 
# ℹ 14 more rows

9.5.1 Letting PKNCA choose the intervals

Without intervals, PKNCAdata() generates them from the dose and sample times (≥ 0.13.0). For these two doses it finds the dosing interval from 0 to 12 hours, and the steady-state interval one τ (12 hours) after the last dose:

o_data_2d_auto <- PKNCAdata(o_conc_2d, o_dose_2d)

o_data_2d_auto$intervals |>
  filter(Subject == 1) |>
  select(Subject, start, end, impute, aucint.last, cmax, half.life)
# A tibble: 2 × 7
  Subject start   end impute        aucint.last cmax  half.life
  <fct>   <dbl> <dbl> <chr>         <lgl>       <lgl> <lgl>    
1 1           0    12 start_cmin    TRUE        TRUE  TRUE     
2 1          12    24 start_predose TRUE        TRUE  TRUE     

The generated intervals request the aucint.* parameters with start_cmin imputation for the dosing interval and start_predose imputation at steady state. Automatic Intervals and Dosing Regimens explains how those choices are made and what happens with missed or late doses and regimen changes.


9.6 Finding tau automatically: find.tau()

When the dosing interval is not known a priori, find.tau() finds it from one subject’s dose times. In 0.13.0 it was rebuilt on find.dose.regimen() (≥ 0.13.0): the dose times are sorted and duplicates dropped, and then the repeating pattern is found. The result is matched to the usual nominal intervals (every 4 hours to every 12 weeks), so doses recorded a little early or late still give the nominal interval. Times without a unit are taken to be hours; give timeu for other units. find.tau() returns 0 for a single dose time and NA when there are no dose times or no interval repeats. Missed or off-schedule doses still give the underlying interval, with a pknca_warning_tau_irregular_dosing warning that names them.

# 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
# Recorded a little off schedule, and with the 36 h dose missed
find.tau(c(0, 11.5, 24, 48, 60.5))
Warning: Dosing is not equally spaced; using the most common interval of 12.
Doses appear to be missing after time 24.
[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

The details, including find.dose.regimen(), time units, and the tau.choices option, are in Automatic Intervals and Dosing Regimens.


9.7 Multiple-dose MRT and Vss

mrt.md.obs, mrt.md.pred, vss.md.obs, and vss.md.pred estimate MRT and the volume of distribution at steady state from a steady-state dosing interval:

\[MRT_{md} = \frac{AUMC_\tau + \tau \cdot (AUC_\infty - AUC_\tau)}{AUC_\tau} \qquad V_{ss} = CL \times MRT_{md}\]

The τ·(AUC∞ − AUCτ) term accounts for the drug still in the body at the end of the interval, which AUMCτ/AUCτ alone leaves out.

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

These parameters need τ. A tau column in the intervals gives it and takes precedence whenever it is present (≥ 0.13.0). Without that column, τ is detected from the group’s dose times with find.tau(), which needs at least two doses. The steady-state dataset above records only the profiled dose, so nothing repeats and the tau column is required:

md_intervals <- data.frame(
  start      = 0,
  end        = tau,
  tau        = tau,
  mrt.md.obs = TRUE,
  vss.md.obs = TRUE
)

o_nca_md <- pk.nca(PKNCAdata(o_conc_ss, o_dose_ss, intervals = md_intervals))

as.data.frame(o_nca_md) |>
  filter(PPTESTCD %in% c("mrt.md.obs", "cl.last", "vss.md.obs")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(Subject)
# A tibble: 4 × 4
  Subject cl.last mrt.md.obs vss.md.obs
  <fct>     <dbl>      <dbl>      <dbl>
1 1          3.17       10.1       32.0
2 2          2.15       14.9       32.0
3 3          2.59       12.3       31.9
4 4          2.73       11.9       32.6

For the one-compartment model used in the simulation, MRT is 1/ke + 1/ka ≈ 13.2 h at the typical ke of 0.08/h. The individual estimates (10.1 to 14.9 h) spread around that value because each subject has its own ke.

If τ can be neither given nor detected, these parameters are NA with a pknca_warning_tau_undetermined warning. PKNCA does not fall back to the single-dose equation. For IV infusions, use mrt.ivmd.obs, mrt.ivmd.pred, vss.ivmd.obs, and vss.ivmd.pred (≥ 0.13.0). They subtract half the infusion duration, without which MRT is high by duration/2 and Vss by CL × duration/2.


9.8 Accumulation ratio

The accumulation ratio compares a steady-state interval with the first dosing interval, for example AUCτ,ss / AUCτ,1. PKNCA calculates it directly as a secondary parameter (≥ 0.13.0): ratio.auclast, ratio.aucint.last, ratio.cmax, and the other ratio.* parameters divide a parameter in one interval by the same parameter in a reference interval. interval_add_secondary() links the steady-state interval to the first-dose interval. See Secondary Parameters for a worked accumulation-ratio example. To predict accumulation from single-dose data instead, see Superposition.