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