#!/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())