ANOVA и апостериорные тесты
Сравните средние значения в трёх и более группах с помощью однофакторного ANOVA и выполните тест Тьюки HSD, чтобы определить, какие пары различаются
«ANOVA и апостериорные тесты» — бесплатный урок Pandas & NumPy Academy на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Pandas & NumPy Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Pandas & NumPy Academy содержит 4 уроков всего.
Почему не использовать несколько t-критериев?
При сравнении средних значений в трёх или более группах выполнение нескольких t-критериев увеличивает вероятность ошибки I рода (вероятность ложноположительного результата). Проверка групп A и B, A и C, а также B и C при α=0,05 для каждого сравнения даёт общую вероятность ложноположительного результата около 14%, а не 5%. Дисперсионный анализ (ANOVA) решает эту проблему, одновременно проверяя все группы в рамках одного критерия и сохраняя вероятность ложноположительного результата ровно на уровне α. Один ANOVA заменяет все попарные t-критерии при проверке общего вопроса: «Различается ли какая-либо из групп?»
Однофакторный ANOVA: общая картина
Однофакторный ANOVA проверяет, значимо ли различаются средние значения в трёх или более группах. Он раскладывает общую дисперсию на межгрупповую дисперсию (объясняемую принадлежностью к группе) и внутригрупповую дисперсию (случайный шум). F-статистика представляет собой их отношение: F = (межгрупповая дисперсия) / (внутригрупповая дисперсия). Большое F означает, что различия между группами велики по сравнению с шумом, поэтому p-значение мало. Нулевая гипотеза: H₀: средние значения всех групп равны.
import numpy as np
from scipy import stats
np.random.seed(0)
# Three product variants with different conversion rates
group_a = np.random.normal(10.0, 2.0, 50) # baseline
group_b = np.random.normal(11.5, 2.0, 50) # slightly better
group_c = np.random.normal(13.0, 2.0, 50) # clearly better
f_stat, p = stats.f_oneway(group_a, group_b, group_c)
print(f'F-statistic: {f_stat:.3f}')
print(f'p-value: {p:.6f}')
print('At least one group differs?', 'Yes' if p < 0.05 else 'No')ANOVA с DataFrames Pandas
На практике данные представлены в DataFrame в длинном формате, где один столбец содержит метку группы, а другой — измерение. Извлеките каждую группу как ряд и передайте их в stats.f_oneway(). Если же у Вас DataFrame в широком формате (по одному столбцу на группу), передайте каждый столбец напрямую. Функция принимает любое количество позиционных аргументов, поэтому её легко расширить для работы с 4, 5 и более группами.
import pandas as pd
import numpy as np
from scipy import stats
np.random.seed(1)
df = pd.DataFrame({
'group': ['A']*40 + ['B']*40 + ['C']*40 + ['D']*40,
'score': np.concatenate([
np.random.normal(70, 10, 40),
np.random.normal(75, 10, 40),
np.random.normal(80, 10, 40),
np.random.normal(72, 10, 40)
])
})
groups = [group['score'].values
for _, group in df.groupby('group')]
f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(df.groupby('group')['score'].mean().round(2))Допущения ANOVA
Однофакторный ANOVA предполагает: (1) независимость — наблюдения не зависят друг от друга; (2) нормальность — остатки в каждой группе примерно распределены нормально (при больших выборках критерий устойчив благодаря CLT); (3) гомоскедастичность — равенство дисперсий между группами. Проверьте допущение о дисперсиях с помощью критерия Левена (stats.levene(*groups)). Если дисперсии неравны, используйте вместо него ANOVA Уэлча, доступный в библиотеке pingouin или вычисляемый с помощью statsmodels.
import numpy as np
from scipy import stats
np.random.seed(0)
g1 = np.random.normal(10, 2, 40)
g2 = np.random.normal(12, 2, 40)
g3 = np.random.normal(11, 8, 40) # Much higher variance
# Test equal variances
stat_l, p_l = stats.levene(g1, g2, g3)
print(f'Levene\'s test: stat={stat_l:.3f}, p={p_l:.4f}')
if p_l < 0.05:
print('Warning: Unequal variances! Standard ANOVA may be unreliable.')
print('Consider Welch\'s ANOVA or Kruskal-Wallis.')Критерий Краскела — Уоллиса: непараметрический ANOVA
Если допущения ANOVA нарушены (распределения не являются нормальными, выборки малы, дисперсии неравны), критерий Краскела — Уоллиса служит непараметрической альтернативой. Он проверяет, различаются ли распределения групп, преобразуя данные в ранги и сравнивая суммы рангов. Используйте scipy.stats.kruskal(*groups). Нулевая гипотеза состоит в том, что все группы имеют одинаковое распределение (это эквивалентно проверке равенства медиан, если формы распределений одинаковы). Критерий всегда применим, но при выполнении допущений обладает меньшей мощностью, чем ANOVA.
import numpy as np
from scipy import stats
np.random.seed(0)
# Non-normal data: customer satisfaction scores (1-10 scale)
g1 = np.random.choice(range(1, 11), 50, p=[0.05]*10)
g2 = np.random.choice(range(1, 11), 50, p=[0.02, 0.03, 0.05, 0.08, 0.12, 0.15, 0.20, 0.15, 0.12, 0.08])
g3 = np.random.choice(range(1, 11), 50, p=[0.15, 0.15, 0.15, 0.15, 0.10, 0.10, 0.08, 0.05, 0.04, 0.03])
stat, p = stats.kruskal(g1, g2, g3)
print(f'Kruskal-Wallis: H={stat:.3f}, p={p:.4f}')
print('Groups differ?', 'Yes' if p < 0.05 else 'No')Апостериорные критерии: зачем они нужны
Значимый результат ANOVA показывает что среднее значение как минимум в одной группе отличается, но не показывает, какие пары различаются. Апостериорные тесты выполняют все попарные сравнения с поправкой на множественные сравнения. Наиболее популярен критерий честно значимых различий Тьюки (Tukey HSD), который точно контролирует семейную вероятность ошибки на уровне α. Он доступен в statsmodels.stats.multicomp.pairwise_tukeyhsd(). Выполняйте апостериорные тесты только после значимого результата ANOVA — не используйте их для поиска различий при незначимых результатах.
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.stats.multicomp import pairwise_tukeyhsd
np.random.seed(0)
data = np.concatenate([
np.random.normal(70, 10, 40),
np.random.normal(80, 10, 40),
np.random.normal(75, 10, 40)
])
groups = ['A']*40 + ['B']*40 + ['C']*40
# First confirm ANOVA is significant
f, p = stats.f_oneway(*[data[i*40:(i+1)*40] for i in range(3)])
print(f'ANOVA p={p:.4f}')
if p < 0.05:
print('Post-hoc Tukey HSD:')
print(pairwise_tukeyhsd(data, groups, alpha=0.05))Интерпретация результатов Tukey HSD
В таблице Tukey HSD для каждой пары групп предусмотрена отдельная строка. Основные столбцы: meandiff (разница средних значений в паре), lower и upper (границы 95%-ного доверительного интервала для разницы), а также reject (True, если различие между парами значимо при уровне α). Если доверительный интервал не включает ноль, различие между парами статистически значимо. Группы с перекрывающимися доверительными интервалами статистически значимо не различаются.
import numpy as np
from statsmodels.stats.multicomp import pairwise_tukeyhsd
np.random.seed(42)
data = np.concatenate([
np.random.normal(10, 2, 60), # Group A
np.random.normal(13, 2, 60), # Group B (clearly higher)
np.random.normal(10.5, 2, 60) # Group C (barely different from A)
])
groups = ['A']*60 + ['B']*60 + ['C']*60
result = pairwise_tukeyhsd(data, groups, alpha=0.05)
print(result)
# A-B: reject=True (B is higher)
# A-C: reject=False (barely different)
# B-C: reject=True (B is higher)Поправки Бонферрони и Бенджамини — Хохберга
В качестве альтернативы Tukey HSD к попарным t-тестам можно применить поправку Бонферрони: выполните все t-тесты, затем скорректируйте порог до α/k, где k — число сравнений. Этот метод консервативен и может не выявить реальные различия. Поправка Бенджамини — Хохберга для контроля доли ложных открытий (FDR) менее консервативна: она контролирует ожидаемую долю ложных обнаружений, позволяя выявить больше истинных положительных результатов ценой немного большего числа ложноположительных результатов. Используйте statsmodels.stats.multitest.multipletests(p_values, method='fdr_bh').
import numpy as np
from scipy import stats
from statsmodels.stats.multitest import multipletests
np.random.seed(0)
groups = [np.random.normal(10 + i*1.5, 2, 40) for i in range(4)]
names = ['A', 'B', 'C', 'D']
# All pairwise t-tests
p_values = []
pairs = []
for i in range(len(groups)):
for j in range(i+1, len(groups)):
_, p = stats.ttest_ind(groups[i], groups[j])
p_values.append(p)
pairs.append(f'{names[i]}-{names[j]}')
# Benjamini-Hochberg correction
reject, adj_p, _, _ = multipletests(p_values, method='fdr_bh')
for pair, p, ap, r in zip(pairs, p_values, adj_p, reject):
print(f'{pair}: raw_p={p:.4f}, adj_p={ap:.4f}, significant={r}')Размер эффекта в ANOVA: эта-квадрат
Для ANOVA аналогом d Коэна служит эта-квадрат (η²) — доля общей дисперсии, объясняемая принадлежностью к группе. η² = SS_between / SS_total. Значения: < 0.01 — малый эффект, 0.06 — средний, > 0.14 — большой. Частичный эта-квадрат (η²_p) чаще используется в опубликованных исследованиях: в знаменателе учитываются только дисперсия между группами и дисперсия ошибки, а не общая дисперсия. Всегда рассчитывайте η² вместе с p-значением ANOVA, чтобы показать практическую значимость эффекта, а не только его статистическую обнаружимость.
import numpy as np
from scipy import stats
np.random.seed(0)
groups = [
np.random.normal(10, 2, 50),
np.random.normal(12, 2, 50),
np.random.normal(14, 2, 50)
]
all_data = np.concatenate(groups)
grand_mean = all_data.mean()
ss_between = sum(len(g) * (g.mean() - grand_mean)**2 for g in groups)
ss_total = sum((x - grand_mean)**2 for g in groups for x in g)
eta_sq = ss_between / ss_total
f, p = stats.f_oneway(*groups)
print(f'F={f:.3f}, p={p:.4f}')
print(f'Eta-squared: {eta_sq:.4f} ({eta_sq:.1%} of variance explained)')Обзор двухфакторного ANOVA
Двухфакторный ANOVA расширяет однофакторный ANOVA и позволяет одновременно анализировать два категориальных фактора. Например, можно проверить, различаются ли результаты экзамена в зависимости одновременно от метода преподавания и уровня опыта учащихся. Он также проверяет эффект взаимодействия: зависит ли влияние метода преподавания от уровня опыта? Двухфакторный ANOVA реализуется в statsmodels с помощью ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit() и anova_lm(model). Это основа для более сложного планирования экспериментов.
import pandas as pd
import numpy as np
from statsmodels.formula.api import ols
from statsmodels.stats.anova import anova_lm
np.random.seed(0)
df = pd.DataFrame({
'method': ['trad']*60 + ['active']*60,
'level': ['beginner']*30 + ['advanced']*30 + ['beginner']*30 + ['advanced']*30,
'score': (np.random.normal(70, 8, 30).tolist() +
np.random.normal(80, 8, 30).tolist() +
np.random.normal(75, 8, 30).tolist() +
np.random.normal(88, 8, 30).tolist())
})
model = ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit()
print(anova_lm(model, typ=2)[['F', 'PR(>F)']].round(4))ANOVA для повторных измерений
ANOVA для повторных измерений используется, когда одни и те же испытуемые измеряются несколько раз — например, при проверке времени реакции через 0, 30 и 60 минут после воздействия. Это обобщение парного t-теста на несколько групп. Благодаря учёту индивидуальных различий этот метод обладает большей статистической мощностью, чем однофакторный ANOVA. В Python используйте pingouin.rm_anova() или pg.pairwise_tests() для апостериорного анализа. Если нарушено условие сферичности, проверяемое тестом Мокли, используйте p-значения с поправкой Гринхауса — Гейссера.
import pandas as pd
import numpy as np
# Example structure for repeated measures (pingouin library)
np.random.seed(0)
subjects = list(range(20)) * 3 # 20 subjects, 3 time points
timepoints = ['T0']*20 + ['T1']*20 + ['T2']*20
scores = np.concatenate([
np.random.normal(50, 10, 20), # baseline
np.random.normal(55, 10, 20), # improved
np.random.normal(60, 10, 20) # further improved
])
df_rm = pd.DataFrame({'subject': subjects, 'time': timepoints, 'score': scores})
print(df_rm.groupby('time')['score'].mean().round(2))
# Use pingouin.rm_anova(data=df_rm, dv='score', within='time', subject='subject')Быстрая проверка
Проверьте, насколько хорошо Вы усвоили концепции анализа данных из этого урока.
Итоги урока
В этом уроке Вы узнали, что scipy.stats.f_oneway() проверяет, различаются ли средние значения в каких-либо группах, контролируя общую вероятность ошибки I рода; апостериорные тесты (Tukey HSD) определяют, какие именно пары различаются после значимого результата ANOVA; а критерий Краскела — Уоллиса служит непараметрическим запасным вариантом, если нарушены предположения о нормальности или равенстве дисперсий. Далее Вы начнёте итоговый проект, объединяющий все навыки в сквозной конвейер обработки данных.
Часто задаваемые вопросы
Урок «ANOVA и апостериорные тесты» бесплатный?
Да — полный текст урока «ANOVA и апостериорные тесты» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Pandas & NumPy Academy, подпишись на CoddyKit PRO. Курс Pandas & NumPy Academy содержит 4 уроков всего.
Чему я научусь в уроке «ANOVA и апостериорные тесты»?
Сравните средние значения в трёх и более группах с помощью однофакторного ANOVA и выполните тест Тьюки HSD, чтобы определить, какие пары различаются Ты практикуешь Pandas & NumPy Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Pandas & NumPy Academy?
Предыдущий опыт не требуется. Pandas & NumPy Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «ANOVA и апостериорные тесты»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Pandas & NumPy Academy?
Да. Каждый урок Pandas & NumPy Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Описательная статистика и проверка нормальности
- T-тесты для сравнения средних
- Критерий хи-квадрат для проверки независимости
- ANOVA и апостериорные тесты