12  Urine Excretion

12.1 Urine NCA overview

Urine NCA characterizes how much drug is excreted in urine over time. Key parameters:

Parameter Formula Meaning
ae Σ(concentration × volume) Amount excreted in urine (mass)
fe ae / dose Fraction of dose excreted unchanged
clr.obs ae / AUCinf.obs Renal clearance — using observed Clast
clr.pred ae / AUCinf.pred Renal clearance — using predicted Clast
clr.last ae / AUClast Renal clearance — no extrapolation

Renal clearance needs values from two profiles (the urine amount and the plasma AUC), which makes clr.* a secondary parameter. Since PKNCA 0.13.0 it is calculated in the same pk.nca() call as the rest of the analysis (shown below); the Secondary Parameters chapter explains the linkage in general.


12.2 Data structure for urine

Urine NCA needs the urine volume collected in each interval, named by the volume argument to PKNCAconc(). The urine data can be a concentration object of their own, or share one with the plasma data, distinguished by a grouping column; sharing one is what lets renal clearance be calculated in the same run (see below).

The key distinction from plasma NCA: urine concentrations represent interval averages rather than timepoint measurements. This dataset records each collection’s end time as time — sufficient for amount parameters like ae, which only need each row to fall inside the analysis interval. PKNCA’s full urine convention is time as the collection start plus a duration column giving the collection length; the excretion-rate section below rebuilds the data that way.

# Simulate urine collection for 4 subjects after a 100 mg IV bolus
# Collections at: 0-2h, 2-6h, 6-12h, 12-24h
set.seed(99)

urine_collections <- expand.grid(
  Subject     = factor(1:4),
  end_time    = c(2, 6, 12, 24)
) |>
  mutate(
    # Simulated urine volume (mL) and concentration (mcg/mL)
    volume_mL  = round(runif(n(), 80, 300)),
    # Fraction excreted: ~30% in first 2h, 25% 2-6h, 20% 6-12h, 15% 12-24h
    ae_target  = 100 * c(0.30, 0.25, 0.20, 0.15)[match(end_time, c(2, 6, 12, 24))],
    ae_indiv   = ae_target * exp(rnorm(n(), 0, 0.1)),  # mild IIV
    conc_mcg_mL = ae_indiv / volume_mL * 1000           # conc = amount / volume
  ) |>
  select(Subject, time = end_time, conc = conc_mcg_mL, volume = volume_mL)

head(urine_collections)
  Subject time      conc volume
1       1    2 138.40812    209
2       2    2 251.02891    105
3       3    2 120.53715    231
4       4    2 110.38941    298
5       1    6 136.09722    198
6       2    6  66.39376    293

Renal clearance needs a plasma AUC, so we also simulate matched plasma profiles.

# Plasma concentrations (IV bolus, same subjects)
ke <- 0.1  # h^-1
d_plasma <- expand.grid(
  Subject = factor(1:4),
  time    = c(0.5, 1, 2, 4, 6, 8, 12, 16, 24)
) |>
  mutate(
    ke_i = ke * exp(rnorm(n(), 0, 0.15)),
    conc = 100 / 30 * exp(-ke_i * time)   # dose/V * e^(-ke*t)
  )

d_dose <- data.frame(
  Subject = factor(1:4),
  dose    = 100,    # mg
  time    = 0,
  route   = "intravascular"
)

12.3 Setting up urine PKNCAconc

Pass the urine volume column name via the volume argument; the unit arguments (concu, amountu, timeu, …) are optional and set units. For assigning and converting units, see Unit Assignment and Conversion with PKNCA. The formula uses the same conc ~ time | Subject structure.

o_conc_plasma <- PKNCAconc(d_plasma, conc ~ time | Subject)
o_conc_urine  <- PKNCAconc(
  urine_collections,
  conc ~ time | Subject,
  volume = "volume"    # column with urine volume (mL)
)

o_dose <- PKNCAdose(d_dose, dose ~ time | Subject, route = "intravascular")
Found column named route, using it for the attribute of the same name.

Requesting a parameter that needs a sample volume (ae, fe, volpk, clr.*) from a PKNCAconc() object without volume is an error naming those parameters (pknca_error_missing_conc_input, ≥ 0.13.0); likewise the excretion-rate parameters ermax, ertmax, ertlst, and erlst need duration. Earlier versions added empty volume and duration columns to every concentration object and reported such parameters as not calculated.


12.4 Renal clearance in one pk.nca() call (≥ 0.13.0)

Put the plasma and urine concentrations in one data frame, distinguished by a grouping column (here specimen), and give the urine volume through volume. Plasma rows have no volume. The volume is converted to liters so that conc × volume (mcg/mL × L = mg) is in the same mass unit as the dose and the plasma AUC (mg·h/L), which makes fe a fraction and clr.* come out in L/h:

d_conc_all <- bind_rows(
  d_plasma |>
    transmute(Subject, specimen = "plasma", time, conc, volume_L = NA_real_),
  urine_collections |>
    transmute(Subject, specimen = "urine", time, conc, volume_L = volume / 1000)
)

o_conc_all <- PKNCAconc(d_conc_all, conc ~ time | specimen + Subject,
                        volume = "volume_L")

The interval specification has one row per specimen. The plasma profile has no sample at the dose time, so the plasma row imputes a zero there (impute column, used through PKNCAdata(impute = "impute")); the urine row imputes nothing, because adding a concentration at the start of a urine collection is not meaningful. interval_add_secondary() then requests clr.last and clr.obs on the urine row and points them at the plasma row:

d_intervals_renal <-
  data.frame(
    specimen   = c("plasma", "urine"),
    start      = 0,
    end        = 24,
    impute     = c("start_conc0", NA),
    auclast    = c(TRUE, FALSE),
    aucinf.obs = c(TRUE, FALSE),
    ae         = c(FALSE, TRUE),
    fe         = c(FALSE, TRUE)
  ) |>
  interval_add_secondary(param = "clr.last",
                         reference = data.frame(specimen = "plasma")) |>
  interval_add_secondary(param = "clr.obs",
                         reference = data.frame(specimen = "plasma"))

o_data_renal <- PKNCAdata(o_conc_all, o_dose,
                          intervals = d_intervals_renal, impute = "impute")
o_nca_renal  <- pk.nca(o_data_renal)

results <- as.data.frame(o_nca_renal) |>
  filter(PPTESTCD %in% c("auclast", "aucinf.obs", "ae", "fe", "clr.obs", "clr.last")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(Subject)

results
# A tibble: 4 × 7
  Subject auclast aucinf.obs    ae    fe clr.last clr.obs
  <fct>     <dbl>      <dbl> <dbl> <dbl>    <dbl>   <dbl>
1 1          29.0       35.2  91.8 0.918     3.17    2.61
2 2          30.8       33.1  78.8 0.788     2.56    2.38
3 3          32.7       36.5  82.2 0.822     2.51    2.25
4 4          30.5       36.1  92.9 0.929     3.05    2.57

The plasma AUCs and the urine amounts come from their own rows, and clr.obs and clr.last are reported on the urine row that requested them (as.data.frame(o_nca_renal) shows each result’s specimen, and a specimen_ref column naming the plasma profile it used). Request clr.pred the same way to divide by AUCinf,pred. fe and clr.* are calculated even when ae is not requested.

When the plasma profile needs no per-interval setting, even the plasma row and the linkage can be left out: requesting clr.obs on a urine interval alone makes pk.nca() find the plasma profile and create the interval it needs, reported with a message. That derivation, group_ref for choosing among several candidate profiles, and how units are composed for a renal clearance whose amount and AUC have different units are covered in the Secondary Parameters chapter.

A renal clearance with no AUC to divide by (no reference interval and none derivable) is NA with the reason in the exclude column. Before 0.13.0 it silently divided by zero and reported Inf.

For a real IV analysis, back-extrapolate C0 instead — see the Intravascular chapter; conc = 0 at t = 0 is used here only to keep the simulation short.


12.5 Interpreting results

results |>
  mutate(across(where(is.numeric), \(x) round(x, 3))) |>
  select(Subject, ae, fe, clr.obs, clr.last) |>
  knitr::kable(
    col.names = c("Subject", "Ae (mg)", "fe", "CLr.obs (L/h)", "CLr.last (L/h)"),
    caption = "Urine NCA results — simulated IV bolus (dose = 100 mg)"
  )
Urine NCA results — simulated IV bolus (dose = 100 mg)
Subject Ae (mg) fe CLr.obs (L/h) CLr.last (L/h)
1 91.844 0.918 2.609 3.172
2 78.781 0.788 2.382 2.558
3 82.223 0.822 2.252 2.511
4 92.938 0.929 2.573 3.049

Interpretation guide:

  • fe near 0.9 means ~90% of the dose is renally excreted unchanged — renal excretion dominates elimination for this simulated drug. A low fe would instead point to metabolism or other elimination routes.
  • clr.obs ≈ GFR (120 mL/min = 7.2 L/h) suggests renal filtration; higher values indicate active secretion.
  • clr.last > clr.obs because the same Ae is divided by the smaller AUClast; the gap grows when sampling misses late elimination.

12.6 Using the PKNCA ae parameter natively

ae can also be calculated from a urine-only concentration object, such as o_conc_urine created above with the volume in mL:

# o_conc_urine (created above) already carries the volume column
ur_intervals <- data.frame(
  start = 0,
  end   = 24,
  ae    = TRUE
)

o_data_ae <- PKNCAdata(o_conc_urine, o_dose, intervals = ur_intervals)
o_nca_ae  <- pk.nca(o_data_ae)

as.data.frame(o_nca_ae) |>
  filter(PPTESTCD == "ae") |>
  select(Subject, PPORRES) |>
  arrange(Subject)
# A tibble: 4 × 2
  Subject PPORRES
  <fct>     <dbl>
1 1        91844.
2 2        78781.
3 3        82223.
4 4        92938.

Note the units: ae here is in mcg (urine concentration in mcg/mL × volume in mL), so ~90,000 mcg matches the ~90 mg of the combined analysis above, where the volume was in liters. Units can be declared with the unit arguments of PKNCAconc(); there is no separate volume unit, so amountu is the unit of conc × volume.


12.7 New urine parameters (≥ 0.13.0)

12.7.1 volpk — total collection volume

volpk is the sum of urine volumes across all collection periods within the analysis interval. When you pass volume = "volume" to PKNCAconc(), PKNCA can compute this automatically:

vol_interval <- data.frame(
  start  = 0,
  end    = 24,
  ae     = TRUE,
  volpk  = TRUE    # sum of urine volumes over the interval
)

o_nca_vol <- pk.nca(PKNCAdata(o_conc_urine, o_dose, intervals = vol_interval))
as.data.frame(o_nca_vol) |>
  filter(PPTESTCD %in% c("ae", "volpk")) |>
  select(Subject, PPTESTCD, PPORRES)
# A tibble: 8 × 3
  Subject PPTESTCD PPORRES
  <fct>   <chr>      <dbl>
1 1       volpk       689 
2 1       ae        91844.
3 2       volpk       737 
4 2       ae        78781.
5 3       volpk       891 
6 3       ae        82223.
7 4       volpk       855 
8 4       ae        92938.

12.7.2 Excretion rate parameters

Parameter Description
ermax Maximum excretion rate (amount/time) within the interval
ertmax Midpoint collection time of the maximum excretion rate period
ertlst Midpoint collection time of the last measurable excretion rate
erlst Last measurable (positive) excretion rate, from the collection at ertlst
erint Excretion rate over the whole interval: ae / (end - start)

ermax, ertmax, ertlst, and erlst mirror the plasma-side cmax / tmax / tlast / clast.obs but apply to the excretion rate of each collection (amount excreted in the collection divided by its duration). erint and erlst (≥ 0.13.0) report the CDISC parameters ERINT and ERLST. erint divides by the interval length rather than by the summed collection durations, so a gap between collections lowers it, and an interval ending at Inf gives NA.

# Excretion rate needs each collection's duration, and PKNCA takes urine `time` as
# the collection START with `duration` extending forward — rebuild the object that way
urine_er <- urine_collections |>
  mutate(
    start_t  = c(0, 2, 6, 12)[match(time, c(2, 6, 12, 24))],
    duration = time - start_t
  ) |>
  transmute(Subject, time = start_t, conc, volume, duration)

o_conc_er <- PKNCAconc(urine_er, conc ~ time | Subject,
                       volume = "volume", duration = "duration")

er_interval <- data.frame(
  start  = 0,
  end    = 24,
  ae     = TRUE,
  ermax  = TRUE,    # maximum excretion rate
  ertmax = TRUE,    # midpoint time of the maximum-rate collection
  ertlst = TRUE,    # midpoint time of the last measurable-rate collection
  erlst  = TRUE,    # excretion rate of that last measurable collection
  erint  = TRUE     # ae / (end - start)
)

o_nca_er <- pk.nca(PKNCAdata(o_conc_er, o_dose, intervals = er_interval))
as.data.frame(o_nca_er) |>
  filter(PPTESTCD %in% c("ae", "ermax", "ertmax", "ertlst", "erlst", "erint")) |>
  select(Subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES)
# A tibble: 4 × 7
  Subject     ae ertlst  ermax ertmax erint erlst
  <fct>    <dbl>  <dbl>  <dbl>  <dbl> <dbl> <dbl>
1 1       91844.     18 14464.      1 3827. 1395.
2 2       78781.     18 13179.      1 3283. 1348.
3 3       82223.     18 13922.      1 3426. 1243.
4 4       92938.     18 16448.      1 3872. 1208.

For every subject erint is ae / 24; ertlst is 18 h, the midpoint of the 12–24 h collection, and erlst is that collection’s amount divided by its 12 h duration.

12.7.3 Dose-normalized renal clearance

The dose-normalized renal clearances are in the parameter registry:

Parameter Definition
clr.last.dn clr.last / dose
clr.obs.dn clr.obs / dose
clr.pred.dn clr.pred / dose
Warning

In the PKNCA development version this book documents, requesting clr.last.dn, clr.obs.dn, or clr.pred.dn in an interval specification stops pk.nca() with the error “The secondary parameter ‘clr.last.dn’ has arguments not covered by formalsmap (dose)”. Calculate clr.* in the run and divide by the dose with pk.calc.dn(), as below.

clr_dn <- results |>   # one-run renal clearance results from above
  mutate(
    clr.last.dn = pk.calc.dn(clr.last, 100),  # dose = 100 mg
    clr.obs.dn  = pk.calc.dn(clr.obs, 100)
  ) |>
  select(Subject, clr.last, clr.obs, clr.last.dn, clr.obs.dn)

clr_dn
# A tibble: 4 × 5
  Subject clr.last clr.obs clr.last.dn clr.obs.dn
  <fct>      <dbl>   <dbl>       <dbl>      <dbl>
1 1           3.17    2.61      0.0317     0.0261
2 2           2.56    2.38      0.0256     0.0238
3 3           2.51    2.25      0.0251     0.0225
4 4           3.05    2.57      0.0305     0.0257