2  Workflow Overview

2.1 The Five-Step Workflow

Every PKNCA analysis follows the same five steps regardless of route or complexity.

flowchart TD
    S1["① Prepare data\nconcentration data.frame\ndose data.frame"] --> S2
    S2["② PKNCAconc()\ndefine conc column, time column,\ngrouping variables, route-specific args"] --> S3
    S3["③ PKNCAdose()\ndefine dose, time, route\n(intravascular / extravascular)"] --> S4
    S4["④ PKNCAdata()\ncombine conc + dose\nspecify intervals + imputation + units"] --> S5
    S5["⑤ pk.nca()\nrun all calculations"] --> S6
    S6["PKNCAresults\nsummary() / as.data.frame()"]

    style S1 fill:#f0f4ff
    style S6 fill:#e8f5e9

2.2 Step-by-step: minimal example

Below is the complete workflow with the built-in Theoph dataset (oral theophylline, 12 subjects).

2.2.1 Step 1 — Prepare your data

PKNCA expects two separate data frames: one for concentrations and one for doses. Both must be in long format (one row per observation).

# Built-in oral theophylline dataset
head(Theoph)
Grouped Data: conc ~ Time | Subject
  Subject   Wt Dose Time  conc
1       1 79.6 4.02 0.00  0.74
2       1 79.6 4.02 0.25  2.84
3       1 79.6 4.02 0.57  6.57
4       1 79.6 4.02 1.12 10.50
5       1 79.6 4.02 2.02  9.66
6       1 79.6 4.02 3.82  8.58

The columns are: Subject, Wt (weight, kg), Dose (mg/kg), Time (h), conc (mg/L).

# Concentration data: keep what we need
d_conc <- Theoph |>
  select(Subject, Time, conc)

# Dose data: one row per subject (single dose at time 0)
d_dose <- Theoph |>
  group_by(Subject) |>
  summarise(Dose = Dose[1] * Wt[1], .groups = "drop") |>  # convert mg/kg to mg
  mutate(Time = 0)

head(d_dose)
# A tibble: 6 × 3
  Subject  Dose  Time
  <ord>   <dbl> <dbl>
1 6        320      0
2 7        320.     0
3 8        319.     0
4 11       320.     0
5 3        319.     0
6 2        319.     0

2.2.2 Step 2 — PKNCAconc

PKNCAconc tells PKNCA the structure of your concentration data via a formula: concentration ~ time | group1 / group2 / ...

o_conc <- PKNCAconc(d_conc, conc ~ Time | Subject)
print(o_conc)
Formula for concentration:
 conc ~ Time | Subject
Data are dense PK.
With 12 subjects defined in the 'Subject' column.
Nominal time column is not specified.

First 6 rows of concentration data:
 Subject Time  conc exclude volume duration
       1 0.00  0.74    <NA>     NA        0
       1 0.25  2.84    <NA>     NA        0
       1 0.57  6.57    <NA>     NA        0
       1 1.12 10.50    <NA>     NA        0
       1 2.02  9.66    <NA>     NA        0
       1 3.82  8.58    <NA>     NA        0

2.2.3 Step 3 — PKNCAdose

PKNCAdose describes the dosing. The route argument is critical — it determines which parameters are estimable.

o_dose <- PKNCAdose(d_dose, Dose ~ Time | Subject, route = "extravascular")
print(o_dose)
Formula for dosing:
 Dose ~ Time | Subject
Nominal time column is not specified.

First 6 rows of dosing data:
 Subject    Dose Time exclude         route duration
       6 320.000    0    <NA> extravascular        0
       7 319.770    0    <NA> extravascular        0
       8 319.365    0    <NA> extravascular        0
      11 319.800    0    <NA> extravascular        0
       3 319.365    0    <NA> extravascular        0
       2 318.560    0    <NA> extravascular        0

2.2.4 Step 4 — PKNCAdata

PKNCAdata combines concentration and dose objects. If you don’t specify intervals, PKNCA automatically determines them from the dosing times.

o_data <- PKNCAdata(o_conc, o_dose)

You can inspect the auto-generated intervals:

o_data$intervals |> head()
# A tibble: 6 × 204
  start   end auclast aucall aumclast aumcall aucint.last aucint.last.dose
  <dbl> <dbl> <lgl>   <lgl>  <lgl>    <lgl>   <lgl>       <lgl>           
1     0    24 TRUE    FALSE  FALSE    FALSE   FALSE       FALSE           
2     0   Inf FALSE   FALSE  FALSE    FALSE   FALSE       FALSE           
3     0    24 TRUE    FALSE  FALSE    FALSE   FALSE       FALSE           
4     0   Inf FALSE   FALSE  FALSE    FALSE   FALSE       FALSE           
5     0    24 TRUE    FALSE  FALSE    FALSE   FALSE       FALSE           
6     0   Inf FALSE   FALSE  FALSE    FALSE   FALSE       FALSE           
# ℹ 196 more variables: aucint.all <lgl>, aucint.all.dose <lgl>,
#   aumcint.last <lgl>, aumcint.last.dose <lgl>, aumcint.all <lgl>,
#   aumcint.all.dose <lgl>, c0 <lgl>, cmax <lgl>, cmin <lgl>, tmax <lgl>,
#   tmin <lgl>, tlast <lgl>, tfirst <lgl>, clast.obs <lgl>, cl.last <lgl>,
#   cl.all <lgl>, cl.int.all <lgl>, cl.int.last <lgl>, f <lgl>, mrt.last <lgl>,
#   mrt.all <lgl>, mrt.int.all <lgl>, mrt.int.last <lgl>, mrt.iv.last <lgl>,
#   vss.last <lgl>, vss.iv.last <lgl>, vss.all <lgl>, vss.int.all <lgl>, …

2.2.5 Step 5 — pk.nca

Run all calculations:

o_nca <- pk.nca(o_data)

2.2.6 Results

# Tidy data frame of all computed parameters
as.data.frame(o_nca) |> head(20)
# A tibble: 20 × 6
   Subject start   end PPTESTCD             PPORRES exclude
   <ord>   <dbl> <dbl> <chr>                  <dbl> <chr>  
 1 1           0    24 auclast              92.4    <NA>   
 2 1           0   Inf cmax                 10.5    <NA>   
 3 1           0   Inf tmax                  1.12   <NA>   
 4 1           0   Inf tlast                24.4    <NA>   
 5 1           0   Inf clast.obs             3.28   <NA>   
 6 1           0   Inf lambda.z              0.0485 <NA>   
 7 1           0   Inf r.squared             1.000  <NA>   
 8 1           0   Inf adj.r.squared         1.000  <NA>   
 9 1           0   Inf lambda.z.corrxy      -1.000  <NA>   
10 1           0   Inf lambda.z.time.first   9.05   <NA>   
11 1           0   Inf lambda.z.time.last   24.4    <NA>   
12 1           0   Inf lambda.z.n.points     3      <NA>   
13 1           0   Inf clast.pred            3.28   <NA>   
14 1           0   Inf half.life            14.3    <NA>   
15 1           0   Inf span.ratio            1.07   <NA>   
16 1           0   Inf aucinf.obs          215.     <NA>   
17 2           0    24 auclast              67.2    <NA>   
18 2           0   Inf cmax                  8.33   <NA>   
19 2           0   Inf tmax                  1.92   <NA>   
20 2           0   Inf tlast                24.3    <NA>   
# Summary table (geometric mean ± CV% by default for most parameters)
summary(o_nca)
 start end  N     auclast        cmax               tmax   half.life aucinf.obs
     0  24 12 74.6 [24.3]           .                  .           .          .
     0 Inf 12           . 8.65 [17.0] 1.14 [0.630, 3.55] 8.18 [2.12] 115 [28.4]

Caption: auclast, cmax, aucinf.obs: geometric mean and geometric coefficient of variation; tmax: median and range; half.life: arithmetic mean and standard deviation; N: number of subjects

2.3 Specifying intervals manually

Auto-interval detection works for simple designs. For complex studies (multiple doses, custom windows), define intervals yourself.

An interval is a row in a data frame with: - start, end: time boundaries - One column per parameter set to TRUE to request that parameter

# Request specific parameters over a specific window
my_intervals <- data.frame(
  start  = 0,
  end    = Inf,
  auclast  = TRUE,
  aucinf.obs = TRUE,
  cmax   = TRUE,
  tmax   = TRUE,
  half.life = TRUE
)

o_data2 <- PKNCAdata(o_conc, o_dose, intervals = my_intervals)
o_nca2  <- pk.nca(o_data2)
as.data.frame(o_nca2)
# A tibble: 192 × 6
   Subject start   end PPTESTCD            PPORRES exclude
   <ord>   <dbl> <dbl> <chr>                 <dbl> <chr>  
 1 6           0   Inf auclast             71.7    <NA>   
 2 6           0   Inf cmax                 6.44   <NA>   
 3 6           0   Inf tmax                 1.15   <NA>   
 4 6           0   Inf tlast               23.8    <NA>   
 5 6           0   Inf clast.obs            0.92   <NA>   
 6 6           0   Inf lambda.z             0.0878 <NA>   
 7 6           0   Inf r.squared            0.998  <NA>   
 8 6           0   Inf adj.r.squared        0.998  <NA>   
 9 6           0   Inf lambda.z.corrxy     -0.999  <NA>   
10 6           0   Inf lambda.z.time.first  2.03   <NA>   
# ℹ 182 more rows

2.4 Controlling calculations with PKNCA.options()

PKNCA.options() is the global settings registry. Call with no arguments to see all current settings.

PKNCA.options()
$adj.r.squared.factor
[1] 1e-04

$max.missing
[1] 0.5

$auc.method
[1] "lin up/log down"

$conc.na
[1] "drop"

$conc.blq
$conc.blq$first
[1] "keep"

$conc.blq$middle
[1] "drop"

$conc.blq$last
[1] "keep"


$debug
NULL

$first.tmax
[1] TRUE

$first.tmin
[1] TRUE

$allow.tmax.in.half.life
[1] FALSE

$keep_interval_cols
NULL

$min.hl.points
[1] 3

$min.span.ratio
[1] 2

$max.aucinf.pext
[1] 20

$min.hl.r.squared
[1] 0.9

$progress
[1] TRUE

$tau.choices
[1] NA

$single.dose.aucs
  start end auclast aucall aumclast aumcall aucint.last aucint.last.dose
1     0  24    TRUE  FALSE    FALSE   FALSE       FALSE            FALSE
2     0 Inf   FALSE  FALSE    FALSE   FALSE       FALSE            FALSE
  aucint.all aucint.all.dose aumcint.last aumcint.last.dose aumcint.all
1      FALSE           FALSE        FALSE             FALSE       FALSE
2      FALSE           FALSE        FALSE             FALSE       FALSE
  aumcint.all.dose    c0  cmax  cmin  tmax  tmin tlast tfirst clast.obs cl.last
1            FALSE FALSE FALSE FALSE FALSE FALSE FALSE  FALSE     FALSE   FALSE
2            FALSE FALSE  TRUE FALSE  TRUE FALSE FALSE  FALSE     FALSE   FALSE
  cl.all cl.int.all cl.int.last     f mrt.last mrt.all mrt.int.all mrt.int.last
1  FALSE      FALSE       FALSE FALSE    FALSE   FALSE       FALSE        FALSE
2  FALSE      FALSE       FALSE FALSE    FALSE   FALSE       FALSE        FALSE
  mrt.iv.last vss.last vss.iv.last vss.all vss.int.all vss.int.last   cav
1       FALSE    FALSE       FALSE   FALSE       FALSE        FALSE FALSE
2       FALSE    FALSE       FALSE   FALSE       FALSE        FALSE FALSE
  cav.int.last cav.int.all ctrough cstart   ptr  tlag deg.fluc swing  ceoi
1        FALSE       FALSE   FALSE  FALSE FALSE FALSE    FALSE FALSE FALSE
2        FALSE       FALSE   FALSE  FALSE FALSE FALSE    FALSE FALSE FALSE
  aucabove.predose.all aucabove.trough.all count_conc count_conc_measured
1                FALSE               FALSE      FALSE               FALSE
2                FALSE               FALSE      FALSE               FALSE
  totdose volpk    ae clr.last clr.obs clr.pred    fe ertlst ermax ertmax
1   FALSE FALSE FALSE    FALSE   FALSE    FALSE FALSE  FALSE FALSE  FALSE
2   FALSE FALSE FALSE    FALSE   FALSE    FALSE FALSE  FALSE FALSE  FALSE
  sparse_auclast sparse_auc_se sparse_auc_df sparse_aumclast sparse_aumc_se
1          FALSE         FALSE         FALSE           FALSE          FALSE
2          FALSE         FALSE         FALSE           FALSE          FALSE
  sparse_aumc_df time_above aucivlast aucivall aucivint.last aucivint.all
1          FALSE      FALSE     FALSE    FALSE         FALSE        FALSE
2          FALSE      FALSE     FALSE    FALSE         FALSE        FALSE
  aucivpbextlast aucivpbextall aucivpbextint.last aucivpbextint.all aumcivlast
1          FALSE         FALSE              FALSE             FALSE      FALSE
2          FALSE         FALSE              FALSE             FALSE      FALSE
  aumcivall aumcivint.last aumcivint.all half.life r.squared adj.r.squared
1     FALSE          FALSE         FALSE     FALSE     FALSE         FALSE
2     FALSE          FALSE         FALSE      TRUE     FALSE         FALSE
  lambda.z.corrxy lambda.z lambda.z.time.first lambda.z.time.last
1           FALSE    FALSE               FALSE              FALSE
2           FALSE    FALSE               FALSE              FALSE
  lambda.z.n.points clast.pred span.ratio tobit_residual adj_tobit_residual
1             FALSE      FALSE      FALSE          FALSE              FALSE
2             FALSE      FALSE      FALSE          FALSE              FALSE
  lambda.z.n.points_blq thalf.eff.last thalf.eff.iv.last kel.last kel.iv.last
1                 FALSE          FALSE             FALSE    FALSE       FALSE
2                 FALSE          FALSE             FALSE    FALSE       FALSE
  kel.all kel.int.all kel.int.last cl.iv.all cl.iv.last cl.ivint.all
1   FALSE       FALSE        FALSE     FALSE      FALSE        FALSE
2   FALSE       FALSE        FALSE     FALSE      FALSE        FALSE
  cl.ivint.last cl.sparse.last mrt.sparse.last mrt.iv.all mrt.ivint.all
1         FALSE          FALSE           FALSE      FALSE         FALSE
2         FALSE          FALSE           FALSE      FALSE         FALSE
  mrt.ivint.last vz.all vz.int.all vz.int.last vz.iv.all vz.iv.last
1          FALSE  FALSE      FALSE       FALSE     FALSE      FALSE
2          FALSE  FALSE      FALSE       FALSE     FALSE      FALSE
  vz.ivint.all vz.ivint.last vz.last vss.iv.all vss.ivint.all vss.ivint.last
1        FALSE         FALSE   FALSE      FALSE         FALSE          FALSE
2        FALSE         FALSE   FALSE      FALSE         FALSE          FALSE
  vss.sparse.last aucinf.obs aucinf.pred aumcinf.obs aumcinf.pred
1           FALSE      FALSE       FALSE       FALSE        FALSE
2           FALSE       TRUE       FALSE       FALSE        FALSE
  aucint.inf.obs aucint.inf.obs.dose aucint.inf.pred aucint.inf.pred.dose
1          FALSE               FALSE           FALSE                FALSE
2          FALSE               FALSE           FALSE                FALSE
  aumcint.inf.obs aumcint.inf.obs.dose aumcint.inf.pred aumcint.inf.pred.dose
1           FALSE                FALSE            FALSE                 FALSE
2           FALSE                FALSE            FALSE                 FALSE
  aucivinf.obs aucivinf.pred aucivpbextinf.obs aucivpbextinf.pred aumcivinf.obs
1        FALSE         FALSE             FALSE              FALSE         FALSE
2        FALSE         FALSE             FALSE              FALSE         FALSE
  aumcivinf.pred aucpext.obs aucpext.pred kel.iv.all kel.ivint.all
1          FALSE       FALSE        FALSE      FALSE         FALSE
2          FALSE       FALSE        FALSE      FALSE         FALSE
  kel.ivint.last kel.sparse.last cl.obs cl.pred cl.int.inf.obs cl.int.inf.pred
1          FALSE           FALSE  FALSE   FALSE          FALSE           FALSE
2          FALSE           FALSE  FALSE   FALSE          FALSE           FALSE
  cl.iv.obs cl.iv.pred mrt.obs mrt.pred mrt.int.inf.obs mrt.int.inf.pred
1     FALSE      FALSE   FALSE    FALSE           FALSE            FALSE
2     FALSE      FALSE   FALSE    FALSE           FALSE            FALSE
  mrt.iv.obs mrt.iv.pred mrt.md.obs mrt.md.pred vz.obs vz.pred vz.int.inf.obs
1      FALSE       FALSE      FALSE       FALSE  FALSE   FALSE          FALSE
2      FALSE       FALSE      FALSE       FALSE  FALSE   FALSE          FALSE
  vz.int.inf.pred vz.iv.obs vz.iv.pred vz.sparse.last vss.obs vss.pred
1           FALSE     FALSE      FALSE          FALSE   FALSE    FALSE
2           FALSE     FALSE      FALSE          FALSE   FALSE    FALSE
  vss.iv.obs vss.iv.pred vss.md.obs vss.md.pred vss.int.inf.obs
1      FALSE       FALSE      FALSE       FALSE           FALSE
2      FALSE       FALSE      FALSE       FALSE           FALSE
  vss.int.inf.pred cav.int.inf.obs cav.int.inf.pred thalf.eff.obs
1            FALSE           FALSE            FALSE         FALSE
2            FALSE           FALSE            FALSE         FALSE
  thalf.eff.pred thalf.eff.iv.obs thalf.eff.iv.pred kel.obs kel.pred kel.iv.obs
1          FALSE            FALSE             FALSE   FALSE    FALSE      FALSE
2          FALSE            FALSE             FALSE   FALSE    FALSE      FALSE
  kel.iv.pred kel.int.inf.obs kel.int.inf.pred auclast.dn aucall.dn
1       FALSE           FALSE            FALSE      FALSE     FALSE
2       FALSE           FALSE            FALSE      FALSE     FALSE
  aucinf.obs.dn aucinf.pred.dn aumclast.dn aumcall.dn aumcinf.obs.dn
1         FALSE          FALSE       FALSE      FALSE          FALSE
2         FALSE          FALSE       FALSE      FALSE          FALSE
  aumcinf.pred.dn cmax.dn cmin.dn clast.obs.dn clast.pred.dn cav.dn ctrough.dn
1           FALSE   FALSE   FALSE        FALSE         FALSE  FALSE      FALSE
2           FALSE   FALSE   FALSE        FALSE         FALSE  FALSE      FALSE
  clr.last.dn clr.obs.dn clr.pred.dn
1       FALSE      FALSE       FALSE
2       FALSE      FALSE       FALSE

$allow_partial_missing_units
[1] FALSE

$hl_method
[1] "log-linear"

$tobit_n_points_penalty
[1] 0

$tobit_optim_control
list()

All options and what they control:

AUC / integration

Option Default Effect
auc.method "lin up/log down" Integration rule: "linear", "lin-log", or "lin up/log down"

Half-life / λz

Option Default Effect
min.hl.points 3 Minimum data points for a valid λz regression
min.hl.r.squared 0.9 Minimum unadjusted R² for λz acceptance (post-hoc exclusion via exclude_nca_min.hl.r.squared())
adj.r.squared.factor 0.0001 Selection tolerance: the longest regression window whose adj.R² is within this value of the best adj.R² is chosen (favours more points when fits are nearly equal)
min.span.ratio 2 Minimum ratio of time span to half-life; rejects regressions over too short a window
allow.tmax.in.half.life FALSE Allow Tmax timepoint in λz regression
hl_method "log-linear" "log-linear" (OLS) or "tobit" (censored-likelihood, ≥ 0.12.2)
tobit_n_points_penalty 0 Penalty per extra terminal point in Tobit log-likelihood; positive values favour shorter windows
tobit_optim_control list() Control list forwarded to stats::optim() for Tobit optimisation

AUCinf extrapolation

Option Default Effect
max.aucinf.pext 20 Flag results where extrapolated % of AUCinf exceeds this threshold

Concentration handling

Option Default Effect
conc.na "drop" NA concentrations: "drop" removes them; a number substitutes that value
conc.blq list(first="keep", middle="drop", last="keep") BLQ concentrations: "drop", "keep", or a number globally; or a named list with keys first (before first non-BLQ), middle (between non-BLQs), last (after last non-BLQ), or before.tmax/after.tmax

Tmax / Tmin

Option Default Effect
first.tmax TRUE TRUE = return first Tmax when tied; FALSE = return last
first.tmin TRUE TRUE = return first Tmin when tied; FALSE = return last

Summary / output

Option Default Effect
max.missing 0.5 Maximum fraction of missing values allowed before suppressing a summary statistic
keep_interval_cols NULL Additional columns from the intervals data frame to carry through to results
allow_partial_missing_units FALSE Allow some parameters to lack unit definitions when a units table is provided
single.dose.aucs (list) Default interval specifications used when no intervals are provided for a single dose
tau.choices NA Candidate dosing intervals (τ) for auto-detection in multiple-dose designs
progress TRUE Show progress bar during pk.nca()
debug NULL Enable internal debug output (not for routine use)

Override for a single analysis by passing options to PKNCAdata():

o_data_strict <- PKNCAdata(
  o_conc, o_dose,
  options = list(
    min.hl.points  = 4,     # require at least 4 points for half-life
    min.hl.r.squared = 0.95 # stricter R² threshold
  )
)

2.5 Extracting and plotting results

results_df <- as.data.frame(o_nca)

# Filter to one parameter
auclast_df <- results_df |>
  filter(PPTESTCD == "auclast")

ggplot(auclast_df, aes(x = Subject, y = PPORRES)) +
  geom_col(fill = "steelblue") +
  labs(title = "AUClast by Subject", x = "Subject", y = "AUClast (h·mg/L)") +
  theme_minimal()


2.6 Re-running with different options

PKNCA does not have an update() method. To rerun with changed settings, pass a new options list to PKNCAdata() and call pk.nca() again:

# Change AUC method and recalculate
o_data_linear <- PKNCAdata(o_conc, o_dose, options = list(auc.method = "linear"))
o_nca_linear  <- pk.nca(o_data_linear)