85 lines
3.9 KiB
Python
85 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
||
"""$0-демо ценности рекомендаций §D корзины 1 на ЖИВОМ банке coldrun-a (вопрос владельца 04.08
|
||
«насколько алгоритмы лучше текущего»):
|
||
A4: intra-run конфликты consolidated×consolidated (containment + лексемный subset — реплика
|
||
CanonConflicts/lexemeSubset из terminology.go) — сколько противоречий сегодня НЕ видит никто;
|
||
A5: сдвиг ЧАСТОТНОГО топа вариантов при фолде голосов по нормализованной форме.
|
||
"""
|
||
import unicodedata
|
||
from pathlib import Path
|
||
import sys
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "bank_autonomy"))
|
||
from parse_common import parse_bankstop # noqa: E402
|
||
|
||
STEM_MIN, SLACK = 3, 2 # stemMinPrefix / stemSuffixSlack (terminology.go:379-386)
|
||
|
||
|
||
def norm(s):
|
||
s = unicodedata.normalize("NFKC", s or "").casefold().replace("ё", "е")
|
||
for ch in "«»\"'":
|
||
s = s.replace(ch, "")
|
||
return " ".join(s.split())
|
||
|
||
|
||
def words(s):
|
||
return [w for w in "".join(c if c.isalnum() else " " for c in norm(s)).split() if len(w) > 1]
|
||
|
||
|
||
def same_lexeme(a, b):
|
||
if a == b:
|
||
return True
|
||
n = 0
|
||
while n < len(a) and n < len(b) and a[n] == b[n]:
|
||
n += 1
|
||
shorter = min(len(a), len(b))
|
||
if (n == len(a) or n == len(b)) and shorter >= STEM_MIN:
|
||
return True
|
||
return n >= max(shorter - SLACK, STEM_MIN)
|
||
|
||
|
||
def lexeme_subset(need, have):
|
||
return all(any(same_lexeme(w, h) for h in have) for w in need) if need else True
|
||
|
||
|
||
def a4_conflicts():
|
||
bs = parse_bankstop()
|
||
finals = {s: r["dst"] for s, r in bs.items() if r.get("dst") and r["dst"] != "—"}
|
||
out = []
|
||
for a, da in finals.items():
|
||
for b, db in finals.items():
|
||
if a == b or b not in a: # b строго содержится в a (containment, как CanonConflicts)
|
||
continue
|
||
if not lexeme_subset(words(db), words(da)):
|
||
out.append((a, da, b, db))
|
||
print(f"A4-демо: финалов с dst {len(finals)}; НЕВИДИМЫХ сегодня внутри-прогонных противоречий "
|
||
f"(составной терм теряет перевод вложенного): {len(out)}")
|
||
for a, da, b, db in sorted(out):
|
||
print(f" {a}→{da!r} не несёт {b}→{db!r}")
|
||
|
||
|
||
def a5_topshift():
|
||
bs = parse_bankstop()
|
||
shifted = []
|
||
for s, r in bs.items():
|
||
vs = r.get("drafts") or []
|
||
if len(vs) < 2:
|
||
continue
|
||
raw_top, raw_top_n = vs[0][0], vs[0][1] # порядок §C2-3 (частота — главный фактор при пустом каноне)
|
||
folded = {}
|
||
for d, n in vs:
|
||
folded[norm(d)] = folded.get(norm(d), 0) + n
|
||
fold_top = max(folded, key=lambda k: (folded[k], k))
|
||
# ЧЕСТНЫЙ сдвиг: слитые голоса ОБОГНАЛИ сырой топ (строго больше). Равенство после фолда —
|
||
# тай-брейк, не эффект нормализации (ловушка первой версии этого скрипта — поймана самопроверкой).
|
||
if norm(raw_top) != fold_top and folded[fold_top] > raw_top_n:
|
||
shifted.append((s, raw_top, fold_top, vs))
|
||
print(f"\nA5-демо: строк со spread>=2: {sum(1 for r in bs.values() if len(r.get('drafts') or [])>=2)}; "
|
||
f"частотный топ ЧЕСТНО сдвигается нормализованным фолдом (слитые голоса обогнали сырой топ) у: {len(shifted)}")
|
||
for s, rt, ft, vs in sorted(shifted):
|
||
print(f" {s}: сырой топ {rt!r} → фолднутый {ft!r} ({' | '.join(f'{d}×{n}' for d,n in vs[:5])})")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
a4_conflicts()
|
||
a5_topshift()
|