# Number of parameters registered by default
length(get.interval.cols())[1] 237
add.interval.col()
formalsmap argumentEvery 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.
add.interval.col()add.interval.col(
name = "my.param", # column name used in the intervals data frame
FUN = "pk.calc.my.param", # function name as a character string, or NA
values = c(FALSE, TRUE), # allowed values in intervals (FALSE = skip, TRUE = compute)
unit_type = "time", # units category (required; see below)
pretty_name = "My parameter", # human-readable label for summary tables
depends = NULL, # other parameters that must be computed first
desc = "What it does", # one-line description; SDTM limit 40 characters
formalsmap = list(), # list(<your function arg> = <data or parameter name>)
FUN_sparse = NA, # sparse-PK estimator (function name), if any
formalsmap_sparse = list(), # formalsmap for FUN_sparse
datatype = "interval", # only "interval" is supported; the others error
pptestcd_cdisc = "MYPARAM", # CDISC PPTESTCD code (defaults to name)
pptest_cdisc = "My Parameter", # CDISC PPTEST label (defaults to desc)
formula = NULL, # LaTeX expression, for documentation
formula_note = NULL, # note on the formula; also the PPANMETH fallback
tier = "uncommon", # "common" or "uncommon"
selection = NULL # list(concept=, route=, dosing=, secondary=)
)Key rule: FUN must be a character string (the function name), not a function object.
What each argument does, as of PKNCA 0.13.0:
| Argument | Notes |
|---|---|
name |
A non-empty string. Names ending in _ref and the name interval_id are reserved for the reference-interval linkage columns (see Example 4) and are an error. |
FUN |
The calculation function’s name, or NA for a parameter that is filled in as one column of another parameter’s data-frame result, or that only a FUN_sparse estimator calculates. |
unit_type |
Required; one of the values listed below. |
desc |
Since 0.13.0, a description longer than 40 characters (the SDTM limit) is still registered but gives a warning. |
formalsmap |
Maps your function’s arguments to data, parameters, constants (I()), or reference-interval values (pknca_ref()); see below. |
FUN_sparse, formalsmap_sparse (≥ 0.13.0) |
A second calculation function used when the data are sparse; see Example 5. |
pptestcd_cdisc, pptest_cdisc (≥ 0.13.0) |
The codes used by as.data.frame(out_format = "cdisc"). Each is a single string, a list with one element route holding a named list by route (list(route = list(extravascular = "CLF/FO", intravascular = "CLO"))), or a list of exactly the elements dense and sparse; anything else is an error. |
formula, formula_note (≥ 0.13.0) |
Documentation of the calculation. When the calculation function sets no "method" attribute, formula_note is reported in the PPANMETH column. |
tier, selection (≥ 0.13.0) |
Classification used by automatic interval selection and reported by pknca_parameter_table(). A custom parameter is "uncommon" by default, so it is calculated only when requested by name. |
sparse |
Retired (≥ 0.13.0). sparse = TRUE is an error; an explicit sparse = FALSE is accepted and does nothing. Register a sparse-only parameter with FUN = NA and FUN_sparse instead. |
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"
formalsmap argumentEach 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.
Values available in a formalsmap (the full list is in ?add.interval.col):
formalsmap value |
What it provides |
|---|---|
"conc", "time" |
Concentrations and times for the current interval (times start at 0 at the interval start) |
"dose", "time.dose", "duration.dose", "route" |
Dose amounts, times, durations, and route for the current interval |
"volume", "duration.conc" |
Sample volume and collection duration (urine and feces) |
"start", "end" |
Interval start and end times (end may be Inf) |
"options" |
The PKNCA.options() in effect for the calculation |
"conc.group", "time.group", "dose.group", … |
The same inputs for the whole group rather than the interval |
"conc.sparse", "time.sparse", "subject", … (≥ 0.13.0) |
The pooled individual samples of sparse data (NULL for dense data) |
| (any parameter name) | The computed value of that parameter in the current interval (list it in depends) |
I(<value>) (≥ 0.13.0) |
A constant, passed to the function unchanged |
pknca_ref("<parameter>") (≥ 0.13.0) |
The value of that parameter in the reference interval |
NULL |
Drop the argument from the call so that it keeps its default |
If your function argument names exactly match PKNCA’s standard names, formalsmap can be omitted or left empty. An argument whose name matches a registered parameter is filled from that parameter when it is not in the formalsmap, so map such an argument to NULL if your parameter does not use it.
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). Requesting exposure.ratio alone is enough: PKNCA calculates everything in depends first, and get.parameter.deps("exposure.ratio", recursive = TRUE) lists what it is ultimately calculated from.
# 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"),
formula = "\\frac{AUC_{last}}{C_{max}}"
)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
A parameter that accesses conc and time directly. PKNCA passes the raw vectors from the concentration data. No depends needed. The target time is passed as a constant with I() (≥ 0.13.0): without I(), 2 would be looked up as a data or parameter name instead of being passed as a value. The same function could be registered again as c.4h with time.out = I(4).
# Concentration at a fixed time (interpolated)
pk.calc.conc.at <- function(conc, time, time.out) {
interp.extrap.conc(conc = conc, time = time, time.out = time.out)
}
add.interval.col(
name = "c.2h",
FUN = "pk.calc.conc.at",
values = c(FALSE, TRUE),
unit_type = "conc",
pretty_name = "Concentration at 2 h",
desc = "Interpolated concentration at 2 h",
depends = NULL,
formalsmap = list(conc = "conc", time = "time", time.out = I(2)),
pptestcd_cdisc = "C2H",
pptest_cdisc = "Concentration at 2 h",
formula_note = "Interpolated with interp.extrap.conc()"
)# A tibble: 12 × 3
subject PPORRES PPANMETH
<ord> <dbl> <chr>
1 6 6.32 Interpolated with interp.extrap.conc()
2 7 6.55 Interpolated with interp.extrap.conc()
3 8 7.56 Interpolated with interp.extrap.conc()
4 11 6.80 Interpolated with interp.extrap.conc()
5 3 7.81 Interpolated with interp.extrap.conc()
6 2 8.25 Interpolated with interp.extrap.conc()
7 4 8.41 Interpolated with interp.extrap.conc()
8 9 6.35 Interpolated with interp.extrap.conc()
9 12 9.72 Interpolated with interp.extrap.conc()
10 10 7.76 Interpolated with interp.extrap.conc()
11 1 9.68 Interpolated with interp.extrap.conc()
12 5 9.37 Interpolated with interp.extrap.conc()
pk.calc.conc.at() sets no "method" attribute, so PPANMETH falls back to the registered formula_note. The CDISC codes appear in the CDISC output:
subject pknca_parameter PPTESTCD PPTEST PPORRES
1 1 c.2h C2H Concentration at 2 h 9.677916
When computing several related quantities (e.g. early AUC, late AUC, and their ratio), register each as its own function. The late-AUC and fraction functions read the already-computed auclast dependency.
pk.calc.auc.early <- function(conc, time) {
t_idx <- time <= 4
if (sum(t_idx) < 2) return(NA_real_)
pk.calc.auc.last(conc[t_idx], time[t_idx])
}
pk.calc.auc.late <- function(conc, time, auclast) {
t_idx <- time <= 4
if (sum(t_idx) < 2) return(NA_real_)
auclast - pk.calc.auc.last(conc[t_idx], time[t_idx])
}
pk.calc.auc.early.frac <- function(conc, time, auclast) {
t_idx <- time <= 4
if (sum(t_idx) < 2) return(NA_real_)
pk.calc.auc.last(conc[t_idx], time[t_idx]) / auclast
}
add.interval.col(
name = "auc.early",
FUN = "pk.calc.auc.early",
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC to last sample <= 4h",
desc = "AUC to the last sample at or before 4 h",
depends = NULL,
formalsmap = list(conc = "conc", time = "time")
)
add.interval.col(
name = "auc.late",
FUN = "pk.calc.auc.late",
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC 4h to last",
desc = "AUC from 4 h to last sample",
depends = "auclast",
formalsmap = list(conc = "conc", time = "time", auclast = "auclast")
)
add.interval.col(
name = "auc.early.fraction",
FUN = "pk.calc.auc.early.frac",
values = c(FALSE, TRUE),
unit_type = "fraction",
pretty_name = "Early AUC fraction",
desc = "Fraction of AUClast in the first 4 h",
depends = "auclast",
formalsmap = list(conc = "conc", time = "time", auclast = "auclast")
)These functions return values from pk.calc.auc.last(), which carry a "method" attribute (“AUC: lin up/log down” with the default auc.method); the attribute survives the subtraction and division, so pk.nca() reports it in the PPANMETH column of these custom parameters too. Return as.numeric() of a value if its method should not be reported.
Note: One function can populate several output parameters at once by returning a one-row data frame whose column names are registered parameter names —
pk.nca()saves every column of the returned data frame. A data frame whose names and values do not pair up (for example, one with zero rows) is an error naming the function (≥ 0.13.0). This is exactly howpk.calc.half.life()populateshalf.life,lambda.z,r.squared, and the other half-life diagnostics. Each output still needs its ownadd.interval.col()registration: register the function on the primary parameter, and register the other columns withFUN = NAplus adependson the primary (formalsmapmay not be given whenFUN = NA). The one-function-per-parameter pattern shown above is the simpler option when each output is cheap to compute.
o_nca_split <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
start = 0, end = Inf,
auclast = TRUE,
auc.early = TRUE,
auc.late = TRUE,
auc.early.fraction = TRUE
)))
as.data.frame(o_nca_split) |>
filter(PPTESTCD %in% c("auclast", "auc.early", "auc.late", "auc.early.fraction")) |>
select(subject, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
arrange(subject)# A tibble: 12 × 5
subject auclast auc.early auc.late auc.early.fraction
<ord> <dbl> <dbl> <dbl> <dbl>
1 6 71.7 18.3 53.4 0.255
2 7 88.0 18.2 69.7 0.207
3 8 86.8 22.0 64.8 0.253
4 11 77.9 23.4 54.5 0.301
5 3 95.9 25.9 70.0 0.270
6 2 88.7 24.9 63.8 0.281
7 4 103. 24.1 78.5 0.235
8 9 83.9 22.8 61.2 0.271
9 12 115. 27.3 87.9 0.237
10 10 136. 24.5 111. 0.181
11 1 147. 32.1 115. 0.218
12 5 118. 29.1 89.0 0.247
The same split can be computed by one multi-output function returning a one-row data frame:
pk.calc.auc.split <- function(conc, time, auclast) {
t_idx <- time <= 4
early <- if (sum(t_idx) < 2) NA_real_ else pk.calc.auc.last(conc[t_idx], time[t_idx])
data.frame(auc.early.df = early, auc.late.df = auclast - early)
}
add.interval.col(
name = "auc.early.df",
FUN = "pk.calc.auc.split", # computes BOTH auc.early.df and auc.late.df
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC 0-4h (multi-output)",
desc = "AUC from 0 to 4 h (multi-output)",
depends = "auclast",
formalsmap = list(conc = "conc", time = "time", auclast = "auclast")
)
add.interval.col(
name = "auc.late.df",
FUN = NA, # populated by pk.calc.auc.split via auc.early.df
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC 4h to last (multi-output)",
desc = "AUC from 4 h to last (multi-output)",
depends = "auc.early.df"
)
o_nca_multi <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
start = 0, end = Inf,
auclast = TRUE, auc.early.df = TRUE, auc.late.df = TRUE
)))
as.data.frame(o_nca_multi) |>
filter(PPTESTCD %in% c("auclast", "auc.early.df", "auc.late.df")) |>
select(subject, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
arrange(subject)# A tibble: 12 × 4
subject auclast auc.early.df auc.late.df
<ord> <dbl> <dbl> <dbl>
1 6 71.7 18.3 53.4
2 7 88.0 18.2 69.7
3 8 86.8 22.0 64.8
4 11 77.9 23.4 54.5
5 3 95.9 25.9 70.0
6 2 88.7 24.9 63.8
7 4 103. 24.1 78.5
8 9 83.9 22.8 61.2
9 12 115. 27.3 87.9
10 10 136. 24.5 111.
11 1 147. 32.1 115.
12 5 118. 29.1 89.0
Some parameters compare two intervals — bioavailability, renal clearance, and the built-in ratio.* parameters. Wrapping a formalsmap value in pknca_ref() (≥ 0.13.0) takes that argument from the reference interval instead of the current one. The intervals then link the two rows: the reference row gets an interval_id, and the row calculating the parameter names it in a <parameter>_ref column. Here a custom parameter subtracts the reference interval’s aucint.last from the current one’s:
pk.calc.aucint.change <- function(aucint.last, aucint_reference) {
aucint.last - aucint_reference
}
add.interval.col(
name = "aucint.change",
FUN = "pk.calc.aucint.change",
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUCint change from reference",
desc = "AUCint,last minus reference AUCint,last",
depends = "aucint.last",
formalsmap = list(aucint_reference = pknca_ref("aucint.last"))
)
intervals_linked <- data.frame(
start = c(0, 8),
end = c(8, 24),
interval_id = c("first8h", NA),
aucint.last = TRUE,
aucint.change = c(FALSE, TRUE),
aucint.change_ref = c(NA, "first8h")
)
o_nca_linked <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = intervals_linked))
as.data.frame(o_nca_linked) |>
filter(subject %in% c("1", "2")) |>
select(subject, start, end, PPTESTCD, PPORRES, PPANMETH) |>
arrange(subject, start)# A tibble: 6 × 6
subject start end PPTESTCD PPORRES PPANMETH
<ord> <dbl> <dbl> <chr> <dbl> <chr>
1 2 0 8 aucint.last 51.3 AUC: lin up/log down. Interpolation…
2 2 8 24 aucint.last 37.2 AUC: lin up/log down. Interpolation…
3 2 8 24 aucint.change -14.1 Reference interval: 0-8
4 1 0 8 aucint.last 65.3 AUC: lin up/log down. Interpolation…
5 1 8 24 aucint.last 80.7 AUC: lin up/log down. Interpolation…
6 1 8 24 aucint.change 15.4 Reference interval: 0-8
A pknca_ref() in the formalsmap makes the parameter secondary (calculated after every interval, from the results), so its function may take only parameter values — every argument comes either from the reference interval through pknca_ref() or from a parameter of the current interval listed in depends. The secondary row’s PPANMETH names the reference interval. Requesting, linking, and automatically deriving reference intervals are covered in the secondary parameters chapter and the Secondary Parameters vignette.
A parameter registered with FUN = NA and a FUN_sparse (≥ 0.13.0) is calculated only for sparse data, from the pooled individual samples. formalsmap_sparse replaces formalsmap for that function and can name the sparse inputs, such as subject. (Before 0.13.0 this was sparse = TRUE, which is now an error.)
pk.calc.n.subjects <- function(subject) {
length(unique(subject))
}
add.interval.col(
name = "n.subjects",
FUN = NA,
FUN_sparse = "pk.calc.n.subjects",
values = c(FALSE, TRUE),
unit_type = "count",
pretty_name = "Subjects contributing samples",
desc = "Number of subjects with samples",
formalsmap_sparse = list(subject = "subject")
)
# Two samples from each of six animals
d_sparse <- data.frame(
animal = rep(1:6, each = 2),
time = c(0.5, 4, 2, 8, 12, 0.5, 24, 2, 4, 12, 8, 24),
conc = c(2, 3, 4, 2.5, 3.6, 2, 1.5, 1, 0.9, 0.5, 0.6, 0.2)
)
o_conc_sparse <- PKNCAconc(d_sparse, conc ~ time | animal, sparse = TRUE)
o_nca_sparse <- pk.nca(PKNCAdata(o_conc_sparse, intervals = data.frame(
start = 0, end = 24, n.subjects = TRUE
)))
as.data.frame(o_nca_sparse) |>
select(start, end, PPTESTCD, PPORRES)# A tibble: 1 × 4
start end PPTESTCD PPORRES
<dbl> <dbl> <chr> <int>
1 0 24 n.subjects 6
Requesting n.subjects for dense data is an error, and pknca_parameter_table() reports the parameter as sparse:
parameter concept tier sparse secondary
1 exposure.ratio <NA> uncommon FALSE FALSE
2 c.2h <NA> uncommon FALSE FALSE
3 aucint.change auc uncommon FALSE TRUE
4 n.subjects <NA> uncommon TRUE FALSE
A custom parameter whose concept cannot be derived (here exposure.ratio, c.2h, and n.subjects) has NA for concept: it is calculated normally when requested by name, but it is never selected automatically. Declare one with selection = list(concept = ...) (see pknca_concepts() for the vocabulary) or with pknca_concept() on the calculation function. aucint.change takes its concept from its dependency and is secondary because of its pknca_ref().
A parameter with both FUN and FUN_sparse uses FUN_sparse for sparse data and FUN for dense data; one with only FUN is calculated on the arithmetic-mean profile for sparse data. Sparse analyses are covered in the sparse NCA chapter.
exposure.ratio registered: TRUE
c.2h registered: TRUE
List of 22
$ FUN : chr "pk.calc.exposure.ratio"
$ FUN_sparse : chr NA
$ values : logi [1:2] FALSE TRUE
$ unit_type : chr "time"
$ pretty_name : chr "Exposure ratio (AUClast / Cmax)"
$ desc : chr "AUClast divided by Cmax"
$ formalsmap : list()
$ formalsmap_sparse : list()
$ depends : chr [1:2] "auclast" "cmax"
$ datatype : chr "interval"
$ pptestcd_cdisc : chr "exposure.ratio"
$ pptest_cdisc : chr "AUClast divided by Cmax"
$ formula : chr "\\frac{AUC_{last}}{C_{max}}"
$ formula_note : NULL
$ tier : chr "uncommon"
$ selection : list()
$ requires_dose_amt : logi FALSE
$ requires_dose_time: logi FALSE
$ requires_dose_dur : logi FALSE
$ requires_volume : logi FALSE
$ requires_conc_dur : logi FALSE
$ arg_spec :List of 2
..$ arglist :List of 2
.. ..$ auclast: chr "auclast"
.. ..$ cmax : chr "cmax"
..$ required: chr [1:2] "auclast" "cmax"
After registering the parameter, set a custom summary rule with PKNCA.set.summary():
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.
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.pk.nca() runs. Define them in the global environment or in a package loaded before calling pk.nca().desc to 40 characters or fewer if the results may go into an SDTM submission; longer descriptions warn.pkgdown reference: PKNCAconc() · PKNCAdose() · PKNCAdata() · pk.nca() · add.interval.col() · get.interval.cols() · get.parameter.deps() · pknca_ref() · pknca_parameter_table() · pknca_concepts() · pknca_concept() · as.data.frame.PKNCAresults() · interp.extrap.conc() · pk.calc.auc.last() · PKNCA.set.summary() · geomean() · geocv()
---
title: "Writing Custom Parameter Functions"
---
```{r setup, include=FALSE}
library(PKNCA)
library(dplyr)
conflicted::conflicts_prefer(dplyr::filter, dplyr::select, .quiet = TRUE)
# Shared dataset
d_conc <- as.data.frame(Theoph) |> rename(time = Time, subject = Subject)
d_dose <- Theoph |> as.data.frame() |>
group_by(Subject) |>
summarise(dose = Dose[1] * Wt[1], .groups = "drop") |>
rename(subject = Subject) |>
mutate(time = 0)
o_conc <- PKNCAconc(d_conc, conc ~ time | subject)
o_dose <- PKNCAdose(d_dose, dose ~ time | subject, route = "extravascular")
```
## 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](https://humanpred.github.io/pknca/articles/v80-writing-parameter-functions.html).
```{r}
# Number of parameters registered by default
length(get.interval.cols())
```
---
## Anatomy of `add.interval.col()`
```r
add.interval.col(
name = "my.param", # column name used in the intervals data frame
FUN = "pk.calc.my.param", # function name as a character string, or NA
values = c(FALSE, TRUE), # allowed values in intervals (FALSE = skip, TRUE = compute)
unit_type = "time", # units category (required; see below)
pretty_name = "My parameter", # human-readable label for summary tables
depends = NULL, # other parameters that must be computed first
desc = "What it does", # one-line description; SDTM limit 40 characters
formalsmap = list(), # list(<your function arg> = <data or parameter name>)
FUN_sparse = NA, # sparse-PK estimator (function name), if any
formalsmap_sparse = list(), # formalsmap for FUN_sparse
datatype = "interval", # only "interval" is supported; the others error
pptestcd_cdisc = "MYPARAM", # CDISC PPTESTCD code (defaults to name)
pptest_cdisc = "My Parameter", # CDISC PPTEST label (defaults to desc)
formula = NULL, # LaTeX expression, for documentation
formula_note = NULL, # note on the formula; also the PPANMETH fallback
tier = "uncommon", # "common" or "uncommon"
selection = NULL # list(concept=, route=, dosing=, secondary=)
)
```
**Key rule:** `FUN` must be a **character string** (the function name), not a function object.
What each argument does, as of PKNCA 0.13.0:
| Argument | Notes |
|---|---|
| `name` | A non-empty string. Names ending in `_ref` and the name `interval_id` are reserved for the reference-interval linkage columns (see [Example 4](#example-4-a-parameter-that-needs-another-interval)) and are an error. |
| `FUN` | The calculation function's name, or `NA` for a parameter that is filled in as one column of another parameter's data-frame result, or that only a `FUN_sparse` estimator calculates. |
| `unit_type` | Required; one of the values listed below. |
| `desc` | Since 0.13.0, a description longer than 40 characters (the SDTM limit) is still registered but gives a warning. |
| `formalsmap` | Maps your function's arguments to data, parameters, constants (`I()`), or reference-interval values (`pknca_ref()`); see [below](#the-formalsmap-argument). |
| `FUN_sparse`, `formalsmap_sparse` (≥ 0.13.0) | A second calculation function used when the data are sparse; see [Example 5](#example-5-a-sparse-only-parameter). |
| `pptestcd_cdisc`, `pptest_cdisc` (≥ 0.13.0) | The codes used by `as.data.frame(out_format = "cdisc")`. Each is a single string, a list with one element `route` holding a named list by route (`list(route = list(extravascular = "CLF/FO", intravascular = "CLO"))`), or a list of exactly the elements `dense` and `sparse`; anything else is an error. |
| `formula`, `formula_note` (≥ 0.13.0) | Documentation of the calculation. When the calculation function sets no `"method"` attribute, `formula_note` is reported in the `PPANMETH` column. |
| `tier`, `selection` (≥ 0.13.0) | Classification used by automatic interval selection and reported by `pknca_parameter_table()`. A custom parameter is `"uncommon"` by default, so it is calculated only when requested by name. |
| `sparse` | Retired (≥ 0.13.0). `sparse = TRUE` is an error; an explicit `sparse = FALSE` is accepted and does nothing. Register a sparse-only parameter with `FUN = NA` and `FUN_sparse` instead. |
### 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"`
---
## 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.
Values available in a `formalsmap` (the full list is in `?add.interval.col`):
| `formalsmap` value | What it provides |
|---|---|
| `"conc"`, `"time"` | Concentrations and times for the current interval (times start at 0 at the interval start) |
| `"dose"`, `"time.dose"`, `"duration.dose"`, `"route"` | Dose amounts, times, durations, and route for the current interval |
| `"volume"`, `"duration.conc"` | Sample volume and collection duration (urine and feces) |
| `"start"`, `"end"` | Interval start and end times (`end` may be `Inf`) |
| `"options"` | The `PKNCA.options()` in effect for the calculation |
| `"conc.group"`, `"time.group"`, `"dose.group"`, ... | The same inputs for the whole group rather than the interval |
| `"conc.sparse"`, `"time.sparse"`, `"subject"`, ... (≥ 0.13.0) | The pooled individual samples of sparse data (`NULL` for dense data) |
| *(any parameter name)* | The computed value of that parameter in the current interval (list it in `depends`) |
| `I(<value>)` (≥ 0.13.0) | A constant, passed to the function unchanged |
| `pknca_ref("<parameter>")` (≥ 0.13.0) | The value of that parameter in the reference interval |
| `NULL` | Drop the argument from the call so that it keeps its default |
If your function argument names **exactly match** PKNCA's standard names, `formalsmap` can be omitted or left empty. An argument whose name matches a registered parameter is filled from that parameter when it is not in the `formalsmap`, so map such an argument to `NULL` if your parameter does not use it.
---
## 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). Requesting `exposure.ratio` alone is enough: PKNCA calculates everything in `depends` first, and `get.parameter.deps("exposure.ratio", recursive = TRUE)` lists what it is ultimately calculated from.
```{r}
# 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"),
formula = "\\frac{AUC_{last}}{C_{max}}"
)
```
```{r}
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)
```
---
## Example 2: computed from raw concentration-time data, with a constant
A parameter that accesses `conc` and `time` directly. PKNCA passes the raw vectors from the concentration data. No `depends` needed. The target time is passed as a constant with `I()` (≥ 0.13.0): without `I()`, `2` would be looked up as a data or parameter name instead of being passed as a value. The same function could be registered again as `c.4h` with `time.out = I(4)`.
```{r}
# Concentration at a fixed time (interpolated)
pk.calc.conc.at <- function(conc, time, time.out) {
interp.extrap.conc(conc = conc, time = time, time.out = time.out)
}
add.interval.col(
name = "c.2h",
FUN = "pk.calc.conc.at",
values = c(FALSE, TRUE),
unit_type = "conc",
pretty_name = "Concentration at 2 h",
desc = "Interpolated concentration at 2 h",
depends = NULL,
formalsmap = list(conc = "conc", time = "time", time.out = I(2)),
pptestcd_cdisc = "C2H",
pptest_cdisc = "Concentration at 2 h",
formula_note = "Interpolated with interp.extrap.conc()"
)
```
```{r}
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, PPANMETH) |>
arrange(subject)
```
`pk.calc.conc.at()` sets no `"method"` attribute, so `PPANMETH` falls back to the registered `formula_note`. The CDISC codes appear in the CDISC output:
```{r}
as.data.frame(o_nca_c2, out_format = "cdisc") |>
filter(subject == "1") |>
select(subject, pknca_parameter, PPTESTCD, PPTEST, PPORRES)
```
---
## Example 3: multiple related parameters sharing a dependency
When computing several related quantities (e.g. early AUC, late AUC, and their ratio), register each as its own function. The late-AUC and fraction functions read the already-computed `auclast` dependency.
```{r}
pk.calc.auc.early <- function(conc, time) {
t_idx <- time <= 4
if (sum(t_idx) < 2) return(NA_real_)
pk.calc.auc.last(conc[t_idx], time[t_idx])
}
pk.calc.auc.late <- function(conc, time, auclast) {
t_idx <- time <= 4
if (sum(t_idx) < 2) return(NA_real_)
auclast - pk.calc.auc.last(conc[t_idx], time[t_idx])
}
pk.calc.auc.early.frac <- function(conc, time, auclast) {
t_idx <- time <= 4
if (sum(t_idx) < 2) return(NA_real_)
pk.calc.auc.last(conc[t_idx], time[t_idx]) / auclast
}
add.interval.col(
name = "auc.early",
FUN = "pk.calc.auc.early",
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC to last sample <= 4h",
desc = "AUC to the last sample at or before 4 h",
depends = NULL,
formalsmap = list(conc = "conc", time = "time")
)
add.interval.col(
name = "auc.late",
FUN = "pk.calc.auc.late",
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC 4h to last",
desc = "AUC from 4 h to last sample",
depends = "auclast",
formalsmap = list(conc = "conc", time = "time", auclast = "auclast")
)
add.interval.col(
name = "auc.early.fraction",
FUN = "pk.calc.auc.early.frac",
values = c(FALSE, TRUE),
unit_type = "fraction",
pretty_name = "Early AUC fraction",
desc = "Fraction of AUClast in the first 4 h",
depends = "auclast",
formalsmap = list(conc = "conc", time = "time", auclast = "auclast")
)
```
These functions return values from `pk.calc.auc.last()`, which carry a `"method"` attribute ("AUC: lin up/log down" with the default `auc.method`); the attribute survives the subtraction and division, so `pk.nca()` reports it in the `PPANMETH` column of these custom parameters too. Return `as.numeric()` of a value if its method should not be reported.
> **Note:** One function *can* populate several output parameters at once by returning a **one-row data frame** whose column names are registered parameter names — `pk.nca()` saves every column of the returned data frame. A data frame whose names and values do not pair up (for example, one with zero rows) is an error naming the function (≥ 0.13.0). This is exactly how `pk.calc.half.life()` populates `half.life`, `lambda.z`, `r.squared`, and the other half-life diagnostics. Each output still needs its own `add.interval.col()` registration: register the function on the primary parameter, and register the other columns with `FUN = NA` plus a `depends` on the primary (`formalsmap` may not be given when `FUN = NA`). The one-function-per-parameter pattern shown above is the simpler option when each output is cheap to compute.
```{r}
o_nca_split <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
start = 0, end = Inf,
auclast = TRUE,
auc.early = TRUE,
auc.late = TRUE,
auc.early.fraction = TRUE
)))
as.data.frame(o_nca_split) |>
filter(PPTESTCD %in% c("auclast", "auc.early", "auc.late", "auc.early.fraction")) |>
select(subject, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
arrange(subject)
```
The same split can be computed by **one** multi-output function returning a one-row data frame:
```{r}
pk.calc.auc.split <- function(conc, time, auclast) {
t_idx <- time <= 4
early <- if (sum(t_idx) < 2) NA_real_ else pk.calc.auc.last(conc[t_idx], time[t_idx])
data.frame(auc.early.df = early, auc.late.df = auclast - early)
}
add.interval.col(
name = "auc.early.df",
FUN = "pk.calc.auc.split", # computes BOTH auc.early.df and auc.late.df
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC 0-4h (multi-output)",
desc = "AUC from 0 to 4 h (multi-output)",
depends = "auclast",
formalsmap = list(conc = "conc", time = "time", auclast = "auclast")
)
add.interval.col(
name = "auc.late.df",
FUN = NA, # populated by pk.calc.auc.split via auc.early.df
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUC 4h to last (multi-output)",
desc = "AUC from 4 h to last (multi-output)",
depends = "auc.early.df"
)
o_nca_multi <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = data.frame(
start = 0, end = Inf,
auclast = TRUE, auc.early.df = TRUE, auc.late.df = TRUE
)))
as.data.frame(o_nca_multi) |>
filter(PPTESTCD %in% c("auclast", "auc.early.df", "auc.late.df")) |>
select(subject, PPTESTCD, PPORRES) |>
tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
arrange(subject)
```
---
## Example 4: a parameter that needs another interval
Some parameters compare two intervals — bioavailability, renal clearance, and the built-in `ratio.*` parameters. Wrapping a `formalsmap` value in `pknca_ref()` (≥ 0.13.0) takes that argument from the **reference** interval instead of the current one. The intervals then link the two rows: the reference row gets an `interval_id`, and the row calculating the parameter names it in a `<parameter>_ref` column. Here a custom parameter subtracts the reference interval's `aucint.last` from the current one's:
```{r}
pk.calc.aucint.change <- function(aucint.last, aucint_reference) {
aucint.last - aucint_reference
}
add.interval.col(
name = "aucint.change",
FUN = "pk.calc.aucint.change",
values = c(FALSE, TRUE),
unit_type = "auc",
pretty_name = "AUCint change from reference",
desc = "AUCint,last minus reference AUCint,last",
depends = "aucint.last",
formalsmap = list(aucint_reference = pknca_ref("aucint.last"))
)
intervals_linked <- data.frame(
start = c(0, 8),
end = c(8, 24),
interval_id = c("first8h", NA),
aucint.last = TRUE,
aucint.change = c(FALSE, TRUE),
aucint.change_ref = c(NA, "first8h")
)
o_nca_linked <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = intervals_linked))
as.data.frame(o_nca_linked) |>
filter(subject %in% c("1", "2")) |>
select(subject, start, end, PPTESTCD, PPORRES, PPANMETH) |>
arrange(subject, start)
```
A `pknca_ref()` in the `formalsmap` makes the parameter *secondary* (calculated after every interval, from the results), so its function may take only parameter values — every argument comes either from the reference interval through `pknca_ref()` or from a parameter of the current interval listed in `depends`. The secondary row's `PPANMETH` names the reference interval. Requesting, linking, and automatically deriving reference intervals are covered in the [secondary parameters chapter](secondary-parameters.qmd) and the [Secondary Parameters vignette](https://humanpred.github.io/pknca/articles/v09-secondary-parameters.html).
---
## Example 5: a sparse-only parameter
A parameter registered with `FUN = NA` and a `FUN_sparse` (≥ 0.13.0) is calculated only for sparse data, from the pooled individual samples. `formalsmap_sparse` replaces `formalsmap` for that function and can name the sparse inputs, such as `subject`. (Before 0.13.0 this was `sparse = TRUE`, which is now an error.)
```{r}
pk.calc.n.subjects <- function(subject) {
length(unique(subject))
}
add.interval.col(
name = "n.subjects",
FUN = NA,
FUN_sparse = "pk.calc.n.subjects",
values = c(FALSE, TRUE),
unit_type = "count",
pretty_name = "Subjects contributing samples",
desc = "Number of subjects with samples",
formalsmap_sparse = list(subject = "subject")
)
# Two samples from each of six animals
d_sparse <- data.frame(
animal = rep(1:6, each = 2),
time = c(0.5, 4, 2, 8, 12, 0.5, 24, 2, 4, 12, 8, 24),
conc = c(2, 3, 4, 2.5, 3.6, 2, 1.5, 1, 0.9, 0.5, 0.6, 0.2)
)
o_conc_sparse <- PKNCAconc(d_sparse, conc ~ time | animal, sparse = TRUE)
o_nca_sparse <- pk.nca(PKNCAdata(o_conc_sparse, intervals = data.frame(
start = 0, end = 24, n.subjects = TRUE
)))
as.data.frame(o_nca_sparse) |>
select(start, end, PPTESTCD, PPORRES)
```
Requesting `n.subjects` for dense data is an error, and `pknca_parameter_table()` reports the parameter as sparse:
```{r}
pknca_parameter_table(c("exposure.ratio", "c.2h", "aucint.change", "n.subjects")) |>
select(parameter, concept, tier, sparse, secondary)
```
A custom parameter whose concept cannot be derived (here `exposure.ratio`, `c.2h`, and `n.subjects`) has `NA` for `concept`: it is calculated normally when requested by name, but it is never selected automatically. Declare one with `selection = list(concept = ...)` (see `pknca_concepts()` for the vocabulary) or with `pknca_concept()` on the calculation function. `aucint.change` takes its concept from its dependency and is secondary because of its `pknca_ref()`.
A parameter with both `FUN` and `FUN_sparse` uses `FUN_sparse` for sparse data and `FUN` for dense data; one with only `FUN` is calculated on the arithmetic-mean profile for sparse data. Sparse analyses are covered in the [sparse NCA chapter](sparse.qmd).
---
## Inspecting registered parameters
```{r}
# Confirm your parameter is registered
cols <- get.interval.cols()
cat("exposure.ratio registered:", "exposure.ratio" %in% names(cols), "\n")
cat("c.2h registered: ", "c.2h" %in% names(cols), "\n")
```
```{r}
# Inspect registration details
str(cols[["exposure.ratio"]])
```
---
## Custom summary statistics for your parameter
After registering the parameter, set a custom summary rule with `PKNCA.set.summary()`:
```{r}
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
))))
```
The vignette's [summary section](https://humanpred.github.io/pknca/articles/v80-writing-parameter-functions.html#tell-pknca-how-to-summarize-the-parameter) covers the same registration.
---
## 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()`.
- **Keep `desc` to 40 characters or fewer** if the results may go into an SDTM submission; longer descriptions warn.
---
::: {.callout-note icon=false appearance="minimal"}
**pkgdown reference:** [PKNCAconc()](https://humanpred.github.io/pknca/reference/PKNCAconc.html) · [PKNCAdose()](https://humanpred.github.io/pknca/reference/PKNCAdose.html) · [PKNCAdata()](https://humanpred.github.io/pknca/reference/PKNCAdata.html) · [pk.nca()](https://humanpred.github.io/pknca/reference/pk.nca.html) · [add.interval.col()](https://humanpred.github.io/pknca/reference/add.interval.col.html) · [get.interval.cols()](https://humanpred.github.io/pknca/reference/get.interval.cols.html) · [get.parameter.deps()](https://humanpred.github.io/pknca/reference/get.parameter.deps.html) · [pknca_ref()](https://humanpred.github.io/pknca/reference/pknca_ref.html) · [pknca_parameter_table()](https://humanpred.github.io/pknca/reference/pknca_parameter_table.html) · [pknca_concepts()](https://humanpred.github.io/pknca/reference/pknca_concepts.html) · [pknca_concept()](https://humanpred.github.io/pknca/reference/pknca_concept.html) · [as.data.frame.PKNCAresults()](https://humanpred.github.io/pknca/reference/as.data.frame.PKNCAresults.html) · [interp.extrap.conc()](https://humanpred.github.io/pknca/reference/interp.extrap.conc.html) · [pk.calc.auc.last()](https://humanpred.github.io/pknca/reference/pk.calc.auxc.html) · [PKNCA.set.summary()](https://humanpred.github.io/pknca/reference/PKNCA.set.summary.html) · [geomean()](https://humanpred.github.io/pknca/reference/geomean.html) · [geocv()](https://humanpred.github.io/pknca/reference/geomean.html)
:::