fix: make every AUCint dose-aware and exclude only when the half-life is used - #632
Open
billdenney wants to merge 4 commits into
Open
fix: make every AUCint dose-aware and exclude only when the half-life is used#632billdenney wants to merge 4 commits into
billdenney wants to merge 4 commits into
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
auclastaucallaucint.lastaucint.allaucint.inf.obsaucint.last.doseaucint.all.doseaucint.inf.obs.doseWhat 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, whichinterp.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 equalsauclastover a dosing intervalaucint.all→ the triangle from Clast to that profile's first BLQ measurement, so it equalsaucallaucint.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 staysNA, 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):
aucint.last(troughs missing)Dose awareness is the only method (#539). Every
aucint/aumcintparameter now takes the dose times, with a fallback to the previous whole-profile interpolation when no dosing data are given. The eight.doseparameters are retired; an interval specification asking for one gets an error naming its replacement, the same wayfpoints atf.obs:The method (
PPANMETH) column reports both halves of #539's second ask: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.znever 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(), andexclude_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 = 24and the half-life excluded for a low span ratio:aucint.inf.obsaucint.inf.obsspan.ratio < 5Bugs found in the dose-aware code
All were reachable before but are load-bearing now that dose awareness is the default:
interp.extrap.conc.dose(), because thepk.calc.aucint.*wrappers had norouteorduration.doseformals forformalsmapto fill. Every dose was treated as extravascular, so an intravenous bolus got no C0 back-extrapolation.iecd_extrap_value()passed a single concentration toextrapolate.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'sclastwhen 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'sclast(which isclast.predfor AUCinf,pred) now applies only when the run of measurements ends at the overall Tlast.iecd_interp_value()calledinterpolate.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 usesinterp.extrap.conc()instead.Notes
aucintvalues above are a user-visible change to existing analyses with dosing data. Analyses without dosing data are unaffected.AUCINTD,AUCINTAD,AUCINTID, andAUCINTPDgo away with the.doseparameters.AUCINTDin CDISC is "AUC from T1 to T2 Normalized by Dose", which is not whataucint.last.dosecalculated.🤖 Generated with Claude Code