Skip to content

Keep an occurrence's score up to date when its best prediction is re-scored, so masked occurrences stay visible - #1467

Draft
mihow wants to merge 1 commit into
mainfrom
fix/determination-score-refresh
Draft

mihow wants to merge 1 commit into
mainfrom
fix/determination-score-refresh

Conversation

@mihow

@mihow mihow commented Oct 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Projects that ran class masking (#999) can have occurrences that are hidden even though their best prediction now scores well. Class masking re-scores a classification under a new algorithm, and in most cases the winning taxon stays the same while the score goes up. The occurrence kept its old, lower score, because the determination refresh only recomputed the score when the taxon changed. With a project score threshold of 0.5, an occurrence whose masked winner scored 0.55 but whose stored score was still 0.38 vanished from the occurrence lists, and its project-scoped detail page returned 404. Occurrences whose taxon did change were fine.

This PR makes the occurrence's score follow its best identification or prediction whenever the score differs, not only when the taxon differs. After a class masking run, re-scored occurrences now stay visible under the default filters. It was found while reviewing #1461.

List of Changes

Change (user effect) How (implementation)
1. An occurrence's score is updated whenever its best prediction is re-scored, even when the winning taxon is unchanged, so masked occurrences are no longer hidden by a stale lower score. update_occurrence_determination picks the winning identification or prediction first and then compares the taxon and the score to the stored values separately, instead of computing both only inside the "taxon changed" branch. A score of exactly 0.0 is now also written (is not None instead of truthiness).
2. A human identification still wins over any prediction and keeps its own score; a later machine re-score does not overwrite it. Unchanged rule, now pinned by tests. Confirming the machine's taxon by hand now also replaces the machine score with the identification's score, which is what already happened when the human picked a different taxon.
3. Nothing changes when the winner and its score already match: the function returns False and does not save. Pinned by a test that checks the return value and that updated_at is untouched.
4. Tests cover the determination refresh for the first time. New TestOccurrenceDeterminationRefresh in ami/main/tests.py (five cases) and one end-to-end class masking case in ami/ml/post_processing/tests/test_class_masking.py. The stale @TODO Add tests note in the docstring is removed.

Related Issues

Relates to #999 (class masking) and #1461 (where the stale score was noticed during review).

Detailed Description

update_occurrence_determination in ami/main/models.py is called from Occurrence.save(), from Identification.save() and Identification.delete(), and from the pipeline results path. Before this change it looked like this:

if top_identification and top_identification.taxon and top_identification.taxon != current_determination:
    new_determination, new_score = top_identification.taxon, top_identification.score
elif not top_identification:
    top_prediction = occurrence.best_prediction
    if top_prediction and top_prediction.taxon and top_prediction.taxon != current_determination:
        new_determination, new_score = top_prediction.taxon, top_prediction.score

new_score was only set when the taxon differed, so a re-scored winner with the same taxon left determination_score untouched. The fix drops the != current_determination guard from the winner selection and keeps the per-field comparisons that already followed it. The Occurrence model on main caches only determination and determination_score, so no other field is affected.

Side effect worth knowing: an occurrence that a human confirms with the same taxon the machine already chose now gets the identification's score (1.0) instead of keeping the machine score. That matches what already happened when the human chose a different taxon, and matches Occurrence.get_determination_score().

How to Test the Changes

Automated:

docker compose run --rm django python manage.py test --noinput \
  ami.main.tests.TestOccurrenceDeterminationRefresh \
  ami.ml.post_processing.tests.test_class_masking

Before the fix, test_score_follows_a_rescored_prediction_with_the_same_taxon fails with False is not true (no update), and test_rescored_unchanged_winner_keeps_the_occurrence_visible fails with the stored score (about 0.44) not matching the masked score (about 0.73). Both pass with the fix. The full backend suite was run in a CI-style compose stack; 748 tests passed with 2 skipped.

Manual: on a project with a score threshold of 0.5, run class masking with a taxa list that excludes a class carrying real probability for some occurrences. Open an occurrence whose winner did not change: its score now equals the masked classification's score, and it stays listed when the threshold would otherwise hide the old score.

Deployment Notes

No migration. Existing occurrences that were masked before this fix still carry their old score until they are re-saved. There is no management command on main that recomputes determinations, and re-running the class masking job does not help because already-masked classifications are skipped. Until a command exists, an operator can refresh a project's occurrences from the Django shell. Re-saving an occurrence recomputes its determination and score in place:

from ami.main.models import Occurrence

for occurrence in Occurrence.objects.filter(project_id=PROJECT_ID).iterator(chunk_size=500):
    occurrence.save(update_determination=True)

Each save is a few queries, so expect it to take a while on a large project; run it from a worker host, not a web request. Adding a refresh_occurrence_determinations management command (project scoped, with a dry run) is a sensible follow-up.

Checklist

  • I have tested these changes appropriately.
  • I have added and/or modified relevant tests.
  • I updated relevant documentation or comments.
  • I have verified that this PR follows the project's coding standards.
  • Any dependent changes have already been merged to main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01L52AN9tabp76yjhjyCZkSJ

…ction is re-scored

`update_occurrence_determination` only recomputed `determination_score` inside
the branch where the winning identification's or prediction's taxon differed
from the current determination. Class masking creates a new terminal
classification that often keeps the same taxon with a different score, so the
occurrence kept its old, lower score. Under the project's default score
threshold such occurrences disappear from occurrence lists and their
project-scoped detail URL returns 404.

The function now picks the winning identification or prediction first and
compares each cached field to it separately, so the score follows the winner
even when the taxon is unchanged. A human identification still wins over any
prediction and keeps its own score. The no-op path still returns False
without saving.

Tests pin the same-taxon re-score, the taxon change, the no-op path, the
human identification rule, and an end-to-end class masking run whose
unchanged winner rises above the score threshold.

Found while reviewing #1461.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L52AN9tabp76yjhjyCZkSJ
@netlify

netlify Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit 5f79a8d
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/6ac08df1a64d060007f68721

@netlify

netlify Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for antenna-ssec canceled.

Name Link
🔨 Latest commit 5f79a8d
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/6ac08df182be5d0008d8c881

@coderabbitai

coderabbitai Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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