---
title: "Dose-Aware Interpolation and Extrapolation"
---
```{r setup, include=FALSE}
library(PKNCA)
library(dplyr)
library(ggplot2)
conflicted::conflicts_prefer(dplyr::filter, dplyr::select, .quiet = TRUE)
```
## Why dose timing matters for interpolation
Standard concentration interpolation (e.g. `interp.extrap.conc()`) assumes a smooth concentration-time curve with no discontinuities. But when a dose is given **at** or **near** the interpolation target time, the correct value depends critically on whether you are asking about the concentration *before* or *after* that dose event.
**`interp.extrap.conc.dose()`** is the dose-aware version. It accounts for:
- Whether the route is intravascular (bolus causes instantaneous jump) or extravascular (no instantaneous change)
- Whether you want the concentration before (`out.after = FALSE`) or after (`out.after = TRUE`) the dose
- Whether the dose is an infusion with a finite duration
---
## The key argument: `out.after`
| `out.after` | Meaning |
|---|---|
| `FALSE` (default) | Return the concentration just **before** the dose event |
| `TRUE` | Return the concentration just **after** the dose event |
`out.after` has a meaningful effect only at the instant of an IV bolus dose (`route.dose = "intravascular"` with `duration.dose = 0`), where the concentration jumps instantaneously: `FALSE` gives the value from the data before the dose (the trough) and `TRUE` gives the value from the data after the dose. For extravascular doses — and for infusions, which have `duration.dose > 0` — there is no instantaneous jump, so `out.after` makes no difference.
---
## Example data: single oral dose
```{r}
# Single-dose oral profile (absorption + elimination)
conc <- c(0.0, 2.5, 5.0, 4.2, 2.8, 1.5, 0.8)
time <- c(0, 1, 2, 3, 5, 7, 10 )
d <- data.frame(time = time, conc = conc)
ggplot(d, aes(x = time, y = conc)) +
geom_line(colour = "steelblue") +
geom_point(size = 3, colour = "steelblue") +
labs(title = "Example single-dose oral profile",
x = "Time (h)", y = "Concentration (mg/L)") +
theme_minimal()
```
---
## Basic interpolation (no dose at target time)
When the target time is **between** observed samples and does not coincide with a dose, the result equals standard log-linear interpolation.
```{r}
# t = 4 h falls between t = 3 and t = 5
interp.extrap.conc.dose(
conc = conc,
time = time,
time.dose = 0, # dose was given at t=0
route.dose = "extravascular",
time.out = 4
)
```
Compare to dose-unaware interpolation:
```{r}
interp.extrap.conc(conc = conc, time = time, time.out = 4)
```
They agree when no dose occurs at the target time.
---
## At the dose time: oral (extravascular)
For an oral dose, concentration does not change instantaneously at the dose time — absorption is gradual. So `out.after` does not change the interpolated value at the dose time itself.
```{r}
# Oral: t=0, before dose
interp.extrap.conc.dose(
conc = conc, time = time,
time.dose = 0, route.dose = "extravascular",
time.out = 0, out.after = FALSE
)
```
```{r}
# Oral: t=0, after dose (same — no jump)
interp.extrap.conc.dose(
conc = conc, time = time,
time.dose = 0, route.dose = "extravascular",
time.out = 0, out.after = TRUE
)
```
---
## At the dose time: IV bolus (intravascular)
For an IV bolus — indicated by `route.dose = "intravascular"` together with `duration.dose = 0` — the dose causes an instantaneous concentration jump. `out.after = FALSE` returns the pre-dose (trough) concentration; `out.after = TRUE` returns the post-dose concentration.
```{r}
# Two IV bolus doses (t=0 and t=6); the t=6 sample is the pre-dose trough
conc_iv <- c(10, 8, 6, 4, 2.5, 9, 6)
time_iv <- c(0, 1, 2, 4, 6, 7, 9)
# Just before the second dose at t=6: the observed trough
interp.extrap.conc.dose(
conc = conc_iv, time = time_iv,
time.dose = c(0, 6), route.dose = "intravascular", duration.dose = 0,
time.out = 6, out.after = FALSE
)
```
```{r}
# Just after the second dose at t=6: the post-dose concentration
interp.extrap.conc.dose(
conc = conc_iv, time = time_iv,
time.dose = c(0, 6), route.dose = "intravascular", duration.dose = 0,
time.out = 6, out.after = TRUE
)
```
The pre-dose value is the measured trough (2.5). The post-dose value (about 11.0) is estimated the same way as `pk.calc.c0()`: log-slope back-extrapolation from the concentrations measured after the dose.
---
## Multiple dose times
The IV bolus example above already passed two dose times — `time.dose` accepts a vector whenever multiple doses were given during the sampling window. Here is the same two-dose scenario with oral dosing.
```{r}
# Oral doses at t=0 and t=6
conc_md <- c(0, 2.5, 5.0, 4.2, 2.8, 4.5, 6.0, 5.1, 3.5, 2.0)
time_md <- c(0, 1, 2, 3, 5, 6, 7, 8, 10, 12 )
# Concentration just before the second dose at t=6
interp.extrap.conc.dose(
conc = conc_md, time = time_md,
time.dose = c(0, 6), route.dose = "extravascular",
time.out = 6, out.after = FALSE
)
```
```{r}
# Concentration just after the second dose at t=6
interp.extrap.conc.dose(
conc = conc_md, time = time_md,
time.dose = c(0, 6), route.dose = "extravascular",
time.out = 6, out.after = TRUE
)
```
For extravascular doses, before and after are the same (no instantaneous jump).
---
## IV infusion: dose duration
For IV infusions, supply `duration.dose` (the infusion duration in the same time units).
```{r}
# 30-minute (0.5h) infusion starting at t=0
conc_inf <- c(0, 3, 5, 4.5, 3, 2, 1 )
time_inf <- c(0, 0.25, 0.5, 1, 2, 4, 8 )
# During the infusion, between the samples at t=0.25 and t=0.5
interp.extrap.conc.dose(
conc = conc_inf, time = time_inf,
time.dose = 0,
route.dose = "intravascular",
duration.dose = 0.5,
time.out = 0.4, out.after = FALSE
)
```
Because `duration.dose > 0`, the dose is treated as an infusion rather than a bolus: there is no instantaneous jump at t = 0, and concentrations during the infusion are interpolated between the observed samples (here 4.2, between the 3 and 5 measurements).
---
## Extrapolating beyond the last sample
Returning to the single oral-dose profile from the top of the page (last sample at t = 10): extrapolation past the last measured concentration requires an elimination rate. Pass `lambda.z` (forwarded via `...` to the extrapolation functions); without it, the result is `NA`.
```{r}
# t=14 is beyond the last sample (t=10); without lambda.z the result is NA
interp.extrap.conc.dose(
conc = conc, time = time,
time.dose = 0, route.dose = "extravascular",
time.out = 14
)
```
```{r}
# With lambda.z, extrapolation follows clast * exp(-lambda.z * (t - tlast))
interp.extrap.conc.dose(
conc = conc, time = time,
time.dose = 0, route.dose = "extravascular",
time.out = 14, lambda.z = 0.25
)
```
---
## How PKNCA uses `interp.extrap.conc.dose()` internally
Within `pk.nca()`, the interval AUC and AUMC parameters (`aucint.*` and `aumcint.*`) call this function to estimate the concentration at the `start` and `end` of their window whenever dosing data are given (≥ 0.13.0); every `aucint`/`aumcint` parameter is dose-aware, and the former `.dose` variants are retired. You normally never call it directly. The rules that apply inside `pk.nca()` are:
- **Only the window boundaries are estimated**, and always as the concentration just before any dose at that time (`out.after = FALSE`). A concentration measured at the same time as a dose counts as measured before it.
- **The profile ends at the next dose.** Concentrations measured after the first dose at or after `end` are not integrated and are not used to interpolate or extrapolate back into the window.
- **A dose inside the window is integrated across** using the concentrations measured on either side of it; no concentration is imputed at that dose.
- **The route and duration of each dose are used**, so an IV bolus inside the window is handled as a bolus, not as an extravascular dose.
- **Without dosing data**, `pk.nca()` uses `interp.extrap.conc()` instead, and the `PPANMETH` column reports "Interpolation: not dose-aware (no dosing data)".
Because the window start is the concentration *before* the dose, an `aucint.*` window that starts at an IV bolus with no sample at that time starts from the pre-dose concentration rather than from a back-extrapolated C0. For IV bolus profiles, use the `aucivint.*` parameters, which back-extrapolate C0:
```{r}
# Two IV bolus doses (t = 0 and t = 12) with no sample at either dose time
d_iv2 <- data.frame(
subject = "A",
time = c(0.5, 1, 2, 4, 6, 12.5, 13, 14, 16, 18, 24),
conc = c(9, 8, 6, 4, 2.8, 10, 9, 7, 5, 3.6, 1.5)
)
d_iv2_dose <- data.frame(subject = "A", time = c(0, 12), dose = 100)
o_conc_iv2 <- PKNCAconc(d_iv2, conc ~ time | subject)
o_dose_iv2 <- PKNCAdose(d_iv2_dose, dose ~ time | subject,
route = "intravascular")
# The dosing interval after the second dose
o_nca_iv2 <- pk.nca(PKNCAdata(
o_conc_iv2, o_dose_iv2,
intervals = data.frame(start = 12, end = 24,
aucint.last = TRUE, aucivint.last = TRUE, c0 = TRUE)
))
as.data.frame(o_nca_iv2) |>
filter(PPTESTCD %in% c("aucint.last", "aucivint.last", "c0")) |>
select(start, end, PPTESTCD, PPORRES)
```
`aucint.last` starts from the concentration estimated just before the 12-hour dose and rises linearly to the 12.5-hour sample, so it is lower than `aucivint.last`, which starts from the back-extrapolated `c0`. The [AUC chapter](auc-methods.qmd#why-dose-awareness-matters) shows the effect of the next-dose boundary on an oral profile, and the [IV dosing chapter](intravascular.qmd) covers the `auciv*` family.
Understanding these rules helps diagnose unusual NCA results when dosing and sampling times coincide — for example, when an interval boundary such as `end = tau` coincides with the next dose (the trough sample time). Every combination of dose events and observations, and the method used for each, is tabulated in the [complete methods table](https://humanpred.github.io/pknca/articles/v21-methods-for-dose-aware-interpolation-and-extrapolation.html#appendix-complete-methods-table) of the dose-aware interpolation vignette, and [Selection of Calculation Intervals](https://humanpred.github.io/pknca/articles/v03-selection-of-calculation-intervals.html) explains how interval boundaries are chosen.
Several edge cases changed in PKNCA 0.13.0: interpolation no longer fails when the concentrations around a dose are all BLQ; the `AUCall` extrapolation finds the first BLQ measurement after Tlast instead of stopping at the last measured concentration; and a concentration estimated before a dose is extrapolated from the Clast of the profile before that dose rather than from the Clast of the interval being calculated.
---
## Argument reference
| Argument | Required | Meaning |
|---|---|---|
| `conc` | Yes | Observed concentrations |
| `time` | Yes | Observation times (same units as `time.dose`) |
| `time.dose` | Yes | Time(s) at which doses were given |
| `route.dose` | No (default `"extravascular"`) | `"extravascular"` or `"intravascular"`; scalar or one value per dose |
| `duration.dose` | No (default `NA`) | Dose duration: `0` marks an IV bolus, a positive value an infusion; scalar or one value per dose |
| `time.out` | Yes | Target time at which to interpolate/extrapolate |
| `out.after` | No (default `FALSE`) | `FALSE` = before dose; `TRUE` = after dose (only meaningful at the instant of an IV bolus) |
| `options` | No | PKNCA options list (controls AUC method / interpolation rule) |
The table is abridged: `interp.extrap.conc.dose()` also accepts `conc.blq` and `conc.na` (BLQ and `NA` concentration handling), `check` (input validation), and `...` (passed through to the interpolation/extrapolation functions, e.g. `lambda.z`).
---
::: {.callout-note icon=false appearance="minimal"}
**pkgdown reference:** [interp.extrap.conc.dose()](https://humanpred.github.io/pknca/reference/interp.extrap.conc.html) · [interp.extrap.conc()](https://humanpred.github.io/pknca/reference/interp.extrap.conc.html) · [pk.calc.c0()](https://humanpred.github.io/pknca/reference/pk.calc.c0.html) · [pk.calc.aucint()](https://humanpred.github.io/pknca/reference/pk.calc.auxcint.html) · [pk.calc.auciv()](https://humanpred.github.io/pknca/reference/pk.calc.auxciv.html) · [PKNCAconc()](https://humanpred.github.io/pknca/reference/PKNCAconc.html) · [PKNCAdose()](https://humanpred.github.io/pknca/reference/PKNCAdose.html) · [PKNCAdata()](https://humanpred.github.io/pknca/reference/PKNCAdata.html) · [pk.nca()](https://humanpred.github.io/pknca/reference/pk.nca.html)
:::