Как посчитать Mann-Whitney test в SQL

Закрепи формулу mann whitney test в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать mann whitney test в Telegram

Зачем Mann-Whitney

Mann-Whitney U test (он же Wilcoxon rank-sum) проверяет, отличаются ли распределения двух выборок. Работает на ранках, не на значениях — поэтому терпим к выбросам и не требует нормальности. Подходит, когда revenue скошенный, есть whales, или метрика порядковая (NPS, rating).

Идея через ранги

  1. Объединяем обе выборки.
  2. Сортируем по значению, присваиваем ранги (1, 2, 3, ...).
  3. Для каждой группы считаем сумму рангов.
  4. 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) / σ_U
WITH 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;
Закрепи формулу mann whitney test в Карьернике
Запомнить надолго — 5 коротких сессий с задачами на эту тему. Бесплатно
Тренировать mann whitney test в Telegram

Сравнение с 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.

Связанные темы

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.