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

Проверь себя · 1/3разбор после ответа
Вы сортируете товары по величине скидки discount по убыванию. Поле discount может быть NULL (скидки нет). Чтобы товары без скидки всегда оказывались внизу независимо от настроек СУБД, какой вариант сортировки выбрать?

Зачем это знать

Сравнение средних — базовая задача аналитика. «CR A/B-теста 10.2% vs 10.5% — это реально разница или случайность?». «Средний чек после релиза 1500 → 1520, значим ли lift?». Без статистического теста ответ гадание.

На собесе спросят: «какой тест для сравнения средних?». Junior скажет t-test. Middle добавит Welch's для неравных variance. Senior обсудит — когда t-test не работает и что делать (Mann-Whitney, bootstrap).

В статье:

  • T-test: Student's vs Welch's
  • Assumptions и когда они нарушаются
  • Mann-Whitney U при non-normal
  • Bootstrap как универсал
  • Python код для всех

Тесты для сравнения средних

Independent samples t-test (Student's)

Сравнение двух independent групп.

Assumptions:

  • Независимость наблюдений
  • Approximately normal distribution (для small N)
  • Equal variance (Student's) или не equal (Welch's)
from scipy.stats import ttest_ind

# Student's (assumes equal variance)
t_stat, p_value = ttest_ind(group_a, group_b, equal_var=True)

# Welch's (не предполагает equal variance) — recommended
t_stat, p_value = ttest_ind(group_a, group_b, equal_var=False)

Paired t-test

Для связанных выборок (до/после, один и тот же user).

from scipy.stats import ttest_rel
t_stat, p_value = ttest_rel(before, after)

Когда t-test не работает

Non-normal distribution

Для heavy-tailed (чек, время) t-test менее точен. Но при N > 100 CLT помогает.

Extreme outliers

Один outlier может сильно повлиять.

Малые выборки non-normal

N < 30 + non-normal → t-test ненадёжен.

Альтернативы

Mann-Whitney U test

Non-parametric. Сравнение медиан (точнее — распределений).

from scipy.stats import mannwhitneyu
stat, p_value = mannwhitneyu(group_a, group_b)

Работает на любых distributions. Теряет power на truly normal.

Bootstrap

Универсальный. Обработка любых метрик.

import numpy as np

def bootstrap_test(a, b, n_iter=10000):
    obs_diff = np.mean(a) - np.mean(b)
    combined = np.concatenate([a, b])
    diffs = []
    for _ in range(n_iter):
        sample = np.random.choice(combined, len(combined), replace=True)
        diff = np.mean(sample[:len(a)]) - np.mean(sample[len(a):])
        diffs.append(diff)
    p_value = np.mean(np.abs(diffs) >= np.abs(obs_diff))
    return p_value

Медленнее, но гибче.

Permutation test

Похож на bootstrap, но перемешивает метки:

def permutation_test(a, b, n_iter=10000):
    obs_diff = np.mean(a) - np.mean(b)
    combined = np.concatenate([a, b])
    count = 0
    for _ in range(n_iter):
        np.random.shuffle(combined)
        diff = np.mean(combined[:len(a)]) - np.mean(combined[len(a):])
        if abs(diff) >= abs(obs_diff):
            count += 1
    return count / n_iter

Как выбрать тест

Normal + equal variance  → Student's t-test
Normal + different variance → Welch's t-test (default recommendation)
Non-normal, large N       → t-test ок (CLT)
Non-normal, small N       → Mann-Whitney U
Сложная metric / custom   → Bootstrap
Paired data               → Paired t-test / Wilcoxon

Практика: A/B-тест

import numpy as np
from scipy.stats import ttest_ind, mannwhitneyu

# control group
control = np.random.lognormal(3, 1, 1000)  # heavy-tailed

# test group (с эффектом)
test = np.random.lognormal(3.05, 1, 1000)

# Welch's
t, p = ttest_ind(control, test, equal_var=False)
print(f't-test p = {p:.4f}')

# Mann-Whitney
u, p = mannwhitneyu(control, test)
print(f'Mann-Whitney p = {p:.4f}')
Готовься к собесу аналитика как в Duolingo
10 минут в день — SQL, Python, A/B, метрики. 1700+ вопросов в Telegram
Открыть Карьерник в Telegram

Проверка assumptions

Нормальность

from scipy.stats import shapiro, normaltest

stat, p = shapiro(data)  # для small samples
stat, p = normaltest(data)  # для large

Если p < 0.05 → не normal. Но на больших N эти тесты почти всегда отвергают.

Визуально

import matplotlib.pyplot as plt
from scipy.stats import probplot

probplot(data, plot=plt)  # Q-Q plot
plt.show()

Prямая линия → нормально.

Equal variance

from scipy.stats import levene
stat, p = levene(group_a, group_b)
# p < 0.05 → разные variance → Welch's

В A/B-тестах

Default рекомендация:

  • Конверсия (proportion): z-test для пропорций или chi-square
  • Continuous с нормальным: Welch's t-test
  • Continuous с heavy tail (чек): Mann-Whitney или bootstrap
  • Составные метрики: bootstrap

На собесе

«Какой тест для сравнения средних?» Welch's t-test (не предполагает equal variance).

«Когда Mann-Whitney?» Non-normal distribution с малой выборкой или heavy tails.

«Bootstrap vs t-test?» Bootstrap универсальнее, но медленнее. T-test — если assumptions выполнены.

«Paired vs independent?» Paired — одни и те же subjects (до/после). Independent — разные.

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

1. Student's вместо Welch's

В A/B variances часто разные. Default — Welch's.

2. t-test на small N с outliers

Ненадёжно. Bootstrap или Mann-Whitney.

3. p < 0.05 → значимо навсегда

Multiple testing! Поправка Bonferroni для множественных сравнений.

4. Игнорировать effect size

Значимость ≠ практическая важность. Смотрите и p-value, и effect size.

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

FAQ

p = 0.049 — significant?

Technically да (при α = 0.05). Но очень близко. Проверьте mbet повторить.

Nonparametric always safer?

Не всегда. На truly normal данных t-test power выше.

В Python или R?

Scipy / statsmodels в Python. В R — более широкий набор.

One-sided или two-sided?

Default — two-sided. One-sided только если теоретически excluded одна сторона.