18  Post-Processing Results

18.1 Why post-process?

After pk.nca() you may need to:

  1. Extract results as a tidy data frame for further analysis
  2. Exclude specific results that failed QC (poor half-life fit, protocol deviation)
  3. Normalize parameters by dose (built-in .dn variants)
  4. Summarize with custom statistics, respecting exclusions
  5. Report with the business.* summary helper functions
  6. Inspect which concentrations each λz regression used, and the fitted line

Data-quality checks that run before or alongside exclusion (missing samples, Tmax coverage, input validation) are covered in the Data Quality Checks chapter.

Most of this operates on the PKNCAresults object without rerunning the analysis (requesting .dn parameters is the exception — they are computed by pk.nca()). The package’s own Post-Processing vignette walks the same workflow.


18.2 Example data and analysis

Every section below post-processes the same PKNCAresults object, built from the Theoph dataset. The interval requests the headline parameters plus the regression- and extrapolation-quality metrics that the exclusion demos rely on (span.ratio, adj.r.squared, aucpext.obs):

d_conc <- as.data.frame(Theoph) |> rename(time = Time, subject = Subject)
d_dose <- Theoph |> as.data.frame() |>
  group_by(Subject) |>
  summarise(dose = Dose[1] * Wt[1], weight = Wt[1], .groups = "drop") |>
  rename(subject = Subject) |>
  mutate(time = 0)

o_conc <- PKNCAconc(d_conc, conc ~ time | subject)
o_dose <- PKNCAdose(d_dose, dose ~ time | subject, route = "extravascular")
o_data <- PKNCAdata(o_conc, o_dose, intervals = data.frame(
  start = 0, end = Inf,
  auclast = TRUE, aucinf.obs = TRUE, cmax = TRUE,
  half.life = TRUE, span.ratio = TRUE, adj.r.squared = TRUE,
  aucpext.obs = TRUE
))
o_nca <- pk.nca(o_data)

18.3 Extracting results

as.data.frame() returns the full tidy result including excluded rows:

df <- as.data.frame(o_nca)

# Columns:
# subject    — group identifier
# start/end  — interval boundaries
# PPTESTCD   — parameter code
# PPORRES    — numeric result
# exclude    — NA if valid; character reason if excluded

df |>
  filter(PPTESTCD == "auclast") |>
  select(subject, PPORRES, exclude) |>
  arrange(subject)
# A tibble: 12 × 3
   subject PPORRES exclude
   <ord>     <dbl> <chr>  
 1 6          71.7 <NA>   
 2 7          88.0 <NA>   
 3 8          86.8 <NA>   
 4 11         77.9 <NA>   
 5 3          95.9 <NA>   
 6 2          88.7 <NA>   
 7 4         103.  <NA>   
 8 9          83.9 <NA>   
 9 12        115.  <NA>   
10 10        136.  <NA>   
11 1         147.  <NA>   
12 5         118.  <NA>   

18.4 Excluding results

18.4.1 Manual exclusion with exclude()

exclude() takes your results object plus a reason string and either a logical mask or a rule FUN. A manual mask excludes exactly the rows you flag — it does not propagate to other parameters. Rule-based exclusion (next section) does propagate: each rule excludes its affected parameters and the parameters that depend on them.

# Exclude subject 3's half-life manually (e.g. failed QC review)
o_nca_excl <- exclude(
  o_nca,
  reason = "half-life failed manual QC",
  mask   = as.data.frame(o_nca)$subject == "3" &
           as.data.frame(o_nca)$PPTESTCD == "half.life"
)

# Only the masked half.life row is excluded
as.data.frame(o_nca_excl) |>
  filter(subject == "3", !is.na(exclude)) |>
  select(subject, PPTESTCD, PPORRES, exclude)
# A tibble: 1 × 4
  subject PPTESTCD  PPORRES exclude                   
  <ord>   <chr>       <dbl> <chr>                     
1 3       half.life    6.77 half-life failed manual QC

The function given as FUN (next section) is called once per group of results and must return either one value or one value per result row of the group; anything else is the pknca_error_exclude_fun_length error (≥ 0.13.0). A warning that the function signals reaches the caller with its class and fields intact (≥ 0.13.0), so it can be caught with withCallingHandlers().

18.4.2 Rule-based exclusion functions

PKNCA provides ready-made rule functions to pass to exclude(FUN=...). Unlike a manual mask, rule-based exclusion propagates: each rule excludes the parameters it affects (for the half-life rules, the whole λz family) and the parameters that depend on them, such as aucinf.obs and aucpext.obs. The built-in rules also supply their own reason text, which overrides the reason argument in the exclude column.

Function Excludes when…
exclude_nca_span.ratio(min.span.ratio) span.ratio < min.span.ratio (default from PKNCA.options("min.span.ratio"))
exclude_nca_min.hl.r.squared(min.hl.r.squared) half-life unadjusted R² < min.hl.r.squared (default from PKNCA.options("min.hl.r.squared"))
exclude_nca_min.hl.adj.r.squared(min.hl.adj.r.squared = 0.9) half-life adjusted R² < min.hl.adj.r.squared (stricter: adjusted for the number of points)
exclude_nca_max.aucinf.pext(max.aucinf.pext) %AUCextrap > max.aucinf.pext (default from PKNCA.options("max.aucinf.pext"))
exclude_nca_tmax_0() Tmax at or before 0 (tmax <= 0); excludes every parameter of the interval. Use this, not exclude_nca_tmax_early(0), to catch a Tmax of exactly 0
exclude_nca_tmax_early(tmax_early = 0) Tmax strictly before tmax_early (tmax < tmax_early); excludes every parameter of the interval. With the default 0, only a negative Tmax is excluded
exclude_nca_count_conc_measured(min_count, exclude_param_pattern = ...) fewer than min_count measured concentrations; excludes the AUC-family parameters matching exclude_param_pattern
exclude_nca_by_param(parameter, min_thr = NULL, max_thr = NULL, affected_parameters = parameter) (≥ 0.12.1) the value of parameter falls outside [min_thr, max_thr]; excludes affected_parameters (default: parameter itself)
exclude_nca_tmax_coverage(min_subjects = 4, k_warn = 1.5, k_exclude = 3) (≥ 0.13.0) the subject has no sample within Tukey’s outer fences around its group’s Tmax values; excludes every result of the interval (subjects outside only the inner fences get a warning)

pknca_exclude_rules() (≥ 0.13.0) lists these rules with their arguments, defaults, the options they use, and the parameters each can exclude. The Tmax-coverage rule and the rule listing are demonstrated in the Data Quality Checks chapter.

# Exclude half-lives where span ratio < 2
o_nca_span <- exclude(
  o_nca,
  reason = "span.ratio < 2",
  FUN    = exclude_nca_span.ratio(2)
)

as.data.frame(o_nca_span) |>
  filter(PPTESTCD == "half.life") |>
  select(subject, PPORRES, exclude) |>
  arrange(subject)
# A tibble: 12 × 3
   subject PPORRES exclude       
   <ord>     <dbl> <chr>         
 1 6          7.89 <NA>          
 2 7          7.85 <NA>          
 3 8          8.51 <NA>          
 4 11         7.26 <NA>          
 5 3          6.77 <NA>          
 6 2          6.66 <NA>          
 7 4          6.98 <NA>          
 8 9          8.41 span.ratio < 2
 9 12         6.29 <NA>          
10 10         9.25 span.ratio < 2
11 1         14.3  span.ratio < 2
12 5          8.00 <NA>          
# Propagation: subject 9 fails the span-ratio rule, so the entire lambda.z
# family and the parameters that depend on it are excluded together
as.data.frame(o_nca_span) |>
  filter(subject == "9", !is.na(exclude)) |>
  select(subject, PPTESTCD, exclude)
# A tibble: 12 × 3
   subject PPTESTCD            exclude       
   <ord>   <chr>               <chr>         
 1 9       lambda.z            span.ratio < 2
 2 9       r.squared           span.ratio < 2
 3 9       adj.r.squared       span.ratio < 2
 4 9       lambda.z.corrxy     span.ratio < 2
 5 9       lambda.z.time.first span.ratio < 2
 6 9       lambda.z.time.last  span.ratio < 2
 7 9       lambda.z.n.points   span.ratio < 2
 8 9       clast.pred          span.ratio < 2
 9 9       half.life           span.ratio < 2
10 9       span.ratio          span.ratio < 2
11 9       aucinf.obs          span.ratio < 2
12 9       aucpext.obs         span.ratio < 2
# Chain multiple exclusion rules
o_nca_multi <- o_nca |>
  exclude(reason = "span.ratio < 2",    FUN = exclude_nca_span.ratio(2)) |>
  exclude(reason = "%AUCextrap > 20%",  FUN = exclude_nca_max.aucinf.pext(20))

as.data.frame(o_nca_multi) |>
  filter(!is.na(exclude)) |>
  select(subject, PPTESTCD, exclude) |>
  arrange(subject, PPTESTCD)
# A tibble: 36 × 3
   subject PPTESTCD            exclude       
   <ord>   <chr>               <chr>         
 1 9       adj.r.squared       span.ratio < 2
 2 9       aucinf.obs          span.ratio < 2
 3 9       aucpext.obs         span.ratio < 2
 4 9       clast.pred          span.ratio < 2
 5 9       half.life           span.ratio < 2
 6 9       lambda.z            span.ratio < 2
 7 9       lambda.z.corrxy     span.ratio < 2
 8 9       lambda.z.n.points   span.ratio < 2
 9 9       lambda.z.time.first span.ratio < 2
10 9       lambda.z.time.last  span.ratio < 2
# ℹ 26 more rows

18.4.3 Exclusions are respected in summary

Excluded values are dropped from the summary statistics: the parameters affected by the span-ratio rule are summarized over fewer subjects, and the caption defines the resulting n= annotation as the number of measurements included in the summary.

summary(o_nca_span)
 start end  N     auclast        cmax         half.life        adj.r.squared
     0 Inf 12 98.7 [22.5] 8.65 [17.0] 7.36 [0.742], n=9 0.997 [0.00331], n=9
       span.ratio      aucinf.obs      aucpext.obs
 2.34 [9.28], n=9 105 [16.4], n=9 11.3 [2.28], n=9

Caption: auclast, cmax, span.ratio, aucinf.obs: geometric mean and geometric coefficient of variation; half.life, adj.r.squared, aucpext.obs: arithmetic mean and standard deviation; N: number of subjects; n: number of measurements included in summary

18.5 Dose-normalized parameters

PKNCA has built-in dose-normalized variants for several parameters. Request them in the interval — they are automatically computed as parameter / dose.

All available dose-normalized parameters, retrieved dynamically from the interval-column registry (get.interval.cols()):

cols <- get.interval.cols()
dn_params <- names(cols)[endsWith(names(cols), ".dn")]
data.frame(
  parameter       = dn_params,
  description     = sapply(cols[dn_params], function(x) x$desc),
  normalized_from = sub("\\.dn$", "", dn_params)
) |> knitr::kable()
parameter description normalized_from
auclast.dn auclast.dn Dose-normalized auclast auclast
aucall.dn aucall.dn Dose-normalized aucall aucall
aucinf.obs.dn aucinf.obs.dn Dose-normalized aucinf.obs aucinf.obs
aucinf.pred.dn aucinf.pred.dn Dose-normalized aucinf.pred aucinf.pred
aumclast.dn aumclast.dn Dose-normalized aumclast aumclast
aumcall.dn aumcall.dn Dose-normalized aumcall aumcall
aumcinf.obs.dn aumcinf.obs.dn Dose-normalized aumcinf.obs aumcinf.obs
aumcinf.pred.dn aumcinf.pred.dn Dose-normalized aumcinf.pred aumcinf.pred
cmax.dn cmax.dn Dose-normalized cmax cmax
cmin.dn cmin.dn Dose-normalized cmin cmin
clast.obs.dn clast.obs.dn Dose-normalized clast.obs clast.obs
clast.pred.dn clast.pred.dn Dose-normalized clast.pred clast.pred
cav.dn cav.dn Dose-normalized cav cav
ctrough.dn ctrough.dn Dose-normalized ctrough ctrough
clr.last.dn clr.last.dn Dose-normalized clr.last clr.last
clr.obs.dn clr.obs.dn Dose-normalized clr.obs clr.obs
clr.pred.dn clr.pred.dn Dose-normalized clr.pred clr.pred

Each .dn parameter is computed as parameter / dose where dose is the dose from the PKNCAdose object for that interval. In PKNCA ≥ 0.13.0 this includes renal clearance variants (clr.last.dn, clr.obs.dn, clr.pred.dn).

Request the .dn variants in the interval like any other parameter:

dn_interval <- data.frame(
  start           = 0,
  end             = Inf,
  auclast         = TRUE,
  auclast.dn      = TRUE,   # AUClast / dose
  aucinf.obs      = TRUE,
  aucinf.obs.dn   = TRUE,   # AUCinf.obs / dose
  aucinf.pred.dn  = TRUE,   # AUCinf.pred / dose
  cmax            = TRUE,
  cmax.dn         = TRUE,   # Cmax / dose
  cmin.dn         = TRUE,   # Cmin / dose
  aumclast.dn     = TRUE,   # AUMClast / dose
  clast.obs.dn    = TRUE,   # Clast.obs / dose
  cav.dn          = TRUE,   # Cav / dose
  half.life       = TRUE    # needed for aucinf.pred
)

o_nca_dn <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = dn_interval))

as.data.frame(o_nca_dn) |>
  filter(PPTESTCD %in% c("auclast", "auclast.dn", "cmax", "cmax.dn",
                          "aucinf.obs", "aucinf.obs.dn")) |>
  select(subject, PPTESTCD, PPORRES) |>
  tidyr::pivot_wider(names_from = PPTESTCD, values_from = PPORRES) |>
  arrange(subject)
# A tibble: 12 × 7
   subject auclast  cmax aucinf.obs auclast.dn aucinf.obs.dn cmax.dn
   <ord>     <dbl> <dbl>      <dbl>      <dbl>         <dbl>   <dbl>
 1 6          71.7  6.44       82.2      0.224         0.257  0.0201
 2 7          88.0  7.09      101.       0.275         0.316  0.0222
 3 8          86.8  7.56      102.       0.272         0.320  0.0237
 4 11         77.9  8          86.9      0.244         0.272  0.0250
 5 3          95.9  8.2       106.       0.300         0.332  0.0257
 6 2          88.7  8.33       97.4      0.279         0.306  0.0261
 7 4         103.   8.6       114.       0.321         0.357  0.0269
 8 9          83.9  9.03       97.5      0.313         0.364  0.0337
 9 12        115.   9.75      126.       0.359         0.392  0.0304
10 10        136.  10.2       168.       0.424         0.524  0.0319
11 1         147.  10.5       215.       0.460         0.672  0.0328
12 5         118.  11.4       136.       0.369         0.426  0.0356

18.5.1 Custom normalization with normalize() (≥ 0.13.0)

For normalization by columns other than dose (e.g. body weight, body surface area), PKNCA 0.13.0 adds normalize(). You supply a norm_table data frame containing the grouping columns, a normalization value, and a unit for each group. With grouped data, every group of the results must be in norm_table, so no result rows are dropped silently; with ungrouped data, norm_table must have exactly one row.

# Normalize AUClast and Cmax by body weight
norm_tbl <- as.data.frame(Theoph) |>
  group_by(Subject) |>
  summarise(normalization = Wt[1], unit = "kg", .groups = "drop") |>
  rename(subject = Subject)

df_results <- as.data.frame(o_nca)
df_bw_norm <- normalize(
  df_results,
  norm_table  = norm_tbl,
  parameters  = c("auclast", "cmax"),
  suffix      = ".bw"
)

df_bw_norm |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD) |>
  head(8)
# A tibble: 8 × 3
  subject PPTESTCD   PPORRES
  <ord>   <chr>        <dbl>
1 6       auclast.bw  0.896 
2 6       cmax.bw     0.0805
3 7       auclast.bw  1.36  
4 7       cmax.bw     0.110 
5 8       auclast.bw  1.23  
6 8       cmax.bw     0.107 
7 11      auclast.bw  1.20  
8 11      cmax.bw     0.123 

normalize() returns only the normalized rows, under new parameter names — auclast becomes auclast.bw, cmax becomes cmax.bw. To keep both original and normalized values, bind the normalized rows onto the original results: bind_rows(df_results, df_bw_norm).

normalize() also accepts the PKNCAresults object itself. It then adds the normalized rows to the results and returns a PKNCAresults object holding both the original and the normalized parameters (marked as modified; see the provenance note in the Regulatory chapter).

18.5.2 Normalizing by a column of the concentration data (≥ 0.13.0)

When the normalizing value is already a column of the concentration data, normalize_by_col() builds the normalization table from it. It takes a PKNCAresults object, the column name, and the unit (a fixed string or the name of a unit column), and returns a PKNCAresults object with the normalized parameters added. The Theoph concentration data carry body weight in Wt:

o_nca_bw <- normalize_by_col(
  o_nca,
  col        = "Wt",
  unit       = "kg",
  parameters = c("auclast", "cmax"),
  suffix     = ".bw"
)

as.data.frame(o_nca_bw) |>
  filter(subject %in% c("1", "2"), PPTESTCD %in% c("auclast", "auclast.bw")) |>
  select(subject, PPTESTCD, PPORRES) |>
  arrange(subject, PPTESTCD)
# A tibble: 4 × 3
  subject PPTESTCD   PPORRES
  <ord>   <chr>        <dbl>
1 2       auclast      88.7 
2 2       auclast.bw    1.23
3 1       auclast     147.  
4 1       auclast.bw    1.85

Subject 1 weighs 79.6 kg, so its auclast.bw is its auclast divided by 79.6. The column must have one value per concentration group; a column whose value changes within a group (for example, a weight recorded at several visits within one profile) is the pknca_error_norm_multiple_values error.


18.6 Filtering and grouping results

dplyr verbs work on PKNCAresults objects. filter() with expressions that use only group columns (the columns after the | in the PKNCAconc() formula) keeps the concentration data, dose data, and intervals in $data consistent with the remaining results (≥ 0.13.0); filters on other result columns (PPTESTCD, PPORRES, exclude) filter only the result table:

o_nca_12 <- filter(o_nca, subject %in% c("1", "2"))
nrow(as.data.frame(o_nca_12))
[1] 34
unique(as.character(o_nca_12$data$conc$data$subject))
[1] "1" "2"

group_vars() on a PKNCAresults object includes the interval start and end with the group columns (≥ 0.13.0), so code that groups or joins results by group_vars() also matches on the interval:

group_vars(o_nca)
[1] "start"   "end"     "subject"

A results object changed by filter(), mutate(), exclude(), or normalize() no longer passes checkProvenance(); see the Regulatory chapter.


18.7 Controlling the summary table

summary() arguments control which groups and parameters appear and how gaps are shown:

Argument Default Effect
drop_group the subject column Group columns summarized over (removed from the rows of the table)
drop_param none Parameters left out of the table
summarize_n NA (add N when there is a subject column) Add a column N with the number of subjects
not_requested "." Text for a parameter not requested in an interval
not_calculated "NC" Text for a requested parameter whose summary is NA
summary(o_nca, drop_param = c("adj.r.squared", "span.ratio"), summarize_n = FALSE)
 start end     auclast        cmax   half.life aucinf.obs aucpext.obs
     0 Inf 98.7 [22.5] 8.65 [17.0] 8.18 [2.12] 115 [28.4] 13.8 [6.34]

Caption: auclast, cmax, aucinf.obs: geometric mean and geometric coefficient of variation; half.life, aucpext.obs: arithmetic mean and standard deviation

When the summary shows the not-calculated text, the caption explains it (≥ 0.13.0). Here, a span-ratio threshold of 20 excludes every half-life, so the λz-dependent parameters are not calculated:

summary(exclude(o_nca, FUN = exclude_nca_span.ratio(20)))
 start end  N     auclast        cmax half.life adj.r.squared span.ratio
     0 Inf 12 98.7 [22.5] 8.65 [17.0]        NC            NC         NC
 aucinf.obs aucpext.obs
         NC          NC

Caption: auclast, cmax, span.ratio, aucinf.obs: geometric mean and geometric coefficient of variation; half.life, adj.r.squared, aucpext.obs: arithmetic mean and standard deviation; N: number of subjects; NC: not calculated

18.8 Custom summary statistics

PKNCA.set.summary() controls how each parameter is presented in summary().

# Arithmetic mean ± SD for AUClast
PKNCA.set.summary(
  "auclast",
  description = "mean ± SD",
  point  = mean,
  spread = sd
)

# Median [min, max] for Tmax
# spread must return a 2-element numeric vector
PKNCA.set.summary(
  "tmax",
  description = "median [min, max]",
  point  = median,
  spread = function(x) c(min(x), max(x))
)

summary(o_nca)
 start end  N    auclast        cmax   half.life   adj.r.squared  span.ratio
     0 Inf 12 101 [23.5] 8.65 [17.0] 8.18 [2.12] 0.998 [0.00301] 2.07 [26.2]
 aucinf.obs aucpext.obs
 115 [28.4] 13.8 [6.34]

Caption: auclast: mean ± SD; cmax, span.ratio, aucinf.obs: geometric mean and geometric coefficient of variation; half.life, adj.r.squared, aucpext.obs: arithmetic mean and standard deviation; N: number of subjects

PKNCA.set.summary() also has a spread_for argument (≥ 0.13.0) that summarizes one parameter with the spread taken from another parameter’s results; PKNCA uses it to show a sparse AUClast as the estimate with its standard error (see the Sparse NCA chapter). PKNCA.set.summary(reset = TRUE) removes every summary instruction and, since that can break summary(), warns (≥ 0.13.0); restore individual parameters as below instead.

Restore the defaults — business.geomean/business.geocv for auclast and business.median/business.range for tmax (the business.* helpers are covered in the next section):

PKNCA.set.summary(
  "auclast",
  description = "geometric mean and geometric coefficient of variation",
  point  = business.geomean,
  spread = business.geocv
)

PKNCA.set.summary(
  "tmax",
  description = "median and range",
  point  = business.median,
  spread = business.range
)

18.9 Business summary helpers

PKNCA exports the full set of business.* summary functions. These are the defaults summary() uses for most parameters, and they are also available directly for custom reporting pipelines.

Function Returns Description
business.geomean(x) scalar Geometric mean: exp(mean(log(x)))
business.geocv(x) scalar Geometric CV%: sqrt(exp(var(log(x)))−1) × 100
business.mean(x) scalar Arithmetic mean
business.sd(x) scalar Standard deviation
business.cv(x) scalar Coefficient of variation %: sd/mean × 100
business.median(x) scalar Median
business.min(x) scalar Minimum
business.max(x) scalar Maximum
business.range(x) length-2 vector c(min, max)
x <- c(10.2, 11.8, 9.6, 12.1, 10.5)
data.frame(
  statistic = c("geomean", "geocv", "mean", "sd", "cv%", "median", "min", "max"),
  value = c(
    business.geomean(x), business.geocv(x),
    business.mean(x),    business.sd(x),    business.cv(x),
    business.median(x),  business.min(x),   business.max(x)
  )
)
  statistic     value
1   geomean 10.798057
2     geocv  9.859166
3      mean 10.840000
4        sd  1.069112
5       cv%  9.862655
6    median 10.500000
7       min  9.600000
8       max 12.100000

The business.* functions return NA for an empty input (≥ 0.13.0), rather than an error, so a summary group with no included values gives NC instead of stopping:

business.geomean(numeric(0))
[1] NA

Use these as point and spread arguments to PKNCA.set.summary() — rerunning summary() shows the new cmax presentation:

# Arithmetic mean ± CV% for Cmax
PKNCA.set.summary(
  "cmax",
  description = "mean [CV%]",
  point  = business.mean,
  spread = business.cv
)

summary(o_nca)
 start end  N     auclast        cmax   half.life   adj.r.squared  span.ratio
     0 Inf 12 98.7 [22.5] 8.76 [16.8] 8.18 [2.12] 0.998 [0.00301] 2.07 [26.2]
 aucinf.obs aucpext.obs
 115 [28.4] 13.8 [6.34]

Caption: auclast, span.ratio, aucinf.obs: geometric mean and geometric coefficient of variation; cmax: mean [CV%]; half.life, adj.r.squared, aucpext.obs: arithmetic mean and standard deviation; N: number of subjects

18.10 Inspecting the terminal regression window

get_halflife_points() returns a logical vector aligned with the original concentration data — TRUE if that observation was used in the λz regression, FALSE if it was available but excluded, NA if half-life was not computed for that interval. In PKNCA ≥ 0.13.0 it also accepts a PKNCAdata object directly, handles start ≠ 0 intervals correctly, and sets points outside any interval to NA. For how PKNCA selects the regression points, see the Half-Life Calculation vignette.

Because the example analysis requested half.life, o_nca already carries the λz fits:

# Logical vector (TRUE = used in lambda.z fit), aligned with the rows of d_conc
hl_used <- get_halflife_points(o_nca)
table(hl_used, useNA = "always")
hl_used
FALSE  TRUE  <NA> 
   86    46     0 
# >= 0.13.0: a PKNCAdata object is accepted directly (no need to run pk.nca first)
head(get_halflife_points(o_data))
[1] FALSE FALSE FALSE FALSE FALSE FALSE

get_halflife_fit() (≥ 0.13.0) returns the fitted terminal line of each interval (intercept and slope on the natural-log scale, and the first and last times used), and get_halflife_curve() (≥ 0.13.0) returns predicted concentrations along it, by default n = 50 points from the first time used, extrapolated to the end of the interval’s data (extrapolate_later = TRUE):

get_halflife_fit(o_nca) |> filter(subject == "1")
  subject start end intercept     slope time_first time_last
1       1     0 Inf  2.368785 -0.048457       9.05     24.37
d_hl_curve <- get_halflife_curve(o_nca)
head(d_hl_curve |> filter(subject == "1"), 3)
  subject start end     time     conc
1       1     0 Inf 9.050000 6.891228
2       1     0 Inf 9.362653 6.787611
3       1     0 Inf 9.675306 6.685552
# Attach to concentration data for plotting
d_conc_annotated <- d_conc |> mutate(in_lambda_z = hl_used)
subj1_conc <- d_conc_annotated |> filter(subject == "1")

ggplot(subj1_conc, aes(x = time, y = conc, colour = in_lambda_z)) +
  geom_line(colour = "grey70") +
  geom_line(data = d_hl_curve |> filter(subject == "1"), colour = "firebrick",
            linetype = "dashed") +
  geom_point(size = 3) +
  scale_colour_manual(values = c("FALSE" = "grey50", "TRUE" = "firebrick"),
                      labels = c("not used", "used in lambda.z"), name = NULL,
                      na.value = "grey80") +
  scale_y_log10() +
  labs(title = "Subject 1 - points used in lambda.z regression (red) and fitted line",
       x = "Time (h)", y = "Concentration (mg/L) [log scale]") +
  theme_minimal()


18.11 Imputation methods

Imputation is a pre-calculation step, not a post-processing step. It is specified before pk.nca() runs via the impute argument to PKNCAdata() — either a method name applied to every interval, or the name of a column in the intervals data frame holding per-interval methods. See the Concentration Imputation chapter for the built-in strategies, chaining, custom methods, and a side-by-side comparison.