---
title: "AUC Types and Integration Methods"
---
```{r setup, include=FALSE}
library(PKNCA)
library(dplyr)
library(ggplot2)
conflicted::conflicts_prefer(dplyr::filter, dplyr::select, .quiet = TRUE)
# Shared dataset for this page: oral theophylline, subject 1
d_one <- as.data.frame(Theoph) |>
filter(Subject == "1") |>
rename(time = Time, subject = Subject)
d_dose <- data.frame(subject = "1", dose = Theoph$Dose[1] * Theoph$Wt[1], time = 0)
o_conc <- PKNCAconc(d_one, conc ~ time | subject)
o_dose <- PKNCAdose(d_dose, dose ~ time | subject, route = "extravascular")
```
## AUC types
PKNCA offers four families of AUC — AUClast, AUCall, AUCinf (in observed and predicted flavors), and partial/interval AUC — each answering a different question. They differ in where integration ends and in how concentrations below the limit of quantification (BLQ) are treated:
| Parameter | End point | BLQ treatment | When to use |
|---|---|---|---|
| `auclast` | Last measurable concentration | BLQs after last non-BLQ are ignored | Default; most studies |
| `aucall` | Last measured timepoint | Post-last-measurable BLQs treated as 0 | When you want to include the BLQ tail |
| `aucinf.obs` | Extrapolated to ∞ (using observed Clast) | — | When half-life is reliable |
| `aucinf.pred` | Extrapolated to ∞ (using predicted Clast from λz fit) | — | When Clast is noisy |
| `aucint.*` | Any user-defined time window (dose-aware, ≥ 0.13.0) | Follows the `.last`, `.all`, or `.inf` suffix | Partial AUC, bioequivalence |
Two related families use the same suffixes: `auciv*` (IV bolus AUCs that back-extrapolate C0; see the [IV dosing chapter](intravascular.qmd)) and, for sparse sampling, the `_se`/`_df` companions such as `auclast_se` (see the [sparse NCA chapter](sparse.qmd)).
Each AUC row in the `pk.nca()` results records its integration method in the `PPANMETH` column (≥ 0.13.0), for example `"AUC: lin up/log down"`. A direct call such as `pk.calc.auc.last()` returns its value with the same text in a `"method"` attribute; use `as.numeric()` to drop it before comparing the value to a bare number.
For a worked example computing these AUC flavors on a full dataset, see the [AUC Calculation with PKNCA vignette](https://humanpred.github.io/pknca/articles/v05-auc-calculation-with-PKNCA.html).
### AUClast vs AUCall
Subject 1's profile ends on a measurable sample (t = 24.37), so there is no BLQ tail and `auclast` equals `aucall` on the raw data. To show the difference, this demo appends a synthetic BLQ tail (concentrations reported as 0 at t = 26 and 28) to a **copy** of the data:
```{r}
# Copy of subject 1's profile with a synthetic BLQ tail (reported as 0)
d_one_blq <- d_one |>
select(subject, time, conc) |>
mutate(subject = as.character(subject)) |>
bind_rows(data.frame(subject = "1", time = c(26, 28), conc = 0))
o_conc_blq <- PKNCAconc(d_one_blq, conc ~ time | subject)
auc_compare <- data.frame(
start = 0,
end = Inf,
auclast = TRUE,
aucall = TRUE
)
o_nca_blq <- pk.nca(PKNCAdata(o_conc_blq, o_dose, intervals = auc_compare))
as.data.frame(o_nca_blq) |>
filter(PPTESTCD %in% c("auclast", "aucall")) |>
select(PPTESTCD, PPORRES)
```
> `aucall` ≥ `auclast` when there are post-last-measurable BLQ timepoints, since those are treated as 0 and add a small triangle to the AUC — here the triangle from Clast at t = 24.37 down to 0 at t = 26.
---
## Partial AUC (AUCint)
For bioequivalence or exposure-in-a-window calculations, use `aucint.*` parameters.
These calculate AUC over an exact time window, interpolating concentrations at the boundaries if needed.
```{r}
# AUC from 0 to 4h and 0 to 12h on the same profile
partial_intervals <- data.frame(
start = c(0, 0),
end = c(4, 12),
aucint.last = TRUE # AUC to the last obs within window (or interpolated boundary)
)
o_data_partial <- PKNCAdata(o_conc, o_dose, intervals = partial_intervals)
o_nca_partial <- pk.nca(o_data_partial)
as.data.frame(o_nca_partial) |>
filter(PPTESTCD == "aucint.last") |>
select(start, end, PPTESTCD, PPORRES)
```
**All partial AUC variants:**
| Parameter | Region after Tlast (within the window) | Uses λz |
|---|---|---|
| `aucint.last` | Contributes zero, as for `auclast` | No |
| `aucint.all` | Triangle from Clast down to the first BLQ sample, as for `aucall` | No |
| `aucint.inf.obs` | Extrapolated with λz from the observed Clast | Only when the window extends past Tlast |
| `aucint.inf.pred` | Extrapolated with λz from the predicted Clast | Only when the window extends past Tlast |
Every `aucint.*` parameter is **dose-aware** (≥ 0.13.0) whenever dosing data are given: it uses dose-aware interpolation (`interp.extrap.conc.dose()`) at the interval start and end, and it uses the route and duration of each dose, so an IV bolus is not treated as an extravascular dose. Three rules follow from that:
- **The profile ends at the next dose.** The profile being integrated ends at the first dose at or after `end`, so a concentration measured after that dose is never interpolated back into the window. When the window covers a whole dosing interval, `aucint.last` and `aucint.all` equal `auclast` and `aucall`.
- **Only `start` and `end` are estimated.** A dose *inside* the window is integrated across using the concentrations measured on either side of it; no concentration is imputed at that dose.
- **λz is used only when it is needed.** A window that ends at or before Tlast is interpolated throughout, so `aucint.inf.obs` and `aucint.inf.pred` are reported, and are not excluded with the half-life, even when λz could not be estimated. When the window extends past Tlast and λz cannot be estimated, the `.inf` forms fall back to the `aucint.all` rule for a finite window.
When no dosing data are given, the calculation falls back to plain interpolation across the whole profile (`interp.extrap.conc()`). The `PPANMETH` column of the results (≥ 0.13.0) says which happened ("Interpolation: dose-aware" or "Interpolation: not dose-aware (no dosing data)") and how the region after Tlast was handled ("Extrapolation: none", "AUClast", "AUCall", or "half-life").
> **Retired names:** the `.dose` variants (`aucint.last.dose`, `aucint.all.dose`, `aucint.inf.obs.dose`, `aucint.inf.pred.dose`, and the matching `aumcint.*.dose` names) no longer exist because every `aucint`/`aumcint` parameter is now dose-aware. Requesting one is an error that names the parameter to use instead (for example, "'aucint.last.dose' is now named 'aucint.last'").
The full decision rules for each combination of doses and samples are in the [dose-aware interpolation and extrapolation vignette](https://humanpred.github.io/pknca/articles/v21-methods-for-dose-aware-interpolation-and-extrapolation.html).
```{r}
# All partial AUC variants over a single window
all_partial <- data.frame(
start = 0,
end = 8,
aucint.last = TRUE,
aucint.all = TRUE,
aucint.inf.obs = TRUE,
aucint.inf.pred = TRUE
)
o_nca_all_partial <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = all_partial))
as.data.frame(o_nca_all_partial) |>
filter(grepl("^aucint", PPTESTCD)) |>
select(PPTESTCD, PPORRES)
```
The 0–8 h window ends before subject 1's Tlast (24.37 h), so the four variants are interpolated throughout and agree.
### Why dose-awareness matters {#why-dose-awareness-matters}
The example below is a two-dose oral profile (doses at 0 and 12 h) whose 12-hour trough sample is missing, so the window 0–12 h has to end somewhere between the 8-hour sample and the post-dose 12.5-hour sample.
```{r}
d_md <- data.frame(
subject = "A",
time = c(0, 0.5, 1, 2, 4, 6, 8, 12.5, 13, 14, 16, 20, 24),
conc = c(0, 2.1, 3.6, 4.4, 3.5, 2.6, 2.0, 3.2, 4.6, 5.3, 4.2, 2.7, 1.7)
)
d_md_dose <- data.frame(subject = "A", time = c(0, 12), dose = 100)
o_conc_md <- PKNCAconc(d_md, conc ~ time | subject)
o_dose_md <- PKNCAdose(d_md_dose, dose ~ time | subject)
window_0_12 <- data.frame(
start = 0,
end = 12,
aucint.last = TRUE,
aucint.inf.obs = TRUE
)
# With dosing data: dose-aware
o_nca_aware <- pk.nca(PKNCAdata(o_conc_md, o_dose_md, intervals = window_0_12))
# Without dosing data: interpolation across the whole profile
o_nca_unaware <- pk.nca(PKNCAdata(o_conc_md, intervals = window_0_12))
d_compare <-
bind_rows(
as.data.frame(o_nca_aware) |> mutate(dosing = "given"),
as.data.frame(o_nca_unaware) |> mutate(dosing = "not given")
) |>
filter(PPTESTCD %in% c("aucint.last", "aucint.inf.obs"))
d_compare |> select(dosing, PPTESTCD, PPORRES)
# The method column records how each value was calculated
d_compare$PPANMETH
```
With the doses given, `aucint.last` stops at Tlast (8 h; 24.4), and `aucint.inf.obs` extrapolates from 8 to 12 h along the terminal slope (30.6). Without the doses, PKNCA cannot know that the 12.5-hour sample follows a new dose, so both parameters interpolate from the 8-hour sample up toward it and overestimate the area (34.6).
### Interval AUMC (`aumcint.*`) (≥ 0.13.0) {#interval-aumc}
The `aumcint.*` family mirrors `aucint.*` for the first-moment curve, with the same dose-aware rules: `aumcint.last`, `aumcint.all`, `aumcint.inf.obs`, and `aumcint.inf.pred`.
```{r}
# The two-dose profile above, with the missing 12-hour trough
aumcint_window <- data.frame(
start = 0,
end = 12,
aucint.last = TRUE,
aumcint.last = TRUE,
aumcint.inf.obs = TRUE
)
o_nca_aumcint <- pk.nca(PKNCAdata(o_conc_md, o_dose_md, intervals = aumcint_window))
as.data.frame(o_nca_aumcint) |>
filter(grepl("^au.*int", PPTESTCD)) |>
select(start, end, PPTESTCD, PPORRES)
```
As with the AUC, `aumcint.last` stops at Tlast (8 h) and `aumcint.inf.obs` extrapolates the moment curve to 12 h along the terminal slope. The interval MRT and IV forms of these parameters (`aumcivint.*` and the rest of the `aumciv*` family) are covered in the [IV dosing chapter](intravascular.qmd).
---
## Concentration interpolation at interval boundaries
When a requested interval boundary (e.g. `end = 5`) falls between two observed timepoints, only the `aucint.*` (and `aumcint.*`) parameters estimate the concentration at that boundary — handled internally by `interp.extrap.conc()`, or by `interp.extrap.conc.dose()` when dosing data are given. `auclast` and the other non-interval AUCs do **not** interpolate: they integrate only the samples inside the interval, up to the last measurable one.
The interpolation method matches the AUC method:
- `"linear"` → linear interpolation
- `"lin-log"` → linear interpolation before Tmax, log-linear after Tmax
- `"lin up/log down"` → log-linear interpolation on the descending phase
```{r}
# end = 5 falls between observed samples t = 3.82 and t = 5.10 for subject 1
interp_interval <- data.frame(start = 0, end = 5, auclast = TRUE, aucint.last = TRUE)
o_nca_interp <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = interp_interval))
as.data.frame(o_nca_interp) |>
filter(PPTESTCD %in% c("auclast", "aucint.last")) |>
select(start, end, PPTESTCD, PPORRES)
```
`auclast` stops at the 3.82-hour sample (32.1), while `aucint.last` adds the area from 3.82 h to the interpolated concentration at 5 h (42.1).
When dosing data are given, the `aucint.*` and `aumcint.*` parameters estimate their boundaries with dose-aware interpolation instead, so a boundary at a dose time takes the concentration from the correct side of the dose — see the [Dose-aware interpolation chapter](dose-aware-interpolation.qmd).
---
## Percent extrapolation (%AUCextrap)
When using `aucinf`, you should check what fraction was extrapolated beyond the last observation.
A high `aucpext` means the terminal phase was poorly characterized.
```{r}
pext_interval <- data.frame(
start = 0,
end = Inf,
auclast = TRUE,
aucinf.obs = TRUE,
aucpext.obs = TRUE,
aucpext.pred = TRUE
)
o_nca_pext <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = pext_interval))
as.data.frame(o_nca_pext) |>
filter(grepl("^auc", PPTESTCD)) |>
select(PPTESTCD, PPORRES)
```
Here about 31.5% of `aucinf.obs` is extrapolated — well above the conventional 20% acceptance limit. Note that `pk.nca()` computes `aucpext` but does **not** flag anything on its own: the `exclude` column (not shown above) is all `NA` despite the high extrapolation. Flagging is a manual step with `exclude()` after `pk.nca()`:
```{r}
o_nca_pext_excl <- exclude(
o_nca_pext,
reason = "aucpext.obs > 20%",
FUN = exclude_nca_max.aucinf.pext()
)
# Show the flagged rows
as.data.frame(o_nca_pext_excl) |>
filter(!is.na(exclude)) |>
select(PPTESTCD, PPORRES, exclude)
```
The `aucinf` results and the parameters derived from them (including `aucpext` itself) are now flagged. Because `FUN` supplies its own exclusion text, the recorded reason is the function's `"aucpext > 20"` rather than the `reason =` string.
The 20% default comes from the `max.aucinf.pext` option — but only the **global** option: called with no argument, `exclude_nca_max.aucinf.pext()` reads `PKNCA.options("max.aucinf.pext")`. A per-object setting such as `PKNCAdata(options = list(max.aucinf.pext = 10))` never reaches the exclusion function (and does not make `pk.nca()` flag anything by itself). To flag at a different threshold, pass it to the function directly: `exclude_nca_max.aucinf.pext(10)`.
---
## Integration methods
The integration method controls how the area of each trapezoid is computed between consecutive timepoints. Each rule, including the zero-concentration fallbacks, is derived in the [AUC integration methods vignette](https://humanpred.github.io/pknca/articles/v23-auc-integration-methods.html).
```{r}
# Numeric comparison of the three methods on one subject
methods <- c("linear", "lin-log", "lin up/log down")
results <- lapply(methods, function(m) {
o <- pk.nca(PKNCAdata(o_conc, o_dose,
intervals = data.frame(start=0, end=Inf, auclast=TRUE),
options = list(auc.method = m)))
as.data.frame(o) |>
filter(PPTESTCD == "auclast") |>
mutate(method = m)
}) |> bind_rows()
results |> select(method, PPORRES) |>
rename(auclast = PPORRES)
```
### Method details
**Linear trapezoidal (`"linear"`)**
Uses the standard trapezoidal rule throughout:
$$\text{AUC}_{t_i \to t_{i+1}} = \frac{(C_i + C_{i+1})}{2} \times (t_{i+1} - t_i)$$
Overestimates during the descending phase (gives too much weight to the higher concentration).
**Lin-log (`"lin-log"`)**
Uses **linear** integration from start up to Tmax, and **log-linear** integration from Tmax onward:
$$\text{AUC}_{t_i \to t_{i+1}} = \frac{(C_i - C_{i+1})}{\ln C_i - \ln C_{i+1}} \times (t_{i+1} - t_i) \quad \text{(post-Tmax)}$$
The switch point is Tmax (not whether concentrations are rising or falling), so any post-Tmax secondary peak is integrated log-linearly even if concentrations are rising. Falls back to linear for any interval where either endpoint is zero. Not recommended — `"lin up/log down"` is almost always preferable as it switches on actual direction rather than Tmax.
**Lin up / log down (`"lin up/log down"`) — default**
Hybrid: linear trapezoidal on the ascending phase (when $C_{i+1} \geq C_i$), log-linear on the descending phase (when $C_{i+1} < C_i$). Best of both worlds for most PK profiles.
```{r}
# Profile with AUClast shaded
d_plot <- d_one |>
filter(conc > 0) |>
arrange(time)
ggplot(d_plot, aes(x = time, y = conc)) +
geom_line(colour = "steelblue", linewidth = 1) +
geom_point(size = 3, colour = "steelblue") +
geom_area(alpha = 0.15, fill = "steelblue") +
labs(title = "Concentration-time profile (shaded area = AUClast)",
x = "Time (h)", y = "Concentration (mg/L)") +
theme_minimal()
```
### When to use which method
| Study type | Recommended method | Notes |
|---|---|---|
| Most oral/extravascular | `"lin up/log down"` (default) | Linear on ascending phase, log-linear on declining phase |
| IV bolus | `"lin up/log down"` (default) | Profile is entirely declining, so log-linear is used throughout — equivalent to `"lin-log"` for pure bolus but without the Tmax ambiguity |
| IV infusion with smooth decline | `"lin up/log down"` | Concentrations rise during infusion (linear) then fall after end of infusion (log-linear) |
| Regulatory (some agencies) | `"linear"` | Some health authorities require the linear trapezoidal rule throughout |
| Highly irregular profiles with zeros | `"linear"` | Zeros alone are handled per-segment by the log methods (they fall back to linear for those segments); choose `"linear"` when you want the same rule everywhere, e.g. for some regulatory templates |
| Never recommended | `"lin-log"` | Switches at Tmax rather than on direction of change — gives wrong method for post-Tmax secondary peaks |
---
## AUMC — area under the first moment curve
The **area under the first moment curve** (AUMC) weights concentrations by time and is used to compute mean residence time (MRT):
$$\text{AUMC} = \int_0^{t_{\mathrm{last}}} t \cdot C(t) \, dt$$
| Parameter | Meaning |
|---|---|
| `aumclast` | AUMC from 0 to last measurable concentration |
| `aumcall` | AUMC from 0 to last, treating BLQ as 0 |
| `aumcinf.obs` | AUMC extrapolated to ∞, observed Clast |
| `aumcinf.pred` | AUMC extrapolated to ∞, predicted Clast |
| `aumcint.last`, `aumcint.all`, `aumcint.inf.obs`, `aumcint.inf.pred` (≥ 0.13.0) | AUMC over a time window, dose-aware (see [Interval AUMC](#interval-aumc)) |
| `aumcivlast`, `aumcivall`, `aumcivinf.obs`, `aumcivinf.pred`, `aumcivint.last`, `aumcivint.all` (≥ 0.13.0) | IV bolus AUMC with C0 back-extrapolation (see the [IV dosing chapter](intravascular.qmd)) |
```{r}
aumc_interval <- data.frame(
start = 0,
end = Inf,
aumclast = TRUE,
aumcall = TRUE,
aumcinf.obs = TRUE,
aumcinf.pred = TRUE
)
o_nca_aumc <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = aumc_interval))
as.data.frame(o_nca_aumc) |>
filter(grepl("^aumc", PPTESTCD)) |>
select(PPTESTCD, PPORRES)
```
---
## Other interval parameters
These interval parameters are requested the same way as AUCs and often accompany them.
### AUC above a threshold (`aucabove.*`)
`aucabove.*` computes the area under the curve above a reference concentration — useful for quantifying exposure above a minimum effective concentration (for the duration above it, see `time_above`).
| Parameter | Reference concentration |
|---|---|
| `aucabove.trough.all` | Ctrough (concentration at end of interval) |
| `aucabove.predose.all` | Cpredose (concentration at interval start) |
```{r}
# ctrough requires an observation exactly at the interval end —
# subject 1's last sample is at t = 24.37 (end = 24 would give ctrough = NA)
above_interval <- data.frame(
start = 0,
end = 24.37,
aucabove.trough.all = TRUE,
aucabove.predose.all = TRUE,
ctrough = TRUE,
cstart = TRUE
)
o_nca_above <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = above_interval))
as.data.frame(o_nca_above) |>
filter(PPTESTCD %in% c("aucabove.trough.all", "aucabove.predose.all", "ctrough", "cstart")) |>
select(PPTESTCD, PPORRES)
```
### Time above a threshold (`time_above`)
`time_above` computes the total duration (in time units) for which the concentration exceeds a specified threshold. The threshold is set via the `conc_above` column in the intervals data frame.
```{r}
# Time above 2 mg/L
time_above_interval <- data.frame(
start = 0,
end = Inf,
time_above = TRUE,
conc_above = 2 # threshold concentration (same units as your data)
)
o_nca_ta <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = time_above_interval))
as.data.frame(o_nca_ta) |>
filter(PPTESTCD == "time_above") |>
select(PPTESTCD, PPORRES)
```
### Total dose (`totdose`)
`totdose` is the total amount administered within the analysis interval — it is extracted from the dose object rather than computed from concentrations. Useful as a reference column in your results data frame.
```{r}
totdose_interval <- data.frame(
start = 0,
end = Inf,
totdose = TRUE,
auclast = TRUE
)
o_nca_td <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = totdose_interval))
as.data.frame(o_nca_td) |>
filter(PPTESTCD %in% c("totdose", "auclast")) |>
select(PPTESTCD, PPORRES)
```
---
### Cav over a specific interval (`cav.int.*`)
`cav.int.*` computes the average concentration over the requested interval, without requiring a full dosing-interval design. The numerator is the corresponding `aucint.*` flavor computed over the window; the divisor is always `end − start`.
> **Warning:** because the divisor is always `end − start`, requesting any `cav.int.*` parameter with `end = Inf` divides a finite AUC by infinity and reports 0 — always use a finite `end`.
| Parameter | Numerator (`aucint.*` flavor over the window) |
|---|---|
| `cav.int.last` | `aucint.last` — stops accumulating area at the last measurable concentration |
| `cav.int.all` | `aucint.all` — post-Tlast BLQ values treated as 0 |
| `cav.int.inf.obs` | `aucint.inf.obs` — λz extrapolation beyond Tlast, observed Clast |
| `cav.int.inf.pred` | `aucint.inf.pred` — λz extrapolation beyond Tlast, predicted Clast |
```{r}
# Two finite windows: 0–24 h lies inside the observed profile (Tlast = 24.37 h),
# 0–48 h extends past it
cav_interval <- data.frame(
start = 0,
end = c(24, 48),
cav.int.last = TRUE,
cav.int.all = TRUE,
cav.int.inf.obs = TRUE,
cav.int.inf.pred = TRUE
)
o_nca_cav <- pk.nca(PKNCAdata(o_conc, o_dose, intervals = cav_interval))
as.data.frame(o_nca_cav) |>
filter(grepl("^cav.int", PPTESTCD)) |>
select(start, end, PPTESTCD, PPORRES)
```
Over 0–24 h all four flavors integrate the same observed curve, so they coincide. Over 0–48 h they separate: the `.inf` forms extrapolate the tail beyond Tlast with λz (differing from each other only through the observed vs. predicted Clast — nearly identical here), while `.last` and `.all` stop accumulating area at Tlast yet still divide by the full 48 h, which is why their average drops from 6.08 to 3.07 mg/L.
---
::: {.callout-note icon=false appearance="minimal"}
**pkgdown reference:** [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) · [pk.calc.aucint()](https://humanpred.github.io/pknca/reference/pk.calc.auxcint.html) · [pk.calc.auc.last()](https://humanpred.github.io/pknca/reference/pk.calc.auxc.html) · [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) · [exclude()](https://humanpred.github.io/pknca/reference/exclude.html) · [exclude_nca_max.aucinf.pext()](https://humanpred.github.io/pknca/reference/exclude_nca.html)
:::