BLQ and Missing Data in NCA: Practical Handling Strategies

Learn practical, transparent approaches for BLQ (below quantification limit) and missing concentration values in NCA workflows, and document assumptions clearly.
Tip

Big idea: BLQ handling is not a “data cleaning detail.” It changes AUC, terminal slope, and half-life. In NCA, you must choose a rule, apply it consistently, and document it.

Learning Objectives

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

  • Describe common BLQ handling rules used in NCA.
  • Distinguish BLQ from truly missing values (NA).
  • Apply simple BLQ handling strategies in a transparent, reproducible way.
  • Understand how BLQ choices affect \(AUC_{0-tlast}\), \(AUC_{0-\infty}\), and \(t_{1/2}\).

Key Ideas

  • BLQ is information (“measured, but below the assay’s quantification limit”), not the same as missing.
  • Common BLQ rules (choose one per analysis and document it):
    • BLQ = 0 (often early timepoints, especially pre-dose)
    • BLQ = LLOQ/2 (simple substitution; not always defensible)
    • BLQ = NA (drop BLQ observations; can bias terminal slope)
    • M1 / M3-type methods are modeling approaches (beyond NCA; mention only)
  • BLQ near the terminal phase can strongly affect \(\lambda_z\) and half-life.
  • If you use log trapezoids, you must avoid non-positive concentrations on the declining phase.

Setup: Start from Theoph (Then Create a BLQ Scenario)

Theoph does not naturally contain BLQ flags, so we’ll simulate a BLQ-like scenario for teaching.

library(tidyverse)
library(PKNCA)

data(Theoph)

theoph_conc <- as_tibble(Theoph) %>%
  transmute(ID = Subject, TIME = Time, CONC = conc)

Let’s define a hypothetical LLOQ and label BLQ:

LLOQ <- 1.0  # hypothetical concentration unit

theoph_blq <- theoph_conc %>%
  mutate(
    BLQ = CONC < LLOQ
  )

theoph_blq %>%
  arrange(ID, TIME) %>%
  head(12)
# A tibble: 12 × 4
   ID     TIME  CONC BLQ  
   <ord> <dbl> <dbl> <lgl>
 1 6      0     0    TRUE 
 2 6      0.27  1.29 FALSE
 3 6      0.58  3.08 FALSE
 4 6      1.15  6.44 FALSE
 5 6      2.03  6.32 FALSE
 6 6      3.57  5.53 FALSE
 7 6      5     4.94 FALSE
 8 6      7     4.02 FALSE
 9 6      9.22  3.46 FALSE
10 6     12.1   2.78 FALSE
11 6     23.8   0.92 TRUE 
12 7      0     0.15 TRUE 

BLQ vs Missing (NA)

  • BLQ: you have a measurement attempt; value is below quantification.
  • NA: value is missing (no usable measurement recorded).

You should keep BLQ status explicit (a BLQ flag or a CENS flag), even if you later apply a substitution.


Three Simple BLQ Handling Rules (for NCA)

We’ll create three derived concentration columns to illustrate the impact.

For teaching, the LLOQ/2 rule below keeps pre-dose BLQ at 0 and uses LLOQ/2 only for post-dose BLQ. That is usually more sensible than assigning a nonzero concentration before dosing.

theoph_blq_rules <- theoph_blq %>%
  mutate(
    CONC_zero = if_else(BLQ, 0, CONC),
    CONC_half = case_when(
      BLQ & TIME == 0 ~ 0,
      BLQ ~ LLOQ / 2,
      TRUE ~ CONC
    ),
    CONC_na = if_else(BLQ, NA_real_, CONC)
  )

theoph_blq_rules %>%
  select(ID, TIME, CONC, BLQ, CONC_zero, CONC_half, CONC_na) %>%
  head(12)
# A tibble: 12 × 7
   ID     TIME  CONC BLQ   CONC_zero CONC_half CONC_na
   <ord> <dbl> <dbl> <lgl>     <dbl>     <dbl>   <dbl>
 1 1      0     0.74 TRUE       0         0      NA   
 2 1      0.25  2.84 FALSE      2.84      2.84    2.84
 3 1      0.57  6.57 FALSE      6.57      6.57    6.57
 4 1      1.12 10.5  FALSE     10.5      10.5    10.5 
 5 1      2.02  9.66 FALSE      9.66      9.66    9.66
 6 1      3.82  8.58 FALSE      8.58      8.58    8.58
 7 1      5.1   8.36 FALSE      8.36      8.36    8.36
 8 1      7.03  7.47 FALSE      7.47      7.47    7.47
 9 1      9.05  6.89 FALSE      6.89      6.89    6.89
10 1     12.1   5.94 FALSE      5.94      5.94    5.94
11 1     24.4   3.28 FALSE      3.28      3.28    3.28
12 2      0     0    TRUE       0         0      NA   
Warning

There is no universally “correct” BLQ rule for NCA. The correct choice is the one you can justify for your context and apply consistently.


A Reusable Helper: Run NCA for a Chosen Concentration Column

To compare strategies, we’ll define a small helper that runs NCA given a concentration column name.

run_nca <- function(conc_data, conc_col) {
  conc_tbl <- conc_data %>%
    transmute(
      ID = ID,
      TIME = TIME,
      CONC = .data[[conc_col]]
    )

  dose_tbl <- as_tibble(Theoph) %>%
    distinct(Subject, Dose) %>%
    transmute(ID = Subject, TIME = 0, DOSE = Dose)

  conc_obj <- PKNCAconc(CONC ~ TIME | ID, data = conc_tbl)
  dose_obj <- PKNCAdose(DOSE ~ TIME | ID, data = dose_tbl)

  intervals_df <- data.frame(
    start = 0,
    end = Inf,
    auclast = TRUE,
    aucinf.obs = TRUE,
    half.life = TRUE
  )

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

Compare BLQ Rules (Focus on AUC + Half-Life)

Run NCA under each rule:

raw_zero <- run_nca(theoph_blq_rules, "CONC_zero")
raw_half <- run_nca(theoph_blq_rules, "CONC_half")
raw_na   <- run_nca(theoph_blq_rules, "CONC_na")

Extract a compact comparison table:

extract_key <- function(raw_df) {
  raw_df %>%
    filter(PPTESTCD %in% c("auclast", "aucinf.obs", "half.life")) %>%
    select(ID, PPTESTCD, PPORRES) %>%
    pivot_wider(names_from = PPTESTCD, values_from = PPORRES) %>%
    select(ID, auclast, aucinf.obs, half.life)
}

k_zero <- extract_key(raw_zero) %>% mutate(rule = "BLQ=0")
k_half <- extract_key(raw_half) %>% mutate(rule = "Pre-dose BLQ=0; post-dose BLQ=LLOQ/2")
k_na   <- extract_key(raw_na)   %>% mutate(rule = "BLQ=NA (drop)")

compare_tbl <- bind_rows(k_zero, k_half, k_na) %>%
  arrange(ID, rule)

compare_tbl %>% head(18)
# A tibble: 18 × 5
   ID    auclast aucinf.obs half.life rule                                
   <ord>   <dbl>      <dbl>     <dbl> <chr>                               
 1 6        51.9       90.3      9.56 BLQ=0                               
 2 6        NA         NA        9.56 BLQ=NA (drop)                       
 3 6        67.5       71.2      5.09 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
 4 7        87.7      101.       7.85 BLQ=0                               
 5 7        NA         NA        7.85 BLQ=NA (drop)                       
 6 7        87.9      101.       7.85 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
 7 8        86.8      102.       8.51 BLQ=0                               
 8 8        NA         NA        8.51 BLQ=NA (drop)                       
 9 8        86.8      102.       8.51 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
10 11       58.7       86.0      7.03 BLQ=0                               
11 11       NA         NA        7.03 BLQ=NA (drop)                       
12 11       74.3       78.0      5.17 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
13 3        95.9      106.       6.77 BLQ=0                               
14 3        NA         NA        6.77 BLQ=NA (drop)                       
15 3        95.9      106.       6.77 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
16 2        67.2       92.5      5.81 BLQ=0                               
17 2        NA         NA        5.81 BLQ=NA (drop)                       
18 2        84.4       87.9      4.79 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
Note

Why can half-life be identical under BLQ = 0 and BLQ = NA?

Half-life is estimated from a log-linear regression of the terminal phase, not from the entire concentration-time profile.

A value of 0 cannot be used on the log scale, and NA values are also excluded. If BLQ handling produces either 0 or NA for the same terminal observation, that point is removed from the regression in both cases.

If the remaining positive terminal points are identical, then:

  • \(\lambda_z\) is unchanged
  • \(t_{1/2}\) is unchanged

Why are AUC values sometimes NA under BLQ = NA?

AUC calculations require a continuous concentration-time profile to integrate across intervals. If BLQ values are converted to NA, some segments of the curve cannot be integrated, so PKNCA may return NA for \(AUC_{0-tlast}\) or \(AUC_{0-\infty}\).

This illustrates an important difference:

  • Half-life depends only on the terminal regression points.
  • AUC depends on the entire concentration-time profile.

What Typically Changes (and Why)

  • AUC to last changes because early/low concentrations contribute area.
  • AUCinf changes because extrapolation depends on the terminal slope.
  • Half-life can change dramatically if BLQ values are near the terminal phase (or if dropping points leaves too few terminal points).

If half.life becomes NA under a rule, that’s often a warning sign:

  • insufficient terminal points
  • poor terminal fit
  • too many dropped values

A Practical Workflow Recommendation

For teaching and many exploratory workflows, a reasonable approach is:

  1. Keep BLQ flagged explicitly.
  2. Use BLQ = 0 for pre-dose BLQ unless you have a strong reason not to.
  3. For post-dose BLQ, compare at least two plausible rules.
  4. Re-run and check sensitivity.
  5. Document the choice in the report.

In regulated analyses, you will follow the study SAP or validated standard.


Strategies

  • Keep a BLQ flag even after substitution.
  • Run a small sensitivity check (two plausible rules) if decisions depend on AUC.
  • If terminal-phase BLQ are common, be very cautious with \(t_{1/2}\) and \(AUC_{0-\infty}\).
  • Always document LLOQ and the BLQ rule used.

Common Mistakes

Warning
  • Reporting half-life without checking whether BLQ affected terminal points.
  • Using log-based methods with zeros created by BLQ=0 substitution.
  • Hiding BLQ handling inside a one-liner with no narrative explanation.
  • Forgetting that BLQ rules are assumptions, not “facts.”

Practice Problems

  1. Executable: Change LLOQ to 2.0 and re-run the comparison. What moves the most: AUC last or half-life?
  2. Executable: For one subject, count how many BLQ points exist. Does that subject show larger sensitivity across rules?
  3. Conceptual: Why is BLQ handling near the terminal phase more consequential than BLQ handling early after dose?

1. Increase LLOQ and compare:

LLOQ <- 2.0

theoph_blq2 <- theoph_conc %>%
  mutate(BLQ = CONC < LLOQ) %>%
  mutate(
    CONC_zero = if_else(BLQ, 0, CONC),
    CONC_half = case_when(
      BLQ & TIME == 0 ~ 0,
      BLQ ~ LLOQ / 2,
      TRUE ~ CONC
    ),
    CONC_na = if_else(BLQ, NA_real_, CONC)
  )

raw_zero2 <- run_nca(theoph_blq2, "CONC_zero")
raw_half2 <- run_nca(theoph_blq2, "CONC_half")
raw_na2   <- run_nca(theoph_blq2, "CONC_na")

bind_rows(
  extract_key(raw_zero2) %>% mutate(rule = "BLQ=0"),
  extract_key(raw_half2) %>% mutate(rule = "Pre-dose BLQ=0; post-dose BLQ=LLOQ/2"),
  extract_key(raw_na2)   %>% mutate(rule = "BLQ=NA (drop)")
) %>% head(18)
# A tibble: 18 × 5
   ID    auclast aucinf.obs half.life rule                                
   <ord>   <dbl>      <dbl>     <dbl> <chr>                               
 1 6        51.6       89.9      9.56 BLQ=0                               
 2 7        61.9      107.       8.92 BLQ=0                               
 3 8        62.8       96.9      7.89 BLQ=0                               
 4 11       58.7       86.0      7.03 BLQ=0                               
 5 3        70.6      120.       9.22 BLQ=0                               
 6 2        66.8       92.0      5.81 BLQ=0                               
 7 4        72.3      133.      10.0  BLQ=0                               
 8 9        58.7      101.       9.36 BLQ=0                               
 9 12       84.7      138.       8.08 BLQ=0                               
10 10      136.       168.       9.25 BLQ=0                               
11 1       147.       215.      14.3  BLQ=0                               
12 5        84.4      129.       7.10 BLQ=0                               
13 6        72.3       84.0      8.11 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
14 7        86.6       96.8      7.11 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
15 8        84.7       95.5      7.48 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
16 11       79.1       90.6      7.95 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
17 3        95.4      105.       6.55 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2
18 2        89.5      100.       7.34 Pre-dose BLQ=0; post-dose BLQ=LLOQ/2

2. Count BLQ points for one subject:

theoph_blq_rules %>%
  filter(ID == 1) %>%
  summarise(n_blq = sum(BLQ), n_total = n())
# A tibble: 1 × 2
  n_blq n_total
  <int>   <int>
1     1      11

Then compare that subject’s rows in compare_tbl.

3. Conceptual:
Terminal-phase points determine \(\lambda_z\). If BLQ handling removes or alters those points, the slope (and therefore \(t_{1/2}\) and extrapolated AUC) can change substantially.


Summary

BLQ handling affects NCA outputs because it changes the curve you integrate and the slope you extrapolate.

  • Keep BLQ flagged explicitly.
  • Choose and justify a rule.
  • Check sensitivity if results matter.
  • Document LLOQ and the rule in your reporting layer.

  • BLQ ≠ missing; keep a BLQ flag.
  • If you change BLQ rules, re-run everything (don’t patch numbers).
  • Terminal BLQ are the main threat to half-life reliability.
  • Always report LLOQ + BLQ rule alongside exposure metrics.