from ingestion.segment.models import Monograph from ingestion.validation.back_index import GroundTruthEntry from ingestion.validation.metrics import compute_recall_precision def _mono(drug_id, drug_name, start_physical): return Monograph(drug_id=drug_id, drug_name=drug_name, source_page_range=[start_physical, start_physical + 1]) def test_perfect_match_recall_and_precision_are_one(): monographs = [_mono("abacavir", "ABACAVIR", 100)] ground_truth = [GroundTruthEntry(name="Abacavir", printed_page=101)] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 1.0 assert result.precision == 1.0 assert result.matched_count == 1 def test_missed_ground_truth_entry_lowers_recall_not_precision(): monographs = [_mono("abacavir", "ABACAVIR", 100)] ground_truth = [ GroundTruthEntry(name="Abacavir", printed_page=101), GroundTruthEntry(name="Acarbose", printed_page=103), ] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 0.5 assert result.precision == 1.0 assert len(result.unmatched_ground_truth) == 1 assert result.unmatched_ground_truth[0].name == "Acarbose" def test_spurious_detected_monograph_lowers_precision_not_recall(): monographs = [ _mono("abacavir", "ABACAVIR", 100), _mono("cac_chuyen_luan_thuoc", "CÁC CHUYÊN LUẬN THUỐC", 98), ] ground_truth = [GroundTruthEntry(name="Abacavir", printed_page=101)] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 1.0 assert result.precision == 0.5 assert len(result.unmatched_detected) == 1 def test_page_tolerance_allows_small_offset(): monographs = [_mono("abacavir", "ABACAVIR", 100)] ground_truth = [GroundTruthEntry(name="Abacavir", printed_page=103)] # +2 tolerance result = compute_recall_precision(monographs, ground_truth) assert result.recall == 1.0 def test_page_beyond_tolerance_does_not_match(): monographs = [_mono("abacavir", "ABACAVIR", 100)] ground_truth = [GroundTruthEntry(name="Abacavir", printed_page=110)] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 0.0 def test_qualifier_suffixed_name_still_matches_base_ground_truth_name(): # SALBUTAMOL (Dùng trong hô hấp) should still match a ground-truth # entry that just says "Salbutamol" monographs = [_mono("salbutamol_dung_trong_ho_hap", "SALBUTAMOL (Dùng trong hô hấp)", 1261)] ground_truth = [GroundTruthEntry(name="Salbutamol", printed_page=1262)] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 1.0 def test_empty_ground_truth_gives_zero_recall_not_error(): result = compute_recall_precision([_mono("a", "A", 1)], []) assert result.recall == 0.0 def test_empty_monographs_gives_zero_precision_not_error(): result = compute_recall_precision([], [GroundTruthEntry(name="A", printed_page=1)]) assert result.precision == 0.0 assert result.recall == 0.0 def test_exact_match_preferred_over_substring_steal_confirmed_real_case(): # Confirmed real case from a whole-book `cli validate` run: "ISOSORBID" # and "ISOSORBID DINITRAT" are two distinct, correctly-segmented # monographs a page apart. A pure substring match lets the shorter name # "steal" both ground-truth entries (it's a substring of the longer one # too) via `next()`'s order-dependent first match, leaving the real # "ISOSORBID DINITRAT" monograph spuriously unmatched even though an # exact match for it exists. monographs = [ _mono("isosorbid", "ISOSORBID", 844), _mono("isosorbid_dinitrat", "ISOSORBID DINITRAT", 845), ] ground_truth = [ GroundTruthEntry(name="Isosorbid", printed_page=845), GroundTruthEntry(name="Isosorbid dinitrat", printed_page=846), ] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 1.0 assert result.precision == 1.0 assert len(result.unmatched_detected) == 0 def test_double_space_in_detected_name_still_matches_confirmed_real_case(): # confirmed real case from a whole-book `cli validate` run: "ALVERIN # CITRAT" (double space) failed to match ground truth's single-spaced # "Alverin citrat" under plain strip+upper comparison. monographs = [_mono("alverin_citrat", "ALVERIN CITRAT", 171)] ground_truth = [GroundTruthEntry(name="Alverin citrat", printed_page=172)] result = compute_recall_precision(monographs, ground_truth) assert result.recall == 1.0