| from __future__ import annotations | |
| import pytest | |
| from deberta_ime.evaluation import EvaluationItem, Prediction, evaluate_predictions | |
| def test_evaluation_uses_input_as_effective_output_when_prediction_abstains() -> None: | |
| report = evaluate_predictions( | |
| ( | |
| EvaluationItem( | |
| item_id="typo-1", | |
| input_text="犬でし", | |
| references=("犬です",), | |
| label="typo", | |
| ), | |
| ), | |
| (), | |
| candidate_limit=8, | |
| ) | |
| assert report["metrics"]["effective_acc_at_1"] == 0.0 | |
| assert report["metrics"]["mean_min_cer"] == pytest.approx(1 / 3) | |
| assert report["metrics"]["abstention_rate"] == 1.0 | |
| def test_overcorrection_requires_an_explicit_clean_label() -> None: | |
| clean = EvaluationItem( | |
| item_id="clean-1", | |
| input_text="猫です", | |
| references=("猫です",), | |
| label="clean", | |
| ) | |
| changed = Prediction(item_id="clean-1", candidates=("犬です",)) | |
| labelled = evaluate_predictions((clean,), (changed,), candidate_limit=8) | |
| unspecified = evaluate_predictions( | |
| ( | |
| EvaluationItem( | |
| item_id="clean-1", | |
| input_text="猫です", | |
| references=("猫です",), | |
| ), | |
| ), | |
| (changed,), | |
| candidate_limit=8, | |
| ) | |
| assert labelled["metrics"]["overcorrection_rate"] == 1.0 | |
| assert labelled["metrics"]["clean_rows"] == 1 | |
| assert unspecified["metrics"]["overcorrection_rate"] is None | |
| assert unspecified["metrics"]["clean_rows"] == 0 | |
| def test_evaluation_separates_candidate_recall_acceptance_and_quality_changes() -> None: | |
| items = ( | |
| EvaluationItem("clean", "猫です", ("猫です",), "clean"), | |
| EvaluationItem("typo", "犬でし", ("犬です",), "typo"), | |
| EvaluationItem("unknown", "鳥でし", ("鳥です",)), | |
| ) | |
| predictions = ( | |
| Prediction("clean", ("犬です", "猫です")), | |
| Prediction("typo", ("犬です",)), | |
| ) | |
| report = evaluate_predictions(items, predictions, candidate_limit=2) | |
| metrics = report["metrics"] | |
| assert metrics["candidate_recall_at_k"] == pytest.approx(2 / 3) | |
| assert metrics["accepted_rows"] == 2 | |
| assert metrics["accepted_accuracy"] == 0.5 | |
| assert metrics["typo_rows"] == 1 | |
| assert metrics["typo_accuracy"] == 1.0 | |
| assert metrics["improved_rows"] == 1 | |
| assert metrics["worsened_rows"] == 1 | |
| assert metrics["unchanged_rows"] == 1 | |
| def test_evaluation_reports_candidate_misses_selection_errors_and_provenance() -> None: | |
| items = ( | |
| EvaluationItem("generator-miss", "犬でし", ("犬です",), "typo"), | |
| EvaluationItem("selection-miss", "橋です", ("箸です",), "typo"), | |
| EvaluationItem("accepted", "猫でし", ("猫です",), "typo"), | |
| ) | |
| predictions = ( | |
| Prediction( | |
| "generator-miss", | |
| ("犬でした",), | |
| provenance="provider", | |
| reason="baseline_best", | |
| margin=0.0, | |
| ), | |
| Prediction( | |
| "selection-miss", | |
| ("橋です", "箸です"), | |
| provenance="deberta", | |
| reason="low_margin", | |
| margin=0.2, | |
| ), | |
| Prediction( | |
| "accepted", | |
| ("猫です",), | |
| provenance="deberta", | |
| reason="accepted", | |
| margin=1.2, | |
| ), | |
| ) | |
| metrics = evaluate_predictions(items, predictions, candidate_limit=8)["metrics"] | |
| assert metrics["accepted_candidate_miss_rows"] == 1 | |
| assert metrics["selection_error_rows"] == 1 | |
| assert metrics["declared_provenance_counts"] == {"deberta": 2, "provider": 1} | |
| assert metrics["selection_reason_counts"] == { | |
| "accepted": 1, | |
| "baseline_best": 1, | |
| "low_margin": 1, | |
| } | |
| assert metrics["reported_margin_rows"] == 3 | |
| assert metrics["mean_reported_margin"] == pytest.approx(1.4 / 3) | |
| def test_evaluation_rejects_ambiguous_or_unmatched_ids() -> None: | |
| item = EvaluationItem("one", "猫", ("猫",), "clean") | |
| with pytest.raises(ValueError, match="duplicate item_id"): | |
| evaluate_predictions((item, item), (), candidate_limit=8) | |
| with pytest.raises(ValueError, match="duplicate prediction item_id"): | |
| evaluate_predictions( | |
| (item,), | |
| (Prediction("one", ("猫",)), Prediction("one", ("猫",))), | |
| candidate_limit=8, | |
| ) | |
| with pytest.raises(ValueError, match="unknown item_id"): | |
| evaluate_predictions( | |
| (item,), | |
| (Prediction("other", ("猫",)),), | |
| candidate_limit=8, | |
| ) | |
| def test_clean_rows_must_name_the_input_as_an_acceptable_reference() -> None: | |
| with pytest.raises(ValueError, match="clean item input must be a reference"): | |
| evaluate_predictions( | |
| (EvaluationItem("bad-clean", "猫", ("犬",), "clean"),), | |
| (), | |
| candidate_limit=8, | |
| ) | |
| def test_public_evaluation_seam_rejects_empty_or_malformed_artifacts() -> None: | |
| with pytest.raises(ValueError, match="evaluation items must not be empty"): | |
| evaluate_predictions((), (), candidate_limit=8) | |
| with pytest.raises(ValueError, match="prediction item_id must not be empty"): | |
| Prediction("", ()) | |
| with pytest.raises(ValueError, match="prediction candidates must be unique"): | |
| Prediction("one", ("猫", "猫")) | |
| with pytest.raises(ValueError, match="prediction candidates must not contain empty"): | |
| Prediction("one", ("",)) | |