Add predict-table command to annotate CSVs with predictor scores (#231) - #234
Merged
Conversation
Downstream evaluation workflows often start from an annotated benchmark table
(sample_id, hit, peptide, genotype/allele columns) and just need external
predictor scores appended. Add a general, additive way to do that.
Library core (mhctools/annotate.py):
* annotate_table(df, specs, peptide_column, allele_column, ...) -> DataFrame.
Runs each predictor ONCE over the union of (peptide, allele) pairs, then a
per-row lookup picks the best allele. Preserves all input columns; appends
one score column per predictor plus a <col>_best_allele provenance column.
* "Best" reuses mhctools.pred.best_direction: score higher-better, affinity
and percentile_rank lower-better.
* Row alleles are normalized with normalize_allele_name_or_raw, so "A0201"
matches a prediction emitted as "HLA-A*02:01" and exotic un-normalizable
alleles round-trip (#220). Multiple alleles per cell (whitespace/comma/
semicolon) are supported.
* AnnotationSpec accepts a built predictor or an alleles->predictor factory,
so commandline predictors are constructed with exactly the table's alleles.
* parse_annotation_spec("NAME:COLUMN:FIELD") builds a spec from the CLI
registry; COLUMN and FIELD default to NAME_FIELD and "affinity".
CLI (mhctools/cli/annotate_table.py): thin I/O wrapper exposed as the
`mhctools predict-table` subcommand (dispatched from cli/script.main). Reads
CSV/CSV.bz2, writes the annotated table, and optionally a --predictor-info
sidecar (predictor, output_column, score_field, higher_is_better).
Exports annotate_table / AnnotationSpec / parse_annotation_spec from the
package. Binary-free tests use a deterministic fixture predictor to assert
direction handling, best-allele selection, column preservation, collision/
overwrite, NaN fallbacks, and spec parsing (added to the CI public subset).
Version 3.20.1 -> 3.21.0.
Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
…ew fixes) Fixes two issues from the #234 review: * Cross-spec output-column collision: the pre-run check only compared each spec against the *existing* input columns, so two specs targeting the same output (or best-allele) column passed and then silently clobbered each other. Now also reject a column planned by more than one spec, regardless of `overwrite` (overwrite replaces an existing column; it can't make two specs coexist in one column). * Peptide join was exact-string: a predictor that upper-cases/strips the peptides it echoes, against a table written lower-case or with stray whitespace, produced silent all-NaN. Peptides are now stripped + upper-cased on both the union sent to the predictor and the per-row lookup key (amino-acid sequences are canonically upper-case, so nothing is lost; the input peptide column is preserved verbatim). Also make the allele-free (by-peptide) fallback fire whenever the (peptide, allele) lookup misses, not only for rows with no alleles, so a processing/allele-free predictor still fills in scores when the table happens to carry an allele column. Only allele-free predictors populate the by-peptide index, so this never rescues a binding predictor's unsupported-allele miss (covered by a regression test). Adds 9 tests: cross-spec duplicate output/best-allele/cross-name collisions (incl. under overwrite), case- and whitespace-insensitive peptide matching with the input column preserved, the allele-free by-peptide path (with and without an allele column), and the binding-predictor unsupported-allele NaN regression guard. 33 passing. Claude-Session: https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG
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.
Closes #231.
Adds a general, additive way to annotate an existing table with external predictor scores — no schema change for current users, no MHCflurry-specific assumptions.
Motivation
Downstream evaluation workflows (e.g. MHCflurry paper-figure generation) often start from an annotated benchmark table with columns like
sample_id,hit,sample_group,peptide, and per-row genotype/allele info, and just need external predictor outputs (NetMHCpan, MixMHCpred, ...) appended as score columns. Until now callers needed a bespoke adapter to join mhctools' long CSV back to their table. This makes it a first-class feature.What it does
mhctools predict-tablereads a CSV, runs each requested predictor once, and appends one score column per predictor — choosing the best allele per row — while preserving every input column unchanged:mhctools predict-table \ --input benchmark.multiallelic.csv.bz2 \ --peptide-column peptide \ --alleles-column hla \ --predictor netmhcpan42-ba:netmhcpan4.2.ba:affinity \ --predictor netmhcpan42-el:netmhcpan4.2.el:score \ --out benchmark.with_external.csv.bz2Each
--predictorspec isNAME[:OUTPUT_COLUMN[:FIELD]](column defaults toNAME_FIELD, field defaults toaffinity).FIELDisaffinity,score, orpercentile_rank— matching the issue's requested direction rules:affinityandpercentile_rankscoreFor multi-allele rows (several alleles per cell, whitespace/comma/semicolon-separated) the best allele per peptide is chosen and recorded in a
<OUTPUT_COLUMN>_best_alleleprovenance column.--predictor-info info.csvwrites the optional sidecar the issue suggested (predictor,output_column,score_field,higher_is_better).Design
annotate_table(df, specs, peptide_column, allele_column, ...) -> DataFrame, unit-testable with no binaries; the CLI is a thin CSV read/write wrapper. Matches mhctools' "there's a DataFrame variant of everything" convention.(peptide, allele)pairs across all rows (leverages the predictors' allele batching from Batch alleles per netMHCpan process for large speedups #218), then a per-row lookup reduces to the best allele. Predictors are not re-invoked per row.mhctools.pred.best_direction, and row alleles are normalized withnormalize_allele_name_or_raw(the Some non-human alleles netMHCpan lists (e.g. H-2-Qa1, BoLA-amani.1) can't be requested #220 helper), so a cell writtenA0201matches a prediction emitted asHLA-A*02:01, and exotic un-normalizable alleles round-trip.AnnotationSpecaccepts either a built predictor or analleles -> predictorfactory, so commandline predictors get constructed with exactly the alleles the table needs.mhctools --output-csvlong schema is untouched;predict-tableis a new subcommand dispatched fromcli/script.main.Behavior notes / limitations
UnsupportedAlleleat construction otherwise — consistent with mhctools elsewhere). Rows whose(peptide, allele)a predictor simply drops getNaN.affinitypinspMHC_affinity.value;score/percentile_rankare kind-agnostic. If a single predictor emitted multiple kinds that both carryscore, the kind-agnostic fields don't disambiguate between them — the kind-pinned aliases (presentation,stability, ...) are the escape hatch. The documented predictors (BA→affinity, EL→score) are single-kind, so this doesn't arise in practice.Tests
tests/test_annotate_table.py(added to the CI public subset) is binary-free: a deterministic fixture predictor lets it assert exact best-allele selection and direction handling (a case where the best allele byaffinitydiffers from the best byscore), plus column preservation/order, no-input-mutation, multi-allele cell splitting + normalization, allele-free path, collision/overwrite,NaNfallbacks, custom provenance column, and spec parsing. ARandomBindingPredictorsmoke test covers the real predict path; the CLI round-trip (dispatch, multi-predictor, sidecar) was verified end-to-end with therandompredictor.Local: 413 passed, 3 skipped (TULIP e2e). Version
3.20.1 → 3.21.0.https://claude.ai/code/session_01LZahFhBSCiehXTESCYQ7wG