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<br/>concentration data frame<br/>dose data frame"] --> S2
    S2["② PKNCAconc()<br/>define conc column, time column,<br/>grouping variables, optional args (duration, volume)"] --> S3
    S3["③ PKNCAdose()<br/>define dose, time, route<br/>(intravascular / extravascular)"] --> S4
    S4["④ PKNCAdata()<br/>combine conc + dose<br/>specify intervals + imputation + units"] --> S5
    S5["⑤ pk.nca()<br/>run all calculations"] --> S6
    S6["PKNCAresults<br/>summary() / 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). The package vignette Computing NCA Parameters for Theophylline works through the same dataset.

2.2.1 Step 1 — Prepare your data

PKNCA typically takes two separate data frames: one for concentrations and one for doses (dose data are optional, but required for dose-dependent parameters such as CL and Vz). 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
       1 0.00  0.74    <NA>
       1 0.25  2.84    <NA>
       1 0.57  6.57    <NA>
       1 1.12 10.50    <NA>
       1 2.02  9.66    <NA>
       1 3.82  8.58    <NA>

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 generates them from the dose times. It finds each group’s dosing regimen and, for each interval it places, asks pknca_interval_table() for the parameters and the imputation that suit that interval’s context (≥ 0.13.0). A single dose, as here, gets one interval from the dose to infinity that requests auclast, cmax, tmax, tlag, count_conc, half.life, aucinf.obs, aucpext.obs, and cl.obs. Its impute column holds start_predose_conc0: a concentration measured at the dose time is kept, a predose concentration is carried forward to the dose time when there is none, and 0 is imputed when there is neither (see the Concentration Imputation chapter). Multiple-dose data get one row per sampled dosing interval instead (typically the first one and the last one), built on the aucint.* family with start_cmin or start_predose imputation; Automatic Intervals and Dosing Regimens explains how the regimen is detected and which intervals each design gets.

o_data <- PKNCAdata(o_conc, o_dose)

You can inspect the auto-generated intervals. The interval table has one logical column per known parameter, so this keeps only the columns that request something:

o_data$intervals |>
  select(where(~ !is.logical(.x) || any(.x))) |>
  head(3)
# A tibble: 3 × 13
  start   end auclast cmax  tmax  tlag  count_conc half.life aucinf.obs
  <dbl> <dbl> <lgl>   <lgl> <lgl> <lgl> <lgl>      <lgl>     <lgl>     
1     0   Inf TRUE    TRUE  TRUE  TRUE  TRUE       TRUE      TRUE      
2     0   Inf TRUE    TRUE  TRUE  TRUE  TRUE       TRUE      TRUE      
3     0   Inf TRUE    TRUE  TRUE  TRUE  TRUE       TRUE      TRUE      
# ℹ 4 more variables: aucpext.obs <lgl>, cl.obs <lgl>, Subject <ord>,
#   impute <chr>
NoteChanged in 0.13.0

Before 0.13.0, a single dose got the intervals in the single.dose.aucs option instead: auclast over 0 to 24 (which assumed hours) plus cmax, tmax, half.life, and aucinf.obs over 0 to infinity, with no imputation. Because the single-dose interval now imputes a concentration at the dose time, results change when no sample was taken at the dose time: AUCs that need a concentration there are now calculated from the imputed one, where the legacy intervals give NA with an exclusion reason (see the Concentration Imputation chapter). PKNCA.options(auto.interval.method = "legacy") (or options = list(auto.interval.method = "legacy") in PKNCAdata()) restores the previous parameter lists. The Interval Selection chapter shows how to build the same tables yourself with pknca_interval_table().

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 × 7
   Subject start   end PPTESTCD             PPORRES PPANMETH             exclude
   <ord>   <dbl> <dbl> <chr>                  <dbl> <chr>                <chr>  
 1 1           0   Inf auclast             147.     Imputation: start_p… <NA>   
 2 1           0   Inf cmax                 10.5    Imputation: start_p… <NA>   
 3 1           0   Inf tmax                  1.12   Imputation: start_p… <NA>   
 4 1           0   Inf tlast                24.4    Imputation: start_p… <NA>   
 5 1           0   Inf clast.obs             3.28   Imputation: start_p… <NA>   
 6 1           0   Inf tlag                  0      Imputation: start_p… <NA>   
 7 1           0   Inf count_conc           11      Imputation: start_p… <NA>   
 8 1           0   Inf lambda.z              0.0485 Imputation: start_p… <NA>   
 9 1           0   Inf r.squared             1.000  Imputation: start_p… <NA>   
10 1           0   Inf adj.r.squared         1.000  Imputation: start_p… <NA>   
11 1           0   Inf lambda.z.corrxy      -1.000  Imputation: start_p… <NA>   
12 1           0   Inf lambda.z.time.first   9.05   Imputation: start_p… <NA>   
13 1           0   Inf lambda.z.time.last   24.4    Imputation: start_p… <NA>   
14 1           0   Inf lambda.z.n.points     3      Imputation: start_p… <NA>   
15 1           0   Inf clast.pred            3.28   Imputation: start_p… <NA>   
16 1           0   Inf half.life            14.3    Imputation: start_p… <NA>   
17 1           0   Inf span.ratio            1.07   Imputation: start_p… <NA>   
18 1           0   Inf aucinf.obs          215.     Imputation: start_p… <NA>   
19 1           0   Inf aucpext.obs          31.5    Imputation: start_p… <NA>   
20 1           0   Inf cl.obs                1.49   Imputation: start_p… <NA>   
# Summary table (geometric mean and geometric CV% by default for most parameters)
summary(o_nca)
 start end  N     auclast        cmax               tmax                 tlag
     0 Inf 12 98.7 [22.5] 8.65 [17.0] 1.14 [0.630, 3.55] 0.000 [0.000, 0.000]
        count_conc   half.life aucinf.obs aucpext.obs      cl.obs
 11.0 [11.0, 11.0] 8.18 [2.12] 115 [28.4] 13.8 [6.34] 2.74 [27.9]

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

2.3 Specifying intervals manually

Automatic intervals cover the common designs. When you need other windows or parameters, define intervals yourself, either as a data frame as below or with the helpers pknca_interval_table() and interval_add_param() described in the Interval Selection chapter.

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 × 7
   Subject start   end PPTESTCD            PPORRES PPANMETH              exclude
   <ord>   <dbl> <dbl> <chr>                 <dbl> <chr>                 <chr>  
 1 6           0   Inf auclast             71.7    "AUC: lin up/log dow… <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 it with no arguments to get a named list of all current settings. See also the vignette Options for Controlling PKNCA.

current_options <- PKNCA.options()
names(current_options)
 [1] "adj.r.squared.factor"        "r.squared.factor"           
 [3] "max.missing"                 "auc.method"                 
 [5] "conc.na"                     "conc.blq"                   
 [7] "debug"                       "first.tmax"                 
 [9] "first.tmin"                  "allow.tmax.in.half.life"    
[11] "keep_interval_cols"          "min.hl.points"              
[13] "max.hl.points"               "min.hl.start.time"          
[15] "min.span.ratio"              "max.aucinf.pext"            
[17] "min.hl.r.squared"            "progress"                   
[19] "tau.choices"                 "auto.interval.method"       
[21] "auto.interval.tolerance"     "single.dose.aucs"           
[23] "allow_partial_missing_units" "hl_method"                  
[25] "sparse_lambda_z_se"          "tobit_n_points_penalty"     
[27] "tobit_optim_control"        

PKNCA 0.12.1.9002 has 27 options. (Printing PKNCA.options() itself shows every value, including single.dose.aucs, which is a full interval data frame with one column per parameter.) Two helpers read the registry without changing it (≥ 0.13.0 for PKNCA_options_defaults()): PKNCA_options_defaults() gives the default of one option or all of them, which is useful after the current values have been changed, and PKNCA.options.describe() gives the package’s own description of an option.

PKNCA_options_defaults("auto.interval.tolerance")
[1] 0.05
PKNCA.options.describe("auto.interval.tolerance")
[1] "When automatically determining the intervals, how far from the boundary of an interval may a sample be drawn and still count as the sample at that boundary?  It is given as a fraction of the interval's length (the dosing interval, tau, for a dosing interval), so that a trough drawn at 167.5 hours still ends an interval nominally ending at 168 hours.  See 'choose.auc.intervals' for more information."

All options and what they control:

AUC / integration

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

Each rule is derived in AUC integration methods.

Half-life / λz

Option Default Effect
min.hl.points 3 Minimum data points for a valid λz regression
max.hl.points Inf Maximum number of points that automatic point selection may use; must be more than min.hl.points (≥ 0.13.0)
min.hl.start.time 0 Earliest time at which automatic point selection may start the λz regression; 0 does not restrict it (≥ 0.13.0)
adj.r.squared.factor 0.0001 Selection tolerance: the regression with the most points whose adjusted R² is within this value of the best adjusted R² is chosen (favors more points when fits are nearly equal)
r.squared.factor NA The same tolerance on the unadjusted R² instead (≥ 0.13.0). Setting either factor takes the other out of use, so exactly one is in use; NA means “select on the other one”
min.hl.r.squared 0.9 Minimum unadjusted R² for λz acceptance (post-hoc exclusion via exclude_nca_min.hl.r.squared())
min.span.ratio 2 Minimum ratio of time span to half-life (post-hoc exclusion via exclude_nca_span.ratio())
allow.tmax.in.half.life FALSE Allow the Tmax timepoint in the λz regression
hl_method "log-linear" "log-linear" (OLS) or "tobit" (censored likelihood) (≥ 0.13.0)
tobit_n_points_penalty 0 Exponent in the Tobit window-selection criterion (residual × n^penalty); positive values favor shorter windows (≥ 0.13.0)
tobit_optim_control list() Control list forwarded to stats::optim() for Tobit optimization (≥ 0.13.0)

Details: the Half-Life chapter, Half-Life Calculation, and Half-life calculation with Tobit regression.

AUCinf extrapolation

Option Default Effect
max.aucinf.pext 20 Maximum percent of AUCinf that may be extrapolated for AUCinf to be considered valid (post-hoc exclusion via exclude_nca_max.aucinf.pext())

Sparse sampling

Option Default Effect
sparse_lambda_z_se "delta" How the standard error of a sparse aucinf.obs or aumcinf.obs accounts for the estimated λz: "delta" adds its uncertainty with the delta method; "none" treats λz as known (≥ 0.13.0). See the Sparse Sampling chapter

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 (≥ 0.13.0)

Automatic intervals

Option Default Effect
auto.interval.method "builder" Where the parameters of automatically generated intervals come from: "builder" asks pknca_interval_table() for the parameters and imputation that suit each interval; "legacy" uses the pre-0.13.0 lists (single.dose.aucs for a single dose; AUClast, Cmax, and Tmax for each dosing interval) (≥ 0.13.0)
auto.interval.tolerance 0.05 How far before an interval boundary a sample may be drawn and still count as the sample at that boundary, as a fraction of the interval length (a trough at 167.5 h still ends an interval nominally ending at 168 h) (≥ 0.13.0)
single.dose.aucs (data frame) Intervals for single-dose data, used only with auto.interval.method = "legacy"
tau.choices NA Nominal dosing intervals (τ) that detected dosing intervals are matched to. NA uses the built-in set from every 4 hours to every 12 weeks (Q4H, QID, TID, BID, QD, …, Q12W); a numeric vector replaces it. An interval that matches none is reported as found in the dose times, with a pknca_warning_tau_not_nominal warning

The Automatic Intervals and Dosing Regimens chapter shows these options at work.

Summary / output / other

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
progress TRUE Show a 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(). To rerun with different settings, build a new PKNCAdata object with the new options and call pk.nca() again:

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

update() has a method for PKNCAresults objects only (there is none for PKNCAconc, PKNCAdose, or PKNCAdata). update(o_nca, data = o_data_new) is meant for the case where only the concentration or dose data change, such as an interactive app that excludes a point: it recalculates the groups whose data changed and keeps the other results. Any other difference between the old and new PKNCAdata objects, including different options, makes it recalculate everything with a pknca_warning_full_recalculation warning, so for a change of settings, calling pk.nca() on the new object directly is the clearer choice.


2.5 Extracting and plotting results

as.data.frame() returns one row per group per interval per parameter — a tidy shape ready for dplyr and ggplot2. More result-handling patterns: Post-Processing.

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