#!/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.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())