Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,14 @@ Initial alpha release.

- Add `Span` object to represent a span annotation
- Add `DocSpans` object to represent a set of span annotations for a document
- Add library `spans.match` of methods for checking for span-level matches
- Add library `span_utils` of methods for matching and scoring `Span` objects

### Alignment logic
### Alignment Logic

- Add `SpanAlignment` object to represent an alignment between two sets of span annotations
- Add library `align` of methods for aligning two sets of span annotations

## Computing Evaluation Metrics

- Add library `eval` of methods for computing the evaluation metrics for a `SpanAlignment`
- Add script `compute_metrics` for computing document- and entity-level aggregated evaluation metrics
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules"
]
requires-python = ">=3.12"
dependencies = []
dependencies = [
"orjsonl>=1.0.0",
"tqdm>=4.70.0",
]

[dependency-groups]
dev = [
Expand Down
75 changes: 72 additions & 3 deletions src/spanerr/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@
numeric score (float). It's used to measure the quality of a match.
"""

from collections.abc import Callable

from spanerr.core import (
CheckSpanPair,
DocSpans,
ScoreSpanPair,
Span,
SpanAlignment,
)
from spanerr.spans.match import partial_overlap
from spanerr.span_utils import CheckSpanPair, ScoreSpanPair, partial_overlap

# Custom function type
AlignSpans = Callable[[DocSpans, DocSpans], SpanAlignment]


def select_first_match(
Expand Down Expand Up @@ -122,3 +125,69 @@ def corppa_align(
final_mapping[ref_span] = [sub_span]
final_sys = DocSpans(sys.doc_id, final_sys_spans)
return SpanAlignment(init_align.ref, final_sys, final_mapping)


def construct_aligner(
strategy: str,
is_match: CheckSpanPair | None = None,
score_match: ScoreSpanPair | None = None,
exclusive: bool | None = None,
) -> AlignSpans:
"""
Construct a span alignment method (AlignSpans) using a given alignment strategy

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Somewhere (probably not here) it would be helpful to have a note explaining why the lambda approach - once I finished reading through the function I saw how many variations there are, which I presume is the motivation. It would be helpful to have that explained at a higher level first - maybe a file- level docstring? (Or maybe a design doc if you add some version of that to the code)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I'll open an issue for tracking this.

and accompanying parameters.

Currently supports the following strategies:
- select_first: corresponds to select_first_match
- select_best: corresponds to select_best_match
- corppa: corresponds to corppa_align
"""
match strategy:
case "select_first":
# Validate input parameters
if is_match is None:
raise ValueError(f"Strategy {strategy} requires is_match parameter")
if score_match is not None:
raise ValueError(
f"Strategy {strategy} does not use score_match parameter"
)
# Construct aligner
if exclusive is None:
return lambda r, s: select_first_match(r, s, is_match)
else:
return lambda r, s: select_first_match(
r, s, is_match, exclusive=exclusive
)
case "select_best":
# Validate input parameters
if is_match is None or score_match is None:
raise ValueError(
f"Strategy {strategy} requires is_match and score_match parameters"
)
# Construct aligner
if exclusive is None:
return lambda r, s: select_best_match(r, s, is_match, score_match)
else:
return lambda r, s: select_best_match(
r, s, is_match, score_match, exclusive=exclusive
)

case "corppa":
# Validate input parameters
if exclusive is not None:
raise ValueError(
f"Strategy {strategy} does not use exclusive parameter"
)
# Construct aligner
if is_match is not None and score_match is not None:
return lambda r, s: corppa_align(
r, s, is_match=is_match, score_match=score_match
)
elif is_match is not None:
return lambda r, s: corppa_align(r, s, is_match=is_match)
elif score_match is not None:
return lambda r, s: corppa_align(r, s, score_match=score_match)
else:
return lambda r, s: corppa_align(r, s)
case _:
raise ValueError(f"Unknown alignment strategy: {strategy}")
Loading