Аналитика данных Неопределённость: доверительные интервалы и размер выборки
0%

Неопределённость: доверительные интервалы и размер выборки

Неопределённость: доверительные интервалы и размер выборки

Есть один вопрос, который отделяет аналитика от человека, умеющего писать GROUP BY: насколько ваша цифра могла бы оказаться другой, если бы вы собрали данные ещё раз?

Пока на него нет ответа, число «конверсия 4,3 процента» — это не измерение, а мнение, оформленное десятичной дробью. Мнение можно оспорить, но нельзя проверить. Измерение выглядит иначе: «конверсия 4,3 процента, интервал от 3,0 до 5,6» — и сразу видно, что разговор про рост с 4,0 до 4,3 закрыт, потому что интервал в четыре раза шире обсуждаемой разницы.

Предыдущие главы разбирали, что данные искажены до того, как вы их увидели: сбор, качество, форма распределения. Эта глава — про другое: даже если сбор идеален и данные чисты, ваша цифра всё равно не равна истине, потому что вы наблюдали конечное число случаев. Задача — научиться количественно говорить, насколько не равна, и, что важнее, сколько данных нужно, чтобы вопрос вообще имел ответ. Аппарат вероятностей, на который это опирается, разобран в математике; здесь — прикладная часть: какие формулы применять, когда они ломаются и что писать в отчёте.

Два источника расхождения, и интервал ловит только один

Наблюдённое число отличается от истинного по двум принципиально разным причинам, и путать их — самая дорогая ошибка в профессии.

Шум — случайность выборки. Вы наблюдали 1000 сессий из потенциально бесконечного потока; в другую тысячу конверсий было бы чуть больше или чуть меньше. Шум симметричен, не сдвигает оценку систематически и лечится объёмом: чем больше n, тем меньше разброс. Смещение — систематический сдвиг из-за способа сбора: трекер не срабатывает у части пользователей, поздние события не долетели, в выгрузку попал не тот сегмент. Смещение не лечится объёмом вообще: миллион наблюдений с блокировщиками рекламы даст тот же неверный ответ, что и тысяча, только с более узким интервалом вокруг него.

Держите в голове ключевое: доверительный интервал измеряет только шум. Он ничего не знает о том, что ваш трекер теряет 30 процентов событий. Красивый узкий интервал вокруг смещённой оценки — самая убедительная форма ошибки, которая существует; про это подробно в главе Откуда берутся данные.

Практическое следствие: прежде чем считать интервал, оцените порядок смещения. Если вы подозреваете потерю событий на уровне 5 процентов, а интервал даёт ±0,3 процента — интервал бесполезен, он описывает не ту неопределённость, которая доминирует. Об этом отдельно в конце главы.

«У нас же все данные» — почему интервал всё равно нужен

Регулярное возражение: «Мы посчитали конверсию по всем пользователям за июнь, это не выборка, а генеральная совокупность. Какая ещё неопределённость?» Возражение звучит логично и почти всегда неверно.

Вы не хотите знать, какой была конверсия в июне. Вы хотите знать, какова конверсия вашего продукта — свойство процесса, а не конкретных 30 дней. Июньские пользователи — одна реализация процесса, который в июле выдаст другую. Формально это суперпопуляция: наблюдённые данные — выборка из гипотетического множества всех исходов, которые процесс мог бы породить. Аналогия: вы подбросили монету 1000 раз и получили 517 орлов; «все данные» по этим броскам у вас есть, но вопрос был про монету, а про неё вы знаете только то, что её смещение лежит примерно между 0,486 и 0,548.

Проверочный тест: если ответ будет использован для решения о будущем — это выборка, и интервал нужен. Если вопрос строго ретроспективный («сколько денег мы выставили в счетах за июнь») — это учёт, а не аналитика.

Исключение, где поправка реальна: выборка из конечного фиксированного списка — например, разобрали руками 500 обращений в поддержку из 2000 закрытых за квартал. Тут работает конечная поправка $\mathrm{SE}_{\text{fpc}} = \mathrm{SE}\sqrt{(N-n)/(N-1)}$: при $N = 2000$, $n = 500$ множитель равен $\sqrt{1500/1999} \approx 0{,}866$, интервал на 13 процентов уже. При $n = N$ множитель обращается в ноль — разобрали всё, неопределённости про этот квартал нет. Но про следующий — есть.

Стандартная ошибка: одна формула, из которой растёт всё

Стандартная ошибка (SE) — стандартное отклонение самой оценки, то есть мера того, как сильно оценка «дрожит» от выборки к выборке.

$$\mathrm{SE}(\bar{x}) = \frac{s}{\sqrt{n}}, \qquad \mathrm{SE}(\hat p) = \sqrt{\frac{\hat p\,(1-\hat p)}{n}}$$

Здесь $s$ — выборочное стандартное отклонение, $\hat p$ — наблюдённая доля, $n$ — число независимых наблюдений (слово «независимых» ниже окажется главным). Обратите внимание на знаменатель: $\sqrt{n}$, а не $n$ — чтобы сузить интервал вдвое, нужно вчетверо больше данных; втрое — в девять раз; на порядок — в сто раз.

Отсюда два вывода, которые экономят недели. Первый: если текущий интервал шире нужного в пять раз, «подождать ещё недельку» не поможет — ждать нужно в 25 раз дольше, то есть полгода, и вопрос надо переформулировать. Второй: если у вас уже 10 миллионов наблюдений, ещё два миллиона не изменят ничего, и усилия стоит потратить на борьбу со смещением, а не на объём.

Числовой ориентир для конверсии около 4 процентов:

n SE 95 % интервал (Wald) Полуширина
1 000 0,62 п.п. 2,79 % … 5,21 % ±1,21 п.п.
10 000 0,20 п.п. 3,62 % … 4,38 % ±0,38 п.п.
100 000 0,06 п.п. 3,88 % … 4,12 % ±0,12 п.п.

Тысяча сессий не отличает 4 процента от 5. Это не пессимизм, это арифметика.

Что на самом деле означает «95 %»

Доверительный интервал строится процедурой: взять оценку, взять её стандартную ошибку, отложить в обе стороны $z$ или $t$ стандартных ошибок. Гарантия относится к процедуре, а не к конкретному интервалу: если повторять весь эксперимент много раз, 95 процентов построенных таким способом интервалов накроют истинное значение.

Двадцать повторов одного и того же замера: 95-процентные интервалы, один из которых промахнулся мимо истины

На картинке — двадцать независимых замеров конверсии при истинном значении 4,0 процента и $n = 1000$. Девятнадцать интервалов накрыли истину, один (замер #4, где случайно выпало 28 конверсий вместо ожидаемых 40) — нет. Это не сбой метода, а ровно то, что метод обещал.

Здесь и прячется тонкость, из-за которой ломаются формулировки в отчётах. Сказать можно так: «процедура, которой построен этот интервал, накрывает истину в 95 процентах случаев», «значения внутри интервала совместимы с наблюдёнными данными, снаружи — плохо совместимы», «данные не позволяют отличить 3,9 процента от 4,3 процента». А вот чего сказать нельзя:

  • «Истинное значение лежит внутри с вероятностью 0,95». Это байесовское утверждение: в частотной постановке истинное значение — константа, оно либо внутри, либо нет, вероятности 0,5 или 0,95 тут не при чём. Чтобы получить право на такую фразу, нужен байесовский доверительный интервал (credible interval) и явно заданное априорное распределение.
  • «95 процентов будущих наблюдений попадут в этот интервал». Это интервал предсказания, он гораздо шире: $\bar{x} \pm t \cdot s\sqrt{1 + 1/n}$. При большом $n$ доверительный интервал стягивается в точку, а интервал предсказания — нет, он упирается в разброс самих данных.
  • «Раз интервал не содержит нуля, эффект есть». Точнее: «данные плохо совместимы с гипотезой об отсутствии эффекта при принятых допущениях» — а допущений там много. Разбор в следующей главе.

Полезная практика последних лет — называть его интервалом совместимости (compatibility interval): термин честнее описывает смысл и не провоцирует на «вероятность 95 процентов». Аргументацию см. в Greenland et al., «Statistical tests, P values, confidence intervals, and power: a guide to misinterpretations» и в комментарии Amrhein, Greenland, McShane в Nature (2019).

Интервал для доли: Wald ломается там, где он интереснее всего

Формула $\hat p \pm z\sqrt{\hat p (1-\hat p)/n}$ (интервал Вальда) — та, которую все помнят, и она плоха ровно в тех случаях, где интервал по-настоящему нужен: редкие события и малые выборки.

k / n Wald Wilson Что не так с Wald
2 / 100 −0,0074 … 0,0474 0,0055 … 0,0700 отрицательная нижняя граница
0 / 500 0,0000 … 0,0000 0,0000 … 0,0076 нулевая ширина: «мы точно знаем, что это невозможно»
40 / 1000 0,0279 … 0,0521 0,0295 … 0,0540 разница уже небольшая
95 / 100 0,9073 … 0,9927 0,8882 … 0,9785 верхняя граница почти упёрлась в 1

Рабочая замена — интервал Уилсона: он всегда лежит внутри отрезка от 0 до 1, не схлопывается при нулевых счётчиках и имеет заметно лучшее реальное покрытие при малых $n$.

$$\tilde p = \frac{\hat p + \dfrac{z^2}{2n}}{1 + \dfrac{z^2}{n}}, \qquad w = \frac{z}{1 + \dfrac{z^2}{n}}\sqrt{\frac{\hat p\,(1-\hat p)}{n} + \frac{z^2}{4n^2}}$$

Интервал — это $\tilde p \pm w$. Сравнительный анализ методов: Brown, Cai, DasGupta, «Interval Estimation for a Binomial Proportion», Statistical Science 2001.

from math import sqrt
from statistics import NormalDist

Z = NormalDist().inv_cdf(0.975)          # 1.959964 — для 95 %

def wilson(k: int, n: int, z: float = Z) -> tuple[float, float]:
    """95-процентный интервал Уилсона для доли k из n."""
    if n == 0:
        return (0.0, 1.0)                # ничего не наблюдали — ничего не знаем
    p = k / n
    denom = 1 + z * z / n
    center = (p + z * z / (2 * n)) / denom
    half = z / denom * sqrt(p * (1 - p) / n + z * z / (4 * n * n))
    return (max(0.0, center - half), min(1.0, center + half))

wilson(2, 100)     # (0.0055, 0.0700)  — против (-0.0074, 0.0474) у Вальда
wilson(0, 500)     # (0.0000, 0.0076)  — против нулевой ширины у Вальда
wilson(40, 1000)   # (0.0295, 0.0540)
wilson(95, 100)    # (0.8882, 0.9785)

В продакшне не пишите это руками: statsmodels.stats.proportion.proportion_confint(count, nobs, method="wilson") даёт то же самое и ещё пять методов, документация.

Правило трёх: что делать, когда событий ноль

Отдельный практический случай: за 500 запусков ни одного отказа. Отказов не бывает? Из $(1-p)^n \le 0{,}05$ следует $p \lesssim -\ln(0{,}05)/n \approx 3/n$. Это правило трёх: при нуле событий из $n$ верхняя 95-процентная граница равна примерно $3/n$ — 0 из 100 даёт «не больше 3 процентов», 0 из 500 — «не больше 0,6 процента», 0 из 10 000 — «не больше 0,03 процента».

Формулировка «падений не зафиксировано; при 500 наблюдениях это означает частоту не выше 0,6 процента» полезна и проверяема, а «падений нет» — нет. Подробнее: Wikipedia, Rule of three (statistics).

Интервал для среднего: где ЦПТ действительно не спасает

Для среднего интервал строится через $t$-распределение:

$$\bar{x} \pm t_{1-\alpha/2,\,n-1} \cdot \frac{s}{\sqrt{n}}$$

При $n \ge 100$ разница между $t$ и $z$ уже в третьем знаке, и на практике все берут 1,96. Опасность не в этом.

Опасность в правиле «при $n > 30$ центральная предельная теорема всё сгладит». Оно верно для симметричных распределений и катастрофически неверно для скошенных — то есть для выручки, длительности сессии, времени отклика, размера чека, всего, чем реально занимается аналитик. Вот измеренное покрытие номинально 95-процентного интервала на логнормальных данных (по 4000–6000 симуляций):

Скошенность распределения n = 30 n = 100 n = 1000 n = 10 000
$\sigma = 0{,}5$ (перекос 1,8) 91,5 % 94,3 % 95,5 % 95,3 %
$\sigma = 1{,}0$ (перекос 6,2) 86,7 % 91,1 % 93,8 % 94,8 %
$\sigma = 1{,}5$ (перекос 33) 76,2 % 85,4 % 92,0 % 94,7 %
$\sigma = 2{,}0$ (перекос 414) 65,1 % 74,2 % 85,8 % 91,5 %

Читается так: при сильно скошенной выручке и сотне заказов ваш «95-процентный» интервал на самом деле накрывает истину в 74 процентах случаев. Каждый четвёртый вывод неверен, а выглядит одинаково уверенно во всех случаях.

Практический ориентир из статьи Boos & Hughes-Oliver, «How Large Does n Have to Be for Z and t Intervals?», The American Statistician 2000: нужно примерно $n \ge 25\,\gamma_1^2$, где $\gamma_1$ — коэффициент асимметрии. Для перекоса 6 это уже 900 наблюдений, для перекоса 33 — десятки тысяч.

Что делать: считать асимметрию до того, как строить интервал (одна строка в запросе, а решает, доверять ли результату); оценивать медиану или урезанное среднее, если бизнес-вопрос это допускает — выбор центра разбирают глава 05 и глава 06; логарифмировать — интервал для среднего логарифма ведёт себя прилично, но обратно преобразуется в интервал для геометрического среднего, а не арифметического, и подменять одно другим нельзя; бутстрапировать — с оговоркой, что он тоже не всесилен.

Бутстрап: интервал там, где формулы нет

Для медианы, 95-го перцентиля, отношения двух метрик, доли пользователей, сделавших три и более заказа, формулы стандартной ошибки либо нет, либо она громоздкая. Бутстрап решает это грубой силой: если выборка репрезентирует популяцию, то пересэмплирование выборки репрезентирует пересэмплирование популяции.

import numpy as np

def bootstrap_ci(values, stat=np.mean, n_boot=10_000, alpha=0.05, seed=42, chunk=500):
    """Перцентильный бутстрап-интервал.
    Время O(n_boot * n), память O(n + chunk * n): считаем блоками, чтобы не съесть RAM."""
    rng = np.random.default_rng(seed)
    values = np.asarray(values, dtype=float)
    n, reps, done = len(values), np.empty(n_boot), 0
    while done < n_boot:
        b = min(chunk, n_boot - done)
        idx = rng.integers(0, n, size=(b, n))          # индексы с возвращением
        reps[done:done + b] = stat(values[idx], axis=1)
        done += b
    return tuple(np.quantile(reps, [alpha / 2, 1 - alpha / 2]))

x = np.random.default_rng(1).lognormal(6.0, 1.2, size=3000)   # похоже на чек
p95 = lambda a, axis: np.quantile(a, 0.95, axis=axis)
bootstrap_ci(x)              # среднее 823.5 -> (770.6, 879.4)
bootstrap_ci(x, np.median)   # медиана 402.9 -> (382.3, 421.9)
bootstrap_ci(x, p95)         # p95    2915.7 -> (2624.2, 3106.0)

Сложность: $O(B \cdot n)$ по времени, $O(n)$ по памяти при блочном счёте. $B = 1000$ хватает для оценки SE, для перцентильных границ берите $B \ge 5000$ — иначе дрожат сами границы.

Чего бутстрап не делает. Он не создаёт информацию, которой нет в выборке. Измеренное покрытие бутстрап-интервала на тех же логнормальных данных ($\sigma = 1{,}5$):

n $t$-интервал Бутстрап
20 76,8 % 76,7 %
50 81,9 % 80,9 %
200 87,4 % 89,5 %
1000 91,8 % 92,0 %

Разница в пределах пары процентных пунктов: на малых выборках со скошенными данными оба метода врут примерно одинаково. Бутстрап полезен там, где формулы просто нет, а не как способ починить нехватку данных. Для скошенных случаев есть улучшенные версии — BCa (bias-corrected and accelerated) и studentized bootstrap; каноническое изложение — Efron & Tibshirani, «An Introduction to the Bootstrap» (1993).

Где бутстрап ломается совсем: на экстремальных квантилях (бутстрап p99 при $n = 200$ пересэмплирует одни и те же две точки из хвоста, интервал выйдет оптимистично узким); на максимуме, минимуме и размахе — статистики, зависящие от границы носителя распределения, он оценивает несостоятельно; и на зависимых наблюдениях — про это следующий раздел, и это самая частая производственная ошибка.

Перцентиль по 40 наблюдениям — это шум, и вот насколько

Обещание из главы про SQL. Логнормальные данные с истинным p95 = 1504, разброс выборочной оценки p95:

n Медианная оценка 95 % разброс оценки Ширина относительно истины
40 1369 881 … 2283 93 %
200 1467 1187 … 1860 45 %
1000 1497 1357 … 1662 20 %
10 000 1504 1453 … 1555 7 %

По сорока наблюдениям p95 определён «плюс-минус в полтора раза». Ещё нагляднее: вероятность того, что все 40 значений окажутся ниже истинного p95, равна $0{,}95^{40} \approx 12{,}9$ процента — примерно в каждом восьмом случае вся выборка целиком лежит ниже величины, которую вы оцениваете, и верхней границы у вас нет в принципе.

Распределённо-свободный интервал для квантиля строится по порядковым статистикам: границами берут элементы с рангами примерно $nq \pm z\sqrt{nq(1-q)}$. Для $n = 40$, $q = 0{,}95$ это ранги с 35-го по 40-й, то есть «p95 где-то между 35-м и самым большим из сорока»; для $n = 1000$ — ранги 936…965, перцентили от 93,6 до 96,5. Отсюда правило: не показывайте перцентиль без числа наблюдений рядом — колонка n в отчёте это единственный способ понять, измерение перед вами или дрожание.

Независимость: главное место, где n оказывается не тем n

Все формулы выше содержат $n$ — число независимых наблюдений. В аналитике продукта его почти всегда завышают, потому что считают события, а события одного пользователя друг от друга не независимы.

Демонстрация на смоделированных данных: 5000 пользователей, 14 931 заказ, средний чек 1413 рубля. У пользователей разный «уровень трат» — богатые покупают дорого стабильно.

Способ SE 95 % интервал
Наивный, по 14 931 заказу 9,38 1394,7 … 1431,5
Правильный, по 5000 пользователей (дельта-метод) 15,21 1383,3 … 1442,9
Правильный, бутстрап по пользователям 15,41

Наивный интервал в 1,6 раза уже правильного. Поскольку дисперсия — квадрат SE, это означает, что расчёт приписывает себе в $1{,}62^2 \approx 2{,}6$ раза больше информации, чем в данных есть. В A/B-тесте это ровно тот механизм, из-за которого «значимые» результаты не воспроизводятся.

Величина эффекта описывается коэффициентом внутрикластерной корреляции $\rho$ и средним размером кластера $\bar m$:

$$\mathrm{DEFF} = 1 + (\bar m - 1)\,\rho, \qquad n_{\text{eff}} = \frac{n}{\mathrm{DEFF}}, \qquad \mathrm{SE}{\text{верно}} = \mathrm{SE}{\text{наивно}} \cdot \sqrt{\mathrm{DEFF}}$$

$\bar m$ (событий на пользователя) $\rho$ DEFF Во сколько раз шире интервал
2 0,2 1,2 1,10
3 0,5 2,0 1,41
5 0,3 2,2 1,48
10 0,1 1,9 1,38

В примере выше $\bar m = 3{,}0$, $\rho \approx 0{,}64$, DEFF $\approx 2{,}3$, $\sqrt{\mathrm{DEFF}} \approx 1{,}5$ — почти в точности наблюдённые 1,6. Отсюда практическое правило: сначала сверните данные до уровня рандомизации, потом считайте. Если эксперимент рандомизирует пользователей — единица наблюдения пользователь, даже если метрика формулируется в кликах.

Отношения метрик и дельта-метод

Отдельная ловушка — метрики вида «отношение сумм»: средний чек = выручка / число заказов, CTR = клики / показы, конверсия из корзины в оплату по событиям. Это не среднее по пользователям, поэтому обычная SE не годится. Работает дельта-метод: пусть $x_i$ и $y_i$ — числитель и знаменатель, агрегированные по пользователю $i$, и $R = \bar x / \bar y$. Тогда

$$\widehat{\mathrm{Var}}(R) \approx \frac{1}{n\,\bar y^{\,2}}\left( s_x^2 - 2R\,s_{xy} + R^2 s_y^2 \right)$$

где $s_x^2$, $s_y^2$ — выборочные дисперсии, $s_{xy}$ — ковариация, $n$ — число пользователей. Ковариационный член обязателен: у активных пользователей и числитель, и знаменатель больше, и игнорирование этой связи снова даёт слишком узкий интервал. Промышленное изложение с деталями для A/B: Deng, Knoblich, Lu, «Applying the Delta Method in Metric Analytics», KDD 2018.

Интервалы прямо в SQL

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

Интервал Уилсона для конверсии по сегментам

-- PostgreSQL. Конверсия по каналам с 95-процентным интервалом Уилсона.
WITH agg AS (
    SELECT channel,
           COUNT(*)                          AS n,
           COUNT(*) FILTER (WHERE converted) AS k
    FROM sessions
    WHERE session_date >= DATE '2026-06-01'
      AND session_date <  DATE '2026-07-01'
    GROUP BY channel
),
base AS (
    SELECT channel, n, k,
           k::double precision / n AS p,
           1.959964                AS z    -- 95 %; для 99 % возьмите 2.575829
    FROM agg WHERE n > 0
)
SELECT channel, n, k,
       ROUND(p::numeric, 4)                                               AS conversion,
       ROUND(((p + z*z/(2*n)) / (1 + z*z/n)
              - z/(1 + z*z/n) * sqrt(p*(1-p)/n + z*z/(4*n*n)))::numeric, 4) AS ci_low,
       ROUND(((p + z*z/(2*n)) / (1 + z*z/n)
              + z/(1 + z*z/n) * sqrt(p*(1-p)/n + z*z/(4*n*n)))::numeric, 4) AS ci_high
FROM base
ORDER BY n DESC;

Такая таблица моментально закрывает разговоры вида «в канале referral конверсия выше на 0,3 п.п., перекладываем бюджет»: если интервалы каналов перекрываются почти целиком, перекладывать нечего.

Интервал для среднего с проверкой на скошенность

-- Средний чек по платформам + асимметрия, чтобы понимать, можно ли верить интервалу.
-- Асимметрия через сырые моменты, один проход:
--   g1 = (E[x^3] - 3*mu*E[x^2] + 2*mu^3) / (E[x^2] - mu^2)^1.5
SELECT platform,
       COUNT(*)                                          AS n,
       ROUND(AVG(amount)::numeric, 1)                    AS mean_amount,
       ROUND((AVG(amount) - 1.959964 * STDDEV_SAMP(amount)
                          / sqrt(COUNT(*)))::numeric, 1) AS ci_low,
       ROUND((AVG(amount) + 1.959964 * STDDEV_SAMP(amount)
                          / sqrt(COUNT(*)))::numeric, 1) AS ci_high,
       ROUND(((AVG(POWER(amount, 3)) - 3 * AVG(amount) * AVG(POWER(amount, 2))
               + 2 * POWER(AVG(amount), 3))
              / POWER(AVG(POWER(amount, 2)) - POWER(AVG(amount), 2), 1.5))::numeric, 2)
                                                         AS skewness
FROM orders
WHERE created_at >= TIMESTAMP '2026-06-01 00:00:00'
  AND created_at <  TIMESTAMP '2026-07-01 00:00:00'
GROUP BY platform
HAVING COUNT(*) >= 100;

В ClickHouse и DuckDB есть готовая skewness(), в PostgreSQL её нет — отсюда явные моменты; на больших n версии совпадают до второго знака. Правило чтения: skewness больше 2 при n меньше 25 * skewness^2 — интервал декоративный, переходите к медиане или бутстрапу.

Дельта-метод для отношения метрик

-- Средний чек как отношение сумм: единица наблюдения — пользователь, а не заказ.
WITH per_user AS (
    SELECT user_id,
           SUM(amount) AS x,      -- числитель: выручка пользователя
           COUNT(*)    AS y       -- знаменатель: число его заказов
    FROM orders
    WHERE created_at >= TIMESTAMP '2026-06-01 00:00:00'
      AND created_at <  TIMESTAMP '2026-07-01 00:00:00'
    GROUP BY user_id
),
stats AS (
    SELECT COUNT(*)::double precision                           AS n,
           AVG(x::double precision)                             AS mx,
           AVG(y::double precision)                             AS my,
           VAR_SAMP(x::double precision)                        AS vx,
           VAR_SAMP(y::double precision)                        AS vy,
           COVAR_SAMP(x::double precision, y::double precision) AS cxy
    FROM per_user
),
ratio AS (
    SELECT n, mx / my AS r,
           sqrt((vx - 2*(mx/my)*cxy + (mx/my)*(mx/my)*vy) / (n * my * my)) AS se
    FROM stats
)
SELECT n                                      AS users,
       ROUND(r::numeric, 2)                   AS avg_order_value,
       ROUND(se::numeric, 2)                  AS se_delta,
       ROUND((r - 1.959964 * se)::numeric, 2) AS ci_low,
       ROUND((r + 1.959964 * se)::numeric, 2) AS ci_high
FROM ratio;

Бутстрап, не выходя из хранилища

Пересэмплирование с возвращением на миллионах строк в SQL неудобно. Промышленная замена — пуассоновский бутстрап: вместо выбора $n$ элементов с возвращением каждому наблюдению независимо присваивается вес из распределения Пуассона с параметром 1. При больших $n$ распределение реплик то же, но считается всё одним проходом и параллелится.

-- 500 реплик ARPU. Вес ~ Poisson(1) задан через обратную функцию распределения.
WITH per_user AS (
    SELECT user_id, SUM(amount) AS revenue FROM orders GROUP BY user_id
),
draws AS (                       -- одна равномерная величина на пару «реплика × пользователь»
    SELECT r.b, u.revenue, random() AS u
    FROM generate_series(1, 500) AS r(b)
    CROSS JOIN per_user u
),
weighted AS (
    SELECT b, revenue,
           CASE WHEN u < 0.367879 THEN 0     -- P(0) = e^-1
                WHEN u < 0.735759 THEN 1     -- P(0)+P(1)
                WHEN u < 0.919699 THEN 2
                WHEN u < 0.981012 THEN 3
                WHEN u < 0.996340 THEN 4
                WHEN u < 0.999406 THEN 5
                ELSE 6 END AS w
    FROM draws
),
arpu AS (
    SELECT b, SUM(revenue * w) / NULLIF(SUM(w), 0) AS arpu_b
    FROM weighted GROUP BY b
)
SELECT ROUND(percentile_cont(0.025) WITHIN GROUP (ORDER BY arpu_b)::numeric, 2) AS ci_low,
       ROUND(percentile_cont(0.500) WITHIN GROUP (ORDER BY arpu_b)::numeric, 2) AS arpu_median,
       ROUND(percentile_cont(0.975) WITHIN GROUP (ORDER BY arpu_b)::numeric, 2) AS ci_high
FROM arpu;

Проверка на смоделированных данных: пуассоновский бутстрап дал 3823,5 … 3889,1, аналитический нормальный интервал — 3820,2 … 3889,2. Цена: запрос читает n × B строк, при 500 репликах и миллионе пользователей это полмиллиарда строк — на колоночном движке (ClickHouse, BigQuery, DuckDB) терпимо, на строчной OLTP-базе нет; про разницу см. колоночные хранилища. Уменьшайте $B$ до 200, если нужна только оценка SE, а не хвостовые перцентили.

Обратная задача: сколько данных нужно

До сих пор мы отвечали «насколько точна цифра, которая уже есть». Планирование задаёт обратный вопрос: какой объём нужен, чтобы различить эффект интересующего размера. Формула для сравнения двух долей (на одну группу, двусторонний критерий):

$$n = \frac{\left(z_{1-\alpha/2} + z_{1-\beta}\right)^2 \left( p_1(1-p_1) + p_2(1-p_2) \right)}{(p_2 - p_1)^2}$$

При стандартных $\alpha = 0{,}05$ и мощности $1-\beta = 0{,}8$ множитель $(1{,}960 + 0{,}842)^2 \approx 7{,}85$. Для среднего аналогично: $n \approx 15{,}7\,\sigma^2 / \Delta^2$ на группу.

from math import sqrt
from statistics import NormalDist

def sample_size_proportion(p_base, mde_rel, alpha=0.05, power=0.80):
    """Размер ОДНОЙ группы для двустороннего теста двух долей.
    mde_rel — относительный эффект: 0.10 значит «хотим ловить рост на 10 %»."""
    z_a, z_b = NormalDist().inv_cdf(1 - alpha / 2), NormalDist().inv_cdf(power)
    p1, p2 = p_base, p_base * (1 + mde_rel)
    return (z_a + z_b) ** 2 * (p1 * (1 - p1) + p2 * (1 - p2)) / (p2 - p1) ** 2

def detectable_effect(p_base, n_per_group, alpha=0.05, power=0.80):
    """Обратная задача: какой минимальный АБСОЛЮТНЫЙ эффект различим при данном n."""
    z_a, z_b = NormalDist().inv_cdf(1 - alpha / 2), NormalDist().inv_cdf(power)
    return (z_a + z_b) * sqrt(2 * p_base * (1 - p_base) / n_per_group)

round(sample_size_proportion(0.04, 0.10))        # 39472 на группу
round(detectable_effect(0.04, 10_000) * 100, 2)  # 0.78 п.п.

Базовая конверсия 4 процента, $\alpha = 0{,}05$, мощность 0,8:

Хотим поймать рост Абсолютный эффект n на группу Дней при 20 000 сессий/день
+50 % (до 6,0 %) 2,00 п.п. 1 860 0,2
+20 % (до 4,8 %) 0,80 п.п. 10 313 1,0
+10 % (до 4,4 %) 0,40 п.п. 39 472 3,9
+5 % (до 4,2 %) 0,20 п.п. 154 301 15,4
+2 % (до 4,08 %) 0,08 п.п. 950 884 95
+1 % (до 4,04 %) 0,04 п.п. 3 785 507 379

Квадратичная зависимость видна невооружённым глазом: переход от «ловим +10 процентов» к «ловим +5 процентов» стоит вчетверо больше данных. Именно поэтому «а давайте измерим эффект точнее» почти всегда означает «давайте потратим на это квартал».

Ветка I — не поражение, а самый ценный результат планирования. Тест, который заведомо не может обнаружить обсуждаемый эффект, потребляет трафик, время и внимание, а на выходе даёт «различий не обнаружено» — фразу, которую все прочитают как «эффекта нет». Про эту подмену и про мощность подробно — в главе про A/B; про решение без эксперимента — в главе про причинность и в MVP и экспериментах.

Что ваш объём вообще способен различить

Более полезная в реальной жизни постановка: трафик фиксирован, вопрос — что вы вообще можете узнать. При базовой конверсии $p$:

$$\Delta_{\min} = \left(z_{1-\alpha/2} + z_{1-\beta}\right)\sqrt{\frac{2\,p(1-p)}{n}}$$

n на группу Минимально различимый эффект при $p = 4\,%$ В относительных величинах
2 000 1,74 п.п. +43 %
10 000 0,78 п.п. +19 %
50 000 0,35 п.п. +9 %
100 000 0,25 п.п. +6 %
500 000 0,11 п.п. +3 %

Читать так: при 10 000 пользователей на группу вы способны надёжно поймать только рост конверсии почти на пятую часть. Изменение цвета кнопки таких эффектов не даёт — значит, этот эксперимент бессмысленен до того, как он запущен, и это можно сказать заранее за две минуты. Быстрая прикидка — калькулятор Эвана Миллера, в коде — statsmodels power.

И один пункт, который относится сюда, хотя разбирается в главе про A/B: интервал честен ровно один раз — в момент, для которого он планировался. Если пересчитывать его каждый день и остановиться, когда он перестал накрывать ноль, реальная частота ложных срабатываний уходит с 5 процентов к 20–30. Правильность формулы это не компенсирует: сломано не вычисление, а процедура, к которой относится гарантия. Механику подглядывания и корректные последовательные критерии разбирает глава 09.

Что делать с интервалом: решение, а не украшение

Интервал приносит пользу тогда, когда его сопоставляют с порогом, меняющим решение, — той самой величиной, ради которой задавался вопрос. Четыре исхода:

Разница между квадрантами 3 и 4 — та, которую путают чаще всего. «Интервал накрывает ноль» означает разное в зависимости от ширины: интервал от −0,2 до +0,25 п.п. при базе 4 процента — это эффекта практически нет, любой заметный эффект данными исключён, содержательный и ценный ответ. Интервал от −4 до +5 п.п. — это мы ничего не узнали: и удвоение конверсии, и обвал совместимы с данными, и писать здесь «влияния не обнаружено» значит дезинформировать.

Формулировки, которые стоит перенести в свои отчёты дословно:

  • «Рост конверсии 0,40 п.п. (95 %: 0,01 … 0,79), то есть от +0,2 до +19,8 процента относительно базы. Эффект скорее положительный, но его величина не определена: нижняя граница не окупает разработку, верхняя окупает вдесятеро».
  • «Различие −0,05 п.п. (95 %: −0,25 … +0,15). Эффект больше 0,25 п.п. в любую сторону данными исключён; порог окупаемости был 0,5 п.п., поэтому решение — не внедрять».
  • «Интервал −4 … +5 п.п. Замер не даёт информации о вопросе; при текущем трафике нужно 12 недель. Предлагаю решать иначе».

Разница между первой и третьей формулировкой — это разница между аналитиком и генератором графиков. Как встроить такое в регулярную отчётность, разбирает глава про дашборды.

Как показывать неопределённость и как её показывать нельзя

Три правила, дальнейшее — в главе про визуализацию.

1. Столбик без усов — утверждение о точности, которого у вас нет. Столбчатая диаграмма из двух столбиков 4,0 и 4,3 читается как «выросло». Те же данные с интервалами читаются как «неизвестно». Если данных на интервал не хватает, это факт про данные, а не повод его скрыть.

2. Перекрывающиеся усы НЕ означают отсутствия различий. Классическая ошибка чтения. Пусть у двух групп одинаковая SE. Их 95-процентные интервалы перекрываются, пока разность меньше $2 z \cdot \mathrm{SE} = 3{,}92\,\mathrm{SE}$. А различие значимо на уровне 0,05, когда разность больше $z\sqrt{2} \cdot \mathrm{SE} = 2{,}77\,\mathrm{SE}$. В зазоре между 2,77 и 3,92 усы перекрываются, а различие значимо. Обратное неверно: непересекающиеся 95-процентные интервалы почти всегда означают значимое различие. Разбор — Cumming & Finch, «Inference by Eye» (American Psychologist, 2005).

Отсюда вывод, экономящий много споров: стройте интервал для разности, а не два интервала для групп — вопрос был про разницу, интервал должен быть про разницу.

$$\mathrm{SE}(\hat p_2 - \hat p_1) = \sqrt{\frac{\hat p_1 (1-\hat p_1)}{n_1} + \frac{\hat p_2 (1-\hat p_2)}{n_2}}$$

Пример: 4,00 против 4,40 процента, по 20 000 в группе. $\mathrm{SE} = 0{,}00201$, разность 0,40 п.п., интервал 0,007 … 0,793 п.п. В относительных величинах — рост от 0,2 до 19,8 процента. Заголовок «конверсия выросла на 10 процентов» формально не соврал и при этом не сообщил почти ничего.

3. Не подписывайте интервал как «разброс данных». Интервал среднего сужается с ростом $n$, разброс самих наблюдений — нет. Нужен разброс — показывайте перцентили или ящик с усами; это разные картинки с разным смыслом.

Типичные ошибки и когда интервал бесполезен

Ошибка или ситуация Как выглядит Что делать
Цифра без интервала «Конверсия 4,3 %» Всегда рядом n и границы
Wald при малых счётчиках Нижняя граница отрицательна Уилсон или Клоппер — Пирсон
«Событий нет, значит не бывает» 0 из 500 → «0 %» Правило трёх: не выше 3/n
n = число событий 100 000 кликов от 8000 юзеров Свернуть до уровня рандомизации
Отношение метрик как обычное среднее CTR через AVG по строкам показов Дельта-метод или бутстрап по пользователям
Среднее по скошенным данным при малом n AVG(revenue), n = 80 Медиана, бутстрап, честная оговорка
Ежедневный пересчёт интервала «Уже почти значимо, подождём» Правило остановки заранее, глава 09
Перекрывающиеся усы как «разницы нет» Два столбика с усами Интервал для разности
«Вероятность 95 %, что истина внутри» В тексте отчёта «Совместимо с данными»
Смещение сбора больше ширины интервала Узко и уверенно мимо Оценить смещение численно, глава 02
Гипотеза выбрана после просмотра данных «Смотрите, в этом сегменте!» Назвать находку поисковой, перепроверить на новых данных
Сравниваются 20 сегментов сразу Один «значимый» ожидаем и без эффекта Поправка на множественность
Величина учётная, а не выборочная Сумма счетов за июнь Просто число, интервал не нужен

Отдельно: интервал не защищает от того, что вы измерили не ту величину. Узкий интервал вокруг метрики, не связанной с решением, — точный ответ на неправильный вопрос; про то, как метрика меняет поведение системы, едва став целью, — глава 14 и локальная оптимизация.

Чеклист перед публикацией цифры

  1. Что здесь единица наблюдения и совпадает ли она с уровнем рандомизации или отбора?
  2. Чему равно $n$ независимых единиц, а не строк в выгрузке?
  3. Каков масштаб известного смещения сбора и больше ли он ширины интервала?
  4. Доля или среднее? Для доли — Уилсон; для среднего — какова асимметрия?
  5. Метрика — отношение сумм? Тогда дельта-метод или бутстрап по кластерам.
  6. Интервал относится к разности, если вопрос был про разность?
  7. Гипотеза сформулирована до того, как вы посмотрели на данные, и сколько метрик вы проверили заодно?
  8. Какой порог меняет решение, как интервал расположен относительно него — и если интервал широкий, написано ли явно «мы не узнали ответ», а не «различий нет»?

Мини-итог

Доверительный интервал — не украшение отчёта, а способ признать конечность данных в проверяемой форме. Он описывает только случайный шум и молчит про смещение сбора, поэтому узкий интервал вокруг смещённой оценки — самая опасная конфигурация из возможных. Точность растёт как $\sqrt{n}$, из чего следует, что вдвое более точный ответ стоит вчетверо дороже, а «уточнить на порядок» обычно означает «отказаться от вопроса». Для долей работает Уилсон, для скошенных средних — бутстрап или отказ от среднего, для отношений — дельта-метод, а $n$ почти всегда меньше, чем кажется, потому что события одного пользователя не независимы. И самое практичное: расчёт минимально различимого эффекта делается до эксперимента и часто его отменяет — это лучший из возможных результатов, потому что стоит две минуты вместо трёх недель.

Источники

Что дальше

Мы научились говорить, насколько цифра могла бы быть другой. Следующий шаг — научиться отвечать на вопрос «а могло ли наблюдённое различие возникнуть само», и разобраться, что при этом означает и, главное, чего не означает знаменитая величина, которую все считают и почти никто не формулирует верно.

Проверка гипотез: что на самом деле означает p-значение

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов