7  Extravascular (Oral/SC) Examples

7.1 The EV dataset: Theophylline

We use datasets::Theoph — oral theophylline (bronchodilator) in 12 subjects, sampled over 25 hours after a single oral dose. The package authors’ worked analysis of this dataset is in the vignette Computing NCA Parameters for Theophylline.

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)

# 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.


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

7.3 Parameter reference

7.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.13.0)
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 Number of non-missing concentrations (including BLQ)
count_conc_measured Number of concentrations above LOQ (non-BLQ)
lambda.z.time.last Last timepoint used in the λz regression

Both counts are taken after imputation and do not distinguish imputed from measured values: an imputed zero (start_conc0) adds to count_conc but not to count_conc_measured, while a concentration carried to the start (start_predose) adds to both. To count only measured samples, calculate the counts in an interval without imputation.

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 the maximum occurs at more than one timepoint (an exact tie), the default is to return the first Tmax. Control this with first.tmax:

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

# Tmax is unchanged for every subject:
identical(
  as.data.frame(o_nca)          |> filter(PPTESTCD == "tmax") |> pull(PPORRES),
  as.data.frame(o_nca_lasttmax) |> filter(PPTESTCD == "tmax") |> pull(PPORRES)
)
[1] TRUE

The option has no effect here because no Theoph subject reaches its maximum concentration at two timepoints — first.tmax only matters when the maximum is an exact tie. Tie-breaking for Tmin is controlled by the first.tmin option (≥ 0.13.0; default TRUE = return first occurrence). All calculation options are cataloged in Options for Controlling PKNCA.

7.3.2 AUC variants

The AUC family covers exposure to the last measurable concentration (auclast) and exposure extrapolated to infinity using either the observed or the λz-predicted Clast (aucinf.obs / aucinf.pred), with aucpext.obs reporting how much of AUCinf was extrapolated. See the AUC chapter for the full AUC family and the integration methods behind it.

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
auc_params <- data.frame(
  start        = 0,
  end          = Inf,
  auclast      = TRUE,
  aucinf.obs   = TRUE,
  aucinf.pred  = TRUE,
  aucpext.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", "aucinf.pred", "aucpext.obs")) |>
  select(subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(subject)
# A tibble: 12 × 5
   subject auclast aucinf.obs aucinf.pred aucpext.obs
   <ord>     <dbl>      <dbl>       <dbl>       <dbl>
 1 6          71.7       82.2        82.4       12.8 
 2 7          88.0      101.        101.        12.9 
 3 8          86.8      102.        102.        15.0 
 4 11         77.9       86.9        86.9       10.4 
 5 3          95.9      106.        106.         9.66
 6 2          88.7       97.4        97.3        8.88
 7 4         103.       114.        114.        10.1 
 8 9          83.9       97.5        97.5       13.9 
 9 12        115.       126.        126.         8.43
10 10        136.       168.        168.        19.2 
11 1         147.       215.        215.        31.5 
12 5         118.       136.        136.        13.3 

7.3.3 Half-life and λz

For EV, the terminal phase reflects elimination (not absorption) once absorption is complete. One extravascular-specific caveat: with slow absorption the terminal slope can reflect absorption rather than elimination (flip-flop kinetics), in which case the apparent half-life is an absorption half-life. The usual λz quality controls — enough points in the regression, adjusted R², and span ratio — apply regardless of route; see the Half-Life chapter for point selection and quality thresholds. The λz point-selection algorithm and quality metrics are detailed in the Half-Life Calculation vignette.

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)
)
o_nca_allow_tmax <- pk.nca(o_data_allow_tmax)

# Subject 8 is the only subject whose selected regression changes
bind_rows(
  as.data.frame(o_nca_hl)         |> mutate(version = "default (Tmax excluded)"),
  as.data.frame(o_nca_allow_tmax) |> mutate(version = "Tmax allowed")
) |>
  filter(subject == "8",
         PPTESTCD %in% c("lambda.z.n.points", "adj.r.squared", "half.life")) |>
  select(version, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES)
# A tibble: 2 × 4
  version                 adj.r.squared lambda.z.n.points half.life
  <chr>                           <dbl>             <dbl>     <dbl>
1 default (Tmax excluded)         0.989                 6      8.51
2 Tmax allowed                    0.991                 7      8.47

For the other 11 subjects the best-fitting regression already excluded Tmax, so nothing changes; subject 8’s best fit (highest adjusted R²) now starts at Tmax, using one more point and shifting the half-life slightly.

7.3.4 Clearance and volume (apparent)

For extravascular dosing the fraction absorbed (F) is unknown, so clearance and volume cannot be separated from it: PKNCA’s cl.* and vz.* values are apparent, i.e. CL/F and Vz/F. The formulas are the same as for IV dosing (Dose / AUC, then CL / λz), with F implicitly in the denominator — both are requested in the basic analysis above.

Parameter Meaning
cl.obs Apparent clearance = Dose / AUCinf.obs (this is CL/F)
vz.obs Apparent volume = CL.obs / λz (this is Vz/F)

Each clearance and volume is available for every AUC basis, named by its suffix: .obs and .pred (AUCinf with observed or predicted Clast), .last (AUClast), .all (AUCall), and .int.last, .int.all, .int.inf.obs, .int.inf.pred (the interval AUCs, aucint.*). PKNCA 0.13.0 completed these families for cl.*, vz.*, vss.*, mrt.*, and kel.* (for example vz.last, vss.int.last, mrt.int.inf.obs); the full list is printed at the end of this chapter.

7.3.5 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.

7.3.7 Volume at steady state (Vss)

Vss is computed from CL and MRT, so 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_params <- data.frame(
  start    = 0,
  end      = Inf,
  vss.obs  = TRUE,   # apparent Vss = CL.obs × MRT.obs
  vss.pred = TRUE,
  vss.last = TRUE    # uses MRTlast
)

o_nca_vss <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = vss_params))
as.data.frame(o_nca_vss) |>
  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

7.3.8 Mean residence time (MRT)

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

7.3.9 Effective half-life and elimination rate constant

The effective half-life is ln(2) × MRT — a rate that reflects overall persistence rather than the terminal slope. The related kel.* parameters are 1/MRT, not λz; the two differ whenever distribution or absorption contributes materially to MRT.

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

7.4 AUC over a specific 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

7.5 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. Full method descriptions and per-interval examples are in the Concentration Imputation chapter and the Data Imputation vignette.

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

Method What it does
start_predose Shifts the most recent measured pre-interval concentration to the interval start, if no concentration exists at the start (nothing is added if there is no pre-interval observation)
start_conc0 Sets the start concentration to 0, replacing any existing value at the start time
start_predose_conc0 Uses a predose concentration when one is available and 0 otherwise, keeping a concentration measured at the start time (≥ 0.13.0)
start_cmin Adds the minimum concentration in the interval at the start time
end_conc_drop Drops a concentration measured exactly at the interval end (≥ 0.13.0)
# 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: start_conc0 adds conc = 0 at the interval start
o_data_imputed <- PKNCAdata(
  o_conc_miss, o_dose,
  intervals = ev_intervals,
  impute = "start_conc0"
)

o_nca_imputed <- pk.nca(o_data_imputed)

# Compare subject 1 AUClast: original data vs. missing predose imputed as 0
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) |>
  as.data.frame()  # base print shows full precision; the difference is small
   version PPTESTCD  PPORRES
1 original  auclast 147.2347
2  imputed  auclast 147.1422

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

o_data_per_interval <- PKNCAdata(
  o_conc_miss, o_dose,
  intervals = d_intervals_impute,
  impute = "impute"   # the name of the intervals column holding the method
)
o_nca_per_interval <- pk.nca(o_data_per_interval)

# Subject 1's missing predose is imputed by the interval row — same result as above
bind_rows(
  as.data.frame(o_nca_imputed)      |> mutate(version = "global impute"),
  as.data.frame(o_nca_per_interval) |> mutate(version = "per-interval impute")
) |>
  filter(subject == "1", PPTESTCD == "auclast") |>
  select(version, PPTESTCD, PPORRES)
# A tibble: 2 × 3
  version             PPTESTCD PPORRES
  <chr>               <chr>      <dbl>
1 global impute       auclast     147.
2 per-interval impute auclast     147.

With a single interval row the per-interval column reproduces the global setting; with several rows, each row’s impute value applies only to its own calculation window. The column can have any name (≥ 0.13.0); PKNCAdata(impute = ) names it: a value that is a column name of the intervals is read as that column, and any other value is the method for every interval.


7.6 Bioavailability

Bioavailability (F) compares the dose-normalized exposure of the extravascular dose with an IV reference (absolute F) or another formulation (relative F). Since PKNCA 0.13.0 it is a secondary parameter calculated in the same pk.nca() run as the AUCs: the intervals carry a treatment (or route, or formulation) grouping column, the test intervals request f.obs, and the reference profile is named with PKNCAdata(group_ref = ), an f.obs_ref pointer, or interval_add_secondary(). f.obs takes the reference dose and AUCinf,obs from the reference interval and the test dose (totdose) and AUCinf,obs from its own interval; dose2/auc2 interval columns, used by older workarounds, are ignored. Variants name the AUC they are built on:

Parameter AUC basis
f.obs aucinf.obs (formerly f)
f.pred aucinf.pred
f.last auclast
f.int.last, f.int.all, f.int.obs, f.int.pred aucint.last, aucint.all, aucint.inf.obs, aucint.inf.pred

Theoph has no reference administration, so the worked example — an IV and oral crossover with f.obs and f.last checked by hand — is in the Secondary Parameters chapter. The same chapter covers food-effect ratios (ratio.cmax, ratio.aucinf.obs) for comparing fed and fasted extravascular doses.


7.7 Multiple-dose / steady-state

For multiple-dose data, request steady-state parameters over a tau interval — see the Multiple-Dose chapter for the full workflow. To simulate steady state from single-dose data, see Superposition of Pharmacokinetic Data; to estimate when steady state is reached, see Noncompartmental evaluation of time to steady-state. Here the interval syntax is demonstrated on the single-dose Theoph data:

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

7.8 Excluding observations

Mark individual timepoints to exclude (e.g. vomiting within 2× median Tmax, suspected contamination):

d_conc_excl <- d_conc |>
  mutate(
    exclude_reason = ifelse(subject == "5" & time == 2.02,
                            "vomiting within 2x median Tmax", NA_character_)
  )

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


# The flagged row stays in the data with its reason
as.data.frame(o_conc_excl) |> filter(!is.na(exclude_reason))
  subject   Wt Dose time conc                 exclude_reason
1       5 54.6 5.86 2.02 9.33 vomiting within 2x median Tmax

The excluded point is dropped from every parameter calculation, while the flagged row remains in the data with its reason (shown above), preserving the audit trail.


7.9 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 predose zero) - middle — BLQs sandwiched between non-BLQ values (dropped by default) - last — BLQs after the last non-BLQ concentration (keep by default)

In Theoph the only BLQ values are the predose zeros, which the default rule keeps, so overriding conc.blq changes little on this dataset. To see the mechanism, simulate a mid-profile BLQ — subject 3’s 3.62 h sample set to 0 — and compare the default against a global "keep":

d_conc_blq <- d_conc |>
  mutate(conc = ifelse(subject == "3" & time == 3.62, 0, conc))
o_conc_blq <- PKNCAconc(d_conc_blq, conc ~ time | subject)

auclast_interval <- data.frame(start = 0, end = Inf, auclast = TRUE)

# Default: the middle BLQ is dropped and the AUC interpolates across the gap
o_nca_blq_default <- pk.nca(PKNCAdata(o_conc_blq, o_dose, intervals = auclast_interval))

# Global override: keep all BLQ values as 0
o_data_blq_keep <- PKNCAdata(
  o_conc_blq, o_dose,
  intervals = auclast_interval,
  options = list(conc.blq = "keep")
)
o_nca_blq_keep <- pk.nca(o_data_blq_keep)

bind_rows(
  as.data.frame(o_nca_blq_default) |> mutate(version = "default (drop middle BLQ)"),
  as.data.frame(o_nca_blq_keep)    |> mutate(version = "conc.blq = \"keep\"")
) |>
  filter(subject == "3", PPTESTCD == "auclast") |>
  select(version, PPTESTCD, PPORRES)
# A tibble: 2 × 3
  version                     PPTESTCD PPORRES
  <chr>                       <chr>      <dbl>
1 "default (drop middle BLQ)" auclast     95.0
2 "conc.blq = \"keep\""       auclast     84.4

Keeping the mid-profile 0 pulls the concentration curve down to 0 at 3.62 h and cuts a wedge out of subject 3’s AUC; the default treats an isolated BLQ between measurable concentrations as an assay artifact and interpolates across it.

Overrides can also be per-position (first/middle/last) or relative to Tmax (before.tmax/after.tmax), and the action can be "keep", "drop", or a numeric replacement value. The objects are built the same way; running them on this data gives the results described below:

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

On Theoph the per-phase rule reproduces the default results (the predose zeros sit before Tmax and are kept), while the global "drop" is a trap for intervals starting at the dose time: it removes the predose zeros too, so AUCs that start at 0 begin before the first remaining measurement and return NA with a warning.

To use BLQ observations in half-life estimation rather than dropping them, see Half-life calculation with Tobit regression (≥ 0.13.0).


7.10 AUC integration methods

The default integration method is lin up/log down (linear interpolation on the ascending phase, log-linear on the descending phase). The alternatives, "linear" and "lin-log", are selected per-analysis with the auc.method option. See the AUC chapter for how each method computes the trapezoids and when to prefer which. Formal definitions of each rule are in the AUC integration methods vignette.

# Switch to the linear trapezoidal method
o_data_lin <- PKNCAdata(o_conc, o_dose, intervals = ev_intervals,
                        options = list(auc.method = "linear"))
as.data.frame(pk.nca(o_data_lin)) |>
  filter(PPTESTCD == "auclast") |>
  select(subject, PPTESTCD, PPORRES) |>
  head(3)
# A tibble: 3 × 3
  subject PPTESTCD PPORRES
  <ord>   <chr>      <dbl>
1 6       auclast     73.8
2 7       auclast     90.8
3 8       auclast     88.6

7.11 Units

Assign units to get automatic unit propagation through derived parameters. See the Units chapter for the full units system, including unit conversion and per-parameter overrides. See also Unit Assignment and Conversion with PKNCA for the full units workflow.

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

7.12 Summary with custom statistics

summary() produces a study-report-style table of summary statistics per parameter, and PKNCA.set.summary() customizes the statistics reported for any parameter — see Post-processing for the full summary workflow. More summary and exclusion workflows are in the Post-Processing vignette.

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

7.13 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", "tmin", "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.all", "aucint.inf.obs", "aucint.inf.pred",
  # AUMC
  "aumclast", "aumcall", "aumcinf.obs", "aumcinf.pred",
  "aumcint.last", "aumcint.all", "aumcint.inf.obs", "aumcint.inf.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",
  "cl.int.last", "cl.int.all", "cl.int.inf.obs", "cl.int.inf.pred",
  # Volume (apparent)
  "vz.obs", "vz.pred", "vz.last", "vz.all",
  "vz.int.last", "vz.int.all", "vz.int.inf.obs", "vz.int.inf.pred",
  "vss.obs", "vss.pred", "vss.last", "vss.all",
  "vss.int.last", "vss.int.all", "vss.int.inf.obs", "vss.int.inf.pred",
  # MRT
  "mrt.last", "mrt.obs", "mrt.pred", "mrt.all",
  "mrt.int.last", "mrt.int.all", "mrt.int.inf.obs", "mrt.int.inf.pred",
  # Effective half-life / kel
  "thalf.eff.last", "thalf.eff.obs", "thalf.eff.pred",
  "kel.obs", "kel.pred", "kel.last", "kel.all",
  "kel.int.last", "kel.int.all", "kel.int.inf.obs", "kel.int.inf.pred",
  # Bioavailability (secondary parameters; need a reference interval)
  "f.obs", "f.pred", "f.last", "f.int.last", "f.int.all", "f.int.obs", "f.int.pred",
  # 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.inf.obs"      "aucint.inf.pred"     "aucint.last"        
[10] "auclast"             "auclast.dn"          "aucpext.obs"        
[13] "aucpext.pred"        "aumcall"             "aumcinf.obs"        
[16] "aumcinf.pred"        "aumcint.all"         "aumcint.inf.obs"    
[19] "aumcint.inf.pred"    "aumcint.last"        "aumclast"           
[22] "cl.all"              "cl.int.all"          "cl.int.inf.obs"     
[25] "cl.int.inf.pred"     "cl.int.last"         "cl.last"            
[28] "cl.obs"              "cl.pred"             "clast.obs"          
[31] "clast.obs.dn"        "clast.pred"          "cmax"               
[34] "cmax.dn"             "cmin"                "count_conc"         
[37] "count_conc_measured" "f.int.all"           "f.int.last"         
[40] "f.int.obs"           "f.int.pred"          "f.last"             
[43] "f.obs"               "f.pred"              "half.life"          
[46] "kel.all"             "kel.int.all"         "kel.int.inf.obs"    
[49] "kel.int.inf.pred"    "kel.int.last"        "kel.last"           
[52] "kel.obs"             "kel.pred"            "lambda.z"           
[55] "lambda.z.n.points"   "lambda.z.time.first" "lambda.z.time.last" 
[58] "mrt.all"             "mrt.int.all"         "mrt.int.inf.obs"    
[61] "mrt.int.inf.pred"    "mrt.int.last"        "mrt.last"           
[64] "mrt.obs"             "mrt.pred"            "r.squared"          
[67] "span.ratio"          "tfirst"              "thalf.eff.last"     
[70] "thalf.eff.obs"       "thalf.eff.pred"      "tlag"               
[73] "tlast"               "tmax"                "tmin"               
[76] "vss.all"             "vss.int.all"         "vss.int.inf.obs"    
[79] "vss.int.inf.pred"    "vss.int.last"        "vss.last"           
[82] "vss.obs"             "vss.pred"            "vz.all"             
[85] "vz.int.all"          "vz.int.inf.obs"      "vz.int.inf.pred"    
[88] "vz.int.last"         "vz.last"             "vz.obs"             
[91] "vz.pred"            
# Every name above is registered
setdiff(ev_relevant, names(interval_cols))
character(0)

This list is curated. pknca_parameter_table() (≥ 0.13.0) classifies every registered parameter, including the routes it applies to, so the complete catalog for extravascular dosing is one filter away:

d_param_table <- pknca_parameter_table()
d_ev_params <- d_param_table[grepl("extravascular", d_param_table$route), ]
nrow(d_ev_params)
[1] 166
head(d_ev_params[, c("parameter", "concept", "tier", "sparse", "secondary")])
   parameter concept     tier sparse secondary
1    auclast     auc   common  FALSE     FALSE
2 auclast_se     auc uncommon   TRUE     FALSE
3 auclast_df     auc uncommon   TRUE     FALSE
4     aucall     auc uncommon  FALSE     FALSE
5  aucall_se     auc uncommon   TRUE     FALSE
6  aucall_df     auc uncommon   TRUE     FALSE

The sparse column flags outputs that apply to sparse sampling (such as the standard error auclast_se), and secondary flags parameters that need a reference interval, such as f.obs.