Результаты А/Б теста принято анализировать в Python или R. Но если данные уже в PostgreSQL, SQL справится сам — включая z-тест, доверительные интервалы и расчёт нужного размера выборки.
Базовое сравнение конверсий
Предположим, есть таблица experiments:
-- experiments: user_id, variant ('control' | 'treatment'), converted (boolean), created_at
Первым делом — простое сравнение:
SELECT
variant,
COUNT(*) AS users,
SUM(converted::int) AS conversions,
ROUND(100.0 * SUM(converted::int) / COUNT(*), 2) AS conversion_pct
FROM experiments
WHERE experiment_name = 'new_checkout_button'
AND created_at BETWEEN '2026-03-01' AND '2026-03-31'
GROUP BY variant
ORDER BY variant;
Пример результата:
| variant | users | conversions | conversion_pct |
|---|---|---|---|
| control | 4821 | 386 | 8.00% |
| treatment | 4934 | 443 | 8.98% |
Разница есть — +0.98%. Но случайна ли она?
Z-тест для пропорций
Формула z-статистики для двух пропорций:
z = (p1 - p2) / sqrt(p_pool * (1 - p_pool) * (1/n1 + 1/n2))
где p_pool = (conv1 + conv2) / (n1 + n2) — объединённая конверсия.
Реализуем в SQL:
WITH stats AS (
SELECT
SUM(CASE WHEN variant = 'control' THEN 1 ELSE 0 END) AS n_control,
SUM(CASE WHEN variant = 'treatment' THEN 1 ELSE 0 END) AS n_treatment,
SUM(CASE WHEN variant = 'control' AND converted THEN 1 ELSE 0 END) AS c_control,
SUM(CASE WHEN variant = 'treatment' AND converted THEN 1 ELSE 0 END) AS c_treatment
FROM experiments
WHERE experiment_name = 'new_checkout_button'
),
rates AS (
SELECT
n_control,
n_treatment,
c_control,
c_treatment,
c_control::float / n_control AS p1, -- конверсия контроля
c_treatment::float / n_treatment AS p2, -- конверсия лечения
(c_control + c_treatment)::float / (n_control + n_treatment) AS p_pool
FROM stats
)
SELECT
ROUND(p1 * 100, 2) AS control_pct,
ROUND(p2 * 100, 2) AS treatment_pct,
ROUND((p2 - p1) * 100, 2) AS lift_pct,
ROUND(
(p2 - p1) /
SQRT(p_pool * (1 - p_pool) * (1.0/n_control + 1.0/n_treatment))
, 4) AS z_stat
FROM rates;
Интерпретация z-статистики
| |z| | Примерный p-value | Вывод | |---|---|---| | < 1.65 | > 0.10 | Не значимо | | 1.65 — 1.96 | 0.05 — 0.10 | Слабый сигнал | | 1.96 — 2.58 | 0.01 — 0.05 | Значимо (p < 0.05) | | > 2.58 | < 0.01 | Высокозначимо (p < 0.01) |
Если |z| > 1.96 — можно отвергнуть нулевую гипотезу при уровне доверия 95%.
Доверительный интервал для конверсии
Для каждого варианта считаем 95%-ный доверительный интервал:
WITH stats AS (
SELECT
variant,
COUNT(*) AS n,
SUM(converted::int) AS c,
SUM(converted::int)::float / COUNT(*) AS p
FROM experiments
WHERE experiment_name = 'new_checkout_button'
GROUP BY variant
)
SELECT
variant,
ROUND(p * 100, 2) AS conversion_pct,
ROUND((p - 1.96 * SQRT(p * (1-p) / n)) * 100, 2) AS ci_lower,
ROUND((p + 1.96 * SQRT(p * (1-p) / n)) * 100, 2) AS ci_upper
FROM stats;
Если доверительные интервалы контроля и лечения не пересекаются — результат статистически значим.
Расчёт нужного размера выборки
Перед запуском теста важно понять: сколько пользователей нужно на каждый вариант? Формула для 95% доверия и 80% мощности:
WITH params AS (
SELECT
0.08 AS p_baseline, -- текущая конверсия (8%)
0.009 AS mde, -- минимальный обнаруживаемый эффект (+0.9%)
1.96 AS z_alpha, -- z для α=0.05 (двусторонний)
0.84 AS z_beta -- z для β=0.20 (мощность 80%)
)
SELECT
CEIL(
2 * POWER(z_alpha + z_beta, 2)
* p_baseline * (1 - p_baseline)
/ POWER(mde, 2)
) AS required_users_per_variant
FROM params;
Запустите этот запрос до теста — и вы будете знать, когда можно останавливаться.
Типичные ошибки в А/Б тестах
1. Peeking problem (подглядывание). Нельзя останавливать тест, как только результат стал «значимым» — это ложная значимость. Определите размер выборки заранее и не смотрите до его достижения.
2. Множественное тестирование. Тестируете 10 метрик одновременно? При α=0.05 в среднем одна из 20 будет «значима» случайно. Применяйте поправку Бонферрони: делите α на число тестов.
-- Вместо z > 1.96 (α = 0.05), при 5 метриках используйте z > 2.58 (α = 0.01)
WHERE ABS(z_stat) > 2.58
3. Сегментированная разбивка после факта. Нашли значимость только в сегменте «iOS + Москва»? Это data dredging — случайная находка, не результат.
4. Неравные условия между вариантами. Убедитесь, что трафик разделён равномерно, а оба варианта работают одновременно, а не последовательно.
Хотите оттачивать аналитические запросы на реальных задачах? Заходите в тренажёр SQLlab.