6  Extravascular (Oral/SC) Examples

6.1 The EV dataset: Theophylline

We use datasets::Theoph — oral theophylline (bronchodilator) in 12 subjects, sampled over 25 hours after a single oral dose.

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
str(Theoph)
Classes 'nfnGroupedData', 'nfGroupedData', 'groupedData' and 'data.frame':  132 obs. of  5 variables:
 $ Subject: Ord.factor w/ 12 levels "6"<"7"<"8"<"11"<..: 11 11 11 11 11 11 11 11 11 11 ...
 $ Wt     : num  79.6 79.6 79.6 79.6 79.6 79.6 79.6 79.6 79.6 79.6 ...
 $ Dose   : num  4.02 4.02 4.02 4.02 4.02 4.02 4.02 4.02 4.02 4.02 ...
 $ Time   : num  0 0.25 0.57 1.12 2.02 ...
 $ conc   : num  0.74 2.84 6.57 10.5 9.66 8.58 8.36 7.47 6.89 5.94 ...
 - attr(*, "formula")=Class 'formula'  language conc ~ Time | Subject
  .. ..- attr(*, ".Environment")=<environment: R_EmptyEnv> 
 - attr(*, "labels")=List of 2
  ..$ x: chr "Time since drug administration"
  ..$ y: chr "Theophylline concentration in serum"
 - attr(*, "units")=List of 2
  ..$ x: chr "(hr)"
  ..$ y: chr "(mg/l)"

Columns: Subject, Wt (body weight, kg), Dose (mg/kg), Time (h), conc (mg/L).

# Prepare concentration data
d_conc <- as.data.frame(Theoph) |>
  rename(time = Time, subject = Subject, conc = conc)

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

# Visualize concentration profiles
ggplot(d_conc, aes(x = time, y = conc, group = subject, colour = subject)) +
  geom_line() + geom_point() +
  labs(title = "Theophylline oral — concentration-time profiles",
       x = "Time (h)", y = "Concentration (mg/L)") +
  theme_minimal()

The typical absorption-distribution-elimination shape is visible: concentrations rise to a peak (Tmax/Cmax) then decline.


6.2 Basic extravascular analysis

o_conc <- PKNCAconc(d_conc, conc ~ time | subject)

o_dose <- PKNCAdose(d_dose, dose ~ time | subject, route = "extravascular")

# Core EV parameters
ev_intervals <- data.frame(
  start       = 0,
  end         = Inf,
  cmax        = TRUE,
  tmax        = TRUE,
  auclast     = TRUE,
  aucinf.obs  = TRUE,
  half.life   = TRUE,
  lambda.z    = TRUE,
  cl.obs      = TRUE,   # apparent CL (= CL/F for EV)
  vz.obs      = TRUE    # apparent Vz (= Vz/F for EV)
)

o_data <- PKNCAdata(o_conc, o_dose, intervals = ev_intervals)
o_nca  <- pk.nca(o_data)

as.data.frame(o_nca) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 216 × 3
   subject PPTESTCD        PPORRES
   <ord>   <chr>             <dbl>
 1 6       adj.r.squared    0.998 
 2 6       aucinf.obs      82.2   
 3 6       auclast         71.7   
 4 6       cl.obs           3.89  
 5 6       clast.obs        0.92  
 6 6       clast.pred       0.941 
 7 6       cmax             6.44  
 8 6       half.life        7.89  
 9 6       lambda.z         0.0878
10 6       lambda.z.corrxy -0.999 
# ℹ 206 more rows

6.3 Parameter reference

6.3.1 Concentration and time landmarks

Parameter Meaning
cmax Maximum observed concentration
tmax Time of Cmax
cmin Minimum observed concentration in the interval
tmin Time of the minimum observed concentration (≥ 0.12.2)
tfirst First time with a non-zero (non-BLQ) concentration
tlast Last time with a measurable concentration
clast.obs Observed concentration at tlast
clast.pred Predicted concentration at tlast (from λz fit)
count_conc Total observations (including BLQ)
count_conc_measured Observations above the LLOQ
lambda.z.time.last Last timepoint used in the λz regression
lm_interval <- data.frame(
  start               = 0,
  end                 = Inf,
  cmax                = TRUE,
  tmax                = TRUE,
  cmin                = TRUE,
  tmin                = TRUE,
  tfirst              = TRUE,
  tlast               = TRUE,
  clast.obs           = TRUE,
  clast.pred          = TRUE,
  count_conc          = TRUE,
  count_conc_measured = TRUE
)

o_nca_lm <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = lm_interval))

as.data.frame(o_nca_lm) |>
  filter(PPTESTCD %in% c("cmax","tmax","cmin","tmin","tfirst","tlast",
                          "clast.obs","clast.pred","count_conc","count_conc_measured")) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 120 × 3
   subject PPTESTCD            PPORRES
   <ord>   <chr>                 <dbl>
 1 6       clast.obs             0.92 
 2 6       clast.pred            0.941
 3 6       cmax                  6.44 
 4 6       cmin                  0    
 5 6       count_conc           11    
 6 6       count_conc_measured  10    
 7 6       tfirst                0.27 
 8 6       tlast                23.8  
 9 6       tmax                  1.15 
10 6       tmin                  0    
# ℹ 110 more rows

When there are multiple peaks (fed state, enterohepatic recirculation), the default is to return the first Tmax. Control this with first.tmax:

Tie-breaking for Tmin is controlled by the first.tmin option (PKNCA ≥ 0.12.2, default TRUE = return first occurrence):

# Use the last Tmax when there are ties
o_data_lasttmax <- PKNCAdata(
  o_conc, o_dose,
  intervals = ev_intervals,
  options = list(first.tmax = FALSE)
)

6.3.2 AUC variants

Same variants as IV, but note that for EV routes CL and Vz are apparent (divided by F, the bioavailability fraction):

Parameter Meaning
auclast AUC from 0 to last measurable concentration
aucinf.obs AUC extrapolated to ∞ (using observed Clast)
aucinf.pred AUC extrapolated to ∞ (using predicted Clast)
aucpext.obs % of AUCinf that is extrapolated
cl.obs Apparent clearance = Dose / AUCinf.obs (this is CL/F)
vz.obs Apparent volume = CL.obs / λz (this is Vz/F)
auc_params <- data.frame(
  start        = 0,
  end          = Inf,
  auclast      = TRUE,
  aucinf.obs   = TRUE,
  aucinf.pred  = TRUE,
  aucpext.obs  = TRUE,
  cl.obs       = TRUE
)

o_data_auc <- PKNCAdata(o_conc, o_dose, intervals = auc_params)
o_nca_auc  <- pk.nca(o_data_auc)

as.data.frame(o_nca_auc) |>
  filter(PPTESTCD %in% c("auclast", "aucinf.obs", "aucpext.obs", "cl.obs")) |>
  select(subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(subject)
# A tibble: 12 × 5
   subject auclast aucinf.obs aucpext.obs cl.obs
   <ord>     <dbl>      <dbl>       <dbl>  <dbl>
 1 6          71.7       82.2       12.8    3.89
 2 7          88.0      101.        12.9    3.17
 3 8          86.8      102.        15.0    3.13
 4 11         77.9       86.9       10.4    3.68
 5 3          95.9      106.         9.66   3.01
 6 2          88.7       97.4        8.88   3.27
 7 4         103.       114.        10.1    2.80
 8 9          83.9       97.5       13.9    2.75
 9 12        115.       126.         8.43   2.55
10 10        136.       168.        19.2    1.91
11 1         147.       215.        31.5    1.49
12 5         118.       136.        13.3    2.35

6.3.3 Half-life and λz

For EV, the terminal phase reflects elimination (not absorption) once absorption is complete. The same quality controls apply as for IV.

hl_params <- data.frame(
  start             = 0,
  end               = Inf,
  lambda.z          = TRUE,
  half.life         = TRUE,
  lambda.z.n.points = TRUE,
  r.squared         = TRUE,
  adj.r.squared     = TRUE
)

o_data_hl <- PKNCAdata(o_conc, o_dose, intervals = hl_params)
o_nca_hl  <- pk.nca(o_data_hl)

as.data.frame(o_nca_hl) |>
  filter(PPTESTCD %in% c("half.life", "lambda.z.n.points", "adj.r.squared")) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 36 × 3
   subject PPTESTCD          PPORRES
   <ord>   <chr>               <dbl>
 1 6       adj.r.squared       0.998
 2 6       half.life           7.89 
 3 6       lambda.z.n.points   7    
 4 7       adj.r.squared       0.998
 5 7       half.life           7.85 
 6 7       lambda.z.n.points   4    
 7 8       adj.r.squared       0.989
 8 8       half.life           8.51 
 9 8       lambda.z.n.points   6    
10 11      adj.r.squared       1.000
# ℹ 26 more rows

Allow or exclude Tmax from the terminal regression:

By default, the Tmax point is excluded from the λz regression (absorption may still be ongoing). You can override this:

o_data_allow_tmax <- PKNCAdata(
  o_conc, o_dose,
  intervals = hl_params,
  options = list(allow.tmax.in.half.life = TRUE)
)

6.3.4 Lag time (tlag)

tlag is the time before absorption begins — the delay between dosing and the first measurable rise in concentration. Common for enteric-coated formulations or SC injections with a diffusion delay.

o_nca_tlag <- pk.nca(PKNCAdata(o_conc, o_dose,
  intervals = data.frame(start = 0, end = Inf, tlag = TRUE, tmax = TRUE, cmax = TRUE)))

as.data.frame(o_nca_tlag) |>
  filter(PPTESTCD %in% c("tlag", "tmax", "cmax")) |>
  select(subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(subject)
# A tibble: 12 × 4
   subject  cmax  tmax  tlag
   <ord>   <dbl> <dbl> <dbl>
 1 6        6.44  1.15     0
 2 7        7.09  3.48     0
 3 8        7.56  2.02     0
 4 11       8     0.98     0
 5 3        8.2   1.02     0
 6 2        8.33  1.92     0
 7 4        8.6   1.07     0
 8 9        9.03  0.63     0
 9 12       9.75  3.52     0
10 10      10.2   3.55     0
11 1       10.5   1.12     0
12 5       11.4   1        0

For the Theoph dataset tlag is 0 for all subjects (no lag). It becomes non-zero when the concentration-time profile shows a flat period after dosing before rising.

6.3.5 Bioavailability (f)

f computes the ratio of AUC between two routes or treatments within the same subject. It requires a reference AUC in the grouping structure (typically set up with a treatment or period column).

# Syntax: request f = TRUE; PKNCA computes AUC_test / AUC_reference
# Requires a grouping column that identifies test vs. reference
# Example structure (illustrative — Theoph is single-arm):
data.frame(start = 0, end = Inf, aucinf.obs = TRUE, f = FALSE)
  start end aucinf.obs     f
1     0 Inf       TRUE FALSE
# f = TRUE only makes sense when the data has paired test/reference periods

See ?pk.calc.f for the full setup. f is typically used in crossover bioequivalence studies.

6.3.6 Volume at steady state (Vss)

Vss is computed from CL and MRT. For EV routes these are apparent values (divided by F).

vss_params <- data.frame(
  start    = 0,
  end      = Inf,
  vss.obs  = TRUE,   # apparent Vss = CL.obs × MRT.obs
  vss.pred = TRUE
)

o_nca_vss <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = vss_params))
as.data.frame(o_nca_vss) |>
  filter(PPTESTCD %in% c("vss.obs", "vss.pred")) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 24 × 3
   subject PPTESTCD PPORRES
   <ord>   <chr>      <dbl>
 1 6       vss.obs     46.8
 2 6       vss.pred    46.9
 3 7       vss.obs     39.5
 4 7       vss.pred    39.5
 5 8       vss.obs     40.2
 6 8       vss.pred    40.2
 7 11      vss.obs     39.7
 8 11      vss.pred    39.7
 9 3       vss.obs     32.9
10 3       vss.pred    32.9
# ℹ 14 more rows

6.3.7 Vss variants

For extravascular dosing, vss.obs and vss.pred are the apparent Vss (i.e., Vss/F). vss.last uses MRTlast instead of the extrapolated MRT.

vss_all_params <- data.frame(
  start    = 0, end = Inf,
  vss.obs  = TRUE,
  vss.pred = TRUE,
  vss.last = TRUE
)
o_nca_vss_all <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = vss_all_params))
as.data.frame(o_nca_vss_all) |>
  filter(grepl("^vss", PPTESTCD)) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 36 × 3
   subject PPTESTCD PPORRES
   <ord>   <chr>      <dbl>
 1 6       vss.last    38.5
 2 6       vss.obs     46.8
 3 6       vss.pred    46.9
 4 7       vss.last    32.9
 5 7       vss.obs     39.5
 6 7       vss.pred    39.5
 7 8       vss.last    32.1
 8 8       vss.obs     40.2
 9 8       vss.pred    40.2
10 11      vss.last    33.0
# ℹ 26 more rows

6.3.8 MRT variants

Parameter Meaning
mrt.last AUMC(0–last) / AUC(0–last)
mrt.obs AUMC(0–∞) / AUC(0–∞), observed Clast
mrt.pred AUMC(0–∞) / AUC(0–∞), predicted Clast
mrt_ev_params <- data.frame(
  start    = 0, end = Inf,
  mrt.last = TRUE,
  mrt.obs  = TRUE,
  mrt.pred = TRUE
)
o_nca_mrt_ev <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = mrt_ev_params))
as.data.frame(o_nca_mrt_ev) |>
  filter(grepl("^mrt", PPTESTCD)) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 36 × 3
   subject PPTESTCD PPORRES
   <ord>   <chr>      <dbl>
 1 6       mrt.last    8.63
 2 6       mrt.obs    12.0 
 3 6       mrt.pred   12.1 
 4 7       mrt.last    9.04
 5 7       mrt.obs    12.5 
 6 7       mrt.pred   12.5 
 7 8       mrt.last    8.71
 8 8       mrt.obs    12.9 
 9 8       mrt.pred   12.8 
10 11      mrt.last    8.04
# ℹ 26 more rows

6.3.9 Effective half-life and kel

All extravascular effective half-life and elimination rate constant variants:

Parameter Based on
thalf.eff.last mrt.last
thalf.eff.obs mrt.obs
thalf.eff.pred mrt.pred
kel.obs 1 / mrt.obs
kel.pred 1 / mrt.pred
kel.last 1 / mrt.last
eff_params <- data.frame(
  start           = 0, end = Inf,
  thalf.eff.last  = TRUE,
  thalf.eff.obs   = TRUE,
  thalf.eff.pred  = TRUE,
  kel.obs         = TRUE,
  kel.pred        = TRUE,
  kel.last        = TRUE
)

o_nca_eff <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = eff_params))
as.data.frame(o_nca_eff) |>
  filter(grepl("^(thalf|kel)", PPTESTCD)) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 72 × 3
   subject PPTESTCD       PPORRES
   <ord>   <chr>            <dbl>
 1 6       kel.last        0.116 
 2 6       kel.obs         0.0832
 3 6       kel.pred        0.0827
 4 6       thalf.eff.last  5.98  
 5 6       thalf.eff.obs   8.33  
 6 6       thalf.eff.pred  8.38  
 7 7       kel.last        0.111 
 8 7       kel.obs         0.0803
 9 7       kel.pred        0.0801
10 7       thalf.eff.last  6.27  
# ℹ 62 more rows

6.3.10 AUC for a specific tau (dosing interval)

For multiple-dose studies, request AUC over the dosing interval (tau):

# Single dose, but demonstrating syntax for AUC(0-tau)
tau_interval <- data.frame(
  start      = 0,
  end        = 24,      # tau = 24 h
  auclast    = TRUE,
  cmax       = TRUE,
  tmax       = TRUE
)

o_data_tau <- PKNCAdata(o_conc, o_dose, intervals = tau_interval)
o_nca_tau  <- pk.nca(o_data_tau)
as.data.frame(o_nca_tau) |> select(subject, PPTESTCD, PPORRES)
# A tibble: 36 × 3
   subject PPTESTCD PPORRES
   <ord>   <chr>      <dbl>
 1 6       auclast    71.7 
 2 6       cmax        6.44
 3 6       tmax        1.15
 4 7       auclast    62.1 
 5 7       cmax        7.09
 6 7       tmax        3.48
 7 8       auclast    62.8 
 8 8       cmax        7.56
 9 8       tmax        2.02
10 11      auclast    58.7 
# ℹ 26 more rows

6.4 Imputation of missing predose concentrations

In some studies, the predose sample is missing or was not collected. PKNCA can impute a time-0 concentration before calculations.

Available built-in methods (comma-separate to chain them):

Method What it does
start_predose Uses the last pre-interval concentration at the interval start (no sample is added if no pre-interval observation exists)
start_conc0 Sets the predose concentration to 0 unconditionally
start_cmin Uses the minimum observed concentration as predose value
# Remove the time=0 observation from subject 1 to simulate missing predose
d_conc_missing <- d_conc |>
  filter(!(subject == "1" & time == 0))

o_conc_miss <- PKNCAconc(d_conc_missing, conc ~ time | subject)

# With imputation: add back a 0 at time 0
o_data_imputed <- PKNCAdata(
  o_conc_miss, o_dose,
  intervals = ev_intervals,
  impute = "start_predose"
)

o_nca_imputed <- pk.nca(o_data_imputed)
Warning: Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
# Compare subject 1 AUC with and without imputation
bind_rows(
  as.data.frame(o_nca) |> filter(subject == "1", PPTESTCD == "auclast") |> mutate(version = "original"),
  as.data.frame(o_nca_imputed) |> filter(subject == "1", PPTESTCD == "auclast") |> mutate(version = "imputed")
) |>
  select(version, PPTESTCD, PPORRES)
# A tibble: 2 × 3
  version  PPTESTCD PPORRES
  <chr>    <chr>      <dbl>
1 original auclast     147.
2 imputed  auclast      NA 

Per-interval imputation — apply different methods to different calculation windows:

# Intervals with an "impute" column specifying method per row
d_intervals_impute <- data.frame(
  start   = 0,
  end     = Inf,
  auclast = TRUE,
  impute  = "start_predose"
)

o_data_per_interval <- PKNCAdata(
  o_conc_miss, o_dose,
  intervals = d_intervals_impute,
  impute = "impute"   # tells PKNCA to look in the intervals column named "impute"
)

6.5 Bioavailability

Bioavailability (F) compares AUC from the EV route to AUC from an IV reference. PKNCA calculates this if you supply both an IV and an EV PKNCAresults object.

# IV reference (using Indometh as stand-in — illustrative syntax only)
d_iv_conc <- as.data.frame(Indometh)
d_iv_dose <- data.frame(Subject = unique(d_iv_conc$Subject), dose = 25, time = 0)

o_iv_conc <- PKNCAconc(d_iv_conc, conc ~ time | Subject)
o_iv_dose <- PKNCAdose(d_iv_dose, dose ~ time | Subject, route = "intravascular")
o_iv_data <- PKNCAdata(o_iv_conc, o_iv_dose, intervals = data.frame(
  start = 0, end = Inf, aucinf.obs = TRUE
))
o_iv_nca <- pk.nca(o_iv_data)
Warning: Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
Requesting an AUC range starting (0) before the first measurement (0.25) is not allowed
# pk.business.rule: bioavailability is computed via pk.calc.f()
# Use pk.calc.f() directly when you have matched IV and EV AUCs
iv_aucinf <- as.data.frame(o_iv_nca) |>
  filter(PPTESTCD == "aucinf.obs") |>
  summarise(mean_aucinf = mean(PPORRES)) |>
  pull(mean_aucinf)

ev_aucinf <- as.data.frame(o_nca_auc) |>
  filter(PPTESTCD == "aucinf.obs") |>
  summarise(mean_aucinf = mean(PPORRES)) |>
  pull(mean_aucinf)

# F (%) = (AUC_ev / Dose_ev) / (AUC_iv / Dose_iv) * 100
# These datasets use different drugs, so this is illustrative only:
cat("Illustrative F calculation:\n")
Illustrative F calculation:
cat(sprintf("  IV AUCinf (mean): %.2f\n", iv_aucinf))
  IV AUCinf (mean): NA
cat(sprintf("  EV AUCinf (mean): %.2f\n", ev_aucinf))
  EV AUCinf (mean): 119.37

For a real bioavailability study, subjects would receive both IV and EV treatments, and pk.calc.f() handles the ratio calculation accounting for dose scaling.


6.6 Multiple-dose / steady-state

For multiple-dose studies, specify the dosing interval in your intervals data frame. PKNCA will compute accumulation-relevant parameters:

# Steady-state interval parameters
ss_params <- c("cmax", "cmin", "tmax", "auclast", "half.life", "lambda.z")

ss_intervals <- data.frame(
  start  = 0,
  end    = 24,
  cmax   = TRUE,
  cmin   = TRUE,
  tmax   = TRUE,
  auclast = TRUE
)

o_data_ss <- PKNCAdata(o_conc, o_dose, intervals = ss_intervals)
o_nca_ss  <- pk.nca(o_data_ss)

as.data.frame(o_nca_ss) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 48 × 3
   subject PPTESTCD PPORRES
   <ord>   <chr>      <dbl>
 1 6       auclast    71.7 
 2 6       cmax        6.44
 3 6       cmin        0   
 4 6       tmax        1.15
 5 7       auclast    62.1 
 6 7       cmax        7.09
 7 7       cmin        0.15
 8 7       tmax        3.48
 9 8       auclast    62.8 
10 8       cmax        7.56
# ℹ 38 more rows

6.7 Excluding observations

Mark individual time points to exclude (e.g., emesis within 2× Tmax, suspected contamination):

d_conc_excl <- d_conc |>
  mutate(
    exclude_reason = ifelse(subject == "5" & time == 7.02,
                            "vomiting within 2h of Tmax", NA_character_)
  )

o_conc_excl <- PKNCAconc(d_conc_excl, conc ~ time | subject, exclude = "exclude_reason")

o_data_excl <- PKNCAdata(o_conc_excl, o_dose, intervals = ev_intervals)
o_nca_excl  <- pk.nca(o_data_excl)

Excluded points appear flagged in as.data.frame() output, preserving audit trail.


6.8 BLQ (below limit of quantification) handling

PKNCA’s conc.blq option controls how BLQ values (typically entered as 0) are treated:

The default is list(first = "keep", middle = "drop", last = "keep"): - first — BLQs before the first non-BLQ concentration (keep, often a pre-dose zero) - middle — BLQs sandwiched between non-BLQ values (dropped by default) - last — BLQs after the last non-BLQ concentration (keep by default)

You can override globally or with positional keys:

# Global: treat all BLQ as 0
o_data_blq_keep <- PKNCAdata(
  o_conc, o_dose,
  intervals = ev_intervals,
  options = list(conc.blq = "keep")
)

# Global: remove all BLQ values
o_data_blq_drop <- PKNCAdata(
  o_conc, o_dose,
  intervals = ev_intervals,
  options = list(conc.blq = "drop")
)

# Per-phase (before/after Tmax):
o_data_blq <- PKNCAdata(
  o_conc, o_dose,
  intervals = ev_intervals,
  options = list(
    conc.blq = list(
      before.tmax = "keep",   # keep 0s before Tmax (ascending phase)
      after.tmax  = "drop"    # drop BLQs after Tmax
    )
  )
)

6.9 AUC integration methods

The default integration method is lin up / log down (linear interpolation on the ascending phase, log-linear on the descending phase). Three methods are available:

# Method 1: linear trapezoidal (simplest)
o_data_lin <- PKNCAdata(o_conc, o_dose, intervals = ev_intervals,
                         options = list(auc.method = "linear"))

# Method 2: lin-log (log trapezoidal throughout — not recommended)
o_data_linlog <- PKNCAdata(o_conc, o_dose, intervals = ev_intervals,
                            options = list(auc.method = "lin-log"))

# Method 3: lin up/log down (default, recommended)
o_data_liuplogdown <- PKNCAdata(o_conc, o_dose, intervals = ev_intervals,
                                 options = list(auc.method = "lin up/log down"))

# Compare AUClast across methods
compare_methods <- function(label, data_obj) {
  nca <- pk.nca(data_obj)
  as.data.frame(nca) |>
    filter(PPTESTCD == "auclast") |>
    mutate(method = label) |>
    select(method, subject, PPORRES)
}

bind_rows(
  compare_methods("linear",           o_data_lin),
  compare_methods("lin up/log down",  o_data_liuplogdown)
) |>
  tidyr::pivot_wider(names_from = method, values_from = PPORRES) |>
  arrange(subject)
# A tibble: 12 × 3
   subject linear `lin up/log down`
   <ord>    <dbl>             <dbl>
 1 6         73.8              71.7
 2 7         90.8              88.0
 3 8         88.6              86.8
 4 11        80.1              77.9
 5 3         99.3              95.9
 6 2         91.5              88.7
 7 4        107.              103. 
 8 9         86.3              83.9
 9 12       120.              115. 
10 10       138.              136. 
11 1        149.              147. 
12 5        121.              118. 

6.10 Units

units_table <- pknca_units_table(
  concu   = "mg/L",
  timeu   = "h",
  doseu   = "mg",
  amountu = "mg"
)

o_conc_u <- PKNCAconc(d_conc, conc ~ time | subject, concu = "mg/L", timeu = "h")
o_dose_u <- PKNCAdose(d_dose, dose ~ time | subject, route = "extravascular",
                      doseu = "mg", timeu = "h")
o_data_u <- PKNCAdata(o_conc_u, o_dose_u, intervals = ev_intervals, units = units_table)
o_nca_u  <- pk.nca(o_data_u)

as.data.frame(o_nca_u) |>
  filter(PPTESTCD %in% c("auclast", "aucinf.obs", "cl.obs", "half.life")) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 48 × 3
   subject PPTESTCD   PPORRES
   <ord>   <chr>        <dbl>
 1 6       aucinf.obs   82.2 
 2 6       auclast      71.7 
 3 6       cl.obs        3.89
 4 6       half.life     7.89
 5 7       aucinf.obs  101.  
 6 7       auclast      88.0 
 7 7       cl.obs        3.17
 8 7       half.life     7.85
 9 8       aucinf.obs  102.  
10 8       auclast      86.8 
# ℹ 38 more rows

6.11 Summary

summary(o_nca)
 start end  N     auclast        cmax               tmax   half.life
     0 Inf 12 98.7 [22.5] 8.65 [17.0] 1.14 [0.630, 3.55] 8.18 [2.12]
      lambda.z aucinf.obs      cl.obs      vz.obs
 0.0868 [21.9] 115 [28.4] 2.74 [27.9] 31.6 [19.2]

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

Customize for specific parameters:

PKNCA.set.summary(
  "tmax",
  description = "median [min, max]",
  point  = median,
  spread = function(x) c(min(x), max(x))  # must return numeric; PKNCA formats the string
)

summary(o_nca)
 start end  N     auclast        cmax               tmax   half.life
     0 Inf 12 98.7 [22.5] 8.65 [17.0] 1.14 [0.630, 3.55] 8.18 [2.12]
      lambda.z aucinf.obs      cl.obs      vz.obs
 0.0868 [21.9] 115 [28.4] 2.74 [27.9] 31.6 [19.2]

Caption: auclast, cmax, lambda.z, aucinf.obs, cl.obs, vz.obs: geometric mean and geometric coefficient of variation; tmax: median [min, max]; half.life: arithmetic mean and standard deviation; N: number of subjects

6.12 Full parameter list for extravascular

All extravascular-relevant parameters you can request in an interval:

interval_cols <- get.interval.cols()
ev_relevant <- c(
  # Concentration / time landmarks
  "cmax", "tmax", "cmin", "tfirst", "tlast", "tlag",
  "clast.obs", "clast.pred", "count_conc", "count_conc_measured",
  # AUC family
  "auclast", "aucall", "aucinf.obs", "aucinf.pred",
  "aucpext.obs", "aucpext.pred",
  "aucint.last", "aucint.last.dose", "aucint.all", "aucint.all.dose",
  "aucint.inf.obs", "aucint.inf.obs.dose",
  # AUMC
  "aumclast", "aumcall", "aumcinf.obs", "aumcinf.pred",
  # Half-life / λz
  "half.life", "lambda.z", "lambda.z.n.points", "lambda.z.time.first", "lambda.z.time.last",
  "r.squared", "adj.r.squared", "span.ratio",
  # Clearance (apparent)
  "cl.obs", "cl.pred", "cl.last", "cl.all",
  # Volume (apparent)
  "vz.obs", "vz.pred", "vss.obs", "vss.pred", "vss.last",
  # MRT
  "mrt.last", "mrt.obs", "mrt.pred",
  # Effective half-life / kel
  "thalf.eff.last", "thalf.eff.obs", "thalf.eff.pred",
  "kel.obs", "kel.pred", "kel.last",
  # Bioavailability
  "f",
  # Dose-normalized
  "auclast.dn", "aucinf.obs.dn", "cmax.dn", "clast.obs.dn"
)

sort(names(interval_cols)[names(interval_cols) %in% ev_relevant])
 [1] "adj.r.squared"       "aucall"              "aucinf.obs"         
 [4] "aucinf.obs.dn"       "aucinf.pred"         "aucint.all"         
 [7] "aucint.all.dose"     "aucint.inf.obs"      "aucint.inf.obs.dose"
[10] "aucint.last"         "aucint.last.dose"    "auclast"            
[13] "auclast.dn"          "aucpext.obs"         "aucpext.pred"       
[16] "aumcall"             "aumcinf.obs"         "aumcinf.pred"       
[19] "aumclast"            "cl.all"              "cl.last"            
[22] "cl.obs"              "cl.pred"             "clast.obs"          
[25] "clast.obs.dn"        "clast.pred"          "cmax"               
[28] "cmax.dn"             "cmin"                "count_conc"         
[31] "count_conc_measured" "f"                   "half.life"          
[34] "kel.last"            "kel.obs"             "kel.pred"           
[37] "lambda.z"            "lambda.z.n.points"   "lambda.z.time.first"
[40] "lambda.z.time.last"  "mrt.last"            "mrt.obs"            
[43] "mrt.pred"            "r.squared"           "span.ratio"         
[46] "tfirst"              "thalf.eff.last"      "thalf.eff.obs"      
[49] "thalf.eff.pred"      "tlag"                "tlast"              
[52] "tmax"                "vss.last"            "vss.obs"            
[55] "vss.pred"            "vz.obs"              "vz.pred"