Theoph_df <- as.data.frame(Theoph)
# Remove the t = 0 observation to create a missing-baseline scenario
d_conc_no0 <- Theoph_df |> filter(Time > 0)
d_dose <- Theoph_df |>
filter(Time == 0) |>
mutate(amt = Dose * Wt)
# Without imputation there is no concentration at the interval start
o_conc <- PKNCAconc(d_conc_no0, conc ~ Time | Subject)
o_dose <- PKNCAdose(d_dose, amt ~ Time | Subject)15 Concentration Imputation
15.1 Why imputation is needed
NCA intervals are defined by a start and end time. For AUC and related parameters, PKNCA needs a concentration at the exact interval start. Two common situations require imputation:
- No t = 0 sample. The earliest measured concentration is after dosing, so there is no observed value at the interval start (time 0).
- Pre-dose sample. A concentration was measured before the nominal dose time (e.g. a residual carry-over sample). The interval starts at time 0 but the only nearby observation has a negative relative time.
Imputation is applied just before each individual interval is calculated, on a per-subject, per-interval basis. The original data frame is never modified. The package’s Data Imputation vignette describes the same mechanism from the implementation side.
15.1.1 Example data: Theoph with t = 0 removed
# What does subject 1's profile look like?
d_conc_no0 |>
filter(Subject == "1") |>
head(5) Subject Wt Dose Time conc
1 1 79.6 4.02 0.25 2.84
2 1 79.6 4.02 0.57 6.57
3 1 79.6 4.02 1.12 10.50
4 1 79.6 4.02 2.02 9.66
5 1 79.6 4.02 3.82 8.58
15.2 The three built-in start-of-interval strategies
You can inspect the signatures with args():
args(PKNCA_impute_method_start_conc0)function (conc, time, start = 0, ..., options = list())
NULL
args(PKNCA_impute_method_start_predose)function (conc, time, start, end, conc.group, time.group, ...,
max_shift = NA_real_, options = list())
NULL
args(PKNCA_impute_method_start_cmin)function (conc, time, start, end, ..., options = list())
NULL
| Name | String to use | What it does |
|---|---|---|
PKNCA_impute_method_start_conc0 |
"start_conc0" |
Inserts a concentration of 0 at the interval start time |
PKNCA_impute_method_start_predose |
"start_predose" |
Uses the last observed concentration before the interval start |
PKNCA_impute_method_start_cmin |
"start_cmin" |
Uses the minimum concentration observed within the interval |
All three built-in methods impute a concentration at the start of the interval; newer development versions of PKNCA also provide PKNCA_impute_method_end_conc_drop, which instead drops a next-dose sample observed exactly at the interval end (not shown here, as it is not yet in a released version).
15.2.1 start_conc0 — zero concentration at interval start
Appropriate in a single-dose setting when no drug is expected before dosing. The drug concentration at time 0 is assumed to be 0.
o_data_c0 <- PKNCAdata(o_conc, o_dose, impute = "start_conc0")
o_data_c0$impute # verify the impute string is stored[1] "start_conc0"
res_c0 <- pk.nca(o_data_c0)
as.data.frame(res_c0) |>
filter(Subject == "1",
PPTESTCD %in% c("auclast", "cmax", "tmax")) |>
select(Subject, PPTESTCD, PPORRES)# A tibble: 3 × 3
Subject PPTESTCD PPORRES
<ord> <chr> <dbl>
1 1 auclast 92.3
2 1 cmax 10.5
3 1 tmax 1.12
15.2.2 start_predose — last pre-dose concentration
Appropriate in multiple-dose or steady-state settings where a true pre-dose (trough) sample was collected just before administration. The method looks outside the current interval for the most recent observed concentration. The max_shift argument caps how far forward that pre-dose sample may be shifted to the interval start: it defaults to 5% of the interval duration (0.05 * (end - start)), or, when end is infinite, 5% of the time from start to the last sample.
o_data_pd <- PKNCAdata(o_conc, o_dose, impute = "start_predose")
o_data_pd$impute[1] "start_predose"
15.2.3 start_cmin — minimum concentration in interval
Appropriate as a conservative fallback: if no pre-dose value is available and zero cannot be assumed (e.g. metabolite or endogenous compound), use the minimum observed concentration in the interval as a lower-bound estimate.
o_data_cmin <- PKNCAdata(o_conc, o_dose, impute = "start_cmin")
o_data_cmin$impute[1] "start_cmin"
15.2.4 The three strategies side by side
Running all three on subject 1 (whose first sample is at t = 0.25) makes the differences concrete:
d1_conc <- d_conc_no0 |> filter(Subject == "1")
d1_dose <- d_dose |> filter(Subject == "1")
o_conc1 <- PKNCAconc(d1_conc, conc ~ Time | Subject)
o_dose1 <- PKNCAdose(d1_dose, amt ~ Time | Subject)
ivl1 <- data.frame(start = 0, end = Inf, auclast = TRUE)
auclast_with <- function(method) {
res <- as.data.frame(pk.nca(PKNCAdata(o_conc1, o_dose1, intervals = ivl1,
impute = method)))
data.frame(impute = method, auclast = res$PPORRES[res$PPTESTCD == "auclast"])
}
bind_rows(auclast_with("start_conc0"),
auclast_with("start_predose"),
auclast_with("start_cmin"))Warning: Requesting an AUC range starting (0) before the first measurement
(0.25) is not allowed
impute auclast
1 start_conc0 147.1422
2 start_predose NA
3 start_cmin 147.4972
start_conc0 adds conc = 0 at t = 0, while start_cmin adds the minimum concentration observed in the interval (here 2.84 mg/L, the first sample) — so start_cmin gives the slightly larger AUC. start_predose looks for a sample at or before the interval start, and this dataset has none: it imputes nothing, the interval still has no concentration at its start time, and auclast is NA (the warning printed by the chunk explains why).
15.3 Applying imputation globally
The single-strategy examples above already used the global form: pass a method name string to the impute argument of PKNCAdata(), and the same strategy is applied to every subject and every interval — the side-by-side comparison restricted itself to subject 1 only to keep the output small.
o_data_global <- PKNCAdata(o_conc, o_dose, impute = "start_conc0")
res_global <- pk.nca(o_data_global)
df_global <- as.data.frame(res_global)
# auclast is populated for all subjects despite the missing t = 0
# (arrange by subject number — Theoph's factor levels are not in numeric order)
df_global |>
filter(PPTESTCD == "auclast") |>
select(Subject, PPORRES) |>
arrange(as.numeric(as.character(Subject))) |>
head(6)# A tibble: 6 × 2
Subject PPORRES
<ord> <dbl>
1 1 92.3
2 2 67.2
3 3 70.6
4 4 72.8
5 5 84.4
6 6 71.7
15.4 Chaining methods
The side-by-side comparison left start_predose with an NA because this dataset has no pre-dose sample. Chaining resolves exactly that situation: provide a comma-separated string to apply methods in sequence. Methods are applied in order; a later method has no effect if an earlier one already added a point at the interval start.
# Try start_predose first; if it cannot find a pre-dose observation,
# fall back to inserting zero
o_data_chain <- PKNCAdata(o_conc, o_dose,
impute = "start_predose,start_conc0")
o_data_chain$impute[1] "start_predose,start_conc0"
res_chain <- pk.nca(o_data_chain)
as.data.frame(res_chain) |>
filter(Subject == "1", PPTESTCD == "auclast") |>
select(Subject, PPTESTCD, PPORRES)# A tibble: 1 × 3
Subject PPTESTCD PPORRES
<ord> <chr> <dbl>
1 1 auclast 92.3
For subject 1 the chained result matches the global start_conc0 run in the previous section — the same auclast (92.3) appears in the first row of both displays: start_predose found no pre-dose sample to carry forward, so the fallback inserted the zero.
15.5 Per-interval imputation
Different intervals in the same dataset can use different strategies. Add an impute column to the intervals data frame, then pass the column name (as a string) to the impute argument of PKNCAdata(). The same setup is shown in the vignette’s per-interval section.
my_intervals <- data.frame(
start = c(0, 0),
end = c(24, Inf),
auclast = c(TRUE, FALSE),
aucinf.obs = c(FALSE, TRUE),
cmax = c(TRUE, TRUE),
impute = c("start_conc0", "start_predose,start_conc0")
)
o_data_per_interval <- PKNCAdata(
o_conc, o_dose,
intervals = my_intervals,
impute = "impute" # name of the column to read
)
o_data_per_interval$impute[1] "impute"
o_data_per_interval$intervals[, c("start", "end", "impute")] start end impute
1 0 24 start_conc0
2 0 Inf start_predose,start_conc0
res_per <- pk.nca(o_data_per_interval)
as.data.frame(res_per) |>
filter(Subject == "1",
PPTESTCD %in% c("auclast", "aucinf.obs", "cmax")) |>
select(Subject, start, end, PPTESTCD, PPORRES) # start/end distinguish the two cmax rows# A tibble: 4 × 5
Subject start end PPTESTCD PPORRES
<ord> <dbl> <dbl> <chr> <dbl>
1 1 0 24 auclast 92.3
2 1 0 24 cmax 10.5
3 1 0 Inf cmax 10.5
4 1 0 Inf aucinf.obs 215.
15.6 Writing a custom imputation function
15.6.1 Naming convention
The function name must follow the pattern PKNCA_impute_method_<name>. PKNCA prepends PKNCA_impute_method_ to the string you supply and looks up that single function on the search path, so the convention is mandatory.
15.6.2 Required signature
Modeled after the built-in functions:
# Minimal required arguments (from args(PKNCA_impute_method_start_conc0)):
PKNCA_impute_method_<name> <- function(conc, time, start = 0, ..., options = list())The function receives:
| Argument | Description |
|---|---|
conc |
Numeric vector of concentrations for the current subject/interval |
time |
Numeric vector of times (same length as conc) |
start |
Interval start time |
... |
Additional arguments (must be accepted and ignored) |
options |
Named list of PKNCA options |
15.6.3 Return value
A data frame with exactly two columns, conc and time, sorted by time. It should include all original observations plus any imputed points.
15.6.4 Example: impute with the mean of the first two observations
PKNCA_impute_method_start_mean2 <- function(conc, time, start = 0, ...,
options = list()) {
# Only impute if the interval start is not already observed
if (!start %in% time) {
# Use the mean of the first two observed concentrations
imputed_conc <- mean(conc[order(time)][seq_len(min(2, length(conc)))])
conc <- c(imputed_conc, conc)
time <- c(start, time)
}
result <- data.frame(conc = conc, time = time)
result[order(result$time), ]
}# Use the custom method by its suffix
o_data_custom <- PKNCAdata(o_conc, o_dose, impute = "start_mean2")
res_custom <- pk.nca(o_data_custom)
as.data.frame(res_custom) |>
filter(Subject == "1", PPTESTCD == "auclast") |>
select(Subject, PPTESTCD, PPORRES)# A tibble: 1 × 3
Subject PPTESTCD PPORRES
<ord> <chr> <dbl>
1 1 auclast 92.8
The vignette’s advanced section shows another worked example.
15.7 When to use each strategy
| Situation | Recommended strategy |
|---|---|
| Single-dose study, no pre-dose drug expected | start_conc0 |
| Multiple-dose or steady-state, trough sample collected | start_predose |
| Endogenous analyte or metabolite, zero implausible | start_cmin |
| Uncertain: try pre-dose, fall back to zero | "start_predose,start_conc0" |
| Non-standard logic required | Custom PKNCA_impute_method_* function |
15.8 Summary
- Imputation is triggered when no observation exists at the interval start time.
- The three built-in start-of-interval methods are referenced by their suffix strings:
"start_conc0","start_predose","start_cmin". - Global imputation:
PKNCAdata(..., impute = "start_conc0"). - Chained methods:
impute = "start_predose,start_conc0". - Per-interval: add an
imputecolumn to the intervals data frame and pass that column name to theimputeargument ofPKNCAdata(). - Custom functions: name them
PKNCA_impute_method_<name>and return a two-column data frame (conc,time).