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 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 builds defaults: for single-dose data, from the built-in single.dose.aucs specification anchored at the dosing times; multiple-dose data get tau-based intervals instead.
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 × 7
Subject start end PPTESTCD PPORRES PPANMETH exclude
<ord> <dbl> <dbl> <chr> <dbl> <chr> <chr>
1 1 0 24 auclast 92.4 "AUC: lin up/log do… <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. "AUC: lin up/log do… <NA>
17 2 0 24 auclast 67.2 "AUC: lin up/log do… <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 and geometric 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
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 with no arguments to see all current settings. See also the vignette Options for Controlling PKNCA.
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" |
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 |
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 (favors more points when fits are nearly equal) |
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 Tmax timepoint in λz regression |
hl_method |
"log-linear" |
"log-linear" (OLS) or "tobit" (censored-likelihood, ≥ 0.12.2) |
tobit_n_points_penalty |
0 |
Exponent in the Tobit window-selection criterion (residual × n^penalty); positive values favor shorter windows (≥ 0.12.2) |
tobit_optim_control |
list() |
Control list forwarded to stats::optim() for Tobit optimization (≥ 0.12.2) |
Details: Half-Life Calculation and Half-life calculation with Tobit regression.
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 (≥ 0.12.2) |
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 |
(data frame) | 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(). PKNCA has no update() method, so to rerun with different settings you 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
)
)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()