Skip to content

feat: carry gold span identity into the evaluator via annotation_span_id - #202

Draft
omri374 wants to merge 3 commits into
fix/binary-level-over-mergefrom
claude/pr-191-review-hjozei
Draft

omri374 wants to merge 3 commits into
fix/binary-level-over-mergefrom
claude/pr-191-review-hjozei

Conversation

@omri374

@omri374 omri374 commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Builds on #191. Where #191 keeps collapsed labels distinguishable through merge keys (finest-grained label per token), this carries exact entity instance identity into the evaluator, so gold span boundaries are recovered rather than inferred from label runs.

Approach

span_to_tag is the code that aligns spans to tokens, so it is also the source of identity: with return_span_ids=True it returns, alongside the per-token labels, the index of the span that produced each label (None for O). Ids refer to the caller's original span order and are threaded through overlap resolution, so a span split by a higher-score overlap keeps one identity, and a token's label and id cannot disagree by construction.

InputSample stores the ids as span_ids when tags are created from spans (create_tags_from_span=True, the path all JSON-loaded datasets take), and get_tags exposes them behind the same opt-in flag. BaseModel.predict_dataset carries them into an annotation_span_id column with a fixed 6-column schema: the column is always present, all None for samples whose tags were provided directly.

SpanEvaluator._merge_key_column prefers the span-id column over the merge-key column when it carries values for the sentence being processed, and _merge_keys_for delegates to it instead of duplicating the resolution. Everything downstream (run-splitting in _create_spans, the identity check in _merge_adjacent_spans) is unchanged; ids flow through the same plumbing as merge keys. CanonicalMapper needs no change: its level projections copy the DataFrame and only rewrite the label columns, so the id column passes through every level untouched.

What this fixes beyond #191

Merge keys distinguish entity types, not instances, so two adjacent entities of the same type were still merged, the limitation #191's description calls out as out of scope:

  • "She visited Paris , London" — two LOCATION gold spans stayed one span at every level. With ids they stay two.
  • Two same-type entities touching with no token between them were one label run and could not be split by merge keys.

Scope

Gold side only. batch_predict returns tags with no instance identity, so prediction spans keep the existing reconstruction (merge keys where present, visible labels otherwise). A prediction_span_id constant is defined and the evaluator resolves it symmetrically, so a source that knows its prediction boundaries can opt in later without evaluator changes.

Fallback chain per sentence: span ids where present with values, else merge keys, else visible labels. test_all_none_id_column_falls_back_to_merge_keys pins the middle step so an all-None id column cannot disable #191's fix.

Verification

  • 748 passed, 2 skipped (full suite), ruff check and format clean on changed files.
  • New tests: 5 in test_span_to_tag.py (id/tag alignment, None for O, distinct ids for adjacent same-type spans, split span keeps one id, default return unchanged), 3 in test_base_model.py (id column values, fixed schema with all-None column, mixed datasets), 5 in test_span_evaluator.py (TestSpanIdColumn: touching same-type entities split, skip-word-separated same-type entities split, same-id fragments still merge, binary level, merge-key fallback), 1 in tests/integration/test_data_objects.py (span_ids populated by create_tags_from_span).
  • The two notebook integration tests assert predict_dataset's exact column list, updated for the new column.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LJ4tF86qwabSqafCuTMZFM

predict_dataset still has access to InputSample.spans before span_to_tag
flattens them to per-token labels, so it now emits an annotation_span_id
column: the index of the gold span covering each token, None for O. The
column is only attached when at least one sample carries spans, keeping
the 5-column contract for span-less datasets.

SpanEvaluator prefers span ids over merge keys when reconstructing
annotation spans (per sentence, falling back when the column carries no
values). Ids identify entity instances rather than types, so two
adjacent same-type entities stay separate at every level, including the
same-type skip-word case merge keys could not distinguish. Prediction
spans keep the existing reconstruction. _merge_keys_for now delegates
column resolution to _merge_key_column instead of duplicating it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ4tF86qwabSqafCuTMZFM
returns None when neither is available, in which case callers compare
the visible label alone.
"""
from presidio_evaluator.entity_mapping.data_objects import ( # noqa: PLC0415

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this import live on the top?

Comment thread presidio_evaluator/models/base_model.py Outdated
:return: DataFrame with exactly 5 columns:
:return: DataFrame with columns:
sentence_id, token, annotation, prediction, start_indices
[, annotation_span_id]

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be empty if not computed, not missing. The API should have a fixed set of columns

Comment thread presidio_evaluator/models/base_model.py Outdated
sentence_id, token, annotation, prediction, start_indices
[, annotation_span_id]
"""
from presidio_evaluator.entity_mapping.data_objects import ( # noqa: PLC0415

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be on top?

Comment thread presidio_evaluator/models/base_model.py Outdated
entity type matches the token's label wins, so a token's label and its
span id never disagree.
"""
spans = sample.spans or []

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we have spans, do we need to iterate tags?

Comment thread presidio_evaluator/models/base_model.py Outdated
return df

@staticmethod
def _annotation_span_ids(sample: InputSample) -> list[int | None]:

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the span id live in span to tag?

…hema

Addresses review feedback on the draft PR:

- span_to_tag optionally returns per-token span ids alongside the labels
  (return_span_ids=True). Ids index the caller's original span list and
  are threaded through overlap resolution, so a span split by a
  higher-score overlap keeps one identity. This replaces the post-hoc
  overlap-matching helper in BaseModel: ids now come from the same code
  that aligns spans to tokens, and iterating tags to stay consistent
  with that alignment is no longer needed.
- InputSample stores the ids as span_ids when tags are created from
  spans; get_tags exposes them behind return_span_ids.
- predict_dataset has a fixed 6-column schema: annotation_span_id is
  always present, all None when the sample's tags were provided directly
  instead of derived from spans. The evaluator already treats a
  value-less id column as absent per sentence, so merge keys still apply.
- Imports of the shared column-name constants moved to module top in
  base_model and span_evaluator.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJ4tF86qwabSqafCuTMZFM
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.

2 participants