ANOVA i testy post-hoc
Porównaj średnie w co najmniej trzech grupach za pomocą jednoczynnikowej ANOVA i wykonaj test Tukeya HSD, aby ustalić, które pary grup się różnią.
ANOVA i testy post-hoc to bezpłatna lekcja Pandas & NumPy Academy na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Pandas & NumPy Academy, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Pandas & NumPy Academy zawiera 4 lekcji w sumie.
Dlaczego nie stosować wielu testów t?
Przy porównywaniu średnich w trzech lub większej liczbie grup wykonywanie wielu testów t zwiększa częstość błędów I rodzaju (częstość wyników fałszywie dodatnich). Testowanie grup A vs B, A vs C oraz B vs C przy α=0,05 dla każdego testu daje łączne prawdopodobieństwo wyniku fałszywie dodatniego wynoszące około 14%, a nie 5%. Analiza wariancji (ANOVA) rozwiązuje ten problem, testując wszystkie grupy jednocześnie w ramach jednego testu i utrzymując częstość wyników fałszywie dodatnich dokładnie na poziomie α. Jeden test ANOVA zastępuje wszystkie pary testów t w przypadku pytania ogólnego: „Czy któreś grupy różnią się od siebie?”
Jednoczynnikowa ANOVA: najważniejsze informacje
Jednoczynnikowa ANOVA sprawdza, czy średnie w trzech lub większej liczbie grup różnią się istotnie. Rozkłada całkowitą wariancję na wariancję międzygrupową (wyjaśnianą przynależnością do grupy) oraz wariancję wewnątrzgrupową (losowy szum). Statystyka F to ich iloraz: F = (wariancja międzygrupowa) / (wariancja wewnątrzgrupowa). Duża wartość F oznacza, że różnice między grupami są duże w porównaniu z szumem, co prowadzi do małej wartości p. Hipoteza zerowa brzmi: H₀: wszystkie średnie grupowe są równe.
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 z ramkami danych Pandas
W praktyce dane znajdują się w ramce danych w formacie długim, w której jedna kolumna zawiera etykietę grupy, a druga — pomiar. Należy wyodrębnić każdą grupę jako obiekt Series i przekazać je do stats.f_oneway(). Jeśli natomiast mają Państwo ramkę danych w formacie szerokim (po jednej kolumnie na grupę), można bezpośrednio przekazać każdą kolumnę. Funkcja przyjmuje dowolną liczbę argumentów pozycyjnych, dzięki czemu łatwo rozszerzyć analizę na 4, 5 lub więcej grup.
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))Założenia ANOVA
Jednoczynnikowa ANOVA zakłada: (1) niezależność — obserwacje są od siebie niezależne; (2) normalność — reszty mają w przybliżeniu rozkład normalny w każdej grupie (dla dużych prób metoda jest odporna dzięki centralnemu twierdzeniu granicznemu); (3) homoscedastyczność — wariancje w grupach są równe. Założenie dotyczące wariancji można sprawdzić za pomocą testu Levene’a (stats.levene(*groups)). Jeśli wariancje nie są równe, należy użyć ANOVA Welcha, dostępnej w bibliotece pingouin lub obliczanej za pomocą 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.')Kruskal-Wallis: nieparametryczna ANOVA
Gdy założenia ANOVA są naruszone (rozkłady nie są normalne, próby są małe lub wariancje są nierówne), alternatywą nieparametryczną jest test Kruskala-Wallisa. Sprawdza on, czy rozkłady grup różnią się od siebie, przekształcając dane na rangi i porównując sumy rang. Należy użyć scipy.stats.kruskal(*groups). Hipoteza zerowa zakłada, że wszystkie grupy mają ten sam rozkład (co jest równoważne testowaniu równych median, gdy rozkłady mają ten sam kształt). Test ten jest zawsze poprawny, ale gdy założenia są spełnione, ma mniejszą moc niż 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')Testy post-hoc: dlaczego są potrzebne
Istotny wynik ANOVA mówi, że średnia co najmniej jednej grupy różni się od pozostałych, ale nie wskazuje, które pary się różnią. Testy post-hoc wykonują wszystkie porównania parami, korygując wynik pod kątem wielokrotnych porównań. Najpopularniejszym z nich jest test istotnej różnicy Tukeya (Tukey HSD), który dokładnie kontroluje rodzinny współczynnik błędu na poziomie α. Jest dostępny w funkcji statsmodels.stats.multicomp.pairwise_tukeyhsd(). Testy post-hoc należy wykonywać dopiero po uzyskaniu istotnego wyniku ANOVA — nie należy stosować ich jako poszukiwania różnic w przypadku wyników nieistotnych.
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))Odczytywanie wyników Tukey HSD
Tabela Tukey HSD zawiera jeden wiersz dla każdej pary grup. Najważniejsze kolumny to: meandiff (różnica średnich dla danej pary), lower i upper (95-procentowy przedział ufności dla różnicy) oraz reject (True, jeśli para różni się istotnie przy α). Przedział ufności, który nie obejmuje zera, oznacza istotną różnicę między parą. Grupy, których przedziały ufności się pokrywają, nie różnią się między sobą istotnie.
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)Korekty Bonferroniego i Benjamina-Hochberga
Jako alternatywę dla testu Tukey HSD można zastosować korektę Bonferroniego do testów t wykonywanych parami: należy wykonać wszystkie testy t, a następnie ustawić próg na α/k, gdzie k oznacza liczbę porównań. Jest to podejście konserwatywne — może nie wykryć rzeczywistych różnic. Korekta Benjamina-Hochberga, czyli kontrola współczynnika fałszywych odkryć (FDR), jest mniej konserwatywna: kontroluje oczekiwany odsetek fałszywych odkryć, pozwalając wykryć więcej wyników prawdziwie dodatnich kosztem nieco większej liczby wyników fałszywie dodatnich. Należy użyć 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}')Miara wielkości efektu w ANOVA: eta-kwadrat
W przypadku ANOVA analogiczną miarą do d Cohena jest eta-kwadrat (η²): odsetek całkowitej wariancji wyjaśniany przynależnością do grupy. η² = SS_between / SS_total. Wartości: < 0.01 oznacza mały efekt, 0.06 — średni, a > 0.14 — duży. Częściowe eta-kwadrat (η²_p) jest częściej stosowane w publikowanych badaniach: mianownik obejmuje jedynie wariancję grupową i wariancję błędu, a nie całkowitą wariancję. Należy zawsze obliczać η² wraz z wartością p ANOVA, aby pokazać, czy efekt ma znaczenie praktyczne, a nie tylko czy można go wykryć statystycznie.
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)')Przegląd dwuczynnikowej ANOVA
Dwuczynnikowa ANOVA rozszerza jednoczynnikową ANOVA na dwa czynniki kategoryczne analizowane jednocześnie. Można na przykład sprawdzać, czy wyniki egzaminu różnią się jednocześnie ze względu na metodę nauczania i poziom doświadczenia uczniów. Testuje także efekt interakcji: czy wpływ metody nauczania zależy od poziomu doświadczenia? Dwuczynnikową ANOVA implementuje się w statsmodels za pomocą ols('score ~ C(method) + C(level) + C(method):C(level)', data=df).fit() oraz anova_lm(model). Stanowi to podstawę bardziej zaawansowanego planowania eksperymentów.
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 dla pomiarów powtarzanych
ANOVA dla pomiarów powtarzanych jest stosowana, gdy te same osoby są mierzone wielokrotnie — na przykład podczas badania czasu reakcji w punktach 0, 30 i 60 minut po zastosowaniu leczenia. Jest wielogrupowym uogólnieniem testu t dla prób zależnych. Dzięki uwzględnieniu różnic indywidualnych ma większą moc niż jednoczynnikowa ANOVA. W Pythonie należy użyć pingouin.rm_anova() lub pg.pairwise_tests() do analizy post-hoc. Gdy założenie sferyczności jest naruszone (co sprawdza się testem Mauchly’ego), należy użyć wartości p skorygowanych metodą Greenhouse’a-Geissera.
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')Szybkie sprawdzenie
Sprawdź swoją wiedzę na temat pojęć związanych z analizą danych z tej lekcji.
Podsumowanie lekcji
W tej lekcji nauczyli się Państwo, że: scipy.stats.f_oneway() sprawdza, czy średnie w którejkolwiek z grup różnią się, kontrolując ogólną częstość błędów I rodzaju; testy post-hoc (Tukey HSD) wskazują, które konkretne pary różnią się po uzyskaniu istotnego wyniku ANOVA; a test Kruskala-Wallisa jest nieparametryczną alternatywą, gdy założenia dotyczące normalności lub równości wariancji są naruszone. Następnie rozpoczniemy projekt końcowy, łącząc wszystkie umiejętności w kompletnym potoku danych.
Często zadawane pytania
Czy lekcja „ANOVA i testy post-hoc” jest bezpłatna?
Tak — pełny tekst „ANOVA i testy post-hoc” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Pandas & NumPy Academy, przejdź na CoddyKit PRO. Kurs Pandas & NumPy Academy zawiera 4 lekcji w sumie.
Co nauczysz się w „ANOVA i testy post-hoc”?
Porównaj średnie w co najmniej trzech grupach za pomocą jednoczynnikowej ANOVA i wykonaj test Tukeya HSD, aby ustalić, które pary grup się różnią. Ćwiczysz Pandas & NumPy Academy z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Pandas & NumPy Academy?
Nie wymagamy żadnego doświadczenia. Pandas & NumPy Academy w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „ANOVA i testy post-hoc”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Pandas & NumPy Academy?
Tak. Każda lekcja Pandas & NumPy Academy zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Statystyki opisowe i testowanie normalności
- Testy t do porównywania średnich
- Test chi-kwadrat niezależności
- ANOVA i testy post-hoc