13  Secondary Parameters

13.1 What a secondary parameter is (≥ 0.13.0)

Almost every NCA parameter is calculated from the concentrations and doses within one interval of one profile. A few cannot be:

  • Renal clearance divides the amount excreted into a urine collection by the plasma AUC over the same times. The amount and the AUC come from different profiles (urine and plasma).
  • Bioavailability compares the dose-normalized exposure of a test administration with a reference administration (for absolute bioavailability, an IV dose).
  • Ratios compare a parameter with the same parameter somewhere else: steady state against the first dose (accumulation), a metabolite against its parent, fed against fasted.

PKNCA calls these secondary parameters. Starting with PKNCA 0.13.0 they are calculated in the same pk.nca() run as everything else, by linking two intervals of the interval specification. Two terms are used throughout this chapter:

  • The requesting interval is the intervals row that asks for the parameter. The result is reported there, on that row’s groups and times.
  • The reference interval is the row that supplies the value from the other profile.

pknca_parameter_table() lists which parameters are secondary:

d_params <- pknca_parameter_table()
d_params |>
  filter(secondary) |>
  select(parameter, concept, sample_type, tier) |>
  knitr::kable(row.names = FALSE)
parameter concept sample_type tier
clr.last renal_clearance interval uncommon
clr.obs renal_clearance interval common
clr.pred renal_clearance interval uncommon
ratio.cmax parameter_ratio spot uncommon
ratio.auclast parameter_ratio spot uncommon
ratio.aucint.last parameter_ratio spot uncommon
ratio.aucint.all parameter_ratio spot uncommon
f.last bioavailability spot uncommon
f.int.last bioavailability spot uncommon
f.int.all bioavailability spot uncommon
f.obs bioavailability spot uncommon
f.pred bioavailability spot uncommon
f.int.obs bioavailability spot uncommon
f.int.pred bioavailability spot uncommon
ratio.aucinf.obs parameter_ratio spot uncommon
ratio.aucinf.pred parameter_ratio spot uncommon
clr.last.dn renal_clearance interval uncommon
clr.obs.dn renal_clearance interval uncommon
clr.pred.dn renal_clearance interval uncommon

Secondary parameters are never selected automatically (by the default intervals or by pknca_interval_table()), because one interval cannot say what the other profile is. Request them by name, like any other parameter. The package vignette Secondary Parameters (Renal Clearance, Bioavailability, and Ratios) covers the same material with more detail on each mechanism.


13.2 Linking intervals by hand

Two columns of the interval specification make the link:

Column Meaning
interval_id A name for this interval, so that another row can point at it. NA on rows nothing points at.
<parameter>_ref On the requesting interval, the interval_id of the reference interval for <parameter> (for example clr.last_ref).

The identifiers may be character strings, factors, or numbers, as long as interval_id and every *_ref column share one class so that they can be compared.

13.2.1 Renal clearance from plasma and urine

These data have two subjects, each with a plasma profile and three urine collections. All concentrations are in one data frame, distinguished by the PCSPEC (specimen) grouping column. For urine, time is the start of each collection, dur its duration, conc the concentration in the collected urine, and vol the collected volume; plasma rows have no volume.

d_plasma <- data.frame(
  subject = rep(1:2, each = 7),
  PCSPEC  = "plasma",
  time    = rep(c(0, 1, 2, 4, 8, 12, 24), 2),
  conc    = c(0, 4.1, 5.0, 4.2, 2.6, 1.6, 0.4,
              0, 3.2, 4.4, 3.9, 2.5, 1.5, 0.4),
  vol     = NA_real_,
  dur     = 0
)
d_urine <- data.frame(
  subject = rep(1:2, each = 3),
  PCSPEC  = "urine",
  time    = rep(c(0, 4, 12), 2),   # collection start
  dur     = rep(c(4, 8, 12), 2),   # collection duration
  conc    = c(30, 25, 8, 26, 22, 9),
  vol     = c(0.30, 0.45, 0.80, 0.35, 0.50, 0.70)
)
d_conc <- bind_rows(d_plasma, d_urine)
d_dose <- data.frame(subject = 1:2, time = 0, dose = 100)

o_conc <- PKNCAconc(d_conc, conc ~ time | PCSPEC + subject,
                    volume = "vol", duration = "dur")
o_dose <- PKNCAdose(d_dose, dose ~ time | subject)

The plasma row gets an interval_id, and the urine row requests clr.last with clr.last_ref pointing at it:

d_intervals_linked <- data.frame(
  PCSPEC       = c("plasma", "urine"),
  start        = 0,
  end          = 24,
  interval_id  = c("plasma_0_24", NA),
  auclast      = c(TRUE, FALSE),
  ae           = c(FALSE, TRUE),
  fe           = c(FALSE, TRUE),
  clr.last     = c(FALSE, TRUE),
  clr.last_ref = c(NA, "plasma_0_24")
)
d_intervals_linked
  PCSPEC start end interval_id auclast    ae    fe clr.last clr.last_ref
1 plasma     0  24 plasma_0_24    TRUE FALSE FALSE    FALSE         <NA>
2  urine     0  24        <NA>   FALSE  TRUE  TRUE     TRUE  plasma_0_24
o_data_linked <- PKNCAdata(o_conc, o_dose, intervals = d_intervals_linked)
o_nca_linked  <- pk.nca(o_data_linked)

as.data.frame(o_nca_linked) |>
  select(PCSPEC, subject, PPTESTCD, PPORRES, PCSPEC_ref) |>
  as.data.frame()
  PCSPEC subject PPTESTCD    PPORRES PCSPEC_ref
1 plasma       1  auclast 47.7481680       <NA>
2 plasma       2  auclast 44.1002874       <NA>
3  urine       1       ae 26.6500000       <NA>
4  urine       1       fe  0.2665000       <NA>
5  urine       2       ae 26.4000000       <NA>
6  urine       2       fe  0.2640000       <NA>
7  urine       1 clr.last  0.5581366     plasma
8  urine       2 clr.last  0.5986356     plasma

The numbers can be checked by hand for subject 1. The amount excreted is \(30 \times 0.30 + 25 \times 0.45 + 8 \times 0.80 = 26.65\), so fe is \(26.65 / 100 = 0.2665\), and the renal clearance is the amount excreted divided by the plasma AUClast: \(26.65 / 47.75 = 0.558\).

Note where each result is reported. clr.last is on the urine row, which requested it, not on the plasma row that supplied the AUC. The PCSPEC_ref column (and subject_ref; one <group>_ref column per grouping column) names the group the reference value came from; it is NA on results that took nothing from another interval.

13.3 Letting PKNCA find the reference for renal clearance

For renal clearance, the pointer is usually unnecessary. When a secondary parameter is requested with no pointer and cannot be calculated within its own interval, pk.nca() derives the reference from the data. Here the interval specification mentions only the urine collections:

d_intervals_urine <- data.frame(
  PCSPEC   = "urine",
  start    = 0,
  end      = 24,
  ae       = TRUE,
  clr.last = TRUE,
  clr.obs  = TRUE
)

o_nca_auto <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = d_intervals_urine))
Secondary parameter 'clr.last': created reference interval 'autoref1' (PCSPEC=plasma, 0-24).
Secondary parameter 'clr.obs': using (PCSPEC=plasma, 0-24) as the reference interval 'autoref1'.
as.data.frame(o_nca_auto, filter_requested = TRUE) |>
  select(PCSPEC, subject, PPTESTCD, PPORRES, PCSPEC_ref) |>
  as.data.frame()
  PCSPEC subject PPTESTCD    PPORRES PCSPEC_ref
1  urine       1       ae 26.6500000       <NA>
2  urine       2       ae 26.4000000       <NA>
3  urine       1 clr.last  0.5581366     plasma
4  urine       2 clr.last  0.5986356     plasma
5  urine       1  clr.obs  0.5209632     plasma
6  urine       2  clr.obs  0.5545363     plasma
# The intervals stored with the results are the ones that went in
o_nca_auto$data$intervals$PCSPEC
[1] "urine"

Each derived link is reported with a pknca_message_secondary_ref_created message (above) and named in the PPANMETH column of the result. The clr.last values are the same as with the hand-written link.

The derivation is mechanical: renal clearance is measured over an interval collection while everything it references is a spot sample, and the concentration data declare a collection volume, so the reference is looked for among the profiles that have no collection volume. Of those, each interval takes the profile that differs from its own groups in the fewest columns (here, the plasma profile of the same subject). The derived interval is used only for the calculation; the intervals stored with the results still hold only the urine row that went in (last line above).

This automatic derivation applies to renal clearance. Bioavailability and the ratio.* parameters have no way to tell from the data which profile is the reference, so they need group_ref, a *_ref pointer, or interval_add_secondary() (shown below); requesting one with none of these is an error that names those three options.

13.3.1 When more than one profile could be the reference

With a second spot-sample specimen (serum as well as plasma), no single profile is nearest. PKNCA does not guess and does not stop the analysis: the affected results are NA, the reason is in the exclude column, and one pknca_warning_secondary_auto_reference warning is raised per parameter:

d_serum <- d_plasma |>
  mutate(PCSPEC = "serum", conc = round(conc * 1.1, 2))
d_conc_3 <- bind_rows(d_plasma, d_serum, d_urine)
o_conc_3 <- PKNCAconc(d_conc_3, conc ~ time | PCSPEC + subject,
                      volume = "vol", duration = "dur")

d_intervals_3 <- data.frame(
  PCSPEC   = c("plasma", "serum", "urine"),
  start    = 0,
  end      = 24,
  auclast  = c(TRUE, TRUE, FALSE),
  ae       = c(FALSE, FALSE, TRUE),
  clr.last = c(FALSE, FALSE, TRUE)
)

o_nca_ambiguous <- pk.nca(PKNCAdata(o_conc_3, o_dose, intervals = d_intervals_3))
Warning: Secondary parameter 'clr.last': no reference value could be determined
for 1 interval(s), so the results are NA (see the exclude column).  More than
one reference profile is equally close ((PCSPEC=plasma), (PCSPEC=serum)).  Set
'clr.last_ref' in the interval specification, give `group_ref` to PKNCAdata(),
or use interval_add_secondary().
as.data.frame(o_nca_ambiguous) |>
  filter(PPTESTCD == "clr.last") |>
  select(PCSPEC, subject, PPTESTCD, PPORRES, exclude) |>
  as.data.frame()
  PCSPEC subject PPTESTCD PPORRES
1  urine       1 clr.last      NA
2  urine       2 clr.last      NA
                                                                             exclude
1 More than one reference profile is equally close ((PCSPEC=plasma), (PCSPEC=serum))
2 More than one reference profile is equally close ((PCSPEC=plasma), (PCSPEC=serum))

The warning is the intended signal that the analysis needs to say which specimen is the reference; everything else (the AUCs and ae) was calculated.

13.3.2 Steering the choice with group_ref

PKNCAdata(group_ref = ) is a data frame of group values saying which profiles may be references. It settles the ambiguity above:

o_data_steered <- PKNCAdata(
  o_conc_3, o_dose,
  intervals = d_intervals_3,
  group_ref = data.frame(PCSPEC = "plasma")
)
o_data_steered
Formula for concentration:
 conc ~ time | PCSPEC + subject
Data are dense PK.
With 2 subjects defined in the 'subject' column.
Nominal time column is not specified.

First 6 rows of concentration data:
 subject PCSPEC time conc vol dur exclude
       1 plasma    0  0.0  NA   0    <NA>
       1 plasma    1  4.1  NA   0    <NA>
       1 plasma    2  5.0  NA   0    <NA>
       1 plasma    4  4.2  NA   0    <NA>
       1 plasma    8  2.6  NA   0    <NA>
       1 plasma   12  1.6  NA   0    <NA>
Formula for dosing:
 dose ~ time | subject
Nominal time column is not specified.

Data for dosing:
 subject time dose exclude         route duration
       1    0  100    <NA> extravascular        0
       2    0  100    <NA> extravascular        0

With 3 rows of interval specifications.
With reference profiles for secondary parameters (group_ref): PCSPEC=plasma
No options are set differently than default.
as.data.frame(pk.nca(o_data_steered)) |>
  filter(PPTESTCD == "clr.last") |>
  select(PCSPEC, subject, PPTESTCD, PPORRES, PCSPEC_ref) |>
  as.data.frame()
Secondary parameter 'clr.last': using (PCSPEC=plasma, 0-24) as the reference
interval 'autoref1'.
  PCSPEC subject PPTESTCD   PPORRES PCSPEC_ref
1  urine       1 clr.last 0.5581366     plasma
2  urine       2 clr.last 0.5986356     plasma

group_ref also supplies the reference for parameters that cannot be derived at all, such as bioavailability and metabolite ratios (both shown below). A plain data frame applies to every secondary parameter. When one analysis needs different steering per parameter (renal clearance by specimen and a metabolite ratio by analyte), add a parameter column, where the columns a row leaves NA do not constrain that parameter, or give a named list of data frames, one per parameter; see ?PKNCAdata and the group_ref section of the vignette.


13.4 Writing the linkage with interval_add_secondary()

interval_add_secondary() writes the linkage into the interval specification, where it can be seen, edited, and saved. Given a parameter and a data frame describing the reference interval (reference; its columns are start, end, and grouping columns of the intervals), it requests the parameter on the other intervals, gives the reference interval an interval_id, and adds the <parameter>_ref pointer.

It returns the intervals after check.interval.specification(), which adds a column for every registered parameter, so the examples here show only the columns that are not all FALSE:

show_set_columns <- function(intervals) {
  keep <- vapply(intervals, function(x) !is.logical(x) || any(x, na.rm = TRUE), logical(1))
  intervals[, keep, drop = FALSE]
}

d_intervals_plain <- data.frame(
  PCSPEC  = c("plasma", "urine"),
  start   = 0,
  end     = 24,
  auclast = c(TRUE, FALSE),
  ae      = c(FALSE, TRUE)
)

interval_add_secondary(
  d_intervals_plain,
  param     = "clr.last",
  reference = data.frame(PCSPEC = "plasma")
) |>
  show_set_columns()
  start end auclast    ae clr.last PCSPEC interval_id clr.last_ref
1     0  24    TRUE FALSE    FALSE plasma        ref1         <NA>
2     0  24   FALSE  TRUE     TRUE  urine        <NA>         ref1

When no interval matches reference, the reference interval is created, requesting what the link reads from it (here auclast), and the creation is reported with a pknca_message_secondary_created_interval message:

interval_add_secondary(
  data.frame(PCSPEC = "urine", start = 0, end = 24, ae = TRUE),
  param     = "clr.last",
  reference = data.frame(PCSPEC = "plasma")
) |>
  show_set_columns()
Created reference interval(s) for 'clr.last': PCSPEC=plasma, start=0, end=24
  start end auclast    ae clr.last PCSPEC interval_id clr.last_ref
1     0  24   FALSE  TRUE     TRUE  urine        <NA>         ref1
2     0  24    TRUE FALSE    FALSE plasma        ref1         <NA>

Other behaviors to know:

  • interval_add_secondary() also accepts a PKNCAdata object and then returns the object with updated intervals. On a PKNCAdata object, reference = NULL (the default) writes out the same derivation that pk.nca() would make on its own (using the object’s group_ref when it has one); anything the derivation cannot resolve is an error there rather than an NA result.
  • target_groups restricts which intervals request the parameter; by default every interval other than the reference requests it.
  • An interval that already points at a different reference for the parameter is left alone with a pknca_warning_secondary_ref_exists warning, so the helper never silently re-points an analysis.
  • ref_id names the reference interval’s interval_id instead of the generated one.

13.5 Bioavailability

Bioavailability is calculated with pk.calc.f(): \(F = (\text{AUC}_\text{test}/\text{Dose}_\text{test}) / (\text{AUC}_\text{ref}/\text{Dose}_\text{ref})\). As a secondary parameter, the reference dose and AUC come from the reference interval and the test dose and AUC from the requesting interval.

Renamed in 0.13.0: the f parameter is now f.obs; an interval specification requesting f is an error pointing at f.obs. f.obs takes the reference values (dose1, auc1) from the reference interval and calculates the test values in its own interval as totdose and aucinf.obs; dose2 and auc2 interval columns are ignored.

The name of each bioavailability parameter says which AUC it is built on:

f_params <- c("f.obs", "f.pred", "f.last", "f.int.last", "f.int.all",
              "f.int.obs", "f.int.pred")
data.frame(
  parameter   = f_params,
  `AUC basis` = vapply(f_params, function(x) get.interval.cols()[[x]]$formalsmap$auc2, ""),
  description = vapply(f_params, function(x) get.interval.cols()[[x]]$desc, ""),
  check.names = FALSE
) |>
  knitr::kable(row.names = FALSE)
parameter AUC basis description
f.obs aucinf.obs Bioavailability from AUCinf,obs
f.pred aucinf.pred Bioavailability from AUCinf,pred
f.last auclast Bioavailability from AUClast
f.int.last aucint.last Bioavailability from AUCint,last
f.int.all aucint.all Bioavailability from AUCint,all
f.int.obs aucint.inf.obs Bioavailability from AUCint,inf,obs
f.int.pred aucint.inf.pred Bioavailability from AUCint,inf,pred

f.obs, f.pred, and f.last use the AUC extrapolated to infinity or the AUC to the last measurable concentration; the f.int.* variants use the interval AUCs (aucint.*), for example over a steady-state dosing interval.

13.5.1 Absolute bioavailability: IV reference, oral test

In this two-period crossover, each subject received a 100 mg IV bolus and a 200 mg oral dose. The route is a column of the dosing data. To keep the example short the IV arm has a measured concentration at time 0; see the Intravascular chapter for handling a bolus without one.

d_conc_ba <- data.frame(
  subject   = rep(1:2, each = 14),
  treatment = rep(rep(c("IV", "oral"), each = 7), 2),
  time      = rep(c(0, 0.5, 1, 2, 4, 8, 12), 4),
  conc      = c(3.33, 3.07, 2.82, 2.39, 1.71, 0.879, 0.451,
                0,    1.72, 2.53, 2.91, 2.35, 1.22,  0.629,
                2.86, 2.62, 2.40, 2.02, 1.42, 0.712, 0.358,
                0,    1.90, 2.80, 3.25, 2.67, 1.42,  0.749)
)
d_dose_ba <- data.frame(
  subject   = rep(1:2, each = 2),
  treatment = c("IV", "oral"),
  time      = 0,
  dose      = c(100, 200),
  route     = c("intravascular", "extravascular")
)

o_conc_ba <- PKNCAconc(d_conc_ba, conc ~ time | treatment + subject)
o_dose_ba <- PKNCAdose(d_dose_ba, dose ~ time | treatment + subject, route = "route")

d_intervals_ba <- data.frame(
  treatment  = c("IV", "oral"),
  start      = 0,
  end        = Inf,
  aucinf.obs = TRUE,
  f.obs      = c(FALSE, TRUE),
  f.last     = c(FALSE, TRUE)
)

o_data_ba <- PKNCAdata(
  o_conc_ba, o_dose_ba,
  intervals = d_intervals_ba,
  group_ref = data.frame(treatment = "IV")
)
o_nca_ba <- pk.nca(o_data_ba)
Secondary parameter 'f.last': using (treatment=IV, 0-Inf) as the reference interval 'autoref1'.
Secondary parameter 'f.obs': using (treatment=IV, 0-Inf) as the reference interval 'autoref1'.
d_ba <- as.data.frame(o_nca_ba)
d_ba |>
  filter(PPTESTCD %in% c("totdose", "aucinf.obs", "auclast", "f.obs", "f.last")) |>
  select(treatment, subject, PPTESTCD, PPORRES, treatment_ref) |>
  arrange(subject, treatment, PPTESTCD) |>
  as.data.frame()
   treatment subject   PPTESTCD     PPORRES treatment_ref
1         IV       1 aucinf.obs  19.9978123          <NA>
2         IV       1    auclast  17.2924438          <NA>
3         IV       1    totdose 100.0000000          <NA>
4       oral       1 aucinf.obs  23.7336169          <NA>
5       oral       1    auclast  19.9158250          <NA>
6       oral       1     f.last   0.5758534            IV
7       oral       1      f.obs   0.5934053            IV
8       oral       1    totdose 200.0000000          <NA>
9         IV       2 aucinf.obs  16.4621787          <NA>
10        IV       2    auclast  14.3945551          <NA>
11        IV       2    totdose 100.0000000          <NA>
12      oral       2 aucinf.obs  27.4045887          <NA>
13      oral       2    auclast  22.6905420          <NA>
14      oral       2     f.last   0.7881641            IV
15      oral       2      f.obs   0.8323500            IV
16      oral       2    totdose 200.0000000          <NA>

The f.obs result is the ratio of dose-normalized AUCinf,obs; for subject 1:

value_of <- function(param, trt, subj = 1) {
  d_ba$PPORRES[d_ba$PPTESTCD == param & d_ba$treatment == trt & d_ba$subject == subj]
}
(value_of("aucinf.obs", "oral") / value_of("totdose", "oral")) /
  (value_of("aucinf.obs", "IV") / value_of("totdose", "IV"))
[1] 0.5934053

The reference for an absolute bioavailability is not derived from the route of administration; it was given here with group_ref. An f.obs_ref pointer or interval_add_secondary() work equally well. For relative bioavailability between two extravascular formulations, the reference is simply the other formulation’s treatment value.


13.6 Ratios

The ratio.* parameters divide a parameter in the requesting interval by the same parameter in the reference interval, using pk.calc.ratio():

Parameter Ratio of
ratio.cmax cmax
ratio.auclast auclast
ratio.aucinf.obs aucinf.obs
ratio.aucinf.pred aucinf.pred
ratio.aucint.last aucint.last
ratio.aucint.all aucint.all

pk.calc.ratio() can be called directly. It returns NA when the reference is missing or not above zero, because a ratio to a zero or negative reference is not interpretable:

pk.calc.ratio(test = c(10, 10, 10), reference = c(20, 0, NA))
[1] 0.5  NA  NA

13.6.1 Accumulation ratio: first dose against steady state

Because reference can name start and end, the same helper links intervals in time rather than across groups. Here two subjects received 100 mg every 24 hours for five days, with full profiles after the first dose (0 to 24 hours) and the fifth dose (96 to 120 hours):

times_profile <- c(0, 1, 2, 4, 8, 12, 24)
d_conc_md <- data.frame(
  subject = rep(1:2, each = 14),
  time    = rep(c(times_profile, times_profile + 96), 2),
  conc    = c(0,   4.1, 5.0, 4.2, 2.6, 1.6, 0.4,
              0.9, 5.4, 6.2, 5.3, 3.4, 2.1, 0.9,
              0,   3.2, 4.4, 3.9, 2.5, 1.5, 0.4,
              0.8, 4.4, 5.6, 5.0, 3.3, 2.0, 0.8)
)
d_dose_md <- data.frame(
  subject = rep(1:2, each = 5),
  time    = rep(seq(0, 96, by = 24), 2),
  dose    = 100
)

d_intervals_md <-
  data.frame(start = c(0, 96), end = c(24, 120), cmax = TRUE, auclast = TRUE) |>
  interval_add_secondary(param = "ratio.auclast",
                         reference = data.frame(start = 0, end = 24)) |>
  interval_add_secondary(param = "ratio.cmax",
                         reference = data.frame(start = 0, end = 24))
show_set_columns(d_intervals_md)
  start end auclast cmax ratio.cmax ratio.auclast interval_id ratio.auclast_ref
1     0  24    TRUE TRUE      FALSE         FALSE        ref1              <NA>
2    96 120    TRUE TRUE       TRUE          TRUE        <NA>              ref1
  ratio.cmax_ref
1           <NA>
2           ref1
o_data_md <- PKNCAdata(
  PKNCAconc(d_conc_md, conc ~ time | subject),
  PKNCAdose(d_dose_md, dose ~ time | subject),
  intervals = d_intervals_md
)
o_nca_md <- pk.nca(o_data_md)

d_md <- as.data.frame(o_nca_md)
d_md |>
  select(subject, start, end, PPTESTCD, PPORRES) |>
  as.data.frame()
   subject start end      PPTESTCD   PPORRES
1        1     0  24       auclast 47.748168
2        1     0  24          cmax  5.000000
3        1    96 120       auclast 65.333457
4        1    96 120          cmax  6.200000
5        2     0  24       auclast 44.100287
6        2     0  24          cmax  4.400000
7        2    96 120       auclast 60.653321
8        2    96 120          cmax  5.600000
9        1    96 120    ratio.cmax  1.240000
10       2    96 120    ratio.cmax  1.272727
11       1    96 120 ratio.auclast  1.368292
12       2    96 120 ratio.auclast  1.375350

ratio.auclast on the 96 to 120 hour interval is the accumulation ratio of AUC over the dosing interval (\(R_{ac}\)), here 1.368 for subject 1 (\(65.33 / 47.75\)). The second call reused the interval_id that the first call gave the 0 to 24 hour interval. The Superposition chapter predicts steady state from single-dose data instead; ratio.* compares observed profiles.

13.6.2 Metabolite-to-parent ratio

Nothing in a dataset says which analyte is the parent, so a metabolite ratio needs group_ref (or a pointer):

d_conc_met <- data.frame(
  subject = 1,
  Analyte = rep(c("parent", "metabolite"), each = 7),
  time    = rep(times_profile, 2),
  conc    = c(0, 4.1, 5.0, 4.2, 2.6, 1.6, 0.4,
              0, 0.6, 1.1, 1.4, 1.2, 0.9, 0.3)
)
d_dose_met <- data.frame(subject = 1, time = 0, dose = 100)

d_intervals_met <- data.frame(
  Analyte          = c("parent", "metabolite"),
  start            = 0,
  end              = Inf,
  cmax             = TRUE,
  aucinf.obs       = TRUE,
  ratio.cmax       = c(FALSE, TRUE),
  ratio.aucinf.obs = c(FALSE, TRUE)
)

o_data_met <- PKNCAdata(
  PKNCAconc(d_conc_met, conc ~ time | Analyte + subject),
  PKNCAdose(d_dose_met, dose ~ time | subject),
  intervals = d_intervals_met,
  group_ref = data.frame(Analyte = "parent")
)

as.data.frame(pk.nca(o_data_met), filter_requested = TRUE) |>
  select(Analyte, PPTESTCD, PPORRES, Analyte_ref) |>
  as.data.frame()
Secondary parameter 'ratio.cmax': using (Analyte=parent, 0-Inf) as the reference interval 'autoref1'.
Secondary parameter 'ratio.aucinf.obs': using (Analyte=parent, 0-Inf) as the reference interval 'autoref1'.
     Analyte         PPTESTCD  PPORRES Analyte_ref
1     parent             cmax  5.00000        <NA>
2     parent       aucinf.obs 51.15524        <NA>
3 metabolite             cmax  1.40000        <NA>
4 metabolite       aucinf.obs 22.98251        <NA>
5 metabolite       ratio.cmax  0.28000      parent
6 metabolite ratio.aucinf.obs  0.44927      parent

The ratio is of the reported concentration units; a molar (molecular-weight-adjusted) metabolite ratio is not yet available.

13.6.3 Food effect: fed against fasted

A food-effect comparison is a ratio with the fasted treatment as the reference:

d_conc_food <- data.frame(
  subject   = rep(1:2, each = 14),
  treatment = rep(rep(c("fasted", "fed"), each = 7), 2),
  time      = rep(times_profile, 4),
  conc      = c(0, 4.1, 5.0, 4.2, 2.6, 1.6, 0.4,   0, 1.8, 3.3, 4.0, 2.9, 1.8, 0.5,
                0, 3.2, 4.4, 3.9, 2.5, 1.5, 0.4,   0, 1.5, 2.9, 3.6, 2.7, 1.7, 0.5)
)
d_dose_food <- data.frame(
  subject   = rep(1:2, each = 2),
  treatment = c("fasted", "fed"),
  time      = 0,
  dose      = 100
)

d_intervals_food <-
  data.frame(treatment = c("fasted", "fed"), start = 0, end = Inf,
             cmax = TRUE, tmax = TRUE, aucinf.obs = TRUE) |>
  interval_add_secondary(param = "ratio.cmax",
                         reference = data.frame(treatment = "fasted")) |>
  interval_add_secondary(param = "ratio.aucinf.obs",
                         reference = data.frame(treatment = "fasted"))

o_nca_food <- pk.nca(PKNCAdata(
  PKNCAconc(d_conc_food, conc ~ time | treatment + subject),
  PKNCAdose(d_dose_food, dose ~ time | treatment + subject),
  intervals = d_intervals_food
))

as.data.frame(o_nca_food) |>
  filter(grepl("^ratio", PPTESTCD)) |>
  select(treatment, subject, PPTESTCD, PPORRES, treatment_ref) |>
  as.data.frame()
  treatment subject         PPTESTCD   PPORRES treatment_ref
1       fed       1       ratio.cmax 0.8000000        fasted
2       fed       2       ratio.cmax 0.8181818        fasted
3       fed       1 ratio.aucinf.obs 0.9855827        fasted
4       fed       2 ratio.aucinf.obs 0.9905326        fasted

Food delays and lowers the peak (ratio.cmax about 0.8) without changing total exposure (ratio.aucinf.obs about 0.99). These are the per-subject ratios; a formal food-effect or bioequivalence assessment (geometric mean ratio with a confidence interval) is described in the Bioequivalence vignette.

reference must name start, end, or grouping columns that are present in the intervals, which is why the intervals here carry a treatment column.


13.8 Units of secondary parameters

A secondary parameter takes one value from the requesting interval and another from the reference interval, so when units differ between groups, its units are composed from both. In this version of the renal example the urine is reported in mg/L and the plasma in ng/mL (unit columns, so the units can vary by group):

d_conc_u <- d_conc |>
  mutate(
    cu   = ifelse(PCSPEC == "plasma", "ng/mL", "mg/L"),
    conc = ifelse(PCSPEC == "plasma", conc * 1000, conc),
    tu   = "hr",
    au   = "mg"
  )
o_conc_u <- PKNCAconc(d_conc_u, conc ~ time | PCSPEC + subject,
                      volume = "vol", duration = "dur",
                      concu = "cu", timeu = "tu", amountu = "au")

o_nca_u <- pk.nca(PKNCAdata(o_conc_u, intervals = d_intervals_linked |> select(-fe)))

as.data.frame(o_nca_u) |>
  filter(subject == 1) |>
  select(PCSPEC, PCSPEC_ref, PPTESTCD, PPORRES, PPORRESU) |>
  as.data.frame()
  PCSPEC PCSPEC_ref PPTESTCD      PPORRES      PPORRESU
1 plasma       <NA>  auclast 4.774817e+04      hr*ng/mL
2  urine       <NA>       ae 2.665000e+01            mg
3  urine     plasma clr.last 5.581366e-04 mg/(hr*ng/mL)

There is no unit argument for the collection volume: amountu is the unit of the amount excreted (conc × vol), so the volume must be recorded in units that make that product an amount in amountu. Here the volumes are in liters, and mg/L × L = mg.

clr.last is reported in mg/(hr*ng/mL): the mg from the urine collection that supplied the amount and the hr*ng/mL from the plasma profile that supplied the AUC. Its value, \(26.65 / 47748 = 0.000558\), is the same number as before in different units; no value is converted. The units table that PKNCAdata() builds gains a <group>_ref column and one row per pair of groups to make this possible:

o_data_u <- PKNCAdata(o_conc_u, intervals = d_intervals_linked |> select(-fe))
o_data_u$units |>
  filter(PPTESTCD %in% c("auclast", "ae", "clr.last"))
   PCSPEC      PPORRESU PPTESTCD PCSPEC_ref      PPSTRESU conversion_factor
1  plasma            mg       ae       <NA>            mg                 1
2  plasma      hr*ng/mL  auclast       <NA>      hr*ng/mL                 1
3  plasma mg/(hr*ng/mL) clr.last       <NA> mg/(hr*ng/mL)                 1
4   urine            mg       ae       <NA>            mg                 1
5   urine       hr*mg/L  auclast       <NA>       hr*mg/L                 1
6   urine  mg/(hr*mg/L) clr.last       <NA>  mg/(hr*mg/L)                 1
7  plasma mg/(hr*ng/mL) clr.last     plasma mg/(hr*ng/mL)                 1
8  plasma  mg/(hr*mg/L) clr.last      urine  mg/(hr*mg/L)                 1
9   urine mg/(hr*ng/mL) clr.last     plasma mg/(hr*ng/mL)                 1
10  urine  mg/(hr*mg/L) clr.last      urine  mg/(hr*mg/L)                 1

The rows with NA in PCSPEC_ref describe results that took nothing from another interval; the rest describe each pair of groups. The table is an ordinary PKNCAdata(units = ) argument, so a row can be edited to report a standardized unit. One mg/(hr·ng/mL) is 1000 L/hr:

d_units_clr <- o_data_u$units
is_clr_mixed <- d_units_clr$PPTESTCD == "clr.last" &
  d_units_clr$PPORRESU == "mg/(hr*ng/mL)"
d_units_clr$PPSTRESU[is_clr_mixed] <- "L/hr"
d_units_clr$conversion_factor[is_clr_mixed] <- 1000

pk.nca(PKNCAdata(o_conc_u, intervals = d_intervals_linked |> select(-fe),
                 units = d_units_clr)) |>
  as.data.frame() |>
  filter(subject == 1) |>
  select(PCSPEC, PPTESTCD, PPORRES, PPORRESU, PPSTRES, PPSTRESU) |>
  as.data.frame()
  PCSPEC PPTESTCD      PPORRES      PPORRESU      PPSTRES PPSTRESU
1 plasma  auclast 4.774817e+04      hr*ng/mL 4.774817e+04 hr*ng/mL
2  urine       ae 2.665000e+01            mg 2.665000e+01       mg
3  urine clr.last 5.581366e-04 mg/(hr*ng/mL) 5.581366e-01     L/hr

When the two sides of a ratio are convertible into each other (a ratio.* between analytes reported in ng/mL and mg/L), the quotient is also standardized to a PPSTRESU of "fraction" in PPSTRES. When they are not (a bioavailability between a dose in mg and a dose in mg/kg), the composite units stand. Nothing is set to NA for unit reasons.


13.9 Reporting secondary parameters

  • PPANMETH names the reference interval on every linked result, as “Reference interval:” followed by how it differs from the requesting interval and its times.
  • <group>_ref columns (PCSPEC_ref, treatment_ref, …) give the group the reference value came from.
  • summary() summarizes a secondary parameter on the intervals that requested it; intervals that did not request it show .:
summary(o_nca_md)
 start end N     auclast        cmax  ratio.cmax ratio.auclast
     0  24 2 45.9 [5.62] 4.69 [9.06]           .             .
    96 120 2 62.9 [5.26] 5.89 [7.21] 1.26 [1.84]  1.37 [0.364]

Caption: auclast, cmax, ratio.cmax, ratio.auclast: geometric mean and geometric coefficient of variation; N: number of subjects
  • as.data.frame(filter_requested = TRUE) drops the source values calculated only to support a link.

13.10 Limitations

These are documented as not yet supported in PKNCA 0.13.0:

  • Sparse concentration data (PKNCAconc(sparse = TRUE)) with any secondary parameter requested is an error.
  • The reference for absolute bioavailability is not derived from the route of administration; give it with group_ref, an f.obs_ref pointer, or interval_add_secondary().
  • The CDISC PPTESTCD of a ratio does not depend on what the ratio compares (accumulation against metabolite ratio).
  • exclude() and the exclude_nca_*() functions do not propagate across a link (see above).
  • Molecular-weight adjustment of a metabolite ratio is not available.

13.11 Defining your own secondary parameter

A custom parameter becomes secondary when its formalsmap in add.interval.col() wraps an argument in pknca_ref(), which says the argument comes from the reference interval rather than the current one (for example formalsmap = list(auc = pknca_ref("auclast"))). add.interval.col() also accepts selection = list(secondary = TRUE), and the names interval_id and anything ending in _ref are reserved for the linkage columns. Arguments taken from the current interval must be listed in depends. See the Custom Parameters chapter and the section Parameters That Need Another Interval of the parameter-writing vignette.