textmachine/eval/dovodka/power.py

253 lines
22 KiB
Python
Raw 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
"""МОЩНОСТЬ СУДЕЙСКИХ ОСЕЙ ФАЗЫ Д — считается ДО покупок. $0.
⚠ Зачем. Эксп-22 купил английскую ось на n=6 и лишь ПОСЛЕ денег обнаружил, что при шести единицах
минимально достижимое p знакового теста = 2⁻⁵ = 0.031, а после Холма по шести контрастам — 0.19,
то есть значимости не могло быть НИ ПРИ КАКОМ результате. Ось была недомощна ПО ПОСТРОЕНИЮ.
Норма фазы Д: у каждой оси напечатан MDE при плановом n, и ось, чей MDE не даёт несущего вывода,
объявляется описательной ЗАРАНЕЕ.
Два ограничения считаются раздельно, потому что упираются они в разное:
* ПОТОЛОК ЗНАЧИМОСТИ — минимально достижимое p точного знакового теста при n наблюдениях с
поправкой Холма по объявленному семейству. Если он ≥0.05, дизайн мёртв независимо от эффекта.
* MDE (минимально различимый эффект) — 2.8·sd/√n, то же правило, по которому риг печатает порог
различимости. Отвечает на «какой перевес прибор вообще способен развести».
⚠ sd берётся из ЗАМЕРЕННЫХ шумовых полов прошлых паков и помечается источником: своего пола у
будущего прохода ещё нет по построению. Плюс поправка ×1.23 на межсессионную компоненту — §12б
эксп-23 показал, что пол, чьи половины судит ОДИН судья, занижает порог на 19%.
Запуск: eval/.venv/bin/python eval/dovodka/power.py
"""
from __future__ import annotations
import math
import statistics
import sys
# Замеренные шумовые полы (sd разности по единицам) — источник указан у каждого.
# ⚠ ЯРЛЫКИ ИСПРАВЛЕНЫ РЕВЬЮ. Первая редакция называла пол `p3` «редакторы над одной базой» —
# это неверно: `p2zh` и `p3` разрешаются в ОДНИ И ТЕ ЖЕ 32 файла (`tp2-floor-p1/p2-E-*`), то есть
# оба меряют СКВОЗНУЮ связку «черновик+редактура», и оба несут те же две клетки `finish=length`.
# Разница 2.90 против 3.53 — чисто МЕЖСЕССИОННАЯ (×1.217), и это, кстати, СВОЯ оценка той самой
# компоненты, ради которой берётся заимствованная ×1.23 из §12б. Совпадение хорошее, но своя
# оценка теперь названа, а не умолчана.
FLOORS = {
"эксп-23 tier (редакторы над ОДНОЙ базой, zh, n=16)": 1.45,
"эксп-23 border (редакторы над ОДНОЙ базой, zh, n=16)": 2.12,
"эксп-22 p2zh (СКВОЗНАЯ связка, те же 32 файла, что p3)": 2.90,
"эксп-22 p3 (ТЕ ЖЕ файлы, другая судейская популяция)": 3.53,
}
BETWEEN = 1.23 # §12б эксп-23: наивная se → se с межсессионной компонентой
# ⚠ ЦЕЛЕВОЙ ЭФФЕКТ — то, ради чего ось покупается. Без него MDE не решает НИЧЕГО: печатать
# «MDE 1.83» и не сказать, меньше он искомого или больше, — это ровно та форма, за которую
# погорела en-ось эксп-22. Цель объявляется ДО денег и обосновывается замеренным, а не желаемым.
AXES = [
# (имя, план n, контрастов, sd-прайор, ЦЕЛЬ, обоснование прайора, обоснование цели)
("Д4 edit-контур zh", 32, 3, 2.12, 1.50,
"армы правят ОДНУ базу — это структура `tier`/`border` (sd 1.45/2.12), берётся худшая",
"решение «дешёвый фиксер вместо переписа» меняется, если разрыв ≥ трети собственного "
"эффекта редактора (+4.44 в эксп-22 Ф3) — это 1.50"),
("Д3 en→ru несущая", 16, 1, 2.12, 2.00,
"своего пола на en нет НИ ОДНОГО; прайор с zh той же структуры, пол докупается в фазе",
"несущий вопрос оси — переносится ли эффект редактора: на zh контроль +3.25, "
"значимым переносом считаем половину, 2.00"),
("Д6 ja→ru разведка", 9, 3, 2.90, 2.00,
"связка end-to-end (как p2zh) — шумнее",
"та же цель, что у en; ось объявлена ОПИСАТЕЛЬНОЙ промтом заранее и остаётся ею "
"независимо от арифметики"),
("Д1 gemini добивка", 16, 1, 2.12, 2.00,
"проход над той же якорной базой, что border — его пол ближайший",
"гипотеза эксп-04 «аутлаер прозы» — про КРУПНЫЙ отрыв; мелкий отрыв её не подтверждает "
"и продуктового смысла при цене 28× не имеет. ⚠ Именно этот порог решил спор о числе "
"клеток: при n=12 (минимум промта) MDE 2.11 БОЛЬШЕ цели и ось была бы ОПИСАТЕЛЬНОЙ; "
"владелец 10.08 выбрал полные 16, и только на них ось несущая"),
]
FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка"} # объявлено промтом, арифметикой не отменяется
# ⚠ ПЕРВИЧНОЕ И ВТОРИЧНОЕ СЕМЕЙСТВА — объявляются ДО покупок, и это НЕ сужение задним числом.
# Поправка Холма делится на РАЗМЕР семейства, поэтому «взять все контрасты несущими» = сделать
# все недомощными. Пре-регистрируется так: первичное семейство несёт по одному контрасту на
# КАЖДУЮ гипотезу владельца и считается с Холмом; остальные контрасты объявлены ОПИСАТЕЛЬНЫМИ,
# печатаются с числами и поправку не несут. Ровно этот приём эксп-22 применил в §0-Ф2 к осям
# «черновик как текст», и он там сработал.
PRIMARY = {
"Д4 edit-контур zh": ["A1B/A0 — флаговый контур: батарея + канон-гейт (H-2а)",
"A3/A0 — смысловой контур (H-2б, ставка владельца)",
"A6/A0 — качественный черновик + контур (H-1)"],
"Д3 en→ru несущая": ["E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)"],
"Д1 gemini добивка": ["R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)"],
}
# ⚠⚠ n У КАЖДОГО КОНТРАСТА СВОЙ, И ЭТО РЕШАЕТ. Прежняя редакция печатала MDE и статус ПО ОСИ при
# плановом n=32 — а члены первичного семейства живут на разных выборках, потому что база у них
# разная. `A6/A0` стоит на 16 единицах ПО ПОСТРОЕНИЮ: черновик dspro эксп-22 существует только у
# 16 старых единиц. При n=16 его MDE вдвое больше цели, то есть НОСИТЕЛЬ H-1 не мог дать несущего
# вывода никогда — и это было известно ДО денег, просто никто не считал мощность поконтрастно.
# Ровно этот класс («ось объявлена несущей, контраст недомощен») погубил en-ось эксп-22.
# Числа ниже — фактические выборки после исключения единицы с эхо-мином (contour.EXCLUDED).
CONTRAST_N = {
"A1B/A0 — флаговый контур: батарея + канон-гейт (H-2а)": 31,
"A3/A0 — смысловой контур (H-2б, ставка владельца)": 31,
"A6/A0 — качественный черновик + контур (H-1)": 16,
"E0/D0 — КОНТРОЛЬ: покупает ли редактор поверх черновика (H-3)": 16,
"R1/R0 — аутлаер ли проза gemini (гипотеза эксп-04)": 16,
}
SECONDARY = {
"Д4 edit-контур zh": ["A1/A0 — ТОЛЬКО канон-флаги: батарея в первой редакции падала (эррата Э-4)",
"A2/A0 — однопроходка уравненного мандата (строка 153 бэклога)",
"A4/A0 — канон-фиксер ПОСЛЕ переписа: его несущая ось — покрытие "
"канона, а оно детерминировано и шума не имеет"],
"Д3 en→ru несущая": ["E1..E5/E0 — армы контуров и однопроходки",
"E6/E0 — ВТОРОЙ редактор glm-5 (H-4): на n=16 порядок жильцов "
"разрешить нельзя, ось описательная и это сказано ДО денег"],
}
# ⚠ ПОЧЕМУ ПРАЙОР 2.12, А НЕ 2.90/3.53. Оба пола эксп-22 меряют СКВОЗНУЮ связку — они несут ещё и
# шум черновика, тогда как армы Д4 правят ОДНУ замороженную базу. Структурно ближе полы эксп-23
# (`tier` 1.45, `border` 2.12, оба над одной базой), и берётся ХУДШИЙ из них. Отдельно: обе пары
# пола эксп-22 содержат по две клетки `finish=length` (§15), что разброс завышает — без затронутых
# единиц тот же пол даёт 3.10; в фазе Д такие клетки в пачку не допускаются вовсе.
# ⚠ И честно: ПРАЙОР РЕШАЕТ. При sd 3.53 MDE оси Д4 был бы 2.48 против цели 1.50, то есть ось
# стала бы описательной. Прежняя редакция писала «прайор всё равно НЕ решает» — неверно, снято
# ревью. Решает он не окончательно: СВОЙ пол прохода меряется в фазе парой, чьи половины судят
# РАЗНЫЕ сессии, и если он выйдет хуже прайора — ось пере-классифицируется ТОГДА ЖЕ, до чтения
# боевых перевесов.
# Пространство дизайна: во что упирается вывод при разных n и разных полах.
GRID_N = (12, 16, 24, 32)
GRID_SD = (1.45, 2.12, 2.90, 3.53)
def min_p(n: int, k: int) -> float:
"""Минимально достижимое двустороннее p знакового теста при n, после Холма по k контрастам."""
return min(1.0, 2.0 ** (1 - n) * k)
def _z(p: float) -> float:
"""Квантиль стандартной нормали."""
return statistics.NormalDist().inv_cdf(p)
def _t(p: float, df: int) -> float:
"""Квантиль Стьюдента. ⚠ Первая редакция считала его самописной бисекцией по неполной бете —
и та молча возвращала НОЛЬ на половине входов, то есть обнуляла MDE и печатала «НЕСУЩАЯ» там,
где мощности нет вовсе. Поймано сверкой с табличными значениями ДО того, как на числе встали.
Мораль та же, что у всей фазы: свою арифметику сверяй с эталоном, а не с ожиданием.
"""
from scipy import stats # noqa: PLC0415
return float(stats.t.ppf(p, df))
# ⚠ ЧИСЛО СТЕПЕНЕЙ СВОБОДЫ ПРАЙОРА. sd берётся не из воздуха и не «известна»: она ОЦЕНЕНА на
# полу прошлого пака с n=16, то есть df=15. Прежняя редакция брала квантиль НОРМАЛИ, как если бы
# дисперсия была известна точно, и это давало оси Д4 MDE 1.49 против цели 1.50 — «несущая» с
# запасом 0.7%. При честном t тот же дизайн даёт 1.56, то есть ОПИСАТЕЛЬНУЮ ось. Разница между
# «несущая» и «описательная» здесь целиком сидела в подмене t на z; поправка внесена 11.08.
PRIOR_DF = 15
def mde(sd: float, n: int, k: int = 1, between: bool = True, df: int = PRIOR_DF) -> float:
"""Минимально различимый эффект при 80% мощности и поправке Холма по k контрастам.
КОНСТАНТА 2.8 ВЕРНА ТОЛЬКО ПРИ k=1, и первая редакция этого файла применяла её всегда.
Поймано адверсариальным ревью снизу вверх. Решающее правило пака — `p` ХОЛМА < 0.05, то есть
на самый строгий контраст семейства приходится α/k; множитель становится
(t_{1α/2k} + t_{0.8}), при k=5 это ≈3.6 вместо 2.8. Пропущенная поправка ×1.221.36 БОЛЬШЕ
той ×1.23, которую файл старательно вносил за межсессионную компоненту, — и она переворачивала
вердикт: Д4 при k=5 давала MDE 1.57 против цели 1.50, то есть ОПИСАТЕЛЬНУЮ ось, объявленную
несущей. Это буквально провал en-оси эксп-22, ради недопущения которого файл и написан.
"""
mult = _t(1 - 0.05 / (2 * max(1, k)), df) + _t(0.80, df)
return mult * sd / math.sqrt(n) * (BETWEEN if between else 1.0)
def main() -> int:
print("ЗАМЕРЕННЫЕ ПОЛЫ, из которых берутся прайоры (sd разности по единицам):")
for k, v in FLOORS.items():
print(f" {k:54s} sd {v:.2f}")
print(f"\nпоправка на межсессионную компоненту: ×{BETWEEN} (§12б эксп-23)\n")
print(f"{'ось':24s}{'n':>4s}{'k':>3s}{'sd':>6s}{'MDE':>7s}{'ЦЕЛЬ':>7s}{'потолок p':>11s}"
" СТАТУС ОСИ")
desc = 0
for name, n, k, sd, target, _w1, _w2 in AXES:
p0 = min_p(n, k)
m_true = mde(sd, n, k)
# ⚠ ДВА УСЛОВИЯ, И ОБА НЕОБХОДИМЫ. (1) значимость достижима вообще; (2) прибор способен
# развести ИСКОМЫЙ эффект. Первая редакция смотрела только на (1) — и объявляла несущей
# ось, чей MDE вдвое больше того, что она ищет. Это и есть en-ось эксп-22, слово в слово.
why = []
if p0 >= 0.05:
why.append(f"значимость недостижима (потолок p {p0:.4f})")
if m_true > target:
why.append(f"MDE {m_true:.2f} БОЛЬШЕ искомого эффекта {target:.2f}")
if name in FORCED_DESCRIPTIVE:
why.append("объявлена описательной ЗАКАЗОМ")
status = "НЕСУЩАЯ" if not why else "⛔ ОПИСАТЕЛЬНАЯ: " + " · ".join(why)
desc += bool(why)
print(f"{name:24s}{n:4d}{k:3d}{sd:6.2f}{m_true:7.2f}{target:7.2f}{p0:11.4f} {status}")
print(f"\nописательных осей: {desc} из {len(AXES)} — объявлено ДО покупок, "
"пере-классификации задним числом не будет")
# ⚠ ПОКОНТРАСТНАЯ МОЩНОСТЬ — то, по чему решение принимается на самом деле.
print("\nПОКОНТРАСТНАЯ МОЩНОСТЬ ПЕРВИЧНЫХ СЕМЕЙСТВ (n у каждого СВОЙ — решает он, не ось):")
print(f" {'контраст':56s}{'n':>4s}{'MDE':>7s}{'ЦЕЛЬ':>7s} вердикт")
weak = []
for name, n_ax, k, sd, target, _w1, _w2 in AXES:
for c in PRIMARY.get(name, []):
n_c = CONTRAST_N.get(c, n_ax)
m = mde(sd, n_c, k)
ok = m <= target
if not ok:
weak.append((c, n_c, m, target))
print(f" {c[:56]:56s}{n_c:4d}{m:7.2f}{target:7.2f} "
f"{'НЕСУЩИЙ' if ok else '⛔ НЕДОМОЩЕН'}")
if weak:
print("\n ⛔ НЕДОМОЩНЫЕ КОНТРАСТЫ ПЕРВИЧНОГО СЕМЕЙСТВА — объявляются ОПИСАТЕЛЬНЫМИ:")
for c, n_c, m, t in weak:
need_sd = t * math.sqrt(n_c) / ((_t(1 - 0.05 / 6, PRIOR_DF) + _t(0.80, PRIOR_DF))
* BETWEEN)
need_n = math.ceil((((_t(1 - 0.05 / 6, PRIOR_DF) + _t(0.80, PRIOR_DF))
* 2.12 * BETWEEN) / t) ** 2)
print(f"{c}\n при n={n_c} MDE {m:.2f} > цели {t:.2f}; несущим станет "
f"либо при своём поле sd ≤ {need_sd:.2f}, либо при n ≥ {need_n}")
print("\nСЕМЕЙСТВА, объявленные ДО покупок:")
for name in [a[0] for a in AXES]:
if name in PRIMARY:
print(f" {name} — ПЕРВИЧНОЕ (Холм по {len(PRIMARY[name])}):")
for c in PRIMARY[name]:
print(f"{c}")
if name in SECONDARY:
print(" описательные (поправку не несут, печатаются с числами):")
for c in SECONDARY[name]:
print(f" · {c}")
print("\nОБОСНОВАНИЯ (прайор шума · искомый эффект):")
for name, _n, _k, _sd, _tg, w1, w2 in AXES:
print(f" {name}\n прайор: {w1}\n цель: {w2}")
print("\nПРОСТРАНСТВО ДИЗАЙНА — честный MDE (с ×1.23) при разных n и разных полах:")
print(" " + "".join(f"{f'sd {s:.2f}':>10s}" for s in GRID_SD))
for n in GRID_N:
print(f" n={n:<4d}" + "".join(f"{mde(s, n, 1):10.2f}" for s in GRID_SD))
print(" (таблица при k=1; на семейство из k контрастов множитель растёт — см. mde())")
print("\n СКОЛЬКО ЕДИНИЦ НУЖНО, чтобы ось была несущей при sd 2.12 и цели:")
for k in (1, 2, 3, 4, 5):
for tgt in (1.50, 2.00):
need = math.ceil(((_z(1 - 0.05 / (2 * k)) + _z(0.80)) * 2.12 * BETWEEN / tgt) ** 2)
print(f" k={k} цель {tgt:.2f} → n ≥ {need}")
print("\n Для калибровки — РЕАЛЬНЫЕ перевесы, уже замеренные на этом материале:")
print(" эксп-22 Ф3: R2 2.12 · R3 2.54 · R4 4.75 · КОНТРОЛЬ +4.44")
print(" эксп-23 tier: T1 0.19 · T2 +0.50 · T3 +0.19 · КОНТРОЛЬ +2.06")
print(" эксп-23 border: R2 0.81")
print(" ⇒ Различия МЕЖДУ близкими редакторами лежат в полосе 0.22.5, то есть под MDE почти\n"
" любой строки этой таблицы. Развести их прибор способен только при малом поле И\n"
" большом n. Контрасты «есть ли редактура вообще» (+2.1…+4.4) берутся легко.")
print("\n⚠ Читать так: MDE — свойство ПЛАНА, а не результата. Свой пол каждого прохода меряется\n"
" в фазе и печатается рядом; если он окажется хуже прайора, ось пере-классифицируется в\n"
" описательную ТОГДА ЖЕ, до чтения боевых перевесов, а не после.")
print("\n⚠ Сравнение с эксп-22: его en-ось шла n=6 при k=6 ⇒ потолок p "
f"{min_p(6, 6):.4f} — дизайн не мог дать значимости. Здесь такой оси нет.")
return 0
if __name__ == "__main__":
sys.exit(main())