133 lines
6.8 KiB
Python
133 lines
6.8 KiB
Python
#!/usr/bin/env python3
|
||
"""Полигон, пакет-8 (замер 2): доля выдуманных dst на мусоре — счёт ИЗ СЫРЬЯ.
|
||
|
||
Разбор ответов делает БОЕВОЙ `terminology.ParseReply` через `termharness -mode parse`, а не реплика:
|
||
именно парсер решает, что считается ответом, что отказом ⟦TM-NO-DST⟧, а что «плохой строкой»
|
||
(эхо исходника, кривая лемма). Реплика тут расходилась бы с продом ровно там, где это важнее всего.
|
||
|
||
Пороги названы в §0.3 отчёта ДО прогона: >50% выдумок на МУСОРЕ = провал; <20% = годно.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import glob
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from collections import Counter
|
||
from pathlib import Path
|
||
|
||
|
||
def parse_with_engine(harness: Path, reply: str, keys: list[str]) -> dict:
|
||
with tempfile.TemporaryDirectory() as td:
|
||
rp, kp, op = Path(td) / "r.txt", Path(td) / "k.json", Path(td) / "o.json"
|
||
rp.write_text(reply, encoding="utf-8")
|
||
kp.write_text(json.dumps(keys, ensure_ascii=False), encoding="utf-8")
|
||
subprocess.run([str(harness), "-mode", "parse", "-reply", str(rp), "-keys", str(kp),
|
||
"-out", str(op)], check=True, capture_output=True)
|
||
return json.loads(op.read_text(encoding="utf-8"))
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--raw-dir", required=True, type=Path)
|
||
ap.add_argument("--labels", required=True, type=Path)
|
||
ap.add_argument("--control", required=True, type=Path, help="JSON кандидатов; хвост + контроль")
|
||
ap.add_argument("--harness", required=True, type=Path)
|
||
ap.add_argument("--out", type=Path)
|
||
a = ap.parse_args()
|
||
|
||
labels = json.loads(a.labels.read_text(encoding="utf-8"))
|
||
cands = json.loads(a.control.read_text(encoding="utf-8"))
|
||
ctrl_srcs = [c["src"] for c in cands if c["src"] not in labels]
|
||
|
||
# Повторы разбираются РАЗДЕЛЬНО: слить их в одну карту значило бы выдать разброс прогона за
|
||
# согласие (урок фазы B′ пакета-7). Основной вывод считается по прогону 0, размах — по всем.
|
||
per_rep: dict[int, dict] = {}
|
||
usd, cin, cout, cached = 0.0, 0, 0, 0
|
||
files = sorted(glob.glob(str(a.raw_dir / "*.json")))
|
||
for f in files:
|
||
d = json.loads(Path(f).read_text(encoding="utf-8"))
|
||
if "error" in d:
|
||
print(f"ВЫЗОВ С ОШИБКОЙ: {f}: {d['error']}", file=sys.stderr)
|
||
continue
|
||
rep = d.get("rep", 0)
|
||
st = per_rep.setdefault(rep, {"answered": {}, "declined": set(), "unanswered": set(), "bad": 0})
|
||
got = parse_with_engine(a.harness, d["response"], d["keys"])
|
||
st["answered"].update(got["answered"])
|
||
st["declined"].update(got["declined"])
|
||
st["unanswered"].update(got["unanswered"])
|
||
st["bad"] += got["bad_lines"]
|
||
usd += d.get("usd", 0.0)
|
||
u = d.get("usage") or {}
|
||
cin += u.get("in", 0); cout += u.get("out", 0); cached += u.get("cached", 0)
|
||
|
||
base = per_rep[min(per_rep)]
|
||
answered, declined, unanswered, bad = base["answered"], base["declined"], base["unanswered"], base["bad"]
|
||
|
||
def bucket(srcs: list[str]) -> Counter:
|
||
c = Counter()
|
||
for s in srcs:
|
||
if s in answered:
|
||
c["выдумала dst"] += 1
|
||
elif s in declined:
|
||
c["⟦TM-NO-DST⟧"] += 1
|
||
elif s in unanswered:
|
||
c["промолчала"] += 1
|
||
else:
|
||
c["не в ответе"] += 1
|
||
return c
|
||
|
||
junk = [s for s, v in labels.items() if v == "МУСОР"]
|
||
grey = [s for s, v in labels.items() if v == "СЕРЫЙ"]
|
||
print(f"вызовов: {len(files)} ${usd:.5f} in={cin} (cached {cached}) out={cout} плохих строк парсера: {bad}")
|
||
rows = []
|
||
for name, srcs in (("МУСОР", junk), ("СЕРЫЙ", grey), ("КОНТРОЛЬ (подписанные)", ctrl_srcs)):
|
||
c = bucket(srcs)
|
||
n = len(srcs)
|
||
inv = c["выдумала dst"]
|
||
print(f"\n{name}: {n} кандидатов")
|
||
for k in ("выдумала dst", "⟦TM-NO-DST⟧", "промолчала", "не в ответе"):
|
||
if c[k]:
|
||
print(f" {k:16s} {c[k]:3d} = {c[k]/n:.3f}")
|
||
rows.append({"class": name, "n": n, "invented": inv, "rate": inv / n if n else None})
|
||
|
||
jr = rows[0]["rate"]
|
||
verdict = ("ПРОВАЛ (>0.50)" if jr > 0.50 else "ГОДНО (<0.20)" if jr < 0.20 else "СЕРАЯ ЗОНА (0.20–0.50)")
|
||
print(f"\nВЕРДИКТ по §0.3 на классе МУСОР (прогон {min(per_rep)}): доля выдуманных {jr:.3f} → {verdict}")
|
||
|
||
# Размах по повторам: вывод «провал» держится, только если он больше разброса прогона.
|
||
if len(per_rep) > 1:
|
||
print("\nразброс по повторам (доля выдуманных dst):")
|
||
spans = {}
|
||
for name, srcs in (("МУСОР", junk), ("СЕРЫЙ", grey), ("КОНТРОЛЬ", ctrl_srcs)):
|
||
vals = []
|
||
for rep in sorted(per_rep):
|
||
st = per_rep[rep]
|
||
vals.append(sum(1 for s in srcs if s in st["answered"]) / len(srcs))
|
||
spans[name] = (min(vals), max(vals))
|
||
print(f" {name:10s} " + " ".join(f"r{r}={v:.3f}" for r, v in zip(sorted(per_rep), vals))
|
||
+ f" размах {max(vals)-min(vals):.3f}")
|
||
rows.append({"spans": {k: list(v) for k, v in spans.items()}})
|
||
|
||
print("\nчто именно роль ответила на МУСОР (все строки):")
|
||
for s in sorted(junk):
|
||
v = answered.get(s)
|
||
print(f" {s}\t{v if v is not None else ('⟦TM-NO-DST⟧' if s in declined else 'промолчала')}")
|
||
print("\nКОНТРОЛЬ — ответ против подписи владельца:")
|
||
ctrl_dst = {c["src"]: c for c in cands}
|
||
for s in ctrl_srcs:
|
||
print(f" {s}\t{answered.get(s, '⟦TM-NO-DST⟧' if s in declined else 'промолчала')}")
|
||
|
||
if a.out:
|
||
a.out.write_text(json.dumps({"rows": rows, "answered": answered,
|
||
"declined": sorted(declined), "unanswered": sorted(unanswered),
|
||
"bad_lines": bad, "usd": usd,
|
||
"usage": {"in": cin, "out": cout, "cached": cached}},
|
||
ensure_ascii=False, indent=1), encoding="utf-8")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|