세 집단 이상을 비교해야 할 때
t-검정은 두 집단을 비교합니다. 세 집단 이상을 비교하려면 어떻게 해야 할까요.
잘못된 방법은 t-검정을 여러 번 반복하는 것입니다. 집단이 A, B, C 세 개라면 A vs B, A vs C, B vs C 세 번의 t-검정을 수행하고 싶어집니다. 하지만 각 검정을 α = 0.05로 수행하면 적어도 하나가 우연히 유의할 확률이 훨씬 높아집니다.
세 번의 독립 검정에서 아무것도 유의하지 않을 확률은 0.95³ = 0.857입니다. 즉, 실제로 효과가 없는데도 1종 오류가 발생할 확률이 1 - 0.857 = 14.3%로 급등합니다.
일원분산분석(One-way ANOVA)은 모든 집단을 한 번에 비교하여 이 문제를 해결합니다.
ANOVA의 핵심 아이디어 — 분산 분해
ANOVA는 전체 변동을 두 부분으로 분해합니다.
- SS_total: 전체 데이터의 총 변동. 모든 관측치와 전체 평균의 차이
- SS_between (집단 간 변동): 집단 평균과 전체 평균의 차이. 집단 효과로 설명되는 변동
- SS_within (집단 내 변동): 각 관측치와 자기 집단 평균의 차이. 설명되지 않는 오차 변동
전체 변동(SST) = 집단 효과(SSB) + 개인차·오차(SSW)
집단 간 변동이 집단 내 변동보다 훨씬 크면, 집단에 따라 평균이 다를 가능성이 높습니다.
F-통계량
여기서 k는 집단 수, N은 전체 관측치 수입니다. MS(Mean Square)는 SS를 자유도로 나눈 값입니다.
F-값이 1보다 훨씬 크면 집단 간 변동이 오차보다 크다는 뜻이므로, H0(모든 집단의 평균이 같다)를 기각합니다.
aov()로 ANOVA 실행
R 내장 데이터 PlantGrowth를 사용합니다. 세 가지 조건(ctrl: 대조군, trt1: 처리 1, trt2: 처리 2)에서 식물 건조 중량을 비교합니다.
data(PlantGrowth)
str(PlantGrowth)
tapply(PlantGrowth$weight, PlantGrowth$group, mean)
tapply(PlantGrowth$weight, PlantGrowth$group, sd)
'data.frame': 30 obs. of 2 variables:
$ weight: num 4.17 5.58 5.18 6.11 4.50 4.61 5.17 4.53 5.33 5.14 ...
$ group : Factor w/ 3 levels "ctrl","trt1","trt2": 1 1 1 1 1 ...
ctrl trt1 trt2
5.032000 4.661000 5.526000
ctrl trt1 trt2
0.5830914 0.7936757 0.4425378
# 정규성 가정 확인
by(PlantGrowth$weight, PlantGrowth$group, shapiro.test)
... p-value = 0.4542 (ctrl)
... p-value = 0.2050 (trt1)
... p-value = 0.5643 (trt2)
세 집단 모두 정규성 가정을 만족합니다.
# ANOVA 실행
aov_result <- aov(weight ~ group, data = PlantGrowth)
summary(aov_result)
Df Sum Sq Mean Sq F value Pr(>F)
group 2 3.766 1.8832 4.846 0.0159 *
Residuals 27 10.492 0.3886
---
Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
ANOVA 결과 해석
summary 테이블을 읽는 방법입니다.
| 항목 | 값 | 의미 |
|---|---|---|
| group Df | 2 | 집단 수 - 1 = 3 - 1 = 2 |
| Residuals Df | 27 | N - k = 30 - 3 = 27 |
| Sum Sq (group) | 3.766 | SS_between |
| Sum Sq (Residuals) | 10.492 | SS_within |
| Mean Sq (group) | 1.883 | MS_between = 3.766 / 2 |
| F value | 4.846 | MS_between / MS_within = 1.883 / 0.389 |
| Pr(>F) | 0.0159 | p-값 |
F(2, 27) = 4.85, p = .016으로 적어도 한 쌍의 집단 평균이 유의하게 다릅니다.
에타 제곱(η²) 효과크기
# η² 계산
ss_between <- 3.766
ss_total <- 3.766 + 10.492
eta_sq <- ss_between / ss_total
cat("η²:", round(eta_sq, 4), "\n")
cat("해석:", ifelse(eta_sq >= 0.14, "큰 효과",
ifelse(eta_sq >= 0.06, "중간 효과", "작은 효과")), "\n")
η²: 0.2641
해석: 큰 효과
η² = 0.26은 식물 중량 변동의 26%가 처리 조건에 의해 설명됨을 의미합니다. 큰 효과입니다.
등분산 가정 확인 — Levene 검정
# car 패키지의 leveneTest 사용
# install.packages("car")
library(car)
leveneTest(weight ~ group, data = PlantGrowth)
Levene's Test for Homogeneity of Variance (center = median)
Df F value Pr(>F)
group 2 1.1192 0.3412
27
p-값 = 0.341로 등분산 가정을 위반하지 않습니다.
등분산 가정이 위반될 때 — Welch ANOVA
등분산 가정이 충족되지 않으면 oneway.test()를 사용합니다.
# Welch ANOVA (등분산 가정 불필요)
oneway.test(weight ~ group, data = PlantGrowth, var.equal = FALSE)
One-way analysis of means (not assuming equal variances)
data: weight and group
F = 5.1809, num df = 2.000, denom df = 17.128, p-value = 0.01807
결과는 비슷하지만 자유도가 달라졌습니다. 등분산이 의심될 때 안전하게 쓸 수 있는 방법입니다.
시각화
library(ggplot2)
ggplot(PlantGrowth, aes(x = group, y = weight, fill = group)) +
geom_boxplot(alpha = 0.7) +
geom_jitter(width = 0.1, alpha = 0.6, size = 2) +
scale_fill_manual(values = c("ctrl" = "gray70",
"trt1" = "steelblue",
"trt2" = "tomato")) +
labs(
title = "처리 조건별 식물 성장",
subtitle = "One-way ANOVA: F(2, 27) = 4.85, p = .016, η² = .26",
x = "처리 조건",
y = "건조 중량 (g)",
fill = "조건"
) +
theme_minimal() +
guides(fill = "none")
ANOVA 결과는 "적어도 하나의 집단이 다르다"는 것만 알려줍니다. 어느 집단 쌍이 다른지는 다음 장의 사후검정으로 알아냅니다.