112 lines
6.2 KiB
Python
112 lines
6.2 KiB
Python
#!/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())
|