17  Writing Custom Parameter Functions

17.1 The parameter registry

Every NCA parameter that PKNCA can compute is registered in an internal registry via add.interval.col(). When you request auclast = TRUE in an interval, PKNCA looks up the registered function for auclast and calls it with the right arguments.

You can extend the registry with your own parameters — new functions that compute anything derivable from concentrations, times, doses, or other already-computed parameters. See also the package vignette Writing PKNCA Parameter Functions.

# Number of parameters registered by default
length(get.interval.cols())
[1] 203

17.2 Anatomy of add.interval.col()

add.interval.col(
  name        = "my.param",         # column name used in intervals data frame
  FUN         = "pk.calc.my.param", # character string: function name
  values      = c(FALSE, TRUE),     # allowed values in intervals (FALSE = skip, TRUE = compute)
  unit_type   = "time",             # units category (see below)
  pretty_name = "My parameter",     # human-readable label
  desc        = "What it does",     # one-line description
  depends     = NULL,               # other parameters that must be computed first
  sparse      = FALSE,              # TRUE if this is a sparse-PK parameter
  formalsmap  = list(),             # list(<your function arg> = "<PKNCA data name>")
  datatype    = "interval",         # "interval" (default), "individual", or "population"
                                    #   (only "interval" is currently supported; the others error)
  pptestcd_cdisc = "MYPARAM",       # CDISC PPTESTCD code for the parameter (≥ 0.12.2)
  pptest_cdisc   = "My Parameter"   # CDISC PPTEST label for the parameter (≥ 0.12.2)
)

Key rule: FUN must be a character string (the function name), not a function object.

17.2.1 Available unit_type values

"unitless", "fraction", "%", "count", "time", "inverse_time", "amount", "amount_dose", "amount_time", "conc", "conc_dosenorm", "dose", "volume", "auc", "aumc", "auc_dosenorm", "aumc_dosenorm", "clearance", "renal_clearance", "renal_clearance_dosenorm"


17.3 The formalsmap argument

Each formalsmap entry has the form <your function's argument> = "<PKNCA data name>" — the names of the list must be your function’s formal arguments, and the values are the PKNCA data or parameter names that supply them. For example, PKNCA registers mrt.obs with formalsmap = list(auc = "aucinf.obs", aumc = "aumcinf.obs"): the computed aucinf.obs value is passed to the function’s auc argument.

Standard PKNCA data names available as formalsmap values:

PKNCA name What it provides
conc Concentration vector for the current interval
time Time vector for the current interval
dose Total dose in the current interval
start Interval start time
end Interval end time
half.life Computed half-life (if in depends)
auclast Computed AUClast (if in depends)
cmax Computed Cmax (if in depends)
(any parameter name) Any other registered parameter in depends

If your function argument names exactly match PKNCA’s standard names, formalsmap can be omitted or left empty.


17.4 Example 1: derived from other parameters

A parameter that divides two already-computed parameters. Use depends to declare what must be computed first (shown here without formalsmap because the function’s argument names match the parameter names).

# Exposure ratio: AUClast / Cmax  (units: time, since AUC/conc = time × conc/conc = time)
pk.calc.exposure.ratio <- function(auclast, cmax) {
  auclast / cmax
}

add.interval.col(
  name        = "exposure.ratio",
  FUN         = "pk.calc.exposure.ratio",
  values      = c(FALSE, TRUE),
  unit_type   = "time",
  pretty_name = "Exposure ratio (AUClast / Cmax)",
  desc        = "AUClast divided by Cmax",
  depends     = c("auclast", "cmax")
)
o_nca_er <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
  start          = 0, end = Inf,
  auclast        = TRUE,
  cmax           = TRUE,
  exposure.ratio = TRUE
)))

as.data.frame(o_nca_er) |>
  filter(PPTESTCD %in% c("auclast", "cmax", "exposure.ratio")) |>
  select(subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  mutate(check = auclast / cmax) |>
  arrange(subject)
# A tibble: 12 × 5
   subject auclast  cmax exposure.ratio check
   <ord>     <dbl> <dbl>          <dbl> <dbl>
 1 6          71.7  6.44          11.1  11.1 
 2 7          88.0  7.09          12.4  12.4 
 3 8          86.8  7.56          11.5  11.5 
 4 11         77.9  8              9.74  9.74
 5 3          95.9  8.2           11.7  11.7 
 6 2          88.7  8.33          10.7  10.7 
 7 4         103.   8.6           11.9  11.9 
 8 9          83.9  9.03           9.30  9.30
 9 12        115.   9.75          11.8  11.8 
10 10        136.  10.2           13.3  13.3 
11 1         147.  10.5           14.0  14.0 
12 5         118.  11.4           10.4  10.4 

17.5 Example 2: computed from raw concentration-time data

A parameter that accesses conc and time directly. PKNCA passes the raw vectors from the concentration data. No depends needed. The identity formalsmap below is shown for explicitness — because the argument names match PKNCA’s standard names, it could equally be omitted.

# Concentration at exactly 2 hours (interpolated)
pk.calc.c_at_2h <- function(conc, time) {
  interp.extrap.conc(conc = conc, time = time, time.out = 2)
}

add.interval.col(
  name        = "c.2h",
  FUN         = "pk.calc.c_at_2h",
  values      = c(FALSE, TRUE),
  unit_type   = "conc",
  pretty_name = "Concentration at 2 h",
  desc        = "Interpolated concentration at t = 2 h",
  depends     = NULL,
  formalsmap  = list(conc = "conc", time = "time")
)
o_nca_c2 <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
  start = 0, end = Inf, c.2h = TRUE
)))

as.data.frame(o_nca_c2) |>
  filter(PPTESTCD == "c.2h") |>
  select(subject, PPORRES) |>
  arrange(subject)
# A tibble: 12 × 2
   subject PPORRES
   <ord>     <dbl>
 1 6          6.32
 2 7          6.55
 3 8          7.56
 4 11         6.80
 5 3          7.81
 6 2          8.25
 7 4          8.41
 8 9          6.35
 9 12         9.72
10 10         7.76
11 1          9.68
12 5          9.37

17.7 Inspecting registered parameters

# Confirm your parameter is registered
cols <- get.interval.cols()
cat("exposure.ratio registered:", "exposure.ratio" %in% names(cols), "\n")
exposure.ratio registered: TRUE 
cat("c.2h registered:          ", "c.2h" %in% names(cols), "\n")
c.2h registered:           TRUE 
# Inspect registration details
str(cols[["exposure.ratio"]])
List of 11
 $ FUN           : chr "pk.calc.exposure.ratio"
 $ values        : logi [1:2] FALSE TRUE
 $ unit_type     : chr "time"
 $ pretty_name   : chr "Exposure ratio (AUClast / Cmax)"
 $ desc          : chr "AUClast divided by Cmax"
 $ sparse        : logi FALSE
 $ formalsmap    : list()
 $ depends       : chr [1:2] "auclast" "cmax"
 $ datatype      : chr "interval"
 $ pptestcd_cdisc: chr "exposure.ratio"
 $ pptest_cdisc  : chr "AUClast divided by Cmax"

17.8 Custom summary statistics for your parameter

After registering the parameter, set a custom summary rule with PKNCA.set.summary():

PKNCA.set.summary(
  "exposure.ratio",
  description = "geometric mean [CV%]",
  point  = PKNCA::geomean,
  spread = PKNCA::geocv
)

summary(pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
  start = 0, end = Inf,
  auclast = TRUE, cmax = TRUE, exposure.ratio = TRUE
))))
 start end  N     auclast        cmax exposure.ratio
     0 Inf 12 98.7 [22.5] 8.65 [17.0]    11.4 [12.0]

Caption: auclast, cmax: geometric mean and geometric coefficient of variation; exposure.ratio: geometric mean [CV%]; N: number of subjects

The vignette’s summary section covers the same registration.


17.9 Important notes

  • Registration is session-scoped. Custom parameters registered with add.interval.col() persist for the life of your R session. Re-run registration code in each session (e.g. in your analysis script or package).
  • FUN must be a character string. Passing a function object causes an error.
  • depends controls evaluation order. List all parameters your function reads. PKNCA resolves the dependency graph and ensures they are computed before your function is called.
  • Functions must be in scope when pk.nca() runs. Define them in the global environment or in a package loaded before calling pk.nca().