-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscorer_audit.py
More file actions
126 lines (102 loc) · 3.88 KB
/
Copy pathscorer_audit.py
File metadata and controls
126 lines (102 loc) · 3.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
#!/usr/bin/env python3
"""Audit deterministic scorer outputs for RigidBench v3.1."""
from __future__ import annotations
import argparse
import json
import re
from pathlib import Path
def result_files(root: Path) -> list[Path]:
# Restrict to the manuscript's top-level model result folders, excluding
# archival runs under results/rigidbench_v3/.
files = sorted({p for p in root.glob("*/*rigidbench_v3_results.jsonl") if p.is_file()})
if not files:
raise FileNotFoundError(f"No rigidbench_v3_results.jsonl files under {root}")
return files
def model_slug(path: Path, root: Path) -> str:
try:
return path.relative_to(root).parts[0]
except ValueError:
return path.parent.name
def normalize_outcome(value: str | None) -> str:
if not value:
return "UNKNOWN"
text = value.strip().upper()
return {
"SEMANTIC_SUB": "SEM_SUB",
"PHONOLOGICAL_SUB": "PHO_SUB",
}.get(text, text)
def load_rows(root: Path) -> list[dict]:
rows: list[dict] = []
for path in result_files(root):
slug = model_slug(path, root)
with path.open("r", encoding="utf-8") as handle:
for line in handle:
if not line.strip():
continue
row = json.loads(line)
row["model_slug"] = slug
row["item_id"] = row.get("item_id") or row.get("triple_id")
row["outcome"] = normalize_outcome(row.get("outcome") or row.get("error_type"))
row["completion"] = row.get("completion") or row.get("raw_completion") or ""
rows.append(row)
return rows
def contains_expected_name(row: dict) -> bool:
name = row.get("proper_noun") or ""
if not name:
return False
pattern = rf"(?<![A-Za-z]){re.escape(name)}(?![A-Za-z])"
return bool(re.search(pattern, row.get("completion", ""), flags=re.IGNORECASE))
def print_case(row: dict) -> None:
print(f"item_id: {row.get('item_id')}")
print(f"model: {row.get('model_slug')}")
print(f"outcome: {row.get('outcome')}")
print(f"expected_name: {row.get('proper_noun', '')}")
print(f"matched_word: {row.get('matched_word', '')}")
print("completion:")
print(row.get("completion", ""))
print("-" * 80)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--results-root", default="results", help="Directory containing model result folders")
parser.add_argument(
"--max-discrepancies",
type=int,
default=0,
help="Maximum scorer/heuristic discrepancies to print; 0 prints all",
)
args = parser.parse_args()
rows = load_rows(Path(args.results_root))
print("SEM_SUB cases")
print("=" * 80)
for row in rows:
if row["outcome"] == "SEM_SUB":
print_case(row)
print("NOISE cases")
print("=" * 80)
for row in rows:
if row["outcome"] == "NOISE":
print_case(row)
agreements = 0
discrepancies: list[dict] = []
for row in rows:
scorer_preserved = row["outcome"] in {"PRES", "ALIAS_OK"}
heuristic_preserved = contains_expected_name(row)
if scorer_preserved == heuristic_preserved:
agreements += 1
else:
discrepancies.append(row)
agreement_rate = agreements / len(rows) if rows else 0.0
print("Scorer agreement summary")
print("=" * 80)
print(f"rows: {len(rows)}")
print(f"agreement_rate: {agreement_rate:.4f}")
print(f"discrepancies: {len(discrepancies)}")
if agreement_rate < 0.99:
print("FLAG: agreement below 99%; inspect discrepancies below.")
limit = len(discrepancies) if args.max_discrepancies == 0 else args.max_discrepancies
for row in discrepancies[:limit]:
print_case(row)
else:
print("PASS: agreement is at least 99%.")
if __name__ == "__main__":
main()