Как посчитать Mann-Whitney test в SQL
Содержание:
Зачем Mann-Whitney
Mann-Whitney U test (он же Wilcoxon rank-sum) проверяет, отличаются ли распределения двух выборок. Работает на ранках, не на значениях — поэтому терпим к выбросам и не требует нормальности. Подходит, когда revenue скошенный, есть whales, или метрика порядковая (NPS, rating).
Идея через ранги
- Объединяем обе выборки.
- Сортируем по значению, присваиваем ранги (1, 2, 3, ...).
- Для каждой группы считаем сумму рангов.
- U = R_A − n_A × (n_A + 1) / 2.
Если выборки одинаковые, U ≈ n_A × n_B / 2.
Mann-Whitney в SQL
WITH combined AS (
SELECT user_id, variant, revenue
FROM ab_users
WHERE experiment_id = 'pricing_v2'
),
ranked AS (
SELECT
variant,
revenue,
RANK() OVER (ORDER BY revenue) AS rnk
FROM combined
),
rank_sums AS (
SELECT
variant,
COUNT(*) AS n,
SUM(rnk) AS rank_sum
FROM ranked
GROUP BY variant
),
pair AS (
SELECT
MAX(CASE WHEN variant = 'A' THEN n END) AS n_a,
MAX(CASE WHEN variant = 'A' THEN rank_sum END) AS r_a,
MAX(CASE WHEN variant = 'B' THEN n END) AS n_b,
MAX(CASE WHEN variant = 'B' THEN rank_sum END) AS r_b
FROM rank_sums
)
SELECT
n_a,
n_b,
r_a - n_a * (n_a + 1) / 2 AS u_a,
r_b - n_b * (n_b + 1) / 2 AS u_b,
LEAST(
r_a - n_a * (n_a + 1) / 2,
r_b - n_b * (n_b + 1) / 2
) AS u_statistic
FROM pair;RANK() присваивает одинаковый ранг для ties и пропускает следующие позиции. Можно DENSE_RANK() или ручную average rank — выбор зависит от того, как считаете дробные ранги при tie-correction.
Z-приближение и p-value
Для n_A, n_B > 20 распределение U приближается нормальным:
μ_U = n_A × n_B / 2
σ_U = sqrt(n_A × n_B × (n_A + n_B + 1) / 12)
Z = (U − μ_U) / σ_UWITH stats AS (
SELECT
n_a, n_b,
LEAST(r_a - n_a * (n_a + 1) / 2, r_b - n_b * (n_b + 1) / 2)::NUMERIC AS u_stat
FROM pair
)
SELECT
u_stat,
n_a * n_b / 2.0 AS mu_u,
SQRT(n_a * n_b * (n_a + n_b + 1)::NUMERIC / 12) AS sigma_u,
(u_stat - n_a * n_b / 2.0)
/ NULLIF(SQRT(n_a * n_b * (n_a + n_b + 1)::NUMERIC / 12), 0) AS z_statistic,
CASE
WHEN ABS((u_stat - n_a * n_b / 2.0)
/ SQRT(n_a * n_b * (n_a + n_b + 1)::NUMERIC / 12)) > 1.96
THEN 'significant at ~0.05'
ELSE 'NOT significant'
END AS verdict
FROM stats;Сравнение с t-test
SELECT
-- t-test (см. рецепт t-test в SQL)
ABS(t_statistic) > 1.96 AS t_significant,
-- Mann-Whitney
ABS(z_statistic) > 1.96 AS mw_significant
FROM (
SELECT 2.1 AS t_statistic, 1.8 AS z_statistic
) example;Если t-test даёт «значимо», а Mann-Whitney — нет, скорее всего, разница тащится выбросами. Mann-Whitney устойчив.
Частые ошибки
Ошибка 1. Не учитывать ties. При большом числе одинаковых значений (например, 80% юзеров с revenue=0) формула σ_U без поправки на ties завышена. Используйте «tie correction» или просто bootstrap.
Ошибка 2. Считать на разных множествах юзеров. В A/B все юзеры в обеих группах должны иметь значение метрики — даже 0. Иначе сравниваете «конвертнувших B vs всех A».
Ошибка 3. Mann-Whitney = t-test для медианы? Нет. Mann-Whitney проверяет «P(X > Y) ≠ 0.5», что отличается от равенства медиан, если распределения разной формы.
Ошибка 4. Парные выборки. Для paired data — Wilcoxon signed-rank, не Mann-Whitney.
Ошибка 5. На маленьких выборках Z-приближение. При n < 20 в каждой группе — точные таблицы U или Monte Carlo. Z даст неверный p-value.
Связанные темы
- Как посчитать t-test в SQL
- Как посчитать chi-square test в SQL
- Что такое Mann-Whitney test
- Mann-Whitney test
FAQ
Когда Mann-Whitney вместо t-test?
Скошенные данные (revenue с whales), мало точек, порядковые метрики (NPS 0–10).
Mann-Whitney vs Wilcoxon?
Mann-Whitney U и Wilcoxon rank-sum — одно и то же. Wilcoxon signed-rank — другой тест, для paired data.
Что показывает U?
Сколько пар (a_i, b_j), где a_i > b_j. Если выборки одинаковые, U ≈ n_A × n_B / 2.
P-value в SQL?
Z-приближение даёт нормальное p — 2 * (1 − Φ(|Z|)). CDF нормали в Postgres нет, считают вне.
Ties как обрабатывать?
RANK() присваивает min-rank, DENSE_RANK() — без пропусков. Стандарт — average rank, его в SQL надо считать вручную или через (ROW_NUMBER + RANK) / 2.