19  Units Assignment and Conversion

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


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

19.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"              "frac_unbound"      "sparse"           
[13] "..."               "concu"             "amountu"          
[16] "timeu"             "concu_pref"        "amountu_pref"     
[19] "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)

19.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)

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

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

19.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.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:

# 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)
  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                ptr

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

identical(o_data_u$units, u_from_data)
[1] TRUE
as.data.frame(pk.nca(o_data_u)) |>
  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   147.   h*mg/L  
2 cmax       10.5  mg/L    
3 tmax        1.12 h       

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:

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")
)
Units should be uniform at least across concentration groups. Review the units for the next group(s):
Subject=1, conc_unit=mg/L, amountu=NA, timeu=h, doseu=mg
Subject=1, conc_unit=ng/mL, amountu=NA, timeu=h, doseu=mg

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

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"))
Units are provided for some but not all parameters; missing for: cmax
This error can be converted to a warning using `PKNCA.options(allow_partial_missing_units = TRUE)` 

19.3.2 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

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.

19.3.3 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] 237
# Sample rows
u[u$PPTESTCD %in% c("cmax", "auclast", "tmax", "cl.obs", "vz.obs"), ]
       PPORRESU PPTESTCD
51            h     tmax
104        mg/L     cmax
143   mg/(mg/L)   vz.obs
160      h*mg/L  auclast
226 mg/(h*mg/L)   cl.obs

19.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   147.   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.


19.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
51         h     tmax      day      4.166667e-02
104     mg/L     cmax     ug/L      1.000000e+03
160   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   147.   h*mg/L    6135.     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.


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

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

19.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    147.  h*mg/L     147.  h*ug/mL 
2 cmax        10.5 mg/L        10.5 ug/mL   

19.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) conditionMessage(e)
)
[1] "Units are provided for some but not all parameters; missing for: half.life, tmax\nThis 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: 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.


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

dose_times_min <- c(0, 1440, 2880, 4320)
find.dose.regimen(dose_times_min)[, c("interval", "label")]
  interval label
1     1344   Q8W
find.dose.regimen(dose_times_min, timeu = "min")[, c("interval", "label")]
  interval label
1     1440    QD

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.

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.


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


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