textmachine/eval/dovodka/power.py

304 lines
29 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
"""МОЩНОСТЬ СУДЕЙСКИХ ОСЕЙ ФАЗЫ Д — считается ДО покупок. $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, и только на них ось несущая"),
# ⚠ ЗАХОД Д17 ВПИСАН 16.08, УЖЕ ПОСЛЕ НАЧАЛА ПОКУПКИ — ЭТО ДЕВИАЦИЯ, И ОНА ОБЪЯВЛЕНА ЗДЕСЬ,
# А НЕ ПРИМЕЧАНИЕМ ПОСТ-ФАКТУМ. Норма заказа (:168) требует НАПЕЧАТАТЬ MDE при плановом n до
# денег; пре-рег Д17.4 объявил ПРАВИЛО («MDE считается по своему полу, не проходит цель — ось
# описательная»), но числа не напечатал, хотя инструмент для этого стоит в зоне. Покупка к
# моменту записи прошла ~40 клеток из 160. Числа ниже посчитаны ДО первого судейского ответа,
# то есть право пере-классифицировать ось до вскрытия вердиктов сохранено.
# ⚠ И ГЛАВНАЯ ОГОВОРКА ПРАЙОРА: все полы выше сняты СТАРОЙ рубрикой, а заход судится
# ПОЧИНЕННОЙ («вольность не штрафуется»), которая по построению убирает часть счёта. Перенос
# прайора между шкалами — допущение, а не замер, и потому статус оси решает НЕ он, а СВОЙ пол
# захода (`ZF` против `Z0`, две генерации боевой связки), снятый на новой шкале.
("Д17 заход zh (новая рубрика)", 16, 4, 2.12, 1.50,
"армы правят ОДНУ базу — структура `tier`/`border` (sd 1.45/2.12), берётся худшая; "
"⚠ прайор снят СТАРОЙ рубрикой и на новую шкалу переносится как допущение",
"та же цель, что у Д4: решение «менять архитектуру» стоит принимать при разрыве ≥ трети "
"собственного эффекта редактора (+4.44 эксп-22 Ф3) — это 1.50"),
("Д17 заход en (новая рубрика)", 16, 4, 2.12, 1.50,
"своего пола на en в новой шкале нет; прайор с zh той же структуры, свой пол докупается",
"цель снижена с 2.00 до 1.50 против оси Д3 сознательно: там вопрос был «переносится ли "
"эффект редактора вообще», здесь — «какая из четырёх архитектур лучше», и решение о смене "
"архитектуры принимается при меньшем разрыве"),
]
# ⚠ РЕШЕНИЕ ВЛАДЕЛЬЦА 16.08, ПРИНЯТО ДО ЕДИНОГО СУДЕЙСКОГО ОТВЕТА. Мощность захода посчитана уже
# после начала покупки (девиация объявлена выше), и она показала: при n=16 и цели 1.50 несущий
# вывод недостижим — нужен свой пол sd ≤ 1.32, а все замеренные полы фазы 1.45…2.63. Владельцу
# предложено добрать китайскую ось до 31 главы за $0.48 (тогда порог требований падает до 1.83,
# то есть достижим); он выбрал НЕ добирать. Значит обе оси захода ОПИСАТЕЛЬНЫЕ, и решающим
# эндпойнтом остаётся слепое чтение — ровно так, как пре-рег Д17 и объявил заранее.
# Пере-классификации задним числом не будет ни при каком исходе арифметики.
FORCED_DESCRIPTIVE = {"Д6 ja→ru разведка",
"Д17 заход zh (новая рубрика)", "Д17 заход en (новая рубрика)"}
# ⚠ ПЕРВИЧНОЕ И ВТОРИЧНОЕ СЕМЕЙСТВА — объявляются ДО покупок, и это НЕ сужение задним числом.
# Поправка Холма делится на РАЗМЕР семейства, поэтому «взять все контрасты несущими» = сделать
# все недомощными. Пре-регистрируется так: первичное семейство несёт по одному контрасту на
# КАЖДУЮ гипотезу владельца и считается с Холмом; остальные контрасты объявлены ОПИСАТЕЛЬНЫМИ,
# печатаются с числами и поправку не несут. Ровно этот приём эксп-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)"],
# Заход Д17: по одному контрасту на КАЖДЫЙ вопрос, ради которого куплен арм. `Z7` в семействе
# нет намеренно — он меряется детерминированным канон-гейтом и судейского слота не занимает.
"Д17 заход zh (новая рубрика)": [
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)",
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)",
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)",
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале"],
"Д17 заход en (новая рубрика)": [
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)",
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)",
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)",
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале"],
}
# ⚠⚠ 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,
# ⚠ У захода n=16 у ВСЕХ четырёх контрастов, и это не совпадение, а условие отбора единиц:
# в пул берутся только те, где ВСЕ бесплатные армы и ответ критика уже есть (`zakhod.pool`).
# Именно поконтрастная проверка n погубила носителя H-1 (`A6/A0` жил на 16 из 32) — здесь
# разъехаться выборкам структурно не на чем.
"Z8/ZD — чинит ли черновик обогащённый промт (вопрос владельца 16.08)": 16,
"Z8R/Z0 — лучше ли связка на лучшем черновике (H-1 в сильной форме)": 16,
"Z1/Z0 — критик → ПОЛНЫЙ перепис (H-2б в сильнейшей форме)": 16,
"ZP/Z0 — однопроходка против связки на ПОЧИНЕННОЙ шкале": 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, k))
print(f" {c[:56]:56s}{n_c:4d}{m:7.2f}{target:7.2f} "
f"{'НЕСУЩИЙ' if ok else '⛔ НЕДОМОЩЕН'}")
if weak:
print("\n ⛔ НЕДОМОЩНЫЕ КОНТРАСТЫ ПЕРВИЧНОГО СЕМЕЙСТВА — объявляются ОПИСАТЕЛЬНЫМИ:")
# ⚠ ПОЧИНКА ПО РЕВЬЮ: множитель Холма был ЗАШИТ как k=3 (`0.05 / 6`) для всех осей, тогда
# как сам `mde()` считает по настоящему k. Печатались ремонтные числа мягче честных: для
# контрастов захода (k=4) выходило «sd ≤ 1.37 либо n ≥ 39» вместо честных 1.32 и 42.
# Купив по этой строке пол с sd 1.35, ось объявили бы несущей ошибочно.
for c, n_c, m, t, k_c in weak:
mult = (_t(1 - 0.05 / (2 * max(1, k_c)), PRIOR_DF) + _t(0.80, PRIOR_DF)) * BETWEEN
need_sd = t * math.sqrt(n_c) / mult
need_n = math.ceil((mult * 2.12 / 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())