textmachine/eval/pkg7/seed_recall.py

100 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Полигон, пакет-7 (A3): recall боевого WHICH-канала против ПОДПИСАННОГО ВЛАДЕЛЬЦЕМ банка.
Вопрос, на который отвечает скрипт: из терминов, которые владелец УЖЕ подписал для этой книги и
которые физически встречаются в срезе, сколько боевой майнер предлагает сам?
Дисциплина замера (иначе цифра врёт в свою пользу):
- знаменатель — только те термины сида, что РЕАЛЬНО встречаются в срезе (иначе меряем не recall,
а длину среза);
- майнер прогоняется с ПУСТЫМ сидом, иначе он по построению исключает уже засеянное
(`miner_emit.go:134` — `seedSurfaces[c.Src]` → continue) и recall выйдет 0 по определению;
- засчитывается и попадание в алиас-кластер: движок считает такой кандидат «алиасом
существующего» и владелец видит его на стопе. Обе цифры печатаются раздельно.
- ключи нормализуются тем же способом, что и в движке (trad→simp, NFKC, lower) — тут повторено
ЧАСТИЧНО (NFKC+casefold): полный `text.NormalizeSourceKey` — Go. Расхождение помечается.
$0, stdlib + PyYAML.
"""
from __future__ import annotations
import argparse
import json
import sys
import unicodedata
from pathlib import Path
import yaml
def nk(s: str) -> str:
return unicodedata.normalize("NFKC", s).casefold().strip()
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--seed", required=True, type=Path)
ap.add_argument("--chunks", required=True, type=Path)
ap.add_argument("--mined-tsv", required=True, type=Path)
ap.add_argument("--out", type=Path)
a = ap.parse_args()
doc = yaml.safe_load(a.seed.read_text(encoding="utf-8")) or {}
terms = doc.get("terms") or []
text = "\n".join(json.loads(l)["source"] for l in a.chunks.open(encoding="utf-8") if l.strip())
mined_primary, mined_all = set(), set()
for i, line in enumerate(a.mined_tsv.open(encoding="utf-8")):
if i == 0:
continue
f = line.rstrip("\n").split("\t")
if not f or not f[0]:
continue
mined_primary.add(nk(f[0]))
mined_all.add(nk(f[0]))
if len(f) > 4 and f[4]:
mined_all.update(nk(x) for x in f[4].split("|") if x)
rows = []
for t in terms:
src = t.get("src") or ""
surfaces = [src] + [al.get("alias", "") for al in (t.get("aliases") or [])]
present = [s for s in surfaces if s and s in text]
if not present:
continue
keys = {nk(s) for s in surfaces if s}
rows.append({
"src": src, "type": t.get("type", ""), "dst": t.get("dst", ""),
"status": t.get("status", ""),
"occurrences": sum(text.count(s) for s in present),
"hit_primary": bool(keys & mined_primary),
"hit_any": bool(keys & mined_all),
})
n = len(rows)
hp = sum(r["hit_primary"] for r in rows)
ha = sum(r["hit_any"] for r in rows)
bytype: dict[str, list[int]] = {}
for r in rows:
b = bytype.setdefault(r["type"] or "", [0, 0])
b[0] += 1
b[1] += int(r["hit_any"])
print(f"термов сида, встречающихся в срезе: {n}")
print(f" предложены майнером как ПЕРВИЧНЫЙ терм: {hp} recall={hp / n:.3f}" if n else "")
print(f" предложены как терм ИЛИ алиас: {ha} recall={ha / n:.3f}" if n else "")
print("по типам (знаменатель / попало):")
for k in sorted(bytype):
c, h = bytype[k]
print(f" {k:8s} {h:3d}/{c:<3d} = {h / c:.3f}")
print("\nНЕ предложенные (топ-25 по частоте):")
for r in sorted((r for r in rows if not r["hit_any"]), key=lambda r: -r["occurrences"])[:25]:
print(f" {r['src']}\t{r['type']}\t{r['occurrences']}×\t{r['dst']}")
if a.out:
a.out.write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())