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