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)
# No concentration remains at the dose time (the interval start)
o_conc <- PKNCAconc(d_conc_no0, conc ~ Time | Subject)
o_dose <- PKNCAdose(d_dose, amt ~ Time | Subject)21 Concentration Imputation
21.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.
21.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
21.1.2 Imputation in the automatic intervals
When PKNCAdata() generates the intervals itself (no intervals argument), each interval already carries an imputation method in an impute column (≥ 0.13.0): start_predose_conc0 after a single dose, start_cmin or start_predose for a dosing interval, none for the back-extrapolated c0 after an intravenous bolus, and only start_conc0 (single dose) or nothing for sparse data. The impute argument of PKNCAdata() keeps its default of NA, which means “read the column named impute”:
o_data_auto <- PKNCAdata(o_conc, o_dose)
o_data_auto$impute[1] NA
o_data_auto$intervals |>
filter(Subject == "1") |>
select(Subject, start, end, impute)# A tibble: 1 × 4
Subject start end impute
<ord> <dbl> <dbl> <chr>
1 1 0 Inf start_predose_conc0
So with these data, a plain pk.nca(o_data_auto) already imputes a concentration at time 0. Passing a method string as impute, as the examples below do, applies that method to every interval instead of the column. The intervals themselves are described in the Interval Selection chapter and the Automatic Intervals and Dosing Regimens chapter. Before 0.13.0, the automatic intervals had no imputation. With auto.interval.method = "legacy", which restores that behavior, these data give NA for auclast and aucinf.obs, excluded with the reason that the interval starts (at 0) before the first measurement.
21.2 The built-in imputation methods
pknca_impute_methods() (≥ 0.13.0) lists the built-in methods with the names that imputation strings use, a one-line description, and their arguments:
d_methods <- pknca_impute_methods()
d_methods$method[1] "end_conc_drop" "start_cmin" "start_conc0"
[4] "start_predose" "start_predose_conc0"
d_methods$description[1] "Drop a concentration measured exactly at the end of the interval, if one is present (usually used with multiple-dose data when a point at the interval boundary belongs to the next dose, e.g. an imputed C0)"
[2] "Add a new concentration of the minimum during the interval at the start time (usually used with multiple-dose data)"
[3] "Set the concentration at the start time to 0, even if a nonzero concentration exists at that time (usually used with single-dose data)."
[4] "Shift a predose concentration to become the time zero concentration (only if a time zero concentration does not exist)."
[5] "Use a predose concentration as the start concentration when one is available and 0 when it is not."
| Name | String to use | What it does |
|---|---|---|
PKNCA_impute_method_start_conc0 |
"start_conc0" |
Sets the concentration at the interval start to 0, adding a row if there is none and replacing a measured value if there is one |
PKNCA_impute_method_start_predose |
"start_predose" |
When there is no concentration at the start, shifts the most recent predose concentration (within max_shift) to the start |
PKNCA_impute_method_start_cmin |
"start_cmin" |
When there is no concentration at the start, adds the minimum concentration observed within the interval there |
PKNCA_impute_method_start_predose_conc0 |
"start_predose_conc0" |
Keeps a concentration measured at the start; otherwise shifts a predose concentration there like start_predose; otherwise adds 0 (≥ 0.13.0) |
PKNCA_impute_method_end_conc_drop |
"end_conc_drop" |
Drops a concentration measured exactly at the interval end from that interval (≥ 0.13.0) |
The four start_* methods act at the start of the interval; end_conc_drop acts at its end. 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
args(PKNCA_impute_method_start_predose_conc0)function (conc, time, start, end, conc.group, time.group, ...,
max_shift = NA_real_, options = list())
NULL
args(PKNCA_impute_method_end_conc_drop)function (conc, time, end, ..., options = list())
NULL
21.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. A concentration measured at the start time is replaced with 0, so a nonzero time-0 sample (3 of the 12 Theoph subjects have one) is discarded.
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 147.
2 1 cmax 10.5
3 1 tmax 1.12
21.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"
21.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"
21.2.4 start_predose_conc0 — predose if available, otherwise zero
Appropriate whenever you want a predose concentration used when one exists and zero when it does not (≥ 0.13.0); it is what the automatic single-dose interval uses. It also keeps a concentration measured at the start time, which matters after an intravenous bolus, where that measurement is the C0 for the dose. The chaining section below shows why the chain "start_predose,start_conc0" does not do the same thing.
o_data_pdc0 <- PKNCAdata(o_conc, o_dose, impute = "start_predose_conc0")
o_data_pdc0$impute[1] "start_predose_conc0"
21.2.5 end_conc_drop — drop a concentration at the interval end
The other methods add or change a point at the start of the interval; end_conc_drop (≥ 0.13.0) removes a concentration observed exactly at the interval end. It is meant for multiple-dose data where a sample at the boundary belongs to the next dose, for example a post-dose concentration (or an imputed C0) recorded at the time the next dose was given. Called directly, it drops the 24 h point:
PKNCA_impute_method_end_conc_drop(
conc = c(10, 7, 5, 3, 11),
time = c(0, 2, 6, 12, 24),
end = 24
) conc time
1 10 0
2 7 2
3 5 6
4 3 12
In an analysis, combine it with a start method in a chain, such as "start_predose,end_conc_drop".
21.2.6 The start strategies side by side
Running the four start methods 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"),
auclast_with("start_predose_conc0"))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
4 start_predose_conc0 147.1422
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). start_predose_conc0 finds no predose sample either, falls back to adding a zero, and so matches start_conc0.
21.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. With automatic intervals, the string replaces the methods in their impute column.
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 147.
2 2 88.7
3 3 95.9
4 4 103.
5 5 118.
6 6 71.7
21.4 Chaining methods
Provide a comma-separated string to apply several methods to the same interval. Methods are applied in order, left to right, each to the output of the previous one, so a later method sees, and can change, what an earlier one did:
start_predose,start_cmin, andstart_predose_conc0only add a point when there is no concentration at the start yet, so after an earlier method has supplied one they have no effect.start_conc0sets the start concentration to 0 whether or not one is there, so it overwrites whatever an earlier method shifted or added."start_predose,start_conc0"therefore always gives the same result as"start_conc0"alone.
To use a predose concentration when one exists and zero otherwise, use start_predose_conc0 rather than a chain. Three small profiles make the difference concrete: one with a predose sample at −0.25 h, one whose first sample is at 0.5 h, and one with a concentration measured at the dose time:
d_conc_pd <- data.frame(
Subject = rep(c("predose", "no_predose", "c0_measured"), c(7, 6, 7)),
time = c(-0.25, 0.5, 1, 2, 4, 8, 24,
0.5, 1, 2, 4, 8, 24,
0, 0.5, 1, 2, 4, 8, 24),
conc = c(2.0, 4.0, 6.0, 5.5, 4.0, 2.5, 0.6,
4.0, 6.0, 5.5, 4.0, 2.5, 0.6,
9.0, 7.5, 6.6, 5.4, 4.0, 2.5, 0.6)
)
d_dose_pd <- data.frame(
Subject = c("predose", "no_predose", "c0_measured"),
time = 0,
dose = 100
)
o_conc_pd <- PKNCAconc(d_conc_pd, conc ~ time | Subject)
o_dose_pd <- PKNCAdose(d_dose_pd, dose ~ time | Subject)
auclast_by_method <- function(method) {
res <- as.data.frame(pk.nca(PKNCAdata(
o_conc_pd, o_dose_pd,
intervals = data.frame(start = 0, end = Inf, auclast = TRUE),
impute = method
)))
res <- res[res$PPTESTCD == "auclast", ]
setNames(res$PPORRES, as.character(res$Subject))
}
methods <- c("start_conc0", "start_predose",
"start_predose,start_conc0", "start_predose_conc0")
round(sapply(methods, auclast_by_method), 2)Warning: Requesting an AUC range starting (0) before the first measurement
(0.5) is not allowed
start_conc0 start_predose start_predose,start_conc0
c0_measured 54.77 57.01 54.77
no_predose 52.73 NA 52.73
predose 52.73 53.23 52.73
start_predose_conc0
c0_measured 57.01
no_predose 52.73
predose 53.23
The warning comes from start_predose for the no_predose profile, which has nothing at or before time 0 to shift, so its auclast is NA. Reading the columns:
start_conc0puts 0 at time 0 for all three, discarding the predose sample and replacing the measured 9.0 at time 0."start_predose,start_conc0"gives exactly thestart_conc0column: the predose 2.0 thatstart_predoseshifted to time 0 is overwritten with 0.start_predose_conc0uses the shifted predose concentration forpredose, adds 0 forno_predose, and keeps the measured 9.0 forc0_measured.
Chains remain useful when a later method fills a gap rather than overwriting, for example "start_predose,start_cmin" (use a predose sample, otherwise the interval minimum), or when the methods act at different ends, as in "start_predose,end_conc_drop". interval_add_impute() (with its after argument) and interval_remove_impute() edit chains in an interval table; see Editing intervals.
21.5 Per-interval imputation
Different intervals in the same dataset can use different strategies. Add a character column of methods to the intervals data frame, then pass the column name (as a string) to the impute argument of PKNCAdata(). The column may have any name (≥ 0.13.0); a column named impute is also found when the impute argument is left at its default of NA. 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_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_conc0
get_impute_column() and get_impute_method() (≥ 0.13.0) apply the same rules that pk.nca() does to find the column and the method for each row:
get_impute_column(o_data_per_interval$intervals, impute = o_data_per_interval$impute)[1] "impute"
get_impute_method(o_data_per_interval$intervals, impute = o_data_per_interval$impute)[1] "start_conc0" "start_predose_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.
21.6 Writing a custom imputation function
21.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. A function of your own is found this way without being registered, so it works in imputation strings but is not listed by pknca_impute_methods().
21.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 |
21.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.
21.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 148.
The vignette’s advanced section shows another worked example.
21.7 When to use each strategy
| Situation | Recommended strategy |
|---|---|
| Single-dose study; use a predose sample if there is one, otherwise assume zero | start_predose_conc0 (the automatic single-dose choice) |
| Single-dose study, no pre-dose drug expected, any time-0 value to be ignored | start_conc0 |
| Multiple-dose or steady-state, trough sample collected | start_predose |
| Endogenous analyte or metabolite, zero implausible | start_cmin |
| Intravenous bolus with a measured concentration at the dose time | start_predose_conc0 (keeps it), or no imputation and let c0 back-extrapolate |
| Sample at the interval end that belongs to the next dose | add end_conc_drop to the chain |
| Sparse sampling | start_conc0 only; the sparse estimators refuse other imputed values (pknca_error_sparse_impute); see the Sparse Sampling chapter |
| Non-standard logic required | Custom PKNCA_impute_method_* function |
21.8 Summary
start_conc0sets the interval start to 0 even when a concentration was measured there; the other start methods act only when no concentration exists at the interval start time.- The built-in methods are referenced by their suffix strings:
"start_conc0","start_predose","start_cmin","start_predose_conc0", and"end_conc_drop";pknca_impute_methods()lists them. - Automatic intervals carry their own
imputecolumn (≥ 0.13.0). - Global imputation:
PKNCAdata(..., impute = "start_conc0"). - Chained methods apply left to right, and
start_conc0overwrites what earlier methods did; usestart_predose_conc0for “predose, else zero”. - Per-interval: add a character column of methods (any name) to the intervals and pass that column name to the
imputeargument ofPKNCAdata(). - Custom functions: name them
PKNCA_impute_method_<name>and return a two-column data frame (conc,time).