91 lines
4.1 KiB
Python
91 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
||
"""НЕЗАВИСИМЫЙ пере-счёт пробы C: свой парсер, своя нормализация, свой знаменатель.
|
||
|
||
Цель — не «повторить 25/45», а проверить число ДРУГИМ кодом. Ничего не импортирую из
|
||
`arbitrate.py`: если унаследованный парсер систематически ошибается, повтор его же логикой
|
||
это не покажет. Совпадение чисел = подтверждение; расхождение = дефект посылки пробы C.
|
||
"""
|
||
from __future__ import annotations
|
||
import json
|
||
import re
|
||
import sys
|
||
import unicodedata
|
||
from collections import Counter
|
||
from pathlib import Path
|
||
|
||
RAW = Path.home() / "books" / "gu-zhenren" / "bank-arbitration"
|
||
GOLDF = Path("/home/ubuntu/projects/textmachine/eval/bank_arbitration/gold/gold.jsonl")
|
||
|
||
|
||
def norm(s: str) -> str:
|
||
"""Нормализация цели: NFKC, регистр, ё→е, снять кавычки/дефисные варианты, схлопнуть пробелы."""
|
||
s = unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е")
|
||
s = re.sub(r"[«»\"'`]", "", s)
|
||
return " ".join(s.split())
|
||
|
||
|
||
def parse(tag: str) -> tuple[dict[str, str], float, dict]:
|
||
"""Свой парсер: строка = src <TAB> dst [<TAB> conf]. Табов нет — берём двойной пробел/'|'."""
|
||
out: dict[str, str] = {}
|
||
cost = 0.0
|
||
diag = Counter()
|
||
for f in sorted(RAW.glob(f"{tag}-b*.json")):
|
||
rec = json.loads(f.read_text(encoding="utf-8"))
|
||
cost += rec["cost_usd"]
|
||
for ln in rec["content"].split("\n"):
|
||
ln = ln.strip()
|
||
if not ln or ln.startswith("#") or ln.startswith("`"):
|
||
diag["skip_empty_or_comment"] += 1
|
||
continue
|
||
parts = [p.strip() for p in re.split(r"\t| {2,}|\s*\|\s*", ln) if p.strip()]
|
||
if len(parts) < 2:
|
||
diag["skip_lt2fields"] += 1
|
||
continue
|
||
src, dst = parts[0], parts[1]
|
||
# третье поле — уверенность (число или слово); в dst её быть не должно
|
||
if re.fullmatch(r"\d{1,3}", dst) and len(parts) >= 3:
|
||
diag["conf_in_second_field"] += 1
|
||
dst = parts[2]
|
||
if src in out and out[src] != dst:
|
||
diag["dup_conflict"] += 1
|
||
out[src] = dst
|
||
diag["parsed"] += 1
|
||
return out, cost, dict(diag)
|
||
|
||
|
||
def main(tags: list[str]) -> None:
|
||
gold = [json.loads(l) for l in GOLDF.read_text(encoding="utf-8").splitlines() if l.strip()]
|
||
scored = [g for g in gold if g.get("gold_dst")]
|
||
print(f"голд: строк {len(gold)}, с подписанным gold_dst {len(scored)}, "
|
||
f"из них in_bank {sum(1 for g in scored if g.get('in_bank'))}")
|
||
print()
|
||
hdr = f"{'пасс':10s} {'строк':>6s} {'точно':>6s} {'+флексии':>9s} {'нет ответа':>11s} {'$':>9s}"
|
||
print(hdr)
|
||
print("-" * len(hdr))
|
||
for tag in tags:
|
||
got, cost, diag = parse(tag)
|
||
exact = infl = missing = 0
|
||
misses = []
|
||
for g in scored:
|
||
src = g["src"]
|
||
if src not in got:
|
||
missing += 1
|
||
continue
|
||
d = norm(got[src])
|
||
if d == norm(g["gold_dst"]):
|
||
exact += 1
|
||
elif any(d == norm(a) for a in g.get("gold_alt", [])):
|
||
infl += 1
|
||
else:
|
||
misses.append((src, got[src], g["gold_dst"]))
|
||
print(f"{tag:10s} {len(got):6d} {exact:6d} {exact + infl:9d} {missing:11d} ${cost:8.6f}")
|
||
if "-v" in sys.argv:
|
||
print(f" диагностика парсера: {diag}")
|
||
for m in misses[:8]:
|
||
print(f" мимо: {m[0]} → «{m[1]}» (голд «{m[2]}»)")
|
||
print(f"\nзнаменатель для счёта: {len(scored)}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main([a for a in sys.argv[1:] if not a.startswith("-")] or
|
||
["glm1", "gro1", "mis1", "ds1", "fdp", "flu", "fte"])
|