Skip to content

A protein with no scoreable truth is not a false positive - #26

Merged
frapercan merged 1 commit into
mainfrom
fix/pk-masses-restricted-to-eligible-rows
Sep 1, 2026
Merged

frapercan merged 1 commit into
mainfrom
fix/pk-masses-restricted-to-eligible-rows

Conversation

@frapercan

Copy link
Copy Markdown
Owner

What was wrong

compute_confusion_matrix_exclude_sparse restricted coverage to eligible rows and left every mass summing over all of them:

tp_totals   = tp_at_tau.sum(axis=0)      # all rows
pred_totals = pred_at_tau.sum(axis=0)    # all rows
eligible_rows = ((gt_sub != 0) & toi_mask[None, :] & (~excluded_mask)).any(axis=1)
metrics[:, 0] = ((pred_at_tau > 0) & eligible_rows[:, None]).sum(axis=0)   # eligible only

A protein whose TOI ground truth was entirely already known in t0 has nothing left to score. normalize() drops it from every denominator, and its predictions still landed in fp. Every one of them is a false positive by construction, because no term it could emit would have counted. Micro precision was charged for proteins the evaluation was not measuring.

The fix

Restrict pred_totals to eligible rows. tp_totals is restricted with it: a no-op under the current invariant, since a column can only be a TP when it is in TOI and not excluded, so an ineligible row already contributes zero. It is included so the two totals share one population instead of resting on that invariant.

Measured, on the fixture this repo already carried

tests/test_pk_coverage_bug.py::_make_pk_scenario has exactly the case: protein 1 is ineligible and predicts terms 2 and 3, neither excluded for it, neither in its truth.

column before after
n 2 2
tp 2 2
fp 4 2
fn 5 5
macro precision 1 1
macro recall 5/6 5/6

Only fp moves. The new test pins all six so a later widening of the restriction cannot pass unnoticed.

Direction and size, for the record

This raises PK micro precision, so published PK figures were conservative and not optimistic. On the 2026-08-30 campaign run the excluded mass is 3,385 of 2,648,460 in BPO (0.128 %) and 576 of 214,491 in MFO (0.269 %), from ten and eight proteins respectively. Order 1e-4 on a base of 0.15.

The macro columns the external board reports do not move at all, since an ineligible protein already contributed zero to both.

It does change stored f_micro_w figures, which is why this was raised as a decision rather than applied as a patch.

Out of scope, flagged not changed

The dense fallback compute_confusion_matrix_exclude has neither this restriction nor the earlier coverage one. It is gated behind CAFAEVAL_SPARSE=0 for A/B comparison, raises NotImplementedError when a per_protein_sink is requested, and has produced no published number. Bringing it into line would change what the A/B measures, so it is left alone and named here.

Downstream

PROTEA's _run_cafa_grid_artifact.py refuses to write a namespace while this defect stands, and tests/test_per_protein_grid_producer.py:1306 is a canary that fails the day this lands. Both need updating in the same migration, leaf first: this merges, PROTEA repins the resolved commit, then the guard and the canary follow.

Checks

Full suite before and after on origin/main: identical, no failures. With this branch: 36 passed, 6 xfailed, 0 failed. ruff check . clean.

The PK kernel restricted coverage to eligible rows and left every mass
summing over all of them. A protein whose TOI ground truth was entirely
already known in t0 has nothing left to score, so normalize() drops it
from every denominator, and its predictions still landed in `fp`.

Every one of those predictions is a false positive by construction: there
is no term it could have emitted that would have counted. Micro precision
was therefore charged for proteins the evaluation was not measuring.

Restricting `pred_totals` to eligible rows is the whole fix. `tp_totals`
is restricted with it, which is a no-op under the current invariant (a
column can only be a TP when it is in TOI and not excluded, so an
ineligible row already contributes zero) and is done so the two totals
share one population rather than resting on that invariant.

Measured on the fixture this file already carried: FP at the low
threshold goes from 4 to 2 and every other column is unchanged, n=2,
tp=2, fn=5, macro precision 1, macro recall 5/6. The new test pins all
six so a later widening of the restriction cannot pass unnoticed.

The docstring of test_pk_precision_recall_unchanged_by_fix said the fix
touched only the `n` column. That was true of the coverage fix and is no
longer true, so it now names which columns each restriction moves and
why the rest cannot.

Direction and size on the campaign: this RAISES PK micro precision, so
published PK figures were conservative rather than optimistic. On the
2026-08-30 run the excluded mass is 3,385 of 2,648,460 in BPO and 576 of
214,491 in MFO, from ten and eight proteins, order 1e-4 on a base of
0.15. The macro columns the external board reports do not move at all.

The dense fallback compute_confusion_matrix_exclude has neither this
restriction nor the earlier coverage one. It is gated behind
CAFAEVAL_SPARSE=0 for A/B comparison, is not reachable with a
per_protein_sink, and has produced no published number, so it is left
alone here and flagged rather than changed.
@frapercan
frapercan merged commit e937e0e into main Sep 1, 2026
6 checks passed
@frapercan
frapercan deleted the fix/pk-masses-restricted-to-eligible-rows branch September 1, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant