252 lines
22 KiB
Python
252 lines
22 KiB
Python
#!/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": ["A1/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 = {
|
||
"A1/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": ["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.22–1.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.2–2.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())
|