Как посчитать chi-square test в SQL

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

Зачем chi-square

Chi-square test (χ²) проверяет, связаны ли две категориальные переменные. Самое типичное применение — A/B-тест на бинарную метрику (конвертнул / не конвертнул) для двух или более групп. Если хочется проверить «реально ли вариант B показывает другой CR, или это шум выборки» — это chi-square.

Таблица сопряжённости

Базовая структура — 2×2 (для A/B):

Конверсия Без конверсии
Группа A 120 880
Группа B 145 855

Сначала эту таблицу нужно построить в SQL, потом считать тест.

Формула

χ² = Σ (O_i − E_i)² / E_i
  • O_i — наблюдаемое значение в ячейке
  • E_i — ожидаемое при независимости = (row_total × col_total) / grand_total

Степени свободы: (rows−1) × (cols−1). Для 2×2 → df=1.

P-value считают по χ²-распределению — в чистом SQL точное значение проблематично, обычно сравнивают с критическим (df=1, α=0.05 → χ² > 3.841).

Chi-square для A/B-теста

WITH cells AS (
    SELECT
        variant,
        SUM(CASE WHEN converted THEN 1 ELSE 0 END) AS conv,
        SUM(CASE WHEN NOT converted THEN 1 ELSE 0 END) AS non_conv,
        COUNT(*) AS row_total
    FROM ab_experiment_users
    WHERE experiment_id = 'checkout_v2'
    GROUP BY variant
),
totals AS (
    SELECT
        SUM(conv) AS total_conv,
        SUM(non_conv) AS total_non_conv,
        SUM(row_total) AS grand_total
    FROM cells
),
expected AS (
    SELECT
        c.variant,
        c.conv AS o_conv,
        c.non_conv AS o_non_conv,
        c.row_total::NUMERIC * t.total_conv / NULLIF(t.grand_total, 0) AS e_conv,
        c.row_total::NUMERIC * t.total_non_conv / NULLIF(t.grand_total, 0) AS e_non_conv
    FROM cells c
    CROSS JOIN totals t
)
SELECT
    SUM(POWER(o_conv - e_conv, 2) / NULLIF(e_conv, 0))
    + SUM(POWER(o_non_conv - e_non_conv, 2) / NULLIF(e_non_conv, 0)) AS chi_square,
    1 AS df,
    CASE
        WHEN SUM(POWER(o_conv - e_conv, 2) / NULLIF(e_conv, 0))
             + SUM(POWER(o_non_conv - e_non_conv, 2) / NULLIF(e_non_conv, 0)) > 3.841
        THEN 'significant at 0.05'
        ELSE 'NOT significant'
    END AS verdict
FROM expected;

Критические значения для df=1: 3.841 (α=0.05), 6.635 (α=0.01), 10.828 (α=0.001).

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

Несколько групп

Для k вариантов (A/B/C/...):

WITH cells AS (
    SELECT
        variant,
        SUM(CASE WHEN converted THEN 1 ELSE 0 END) AS conv,
        SUM(CASE WHEN NOT converted THEN 1 ELSE 0 END) AS non_conv,
        COUNT(*) AS row_total
    FROM ab_experiment_users
    WHERE experiment_id = 'multivariate_v1'
    GROUP BY variant
),
totals AS (
    SELECT
        COUNT(*) AS k,
        SUM(conv) AS total_conv,
        SUM(non_conv) AS total_non_conv,
        SUM(row_total) AS grand_total
    FROM cells
),
chi AS (
    SELECT
        SUM(POWER(c.conv - c.row_total::NUMERIC * t.total_conv / t.grand_total, 2)
            / NULLIF(c.row_total::NUMERIC * t.total_conv / t.grand_total, 0))
        + SUM(POWER(c.non_conv - c.row_total::NUMERIC * t.total_non_conv / t.grand_total, 2)
            / NULLIF(c.row_total::NUMERIC * t.total_non_conv / t.grand_total, 0)) AS chi_square,
        t.k - 1 AS df
    FROM cells c
    CROSS JOIN totals t
    GROUP BY t.k
)
SELECT chi_square, df FROM chi;

P-value уже считают вне SQL — Python scipy.stats.chi2.sf(chi_square, df).

Частые ошибки

Ошибка 1. Применять на малых частотах. Если ожидаемое в ячейке < 5 — chi-square даёт неверный p-value. Используйте Fisher exact test (на малых выборках).

Ошибка 2. Integer division. row_total * total_conv / grand_total в Postgres = 0 для маленьких ячеек без ::NUMERIC. Приводите к NUMERIC до деления.

Ошибка 3. Зависимые наблюдения. Если один юзер попал в несколько строк (например, posthog event_count), независимость нарушена. Считайте на уровне user_id.

Ошибка 4. Сравнивать с z-test для пропорций без понимания. Для 2×2 chi-square даёт ровно тот же p-value, что two-proportion z-test. Это эквивалентно.

Ошибка 5. Yates correction в больших таблицах. Поправка Йейтса (вычитание 0.5) уместна только для df=1 и маленьких выборок. На больших — занижает chi-square.

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

FAQ

Когда chi-square, а не t-test?

Chi-square — бинарные или категориальные исходы. T-test — непрерывные (revenue, time-on-page).

Какой минимум на ячейку?

Правило большого пальца: ожидаемое в каждой ячейке ≥ 5. Иначе Fisher exact.

Где брать p-value?

В Postgres нет встроенной CDF. Считают вне SQL: scipy, R, или табличный лук-ап для крит-значений.

Yates correction обязательна?

Только для 2×2 и маленьких выборок. На N > 1000 — игнорируйте.

Multiple comparisons?

Сравниваете 3+ группы попарно — применяйте Bonferroni или Holm-Bonferroni к p-value.