Source code for conllu_tools.evaluation.base

"""Base classes for CoNLL-U evaluation."""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    import conllu


[docs] class UDError(Exception): """Raised when there is an error in the UD data or evaluation process."""
[docs] @dataclass(frozen=True) class UDSpan: """Represents a span (start and end position) in the character array.""" start: int end: int # Note: end marks the first position AFTER the end
[docs] @dataclass class UDWord: """Represents a word with its span and CoNLL-U token.""" span: UDSpan token: conllu.Token is_multiword: bool # True if this word is part of a multi-word token enhanced_deps: list[tuple[int | UDWord, list[str]]] | None = None # Processed enhanced dependencies functional_children: list[UDWord] | None = None # List of functional children for MLAS
[docs] def __hash__(self) -> int: """Make UDWord hashable for use in dictionaries.""" return hash((self.span, id(self.token), self.is_multiword))
[docs] @dataclass class AlignmentWord: """Represents an aligned pair of gold and system words.""" gold_word: UDWord system_word: UDWord
[docs] class Alignment: """Represents the alignment between gold and system words."""
[docs] def __init__(self, gold_words: list[UDWord], system_words: list[UDWord]) -> None: """Initialize alignment. Args: gold_words: List of gold words system_words: List of system words """ self.gold_words = gold_words self.system_words = system_words self.matched_words: list[AlignmentWord] = [] self.matched_words_map: dict[UDWord, UDWord] = {}
[docs] def append_aligned_words(self, gold_word: UDWord, system_word: UDWord) -> None: """Add an aligned word pair. Args: gold_word: Gold word system_word: System word """ self.matched_words.append(AlignmentWord(gold_word, system_word)) self.matched_words_map[system_word] = gold_word
[docs] @dataclass class Score: """Represents evaluation scores for a particular metric.""" gold_total: int | None system_total: int | None correct: int | None aligned_total: int | None = None @property def precision(self) -> float: """Calculate precision.""" if self.system_total and self.system_total > 0: return self.correct / self.system_total if self.correct is not None else 0.0 return 0.0 @property def recall(self) -> float: """Calculate recall.""" if self.gold_total and self.gold_total > 0: return self.correct / self.gold_total if self.correct is not None else 0.0 return 0.0 @property def f1(self) -> float: """Calculate F1 score.""" if self.system_total and self.gold_total and self.correct is not None: total = self.system_total + self.gold_total if total > 0: return 2 * self.correct / total return 0.0 @property def aligned_accuracy(self) -> float | None: """Calculate aligned accuracy.""" if self.aligned_total and self.aligned_total > 0 and self.correct is not None: return self.correct / self.aligned_total return None