library(tidyverse)
library(PKNCA)
data(Theoph)
theoph_conc <- as_tibble(Theoph) %>%
transmute(ID = Subject, TIME = Time, CONC = conc)
theoph_dose <- as_tibble(Theoph) %>%
distinct(Subject, Dose) %>%
transmute(ID = Subject, TIME = 0, DOSE = Dose)
conc_obj <- PKNCAconc(CONC ~ TIME | ID, data = theoph_conc)
dose_obj <- PKNCAdose(DOSE ~ TIME | ID, data = theoph_dose)
intervals_df <- data.frame(
start = 0,
end = Inf,
cmax = TRUE,
auclast = TRUE,
aucinf.obs = TRUE,
half.life = TRUE,
cl.obs = TRUE
)
nca_data <- PKNCAdata(conc_obj, dose_obj, intervals = intervals_df)Running pk.nca() and Exploring the PKNCAresults Object
Big idea: Running pk.nca() produces structured results — not a final report. Understanding the PKNCAresults object is essential before summarizing or exporting.
Learning Objectives
By the end of this lesson, you will be able to:
- Run NCA calculations using
pk.nca(). - Inspect the structure of a
PKNCAresultsobject. - Distinguish between raw NCA results and summarized outputs.
- Extract results for further inspection or reporting.
- Interpret clearance correctly when dose is recorded in mg/kg.
Key Ideas
pk.nca()performs the actual exposure calculations.- The result is stored in a
PKNCAresultsobject. - Raw results contain subject-level parameter estimates.
summary()produces report-ready summaries.- Calculation and reporting are deliberately separated.
- Dose units propagate into clearance units.
Recreate Structured NCA Data
Running pk.nca()
results_obj <- pk.nca(nca_data)
results_obj$result
# A tibble: 192 × 6
ID 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.time.first 2.03 <NA>
10 6 0 Inf lambda.z.time.last 23.8 <NA>
# ℹ 182 more rows
$data
Formula for concentration:
CONC ~ TIME | ID
Data are dense PK.
With 12 subjects defined in the 'ID' column.
Nominal time column is not specified.
First 6 rows of concentration data:
ID 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
Formula for dosing:
DOSE ~ TIME | ID
Nominal time column is not specified.
First 6 rows of dosing data:
ID TIME DOSE exclude route duration
1 0 4.02 <NA> extravascular 0
2 0 4.40 <NA> extravascular 0
3 0 4.53 <NA> extravascular 0
4 0 4.40 <NA> extravascular 0
5 0 5.86 <NA> extravascular 0
6 0 4.00 <NA> extravascular 0
With 1 rows of interval specifications.
With imputation: NA
Options changed from default are:
$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
$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 c0 cmax cmin tmax tlast tfirst clast.obs
1 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE TRUE FALSE TRUE FALSE FALSE FALSE
cl.last cl.all f mrt.last mrt.iv.last vss.last vss.iv.last cav
1 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE 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 ae clr.last clr.obs clr.pred fe sparse_auclast sparse_auc_se
1 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
sparse_auc_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 half.life
1 FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE TRUE
r.squared adj.r.squared lambda.z lambda.z.time.first lambda.z.time.last
1 FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE
lambda.z.n.points clast.pred span.ratio thalf.eff.last thalf.eff.iv.last
1 FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE
kel.last kel.iv.last aucinf.obs aucinf.pred aumcinf.obs aumcinf.pred
1 FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE 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
aucivinf.obs aucivinf.pred aucivpbextinf.obs aucivpbextinf.pred aucpext.obs
1 FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE
aucpext.pred cl.obs cl.pred mrt.obs mrt.pred mrt.iv.obs mrt.iv.pred
1 FALSE FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE FALSE FALSE
mrt.md.obs mrt.md.pred vz.obs vz.pred vss.obs vss.pred vss.iv.obs vss.iv.pred
1 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
vss.md.obs vss.md.pred cav.int.inf.obs cav.int.inf.pred thalf.eff.obs
1 FALSE FALSE FALSE FALSE FALSE
2 FALSE 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 auclast.dn aucall.dn aucinf.obs.dn aucinf.pred.dn aumclast.dn
1 FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE FALSE
aumcall.dn aumcinf.obs.dn aumcinf.pred.dn cmax.dn cmin.dn clast.obs.dn
1 FALSE FALSE FALSE FALSE FALSE FALSE
2 FALSE FALSE FALSE FALSE FALSE FALSE
clast.pred.dn cav.dn ctrough.dn
1 FALSE FALSE FALSE
2 FALSE FALSE FALSE
$allow_partial_missing_units
[1] FALSE
$columns
$columns$exclude
[1] "exclude"
attr(,"class")
[1] "PKNCAresults" "list"
attr(,"provenance")
Provenance hash 3058fbcf424973705eed8bbb1b2053d3 generated on 2026-05-29 22:18:06.490661 with R version 4.5.2 (2025-10-31).
This produces a PKNCAresults object containing subject-level parameter estimates.
Inspecting the Results Object
Check its class:
class(results_obj)[1] "PKNCAresults" "list"
Convert to a data frame for inspection:
raw_df <- as.data.frame(results_obj)
raw_df %>% head(12)# A tibble: 12 × 6
ID 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.time.first 2.03 <NA>
10 6 0 Inf lambda.z.time.last 23.8 <NA>
11 6 0 Inf lambda.z.n.points 7 <NA>
12 6 0 Inf clast.pred 0.941 <NA>
Each row represents:
- One subject
- One interval
- One parameter
Common parameters include:
cmaxtmaxauclastaucinf.obshalf.lifecl.obs
Important: Interpreting Clearance Units
In Theoph, dose is recorded in mg/kg.
Since:
\[ CL/F = \frac{\text{Dose}}{AUC_{\infty}} \]
and dose is in mg/kg while AUC is in mg·h/L,
clearance will be reported in:
\[ \mathrm{L/h/kg} \]
That means cl.obs here is weight-normalized clearance, not total clearance (L/h).
Always verify dose units before interpreting clearance values.
Understanding Raw vs Summary Results
Raw results:
raw_df %>% head()# A tibble: 6 × 6
ID 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>
Summarized results:
summary_obj <- summary(results_obj)
summary_obj start end N auclast cmax half.life aucinf.obs cl.obs
0 Inf 12 98.7 [22.5] 8.65 [17.0] 8.18 [2.12] 115 [28.4] 0.0398 [29.4]
Caption: auclast, cmax, aucinf.obs, cl.obs: geometric mean and geometric coefficient of variation; half.life: arithmetic mean and standard deviation; N: number of subjects
summary() aggregates across subjects and produces statistics such as:
- Mean
- SD
- CV%
- Geometric mean (for selected parameters)
Extracting Specific Parameters
Filter for AUC and half-life:
raw_df %>%
filter(PPTESTCD %in% c("auclast", "aucinf.obs", "half.life")) %>%
select(ID, PPTESTCD, PPORRES) %>%
head(12)# A tibble: 12 × 3
ID PPTESTCD PPORRES
<ord> <chr> <dbl>
1 6 auclast 71.7
2 6 half.life 7.89
3 6 aucinf.obs 82.2
4 7 auclast 88.0
5 7 half.life 7.85
6 7 aucinf.obs 101.
7 8 auclast 86.8
8 8 half.life 8.51
9 8 aucinf.obs 102.
10 11 auclast 77.9
11 11 half.life 7.26
12 11 aucinf.obs 86.9
You can also extract clearance explicitly:
raw_df %>%
filter(PPTESTCD == "cl.obs") %>%
select(ID, PPORRES) %>%
head()# A tibble: 6 × 2
ID PPORRES
<ord> <dbl>
1 6 0.0487
2 7 0.0490
3 8 0.0443
4 11 0.0566
5 3 0.0427
6 2 0.0452
Strategies
- Always inspect raw results before summarizing.
- Confirm expected parameters are present.
- Check half-life values for plausibility.
- Verify units before interpreting clearance.
- Treat summary output as reporting layer, not validation layer.
Common Mistakes
- Treating
summary()as validation. - Reporting AUCinf when extrapolated fraction is large.
- Ignoring terminal slope diagnostics.
- Comparing L/h/kg to L/h without recognizing scaling differences.
Practice Problems
Executable
- Count how many unique parameters were computed.
- Extract only Cmax values and compute their mean manually using
dplyr. - Identify the subject with the largest half-life.
Conceptual
- Why is it important to inspect raw results before calling
summary()? - Why must you verify dose units before interpreting clearance?
1. Count parameters:
raw_df %>%
summarise(n_parameters = n_distinct(PPTESTCD))# A tibble: 1 × 1
n_parameters
<int>
1 16
2. Manual Cmax mean:
raw_df %>%
filter(PPTESTCD == "cmax") %>%
summarise(mean_cmax = mean(PPORRES, na.rm = TRUE))# A tibble: 1 × 1
mean_cmax
<dbl>
1 8.76
3. Largest half-life:
raw_df %>%
filter(PPTESTCD == "half.life") %>%
arrange(desc(PPORRES)) %>%
slice(1)# A tibble: 1 × 6
ID start end PPTESTCD PPORRES exclude
<ord> <dbl> <dbl> <chr> <dbl> <chr>
1 1 0 Inf half.life 14.3 <NA>
4. Conceptual reason:
Raw inspection ensures parameters are plausible and correctly calculated before statistical aggregation masks potential issues.
5. Clearance units:
If dose is in mg/kg, clearance is L/h/kg. Misinterpreting units can lead to incorrect clinical or modeling conclusions.
Summary
You have now:
- Structured concentration data.
- Structured dose data.
- Defined intervals.
- Executed
pk.nca(). - Inspected raw and summarized outputs.
- Interpreted clearance correctly given dose units.
You now understand the full NCA calculation pipeline — and how to validate it before reporting.
- Always inspect raw results first.
- Confirm expected parameters are present.
- Treat summary output as reporting, not validation.
- Verify dose units before interpreting clearance.
- Label units explicitly in reporting tables.