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 Workflow Overview
2.1 The five-step workflow
Every PKNCA analysis follows the same five steps regardless of route or complexity.
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>
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
TRUEto 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()