library(tidyverse)
library(PKNCA)
data(Theoph)
theoph_conc <- as_tibble(Theoph) %>%
transmute(ID = Subject, TIME = Time, CONC = conc)BLQ and Missing Data in NCA: Practical Handling Strategies
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.
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
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
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:
- Keep BLQ flagged explicitly.
- Use BLQ = 0 for pre-dose BLQ unless you have a strong reason not to.
- For post-dose BLQ, compare at least two plausible rules.
- Re-run and check sensitivity.
- Document the choice in the report.
In regulated analyses, you will follow the study SAP or validated standard.
Strategies
- Keep a
BLQflag 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
- 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
- Executable: Change
LLOQto 2.0 and re-run the comparison. What moves the most: AUC last or half-life? - Executable: For one subject, count how many BLQ points exist. Does that subject show larger sensitivity across rules?
- 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.