---
title: "Units Assignment and Conversion"
---
```{r setup, include=FALSE}
library(PKNCA)
library(dplyr)
library(ggplot2)
conflicted::conflicts_prefer(dplyr::filter, dplyr::select, .quiet = TRUE)
```
## Why units matter
By default PKNCA returns bare numbers. When you provide units, every row in
`as.data.frame()` gains columns that mirror the CDISC SDTM (Study Data
Tabulation Model) PP (PK Parameters) domain:
| Column | Meaning |
|--------|---------|
| `PPORRESU` | Original reporting unit (the unit the result is expressed in) |
| `PPSTRESU` | Standardized unit (populated when a conversion is requested) |
| `PPSTRES` | Result expressed in the standardized unit |
This makes the output submission-ready and prevents silent unit mismatches when
combining results from multiple studies or analytes. See also the package
vignette [Unit Assignment and Conversion with PKNCA](https://humanpred.github.io/pknca/articles/v07-unit-conversion.html).
---
## Assigning units at object creation
Units can be supplied directly to `PKNCAconc()` and `PKNCAdose()` as string
literals or as the name of a column in your data frame that holds the unit
string per row.
### Arguments in `PKNCAconc()`
You can list the full argument set (the unit arguments are at the end) with:
```{r}
formals(getS3method("PKNCAconc", "data.frame")) |> names()
```
The unit-related arguments are:
- `concu` — concentration unit (e.g. `"mg/L"`)
- `timeu` — time unit (e.g. `"h"`)
- `amountu` — amount unit, used for urine/fecal data (e.g. `"mg"`)
- `concu_pref` — preferred output unit for concentration (triggers conversion)
- `timeu_pref` — preferred output unit for time (triggers conversion)
- `amountu_pref` — preferred output unit for amount (triggers conversion)
### Arguments in `PKNCAdose()`
- `doseu` — dose unit (e.g. `"mg"`); like the `PKNCAconc()` unit arguments, it also accepts the name of a column in the dose data holding per-row unit strings
- `doseu_pref` — preferred output unit for dose (triggers conversion)
### Example: units as string literals
```{r}
Theoph_df <- as.data.frame(Theoph)
d_dose <- Theoph_df |>
filter(Time == 0) |>
mutate(amt = Dose * Wt)
o_conc <- PKNCAconc(Theoph_df, conc ~ Time | Subject,
concu = "mg/L", timeu = "h")
o_dose <- PKNCAdose(d_dose, amt ~ Time | Subject,
doseu = "mg")
```
### Example: units from a data column (≥ 0.12.0)
```{r}
Theoph_units <- Theoph_df |>
mutate(conc_unit = "mg/L", time_unit = "h")
o_conc_col <- PKNCAconc(Theoph_units, conc ~ Time | Subject,
concu = "conc_unit", timeu = "time_unit")
```
---
## `pknca_units_table()`
This function builds a complete mapping from every NCA parameter to its
reporting unit, derived from the four base units you supply.
In PKNCA ≥ 0.13.0, `pknca_units_table()` is an **S3 generic**. The most useful new method is `pknca_units_table.PKNCAdata()`, which extracts units directly from a fully assembled `PKNCAdata` object — useful when units are already embedded in `PKNCAconc` and `PKNCAdose`:
```{r}
# Units already embedded in the data objects
o_conc_u <- PKNCAconc(as.data.frame(Theoph) |> rename(time=Time, subject=Subject),
conc ~ time | subject, concu = "mg/L", timeu = "h")
o_dose_u <- PKNCAdose(
as.data.frame(Theoph) |>
group_by(Subject) |>
summarise(dose = Dose[1]*Wt[1], .groups="drop") |>
rename(subject=Subject) |>
mutate(time = 0),
dose ~ time | subject, route = "extravascular",
doseu = "mg"
)
o_data_u <- PKNCAdata(o_conc_u, o_dose_u)
# Extract units from the PKNCAdata object (≥ 0.13.0)
u_from_data <- pknca_units_table(o_data_u)
head(u_from_data, 6)
```
### Built automatically by `PKNCAdata()` (≥ 0.13.0)
You rarely need to call the `PKNCAdata` method yourself: when `PKNCAdata()` is called without a `units` argument, it builds the same table from the unit arguments of the `PKNCAconc` and `PKNCAdose` objects and stores it in the object. Every result of `pk.nca()` then has a `PPORRESU` column:
```{r}
identical(o_data_u$units, u_from_data)
as.data.frame(pk.nca(o_data_u)) |>
filter(subject == "1", PPTESTCD %in% c("auclast", "cmax", "tmax")) |>
select(PPTESTCD, PPORRES, PPORRESU)
```
When the unit arguments name columns of the data, the table is stratified by the groups, so that each analyte or specimen can have its own units. A unit column must have one value within each concentration group; a value that changes within a group (including `NA` for some rows) is the `pknca_error_units_nonuniform_groups` error naming the groups:
```{r}
Theoph_mixed <- Theoph_df |> mutate(conc_unit = ifelse(Time > 10, "ng/mL", "mg/L"))
tryCatch(
PKNCAdata(PKNCAconc(Theoph_mixed, conc ~ Time | Subject, concu = "conc_unit", timeu = "h"),
o_dose),
# Show the first lines of the message, which lists every group
error = function(e) cat(head(strsplit(conditionMessage(e), "\n")[[1]], 3), sep = "\n")
)
```
Because the table is built from whatever units were given, give all of the base units that the requested parameters need. With only `timeu` given, the table has units for times but not for concentrations, and `pk.nca()` stops for the concentration-based parameters (see [Allowing partial units](#partial-units) below):
```{r}
o_data_timeu_only <- PKNCAdata(
PKNCAconc(Theoph_df, conc ~ Time | Subject, timeu = "h"),
PKNCAdose(d_dose, amt ~ Time | Subject),
intervals = data.frame(start = 0, end = Inf, cmax = TRUE, tmax = TRUE)
)
tryCatch(pk.nca(o_data_timeu_only), error = function(e) cat(conditionMessage(e), "\n"))
```
### Arguments (default method)
```{r}
args(getS3method("pknca_units_table", "default"))
```
PKNCA derives composite units automatically (e.g. `auclast` gets `h*mg/L`
from `timeu="h"` and `concu="mg/L"`). Each base unit is optional: a parameter
whose unit needs a base unit that was not given (for example, `ae` without
`amountu`) gets an `NA` unit in the table.
### Output columns
The returned data frame has exactly two columns when no preferred units are
requested:
| Column | Content |
|--------|---------|
| `PPORRESU` | Reporting unit for this parameter |
| `PPTESTCD` | Parameter code (e.g. `"cmax"`, `"auclast"`, `"tmax"`) |
When preferred units are requested, two additional columns appear:
`PPSTRESU` (target unit) and `conversion_factor`.
```{r}
u <- pknca_units_table(concu = "mg/L", timeu = "h",
doseu = "mg", amountu = "mg")
# The table covers all NCA parameters
nrow(u)
# Sample rows
u[u$PPTESTCD %in% c("cmax", "auclast", "tmax", "cl.obs", "vz.obs"), ]
```
---
## Passing the units table to `PKNCAdata()`
Pass the table via the `units` argument, reusing `u` from above. If `units` is
provided, it takes precedence: any units given directly to `PKNCAconc()` or
`PKNCAdose()` are ignored. The example below sets a deliberately different
concentration unit (`ng/mL`) on the `PKNCAconc` object to demonstrate this —
the results report `mg/L` from the table.
```{r}
# Units set on the objects are superseded by the units table passed to PKNCAdata()
o_conc2 <- PKNCAconc(Theoph_df, conc ~ Time | Subject,
concu = "ng/mL", timeu = "h")
o_dose2 <- PKNCAdose(d_dose, amt ~ Time | Subject, doseu = "mg")
o_data <- PKNCAdata(o_conc2, o_dose2, units = u) # u built in the section above
# Run NCA
res <- pk.nca(o_data)
# Units appear in the PPORRESU column: mg/L from the table,
# not the ng/mL set on the object
df <- as.data.frame(res)
stopifnot("PPORRESU" %in% names(df)) # unit column present on every row
df |>
filter(Subject == "1",
PPTESTCD %in% c("auclast", "cmax", "tmax")) |>
select(PPTESTCD, PPORRES, PPORRESU)
```
Whenever a units table is in effect, `PPORRESU` is populated for every result
row. The standardized-unit columns `PPSTRESU` and `PPSTRES` appear only when
preferred or custom conversions are requested, as the next sections show.
---
## Preferred units
Setting `concu_pref`, `timeu_pref`, `doseu_pref`, or `amountu_pref` in
`pknca_units_table()` tells PKNCA to convert results to those preferred units.
PKNCA calculates the conversion factor automatically for metric prefixes and
time unit changes.
When preferred units are active the results data frame gains:
- `PPSTRESU` — the preferred unit
- `PPSTRES` — the result in the preferred unit
```{r}
u_pref <- pknca_units_table(
concu = "mg/L", timeu = "h",
doseu = "mg", amountu = "mg",
concu_pref = "ug/L", # mg/L → ug/L (× 1000)
timeu_pref = "day" # h → day (× 1/24)
)
# Inspect what the table says for key parameters
u_pref[u_pref$PPTESTCD %in% c("cmax", "auclast", "tmax"), ]
```
```{r}
o_data_pref <- PKNCAdata(o_conc2, o_dose2, units = u_pref)
res_pref <- pk.nca(o_data_pref)
as.data.frame(res_pref) |>
filter(Subject == "1",
PPTESTCD %in% c("auclast", "cmax", "tmax")) |>
select(PPTESTCD, PPORRES, PPORRESU, PPSTRES, PPSTRESU)
```
`PPORRES`/`PPORRESU` hold the original value and unit; `PPSTRES`/`PPSTRESU`
hold the converted value and unit.
---
## Custom unit conversions
For conversions PKNCA cannot derive automatically (e.g. mass ↔ molar, or a
non-standard reporting preference), pass a `conversions` data frame to
`pknca_units_table()`. For a molar-conversion example and per-analyte unit
tables, see [the vignette's analyte section](https://humanpred.github.io/pknca/articles/v07-unit-conversion.html#how-do-i-add-different-unit-conversions-for-different-analytes).
### Required columns
| Column | Type | Meaning |
|--------|------|---------|
| `PPORRESU` | character | The original unit to match |
| `PPSTRESU` | character | The target unit |
| `conversion_factor` | numeric | Multiplier (original × factor = target). Use `NA` to ask PKNCA to compute it. |
### Example: report concentration as ug/mL instead of mg/L
`mg/L` and `ug/mL` are numerically equal (factor = 1), but differ in label.
```{r}
my_conversions <- data.frame(
PPORRESU = c("mg/L", "h*mg/L"),
PPSTRESU = c("ug/mL", "h*ug/mL"),
conversion_factor = c(1, 1)
)
u_custom <- pknca_units_table(
concu = "mg/L", timeu = "h",
doseu = "mg", amountu = "mg",
conversions = my_conversions
)
o_data_custom <- PKNCAdata(o_conc2, o_dose2, units = u_custom)
res_custom <- pk.nca(o_data_custom)
as.data.frame(res_custom) |>
filter(Subject == "1",
PPTESTCD %in% c("auclast", "cmax")) |>
select(PPTESTCD, PPORRES, PPORRESU, PPSTRES, PPSTRESU)
```
---
## Allowing partial units (≥ 0.12.0) {#partial-units}
`pknca_units_table()` always returns a complete table, but a hand-edited or
reused units table can end up missing entries for some of the parameters an
analysis requests. By default PKNCA treats that as an error when `pk.nca()`
runs, so no partially-unitless results are produced by accident. Here the
`half.life` and `tmax` rows are removed from the table built above:
```{r}
u_partial <- u |> filter(!(PPTESTCD %in% c("half.life", "tmax")))
o_data_partial <- PKNCAdata(o_conc2, o_dose2, units = u_partial)
# The missing entries are an error when the calculation runs
tryCatch(
pk.nca(o_data_partial),
error = function(e) conditionMessage(e)
)
```
Setting `allow_partial_missing_units = TRUE` converts that error to a warning
so the run completes. It can be set per-analysis through the `options` argument
of `PKNCAdata()` (as below) or globally with
`PKNCA.options(allow_partial_missing_units = TRUE)`:
```{r}
o_data_partial_ok <- PKNCAdata(
o_conc2, o_dose2, units = u_partial,
options = list(allow_partial_missing_units = TRUE)
)
res_partial <- pk.nca(o_data_partial_ok)
as.data.frame(res_partial) |>
filter(Subject == "1",
PPTESTCD %in% c("cmax", "tmax", "half.life")) |>
select(PPTESTCD, PPORRES, PPORRESU)
```
The warning above names the parameters that lack units; it is expected here and
should be reviewed, not silenced. The affected parameters (`tmax`,
`half.life`) are still calculated but reported without a unit — their
`PPORRESU` is `NA` — while parameters present in the table (`cmax`) keep
theirs.
---
## The time unit and the dosing interval (≥ 0.13.0)
The time unit of the concentration data (`timeu` in `PKNCAconc()`) is not only a label. `PKNCAdata()` uses it when it detects the dosing interval (tau) for automatically generated intervals, and `pk.nca()` uses it for the tau of multiple-dose parameters: dose times are compared with nominal dosing intervals (every 4 hours to every 12 weeks) in that unit. **Times without a unit are taken to be hours.** Data recorded in minutes or days without saying so are therefore compared with intervals in hours. For once-daily doses recorded in minutes, the 1440-minute spacing is read as 1440 hours and snapped to the nearest nominal interval, every 8 weeks:
```{r}
dose_times_min <- c(0, 1440, 2880, 4320)
find.dose.regimen(dose_times_min)[, c("interval", "label")]
find.dose.regimen(dose_times_min, timeu = "min")[, c("interval", "label")]
```
Give `timeu` to `PKNCAconc()` whenever the times are not in hours (together with the other base units; see above). An interval that matches no nominal interval gives the `pknca_warning_tau_not_nominal` warning, and without the units package a `timeu` other than hours gives the `pknca_message_tau_units_fallback` message and leaves the interval to the dose times. Interval and regimen detection are covered in the [Automatic Intervals and Dosing Regimens chapter](auto-intervals.qmd).
With date-time (`POSIXct`) concentration and dose times, the analysis unit is `timeu_pref` when given, otherwise `timeu`, otherwise hours, and the times are converted directly into it; see the [Date-Time Data and Time References chapter](datetime.qmd).
---
## Units of secondary parameters (≥ 0.13.0)
A secondary parameter, such as a renal clearance or a metabolite ratio, is calculated from values of two different profiles, and its unit is composed from the units of the values it was actually calculated from. A renal clearance dividing a urine amount in `mg` by a plasma AUC in `hr*ng/mL` is reported in `mg/(hr*ng/mL)`, not in units built from the urine group alone. The composition happens in the units table, which gains `<group column>_ref` columns and one row per pair of groups, and results gain the same columns naming the group each secondary value took its reference from.
No value is converted or made `NA` for unit reasons. Where the two sides of a quotient are convertible (a `ratio.*` of two analytes reported in `ng/mL` and `mg/L`), the raw quotient is standardized through `PPSTRES` to a `PPSTRESU` of `"fraction"`; where they are not (a bioavailability between a dose in `mg` and a dose in `mg/kg`), the composite units stand. An analysis with such a quotient therefore gains `PPSTRES` and `PPSTRESU` columns, which equal `PPORRES` and `PPORRESU` for every other parameter. Worked examples are in the [Secondary Parameters chapter](secondary-parameters.qmd).
---
## Summary
| Goal | How |
|------|-----|
| Assign units at data creation | `concu`, `timeu`, `doseu` args in `PKNCAconc()` / `PKNCAdose()` |
| Build a complete unit table | `pknca_units_table(concu, doseu, amountu, timeu)` |
| Build the units table from the unit arguments (≥ 0.13.0) | Automatic in `PKNCAdata()` when `units` is not given |
| Pass units to the analysis | `PKNCAdata(..., units = u)` |
| Convert to preferred output units | Add `concu_pref`, `timeu_pref`, etc. to `pknca_units_table()` |
| Custom / non-metric conversions | `conversions` argument in `pknca_units_table()` |
| Inspect units in results | `as.data.frame(res)$PPORRESU` |
| Extract units from a PKNCAdata object (≥ 0.13.0) | `pknca_units_table(o_data)` |
| Match dosing intervals in the right unit (≥ 0.13.0) | `timeu` in `PKNCAconc()` (unitless times are hours) |
| Allow some parameters without units (≥ 0.12.0) | `PKNCA.options(allow_partial_missing_units = TRUE)` |
---
::: {.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) · [pknca_units_table()](https://humanpred.github.io/pknca/reference/pknca_units_table.html) · [find.dose.regimen()](https://humanpred.github.io/pknca/reference/find.dose.regimen.html) · [PKNCA.options()](https://humanpred.github.io/pknca/reference/PKNCA.options.html)
:::