Repository navigation
feat(rescore): an out-of-fold prefilter before the classifier (rescore.prefilter_keep_decoys, default 0.35) - #210
Merged
Conversation
…e.prefilter_keep, opt-in) The classifier's cost grows with the pool, and on diaPASEF the pool is large (28.5 M PSMs on six HYE runs). rescore.prefilter_keep keeps that fraction of the competed PSMs: a cross-validated linear model on a few feature columns (rescore.prefilter_features, or a default list of agreement, co-elution, intensity, matched-count, retention-time, MS1 and peak-shape scores) is fitted per experiment, on the rescore's own fold keys: for each fold a logistic regression of the targets at train_fdr against the decoys of a 400,000-row sample of the other folds, refitted three times, and every PSM is scored by the model of the fold it is held out of. The top fraction is kept whatever the label; every per-row column is cut to it and the feature stream skips the dropped rows (an arrow filter per batch), so the classifier, the q values and the scored table see the kept rows only. When the prefilter cannot fit (no column, a non-finite value, too few confident targets) every PSM goes to the classifier with a warning. The report records the cut. 0 (default) changes nothing. Benchmark-gated: a cut on a model trained against the decoys must be checked with an entrapment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…coy share A fixed share of rows does not carry across acquisitions: keeping 35% of the PSMs cost 0.27% of the peptides on the six-run diaPASEF pool (33.6% of the decoys kept) but 0.4-1.0% on the six-run Astral pool (19.1% of the decoys kept), where most targets rank above most decoys. Keeping 50% on Astral (36.6% of the decoys) was neutral. prefilter_keep_decoys keeps the highest-scoring PSMs down to the one that brings in that fraction of the decoys, so the share of rows kept follows the pool. At most one of the two cuts may be set; 0 (default) is off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ements Drop rescore.prefilter_keep, the cut by a fixed share of rows: keeping 35% lost 0.75% of the peptides on six Astral runs, while the cut by decoy share (rescore.prefilter_keep_decoys) was neutral on every pool tested. One knob remains, still opt-in. docs/35 section 16 records the prefilter measurements (diaPASEF, Astral, Orbitrap HYE; HeLa species entrapment; single cells with the carbamidomethyl null) and the diaPASEF gate's validation on the 5-min HYE runs and the E. coli entrapment run. CHANGELOG entries for both. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
At 0.35 the prefilter kept every pool within 0.11% of its peptides at 1% (six HYE diaPASEF runs 98,412 against 98,524; six Astral runs 116,660 against 116,571 and one HYE Orbitrap run 63,053 against 62,960, means over three NN seeds), left the species-entrapment FDP of 22 HeLa runs at 1.81% against 1.79% and the carbamidomethyl null of 24 unalkylated single cells at 379 against 374, and cut the rescore's wall time by 41-45% and its peak memory by 54-60%. 0 restores the previous behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the smoke fixture's tag-prefilter arm the pool had 316 PSMs and one decoy. A cut at 35% of the decoys still keeps that one decoy, so it fell there and kept 96 targets; the target-decoy q floor (decoys + 1) / targets then sat at 1.04% and nothing passed 1%. The prefilter now needs at least rescore.min_train_decoys decoys, and 100 at least, the classifier's own minimum; below that every PSM goes to the classifier with a warning. ci/smoke.sh passes again (151 of 152 planted peptides). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under native_tda the prefilter raises sensitivity at an unchanged entrapment FDP: six Astral runs 96,840 -> 105,400 peptides at 1%, the 22 HeLa runs 197,636 -> 254,382 human precursors per run at a species-entrapment FDP of 2.01% against 2.08%, in 14:36 instead of 39:02. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged
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.
On a large pool the classifier is the rescore's cost, and most of what it scores is never identified.
rescore.prefilter_keep_decoys(default 0.35; 0 turns it off) removes that part before the full feature set is read.How it works
rescore.prefilter_featuresreplaces the list) is fitted per fold on at most 400,000 rows of the other folds: the targets attrain_fdragainst the decoys, refitted three times. Every PSM is scored by the model of the fold it is held out of, on the rescore's own fold keys.prefilter.Why decoys, not rows. A first version cut a fixed share of rows. Keeping 35% lost 0.27% of the peptides on diaPASEF but 0.75% on Astral, where the same 35% kept only 19% of the decoys. The loss followed the share of decoys kept on every pool, so only the decoy-share cut ships.
Measured at 0.35 (pooled rescores of stored competed tables, NN seeds 0-2 except where stated; docs/35 section 16):
The model sees the target/decoy labels, so the FDR was checked against nulls it does not see:
Default. 0.35, chosen by the user after the measurements above (fd7fbeb). 0 restores the previous behaviour. CLAUDE.md, docs/35 and the CHANGELOG record it.
Under
native_tda(one seed each; it is deterministic) the prefilter is also a sensitivity gain at an unchanged entrapment FDP, because the linear model fits the boundary far better without the weak bulk:Few-decoy guard (0e9ca88): a pool with fewer decoys than
rescore.min_train_decoys(100 at least) is not cut. On the smoke fixture's tag-prefilter arm, 1 decoy in 316 PSMs put the cut at that decoy and kept 96 targets. The q floor (decoys + 1) / targets then sat at 1.04%, so nothing passed.ci/smoke.shpasses again.Still running: the E. coli diaPASEF entrapment run (three seeds) with the prefilter.
The PR also records the diaPASEF gate's validation (from #206) in docs/35 and the CHANGELOG. On the 5-min HYE runs, gate 0.845 gives -1.75% peptides at -48% wall. On the E. coli entrapment run, the FDP is 0.32-0.35% against 0.30-0.38%.
Checked:
cargo fmt, clippy-D warnings, workspace tests,cargo doc -D warnings, config reference regenerated.🤖 Generated with Claude Code