Раздел 42 · Аналитика данных с нуля

Мощность, MDE и размер выборки: сколько ждать результата теста

middle~20 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

До истории с выручкой после 2.0 Ким ответил бы «хватит» через пять минут. Теперь он сам подходит к твоему столу.

Бран спрашивает, сколько ждать. На этот вопрос отвечают до старта, и ответ зависит от трёх вещей: насколько большой эффект мы хотим поймать, сколько игроков успеет попасть в тест и насколько шумная метрика. Сегодня считаем все три на июньском трафике игры.

Метрика Брана: доля платящих, то есть игроков, которые за время теста хоть раз что-то купили. Базу берём из истории. В june_players.csv по строке на каждого игрока, который заходил в игру с 1 по 28 июня 2026: paid равно 1, если он за эти четыре недели заплатил, и revenue_rub, сколько заплатил.

import numpy as np
import pandas as pd
from scipy import stats

players = pd.read_csv("june_players.csv")
p_base = players["paid"].mean()
len(players), players["paid"].sum(), p_base    # (938, 86, 0.0917)

За четыре недели в игру зашли 938 человек, заплатили 86: доля 9,17%. Бран надеется, что дешёвый набор поднимет её на 20%, до 11,0%.

Сеть с крупной ячейкой

Рыбак бросает сеть с ячейкой в ладонь. Крупная рыба застрянет почти всегда, а мелкая проскочит, хотя она в воде есть. Пустая сеть не доказывает, что в озере нет мальков: сеть не умеет их ловить.

A/B-тест это такая сеть. Ячейку задают размер групп и шум метрики. В уроке про гипотезы мы разобрали две ошибки: первого рода (alpha, ложная тревога) и второго рода (beta, пропуск). Доля настоящих эффектов, которые тест всё-таки ловит, называется мощностью: power = 1 - beta.

Мощность на симуляции

Мощность проще всего увидеть, проведя тест тысячи раз. Пусть дешёвый набор правда поднимает долю платящих с 9,17% до 11,0%. За четыре недели в тест попадут те же 938 игроков, по 469 в группе. Разыграем 10 000 таких тестов и в каждом посчитаем z-тест для долей из урока 54:

def simulate(p_a, p_b, n, sims=10_000, seed=68):
    rng = np.random.default_rng(seed)
    x_a = rng.binomial(n, p_a, size=sims)   # платящих в контроле: одно число на каждый тест
    x_b = rng.binomial(n, p_b, size=sims)   # платящих в тесте
    p = (x_a + x_b) / (2 * n)
    se = np.sqrt(p * (1 - p) * 2 / n)
    z = (x_b - x_a) / n / se
    p_values = 2 * stats.norm.sf(np.abs(z))
    return (p_values < 0.05).mean()

simulate(p_base, p_base, 469)         # 0.0493: эффекта нет
simulate(p_base, p_base * 1.2, 469)   # 0.1495: эффект +20%
simulate(p_base, p_base * 1.5, 469)   # 0.5993: эффект +50%

rng.binomial(n, p, size=sims) одним вызовом разыгрывает 10 000 групп: сколько из n игроков заплатили, если каждый платит с вероятностью p.

Первая строка это проверка самой симуляции. Без эффекта тест срабатывает в 4,9% случаев, как и обещает alpha = 5%. Вторая строка отвечает Брану: даже за четыре недели настоящий эффект +20% тест поймает в 15% случаев. В 85% он скажет «не значимо», и Бран решит, что скидка не работает. Эффект +50% тест ловит чуть чаще, чем монетка выпадает орлом.

Мощность формулой

Симуляция честная, но для каждого n её надо гонять заново. Формула даёт то же самое сразу. Разница долей в тесте шумит со стандартной ошибкой из урока про ЦПТ, а значимой мы её признаём, когда она больше z_alpha таких ошибок:

def power(p_a, p_b, n, alpha=0.05):
    se = np.sqrt((p_a * (1 - p_a) + p_b * (1 - p_b)) / n)
    z_alpha = stats.norm.ppf(1 - alpha / 2)    # 1.96
    return stats.norm.cdf(abs(p_b - p_a) / se - z_alpha)

power(p_base, p_base * 1.2, 469)   # 0.152
power(p_base, p_base * 1.5, 469)   # 0.599

stats.norm.ppf обратна stats.norm.cdf: по доле площади слева возвращает точку на оси. ppf(0.975) это 1,96, тот самый порог двустороннего теста. Смысл формулы: настоящая разница отстоит от нуля на abs(p_b - p_a) / se стандартных ошибок. Тест сработает, если шум не утащит её ниже 1,96. Формула и симуляция расходятся не больше чем на 0,3 пункта, это шум 10 000 розыгрышей.

урок из курса

Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.

Оплата пока не подключена: премиум и курсы открываются промокодом.