Terminal Phase Diagnostics and Half-Life Reliability

Evaluate terminal slope estimation (lambda_z), assess half-life reliability, and understand when AUCinf and t1/2 should not be trusted.
Tip

Big idea: Half-life is only as good as the terminal phase used to estimate it. Poor terminal slope selection leads to unreliable \(t_{1/2}\) and \(AUC_{0-\infty}\).

Learning Objectives

By the end of this lesson, you will be able to:

  • Explain how NCA estimates the terminal slope (\(\lambda_z\)).
  • Use simple diagnostics to judge whether half-life is trustworthy.
  • Compute and interpret the extrapolated AUC fraction.
  • Extract terminal-phase parameters from a PKNCAresults object.

Key Ideas

  • \(\lambda_z\) is estimated from the log-linear terminal portion of the profile.
  • \[t_{1/2} = \frac{\ln(2)}{\lambda_z}\]
  • If terminal sampling is weak, \(\lambda_z\) becomes unstable → half-life becomes unstable.
  • \(AUC_{0-\infty}\) depends on extrapolation from the terminal slope; if \(\lambda_z\) is unstable, extrapolated exposure becomes unreliable.
  • Always check diagnostics before reporting half-life or \(AUC_{0-\infty}\).

Setup: Run NCA on Theoph

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)

# Be explicit about outputs to reduce version-to-version surprises
intervals_df <- data.frame(
  start = 0,
  end = Inf,
  cmax = TRUE,
  auclast = TRUE,
  aucinf.obs = TRUE,
  half.life = TRUE
)

nca_data <- PKNCAdata(conc_obj, dose_obj, intervals = intervals_df)
results_obj <- pk.nca(nca_data)
raw_df <- as.data.frame(results_obj)

Step 1: See what parameters you actually got

Different PKNCA versions/settings can label parameters slightly differently. Before you hard-code names, quickly inspect what parameters are available:

sort(unique(raw_df$PPTESTCD))
 [1] "adj.r.squared"       "aucinf.obs"          "auclast"            
 [4] "clast.obs"           "clast.pred"          "cmax"               
 [7] "half.life"           "lambda.z"            "lambda.z.n.points"  
[10] "lambda.z.time.first" "lambda.z.time.last"  "r.squared"          
[13] "span.ratio"          "tlast"               "tmax"               

Keep this in your pocket whenever a parameter “is not found”.


Step 3: Extrapolated AUC fraction

We want:

\[ \text{Extrapolated Fraction} = \frac{AUC_{0-\infty} - AUC_{0-tlast}}{AUC_{0-\infty}} \]

Build a small AUC table

auc_wide <- raw_df %>%
  filter(PPTESTCD %in% c("aucinf.obs", "auclast")) %>%
  select(ID, PPTESTCD, PPORRES) %>%
  pivot_wider(names_from = PPTESTCD, values_from = PPORRES) %>%
  mutate(extrap_fraction = (aucinf.obs - auclast) / aucinf.obs)

auc_wide %>% head()
# A tibble: 6 × 4
  ID    auclast aucinf.obs extrap_fraction
  <ord>   <dbl>      <dbl>           <dbl>
1 6        71.7       82.2          0.128 
2 7        88.0      101.           0.129 
3 8        86.8      102.           0.150 
4 11       77.9       86.9          0.104 
5 3        95.9      106.           0.0966
6 2        88.7       97.4          0.0888
Tip

Interpretation: Extrapolated fraction reflects how much of the total exposure estimate depends on extrapolation beyond the last measured sample.

A common rule of thumb is to treat extrapolated fractions above ~20% as a caution flag (context-dependent).


Step 4: Terminal-fit quality

In this PKNCA output, terminal-fit diagnostics appear as:

  • r.squared
  • adj.r.squared
  • lambda.z.n.points
  • lambda.z.time.first
  • lambda.z.time.last

We can assemble them into a subject-level diagnostic table:

lambda_diag <- raw_df %>%
  filter(PPTESTCD %in% c(
    "lambda.z",
    "r.squared",
    "adj.r.squared",
    "lambda.z.n.points",
    "lambda.z.time.first",
    "lambda.z.time.last",
    "half.life"
  )) %>%
  select(ID, PPTESTCD, PPORRES) %>%
  pivot_wider(names_from = PPTESTCD, values_from = PPORRES)

lambda_diag %>%
  select(
    ID, lambda.z, r.squared, adj.r.squared,
    lambda.z.n.points, half.life
  ) %>%
  head(12)
# A tibble: 12 × 6
   ID    lambda.z r.squared adj.r.squared lambda.z.n.points half.life
   <ord>    <dbl>     <dbl>         <dbl>             <dbl>     <dbl>
 1 6       0.0878     0.998         0.998                 7      7.89
 2 7       0.0883     0.999         0.998                 4      7.85
 3 8       0.0815     0.991         0.989                 6      8.51
 4 11      0.0955     1.000         1.000                 3      7.26
 5 3       0.102      0.999         0.999                 3      6.77
 6 2       0.104      0.997         0.996                 4      6.66
 7 4       0.0993     0.999         0.998                 3      6.98
 8 9       0.0825     0.999         0.999                 3      8.41
 9 12      0.110      0.999         0.999                 3      6.29
10 10      0.0750     1.000         0.999                 3      9.25
11 1       0.0485     1.000         1.000                 3     14.3 
12 5       0.0866     0.999         0.998                 4      8.00

These diagnostics help answer practical questions:

  • Was the terminal regression well fit?
  • How many points defined the terminal phase?
  • Which portion of the curve was used?

When Half-Life Should Not Be Trusted

  • Too few terminal points
  • Large extrapolated AUC fraction
  • No clear log-linear decline
  • Terminal samples too sparse or too noisy

NCA does not “fix” poor sampling design.


Strategies

  • Always compute and inspect extrapolated fraction.
  • Check terminal-fit diagnostics such as \(R^2\) and number of points.
  • Treat half-life as a data-quality-dependent summary.
  • Report context: sampling window, terminal points, extrapolation fraction.

Common Mistakes

Warning
  • Reporting \(AUC_{0-\infty}\) when extrapolated fraction is large.
  • Ignoring terminal fit quality.
  • Assuming automated selection is always correct.
  • Treating half-life as more precise than sampling allows.

Practice Problems

  1. Executable: List subjects with extrapolated fraction > 0.2.
  2. Executable: Identify the top 5 largest half-life values.
  3. Conceptual: Why can sparse terminal sampling inflate half-life?

1. Extrapolated fraction > 0.2:

auc_wide %>%
  filter(!is.na(extrap_fraction), extrap_fraction > 0.2)
# A tibble: 1 × 4
  ID    auclast aucinf.obs extrap_fraction
  <ord>   <dbl>      <dbl>           <dbl>
1 1        147.       215.           0.315

2. Largest half-life values:

raw_df %>%
  filter(PPTESTCD == "half.life") %>%
  arrange(desc(PPORRES)) %>%
  slice_head(n = 5)
# A tibble: 5 × 6
  ID    start   end PPTESTCD  PPORRES exclude
  <ord> <dbl> <dbl> <chr>       <dbl> <chr>  
1 1         0   Inf half.life   14.3  <NA>   
2 10        0   Inf half.life    9.25 <NA>   
3 8         0   Inf half.life    8.51 <NA>   
4 9         0   Inf half.life    8.41 <NA>   
5 5         0   Inf half.life    8.00 <NA>   

3. Conceptual reason:
Sparse terminal points make the log-linear regression unstable. Small changes in which points are included can cause large swings in \(\lambda_z\), which inflates or deflates half-life.


Summary

Half-life reliability depends on:

  • Adequate terminal sampling
  • Reasonable extrapolated AUC fraction
  • Acceptable terminal-fit diagnostics

Never report \(t_{1/2}\) or \(AUC_{0-\infty}\) without reviewing diagnostics.


  • Start by checking unique(PPTESTCD) when something “is not found”.
  • Extrapolated fraction is often your most practical reliability flag.
  • Half-life is not a constant — it is an estimate that depends on data quality.
  • Diagnostics first, reporting second.