14  Units Assignment and Conversion

14.1 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.


14.2 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.

14.2.1 Arguments in PKNCAconc()

You can list the full argument set (the unit arguments are at the end) with:

formals(getS3method("PKNCAconc", "data.frame")) |> names()
 [1] "data"              "formula"           "subject"          
 [4] "time.nominal"      "exclude"           "duration"         
 [7] "volume"            "exclude_half.life" "include_half.life"
[10] "lloq"              "sparse"            "..."              
[13] "concu"             "amountu"           "timeu"            
[16] "concu_pref"        "amountu_pref"      "timeu_pref"       

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)

14.2.2 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)

14.2.3 Example: units as string literals

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")

14.2.4 Example: units from a data column (≥ 0.12.0)

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")

14.3 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.12.2, 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:

# 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.12.2)
u_from_data <- pknca_units_table(o_data_u)
head(u_from_data, 6)
  PPORRESU           PPTESTCD
1 unitless          r.squared
2 unitless      adj.r.squared
3 unitless    lambda.z.corrxy
4 unitless     tobit_residual
5 unitless adj_tobit_residual
6 fraction                  f

14.3.1 Arguments (default method)

args(getS3method("pknca_units_table", "default"))
function (concu, doseu, amountu, timeu, concu_pref = NULL, doseu_pref = NULL, 
    amountu_pref = NULL, timeu_pref = NULL, conversions = data.frame(), 
    ...) 
NULL

All four of concu, doseu, amountu, timeu are required. PKNCA derives composite units automatically (e.g. auclast gets h*mg/L from timeu="h" and concu="mg/L").

14.3.2 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.

u <- pknca_units_table(concu = "mg/L", timeu = "h",
                       doseu = "mg", amountu = "mg")

# The table covers all NCA parameters
nrow(u)
[1] 203
# Sample rows
u[u$PPTESTCD %in% c("cmax", "auclast", "tmax", "cl.obs", "vz.obs"), ]
       PPORRESU PPTESTCD
27            h     tmax
78         mg/L     cmax
117   mg/(mg/L)   vz.obs
132      h*mg/L  auclast
192 mg/(h*mg/L)   cl.obs

14.4 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.

# 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)
# A tibble: 3 × 3
  PPTESTCD PPORRES PPORRESU
  <chr>      <dbl> <chr>   
1 auclast    92.4  h*mg/L  
2 cmax       10.5  mg/L    
3 tmax        1.12 h       

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.


14.5 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
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"), ]
    PPORRESU PPTESTCD PPSTRESU conversion_factor
27         h     tmax      day      4.166667e-02
78      mg/L     cmax     ug/L      1.000000e+03
132   h*mg/L  auclast day*ug/L      4.166667e+01
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)
# A tibble: 3 × 5
  PPTESTCD PPORRES PPORRESU    PPSTRES PPSTRESU
  <chr>      <dbl> <chr>         <dbl> <chr>   
1 auclast    92.4  h*mg/L    3849.     day*ug/L
2 cmax       10.5  mg/L     10500      ug/L    
3 tmax        1.12 h            0.0467 day     

PPORRES/PPORRESU hold the original value and unit; PPSTRES/PPSTRESU hold the converted value and unit.


14.6 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.

14.6.1 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.

14.6.2 Example: report concentration as ug/mL instead of mg/L

mg/L and ug/mL are numerically equal (factor = 1), but differ in label.

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)
# A tibble: 2 × 5
  PPTESTCD PPORRES PPORRESU PPSTRES PPSTRESU
  <chr>      <dbl> <chr>      <dbl> <chr>   
1 auclast     92.4 h*mg/L      92.4 h*ug/mL 
2 cmax        10.5 mg/L        10.5 ug/mL   

14.7 Allowing partial units (≥ 0.12.0)

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:

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) cat("Error:", conditionMessage(e), "\n")
)
Error: Units are provided for some but not all parameters; missing for: half.life, tmax
This error can be converted to a warning using `PKNCA.options(allow_partial_missing_units = TRUE)` 

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):

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)
Warning in pknca_unit_conversion(result = result, units = data$units,
allow_partial_missing_units = data$options$allow_partial_missing_units): Units
are provided for some but not all parameters; missing for: half.life, tmax
as.data.frame(res_partial) |>
  filter(Subject == "1",
                PPTESTCD %in% c("cmax", "tmax", "half.life")) |>
  select(PPTESTCD, PPORRES, PPORRESU)
# A tibble: 3 × 3
  PPTESTCD  PPORRES PPORRESU
  <chr>       <dbl> <chr>   
1 cmax        10.5  mg/L    
2 tmax         1.12 <NA>    
3 half.life   14.3  <NA>    

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.


14.8 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)
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.12.2) pknca_units_table(o_data)
Allow some parameters without units (≥ 0.12.0) PKNCA.options(allow_partial_missing_units = TRUE)