ANOVA와 사후 검정
일원 ANOVA로 세 개 이상의 그룹에서 평균을 비교하고, Tukey HSD를 실행해 어떤 쌍이 서로 다른지 확인합니다.
ANOVA와 사후 검정은(는) CoddyKit의 무료 Pandas & NumPy Academy 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 Pandas & NumPy Academy 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. Pandas & NumPy Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
t-검정을 여러 번 사용하면 안 되는 이유
세 개 이상의 그룹에서 평균을 비교할 때 여러 번의 t-test를 수행하면 제1종 오류율(거짓 양성률)이 부풀려집니다. A 대 B, A 대 C, B 대 C를 각각 α=0.05로 검정하면 전체 거짓 양성 가능성은 5%가 아니라 약 14%가 됩니다. 분산분석(ANOVA)은 하나의 검정에서 모든 그룹을 동시에 검정하여 이 문제를 해결하고 거짓 양성률을 정확히 α로 유지합니다. 전체적인 질문인 ‘어떤 그룹이라도 차이가 있는가?’에 대해서는 하나의 ANOVA가 모든 쌍별 t-test를 대신합니다.
일원 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')Pandas DataFrames를 사용한 ANOVA
실제로는 한 열에 그룹 레이블이 있고 다른 열에 측정값이 있는 긴 형식의 DataFrame에 데이터가 저장되어 있습니다. 각 그룹을 Series로 추출하여 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) 등분산성 — 그룹 간 분산이 같습니다. Levene 검정(stats.levene(*groups))으로 분산 가정을 검정합니다. 분산이 같지 않다면 Welch 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.')Kruskal-Wallis: 비모수 ANOVA
ANOVA의 가정이 충족되지 않을 때(비정규분포, 작은 표본, 서로 다른 분산)에는 Kruskal-Wallis 검정이 비모수 대안이 됩니다. 이 검정은 데이터를 순위로 변환하고 순위 합을 비교하여 그룹의 분포가 서로 다른지 검정합니다. 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의 정직한 유의차(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)입니다. 0을 포함하지 않는 신뢰구간은 해당 쌍이 유의하게 다르다는 뜻입니다. 신뢰구간이 서로 겹치는 그룹은 서로 유의하게 다르지 않습니다.
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)Bonferroni 및 Benjamini-Hochberg 보정
Tukey HSD의 대안으로 쌍별 t-test에 Bonferroni 보정을 적용할 수 있습니다. 모든 t-test를 수행한 다음, 비교 횟수 k를 사용하여 기준을 α/k로 조정합니다. 이 방법은 보수적이므로 실제 차이를 놓칠 수 있습니다. Benjamini-Hochberg 거짓 발견률(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에서 Cohen의 d에 해당하는 측정값은 에타 제곱(η²)으로, 그룹 소속으로 설명되는 전체 분산의 비율입니다. η² = SS_between / SS_total. 값이 0.01 미만이면 작음, 0.06이면 중간, 0.14 초과이면 큼으로 해석합니다. 부분 에타 제곱(η²_p)은 발표된 연구에서 더 흔히 사용됩니다. 전체 분산이 아니라 그룹 분산과 오류 분산의 합으로만 나눕니다. 효과가 통계적으로 검출될 뿐 아니라 실제로 의미가 있는지도 보여 주려면 ANOVA p값과 함께 항상 η²를 계산하십시오.
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-test를 여러 그룹으로 일반화한 방법입니다. 개인차를 고려하므로 일원 ANOVA보다 검정력이 높습니다. Python에서는 pingouin.rm_anova()를 사용하고, 사후 분석에는 pg.pairwise_tests()를 사용하십시오. 구형성 가정이 위반되면(Mauchly 검정으로 검정) Greenhouse-Geisser 보정 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()는 전체 제1종 오류율을 통제하면서 그룹 간 평균 차이가 있는지 검정하고, 사후 검정(Tukey HSD)은 유의한 ANOVA 이후 구체적으로 어떤 쌍이 다른지 식별하며, Kruskal-Wallis는 정규성 또는 등분산성 가정이 위반되었을 때 사용하는 비모수 대안입니다. 다음으로 모든 기술을 종단 간 데이터 파이프라인에 통합하는 최종 프로젝트를 시작합니다.
AI 튜터와 함께 Python을(를) 배우세요 — 무료
브라우저에서 실제 코드를 작성하고 실행하며, 24/7 AI 튜터로부터 즉각적인 도움을 받고, 웹이나 앱에서 중단한 부분부터 계속 학습하세요.
- 코스
- 30
- 레슨
- 120
자주 묻는 질문
“ANOVA와 사후 검정” 강의는 무료인가요?
네 — “ANOVA와 사후 검정” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 Pandas & NumPy Academy 강의 전체를 잠금 해제할 수 있습니다. Pandas & NumPy Academy 강의에는 총 4개의 강의가 포함되어 있습니다.
“ANOVA와 사후 검정”에서 뭘 배우나요?
일원 ANOVA로 세 개 이상의 그룹에서 평균을 비교하고, Tukey HSD를 실행해 어떤 쌍이 서로 다른지 확인합니다. 브라우저에서 직접 실행하는 실습 코드로 Pandas & NumPy Academy을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
Pandas & NumPy Academy을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 Pandas & NumPy Academy은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“ANOVA와 사후 검정” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 Pandas & NumPy Academy 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 Pandas & NumPy Academy 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 기술 통계와 정규성 검정
- 평균 비교를 위한 t 검정
- 독립성에 대한 카이제곱 검정
- ANOVA와 사후 검정