textmachine/eval/exp14/exp14_kpi.py

108 lines
4.8 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
"""exp14 — детерминированный структурный KPI (Претензия 1, БЕЗ судьи) + гварды.
$0. Метрика Претензии 1: распределение предложений/нарратив-абзац (baseline P0: медиана 1.0,
среднее 1.70, доля 1-предл. 0.58). Гварды: CJK-утечка=0, length-ratio, glossary-присутствие,
диалог-тире-парность. Аварийный гейт: нельзя «побеждать» только МЕНЬШИМ числом абзацев →
KPI парно с atom-coverage-прокси (длина не должна коллапсировать).
Использование: exp14_kpi.py [--scope trap|stage2]
"""
from __future__ import annotations
import argparse, json, re, statistics
from pathlib import Path
import exp14_common as C
ARMS = C.DIAG / "arms"
MAT = json.load(open(C.DIAG / "material.json"))
def paras(t): return [p for p in re.split(r"\n\s*\n", t.strip()) if p.strip()]
def sents(p): return [s for s in re.split(r"(?<=[.!?…])\s+", p.strip()) if s.strip()]
def is_dlg(p): return p.strip().startswith("") or p.strip().startswith("")
def han(t): return sum(1 for c in t if "" <= c <= "鿿" or "" <= c <= "䶿")
def kpi_of(text: str) -> dict:
ps = paras(text)
narr = [p for p in ps if not is_dlg(p)]
dlg = [p for p in ps if is_dlg(p)]
sp = [len(sents(p)) for p in narr] or [0]
return dict(
n_para=len(ps), n_narr=len(narr), n_dlg=len(dlg),
median_spp=statistics.median(sp), mean_spp=round(statistics.mean(sp), 2),
share_1sent=round(sum(1 for x in sp if x == 1) / len(sp), 3),
chars=len(text), han=han(text),
)
def gloss_presence(text: str, editor_constraint: str) -> float:
"""Доля dst-форм из инъекции, чьё СТЕММ (первые 4 симв.) встречается в выходе (прокси)."""
dsts = re.findall(r"«([^»]+)»", editor_constraint)
if not dsts:
return 1.0
hit = 0
for d in dsts:
stem = d.split()[0][:4]
if stem and stem.lower() in text.lower():
hit += 1
return round(hit / len(dsts), 3)
def unit_map(scope):
if scope == "trap":
return {f"{u['chapter']}.{u['chunk_idx']}": u for u in MAT["trap_chunks"]}
m = {}
for ch, chunks in MAT["stage2"].items():
for u in chunks:
m[f"{ch}.{u['chunk_idx']}"] = dict(chapter=int(ch), **u)
return m
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--scope", default="trap")
a = ap.parse_args()
units = unit_map(a.scope)
arms = sorted(d.name for d in ARMS.iterdir() if d.is_dir())
print(f"=== exp14 KPI ({a.scope}, {len(units)} chunks) — Претензия 1 + гварды ===\n")
rows = {}
for arm in arms:
agg = []
han_total, chars_total, chars_p0 = 0, 0, 0
gl = []
for uid, u in units.items():
fp = ARMS / arm / f"{uid}.txt"
if not fp.exists():
continue
t = fp.read_text(encoding="utf-8")
k = kpi_of(t)
agg.append(k); han_total += k["han"]; chars_total += k["chars"]
chars_p0 += len(u["final_p0"])
gl.append(gloss_presence(t, u.get("editor_constraint", "")))
if not agg:
continue
allsp_median = statistics.median([k["median_spp"] for k in agg])
mean_spp = round(statistics.mean([k["mean_spp"] for k in agg]), 2)
share1 = round(statistics.mean([k["share_1sent"] for k in agg]), 3)
lr = round(chars_total / max(1, chars_p0), 3)
rows[arm] = dict(n=len(agg), mean_spp=mean_spp, share_1sent=share1,
han=han_total, len_ratio_vs_p0=lr,
gloss=round(statistics.mean(gl), 3))
hdr = f"{'arm':<12}{'n':>3}{'mean_spp':>10}{'share_1s':>10}{'CJK':>6}{'len/P0':>8}{'gloss':>7}"
print(hdr); print("-" * len(hdr))
# baseline first
order = [a for a in ["P0", "P1a", "P1b", "P1c", "A-layout", "A-2pass",
"A-ref-prev", "A-ref-next", "A-ref-both", "F-base", "F-disc"] if a in rows]
order += [a for a in rows if a not in order]
for arm in order:
r = rows[arm]
print(f"{arm:<12}{r['n']:>3}{r['mean_spp']:>10}{r['share_1sent']:>10}"
f"{r['han']:>6}{r['len_ratio_vs_p0']:>8}{r['gloss']:>7}")
print("\nПретензия-1 сигнал: mean_spp↑ и share_1sent↓ vs P0 = меньше рублености (лучше);")
print("гейт: len/P0 не должен коллапсировать (<~0.9 = подозрение на потерю атомов); CJK=0 обяз.")
(C.DIAG / f"kpi_{a.scope}.json").write_text(json.dumps(rows, ensure_ascii=False, indent=1), encoding="utf-8")
if __name__ == "__main__":
main()