textmachine/eval/pkg7/p1_consistency.py

112 lines
6.2 KiB
Python
Raw 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
"""Полигон, пакет-8 (добор D39.51-б): P1 consistency как разделитель термов и мусора. $0.
Метрика: Σ единиц, где чтец объявил строку термином / Σ единиц, где строка встречается. Единица —
ГЛАВА (тексты боевых чанков прогона не сохранены; отклонение и его цена названы в §7.2 отчёта).
Арм без фактора — голая частота на том же наборе и том же критерии: если частота проходит, P1 не
несёт информации сверх неё и вся конструкция беспредметна.
Критерий из санкции, названный ДО счёта: существует порог, выше которого ≥80% TERM, а ниже — ≥60%
JUNK. Дополнительно печатается вырожденность (строки, встречающиеся в ≤1 единице, получают 1.0 по
построению и разделять не могут) — при её доле >0.5 критерий объявляется неприменимым.
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
def best_threshold(scores: dict[str, float], labels: dict[str, str], want_term=0.80, want_junk=0.60):
"""Ищем порог, дающий ≥want_term доли TERM выше и ≥want_junk доли JUNK ниже. Возвращаем лучший
по сумме двух долей — и признак, выполнен ли критерий хоть на одном пороге."""
terms = [s for s, l in labels.items() if l == "TERM" and s in scores]
junk = [s for s, l in labels.items() if l == "JUNK" and s in scores]
if not terms or not junk:
return None
cands = sorted({scores[s] for s in scores})
best = None
for t in cands:
# "выше порога" = строго больше или равно; пробуем оба, берём лучший — порог как таковой
# заранее не назывался, назывались только доли.
for mode in (">=", ">"):
hi = (lambda v: v >= t) if mode == ">=" else (lambda v: v > t)
tp = sum(1 for s in terms if hi(scores[s])) / len(terms)
tn = sum(1 for s in junk if not hi(scores[s])) / len(junk)
row = (tp, tn, t, mode, tp >= want_term and tn >= want_junk)
if best is None or (row[0] + row[1]) > (best[0] + best[1]):
best = row
return best
def report(name: str, scores: dict[str, float], labels: dict[str, str]) -> dict:
b = best_threshold(scores, labels)
if b is None:
print(f"{name}: нет обоих классов")
return {}
tp, tn, t, mode, ok = b
n_t = sum(1 for s, l in labels.items() if l == "TERM" and s in scores)
n_j = sum(1 for s, l in labels.items() if l == "JUNK" and s in scores)
print(f"{name}: лучший порог {mode}{t:.3f} → TERM выше {tp:.3f} ({n_t} шт) · JUNK ниже {tn:.3f} ({n_j} шт)"
f" критерий (≥0.80/≥0.60): {'ВЫПОЛНЕН' if ok else 'НЕ ВЫПОЛНЕН'}")
return {"tpr": tp, "tnr": tn, "threshold": t, "mode": mode, "passes": ok, "n_term": n_t, "n_junk": n_j}
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--surfaces", required=True, type=Path)
ap.add_argument("--labels", required=True, type=Path)
ap.add_argument("--chunks", required=True, type=Path)
ap.add_argument("--out", type=Path)
a = ap.parse_args()
items = json.loads(a.surfaces.read_text(encoding="utf-8"))
labels = json.loads(a.labels.read_text(encoding="utf-8"))["majority"]
rows = [json.loads(l) for l in a.chunks.open(encoding="utf-8") if l.strip()]
by_ch: dict[int, str] = {}
for r in rows:
by_ch[r["chapter"]] = by_ch.get(r["chapter"], "") + "\n" + r["source"]
p1, freq, degen = {}, {}, []
for it in items:
occ = it["occ_chapters"]
prop = [c for c in it["proposed_chapters"] if c in occ]
if not occ:
continue # предложена там, где не встречается — вне определения метрики
p1[it["src"]] = len(prop) / len(occ)
freq[it["src"]] = sum(by_ch[c].count(it["src"]) for c in occ)
if len(occ) <= 1:
degen.append(it["src"])
n = len(p1)
print(f"поверхностей в счёте: {n} TERM={sum(1 for s in p1 if labels.get(s)=='TERM')} "
f"JUNK={sum(1 for s in p1 if labels.get(s)=='JUNK')}")
print(f"ВЫРОЖДЕННЫХ (встречаются в ≤1 главе → P1 = 1.0 по построению): {len(degen)} = {len(degen)/n:.3f}")
dt = sum(1 for s in degen if labels.get(s) == "TERM")
print(f" из них TERM {dt}, JUNK {len(degen)-dt}обе метки получают одинаковый максимум")
print("\n--- весь набор ---")
r_all = report("P1 consistency ", p1, labels)
r_fq = report("частота (арм-без-фактора)", freq, labels)
live = {s: v for s, v in p1.items() if s not in degen}
live_f = {s: v for s, v in freq.items() if s not in degen}
print(f"\n--- только НЕвырожденные ({len(live)} поверхностей) ---")
r_live = report("P1 consistency ", live, labels)
r_lf = report("частота (арм-без-фактора)", live_f, labels)
print("\nневырожденные поверхности (P1 · частота · метка):")
for s in sorted(live, key=lambda s: -live[s]):
print(f" {s}\t{live[s]:.2f}\t{live_f[s]:3d}\t{labels.get(s)}")
if a.out:
a.out.write_text(json.dumps({"p1": p1, "freq": freq, "degenerate": degen,
"all": {"p1": r_all, "freq": r_fq},
"live": {"p1": r_live, "freq": r_lf}},
ensure_ascii=False, indent=1), encoding="utf-8")
return 0
if __name__ == "__main__":
raise SystemExit(main())