Skip to content

fix: make every AUCint dose-aware and exclude only when the half-life is used - #632

Open
billdenney wants to merge 4 commits into
mainfrom
claude/pknca-param-exclusion-half-life-b3d7ae
Open

fix: make every AUCint dose-aware and exclude only when the half-life is used#632
billdenney wants to merge 4 commits into
mainfrom
claude/pknca-param-exclusion-half-life-b3d7ae

Conversation

@billdenney

Copy link
Copy Markdown
Member

Fixes #270, fixes #508, fixes #539.

The problem (#508)

aucint.* reached across the doses on either side of the interval to find the concentration at the interval boundary. When a run of below-the-limit-of-quantification (BLQ) measurements sat before the next dose, clean.conc.blq() classified them as middle BLQ values — because the group's Tlast was in a later profile — and dropped them, so the interpolation ran up from Clast, through the BLQ region and over the next dose, to a measurement belonging to the next profile.

Minimal reproduction of the reported case — dosed at 0 and 24.1 hr, measurable through 7.59 hr, BLQ at 12/16/24 hr, measurable again at 24.654 hr, integrating 0–24 hr:

parameter before after
auclast 13.0 13.0
aucall 14.8 14.8
aucint.last 29.3 13.0
aucint.all 29.3 14.8
aucint.inf.obs 29.3 16.9
aucint.last.dose 19.6 (retired)
aucint.all.dose 19.6 (retired)
aucint.inf.obs.dose 16.9 (retired)

What changed

The doses bound the profile. The profile being integrated ends at the first dose at or after end; a concentration measured after that dose belongs to a later profile and is neither integrated nor used to interpolate into this interval. The dose that starts the interval is not a bound in the same way — the trough at the start of a later dosing interval is still estimated from the profile before it, which interp.extrap.conc.dose() does without interpolating across the dose.

Each dose within the interval starts a new profile. The interval asks for the profiles on both sides of an interior dose to be integrated together, but the dose is a point to integrate to, the concentrations are cleaned one profile at a time, and the integration method is chosen one profile at a time. So the region after each profile's Tlast is handled by the AUC type within that profile, and an AUC over several dosing intervals is the sum of the AUCs over each of them.

The region after Tlast is handled the way the matching AUC parameter handles it, which is what the AUC vignette has always promised:

  • aucint.last → contributes zero, so it equals auclast over a dosing interval
  • aucint.all → the triangle from Clast to that profile's first BLQ measurement, so it equals aucall
  • aucint.inf.obs / aucint.inf.pred → extrapolated with the half-life, always logarithmically; over a finite interval with no estimable half-life it falls back to AUCall (issue AUCINT differences with mixed BLQ and above LOQ time points #508, item 3). Over an infinite interval it stays NA, since there is no bounded tail to fall back to.

Verified on q12h dosing with doses at 0/12/24/36, both with a pre-dose sample at every dose and with the 12 and 24 hr troughs missing, for intervals aligned with the doses and offset from them (6–30 hr):

0–12 12–24 24–36 0–36
aucint.last (troughs missing) 37.05 43.65 75.22 155.92 = the sum

Dose awareness is the only method (#539). Every aucint/aumcint parameter now takes the dose times, with a fallback to the previous whole-profile interpolation when no dosing data are given. The eight .dose parameters are retired; an interval specification asking for one gets an error naming its replacement, the same way f points at f.obs:

The following columns in 'intervals' are not allowed: aucint.last.dose
  'aucint.last.dose' is now named 'aucint.last'.

The method (PPANMETH) column reports both halves of #539's second ask:

AUC: lin up/log down. Interpolation: dose-aware. Extrapolation: half-life
AUC: lin up/log down. Interpolation: not dose-aware (no dosing data). Extrapolation: none
AUC: lin up/log down. Interpolation: dose-aware. Extrapolation: AUCall (half-life not estimable)

A half-life exclusion only reaches an AUCint that used the half-life (#270). An interval ending at or before Tlast is interpolated throughout, so lambda.z never enters the result. Those results are no longer excluded with the half-life, and they are calculated at all when the half-life is not estimable (which also revises #450). exclude_nca_span.ratio(), exclude_nca_min.hl.r.squared(), and exclude_nca_min.hl.adj.r.squared() read the extrapolation from the method column to decide, and keep the exclusion for any result that does not report one.

For a profile with Tlast = 24 and the half-life excluded for a low span ratio:

interval parameter exclude
0–12 aucint.inf.obs (none — interpolation only)
0–36 aucint.inf.obs span.ratio < 5

Bugs found in the dose-aware code

All were reachable before but are load-bearing now that dose awareness is the default:

  • The route and duration of dosing never reached interp.extrap.conc.dose(), because the pk.calc.aucint.* wrappers had no route or duration.dose formals for formalsmap to fill. Every dose was treated as extravascular, so an intravenous bolus got no C0 back-extrapolation.
  • iecd_extrap_value() passed a single concentration to extrapolate.conc(), so its AUCall branch could never find the first BLQ measurement after Tlast and always returned zero. It now passes the run of measurements back to the dose that started them.
  • iecd_extrap_value() also used the caller's clast when extrapolating the trough before a dose, so the trough at the start of a dosing interval was computed from the Clast of the interval being calculated rather than from the Clast of the profile it decays from. The caller's clast (which is clast.pred for AUCinf,pred) now applies only when the run of measurements ends at the overall Tlast.
  • iecd_interp_value() called interpolate.conc(), which aborts past Tlast. With a profile's trailing BLQ values now retained, both surrounding concentrations can be past Tlast, which is extrapolation; it uses interp.extrap.conc() instead.

Notes

  • The aucint values above are a user-visible change to existing analyses with dosing data. Analyses without dosing data are unaffected.
  • AUCINTD, AUCINTAD, AUCINTID, and AUCINTPD go away with the .dose parameters. AUCINTD in CDISC is "AUC from T1 to T2 Normalized by Dose", which is not what aucint.last.dose calculated.

🤖 Generated with Claude Code

billdenney and others added 4 commits August 31, 2026 17:16
… is used

AUCint reached across the doses on either side of the interval, so a
concentration measured after the next dose was interpolated back into the
interval.  With a run of below-the-limit-of-quantification measurements
before that dose the interpolation ran up through them, and AUCint came out
far larger than AUClast over the same interval.

The doses around the interval now bound the profile being integrated.  Doing
that before cleaning also lets clean.conc.blq() see the trailing BLQ values
of the profile as trailing rather than as middle values, which is what lets
the region after Tlast be handled the way the matching AUC parameter handles
it: AUClast contributes zero, AUCall draws its triangle to the first BLQ
measurement, and AUCinf extrapolates with the half-life (falling back to
AUCall over a finite interval when the half-life is not estimable).

Dose awareness is now the only method, with a fallback to the previous
whole-profile interpolation when no dosing data are given, so the `.dose`
parameters are retired and an interval specification asking for one gets an
error naming its replacement.  The method column reports which interpolation
and which extrapolation were used.

Because AUCint only uses the half-life when the interval reaches past Tlast,
the half-life exclusions no longer reach an AUCint that merely interpolated,
and aucint.inf.obs/aucint.inf.pred are calculated over such an interval even
when the half-life is not estimable.

Two bugs in the dose-aware interpolation surfaced along the way: it assumed
every dose was extravascular because the route and duration never reached it,
and its AUCall extrapolation could not see the first BLQ measurement after
Tlast because only one concentration was passed to extrapolate.conc().

Fixes #270
Fixes #508
Fixes #539

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An interval that spans more than one dose was integrated against a single
Tlast, so a profile whose tail was not measured had the area under a
concentration extrapolated down to the next dose counted against it -- the
area that AUClast leaves out -- while the same gap at the end of the interval
contributed nothing.  An AUC over several dosing intervals was therefore not
the sum of the AUCs over each of them.

The integration method is now chosen one profile at a time, and the
concentrations are cleaned one profile at a time so that a value below the
limit of quantification which trails its own profile is treated as trailing
rather than as a middle value of the interval (a middle value is dropped,
which drew that profile's AUCall triangle to the next dose instead of to it).

Two more corrections to the dose-aware code:  the dose that starts the
interval no longer bounds the data, so the trough at the start of a later
dosing interval can be extrapolated from the profile before it; and that
extrapolation uses that profile's own Clast rather than the Clast of the
interval being calculated, which is what `clast.pred` describes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An interval spanning more than one dose estimated a concentration at each dose
within it -- what the `.dose` parameters have always done -- and integrated to
that estimate.  For AUClast that estimate is zero, so a profile whose tail was
not measured had an unmeasured concentration placed in the middle of the
interval and the area under the decline down to it counted.

Only `start` and `end` are estimated now.  A dose within the interval is
integrated across using the concentrations measured on either side of it,
because the interval asks for the profiles on both sides of that dose to be
integrated together, and one Tlast applies to the whole interval.  An AUCint
over any interval therefore matches the AUC parameter it is named for over the
same interval.

The dose after the interval still bounds the profile, which is the fix for the
reported bug.

AUCall is AUClast when nothing was measured after Tlast:  without a measurement
below the limit of quantification to draw its triangle to, the concentration
estimated at the end of the interval must not become the point it is drawn to.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…clusion-half-life-b3d7ae

# Conflicts:
#	NEWS.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant