R로 기초 통계 검정하기 — t-검정·ANOVA·상관·비모수 (R 실전)

TL;DR — 두 그룹 비교는
t.test(), 세 그룹 이상은aov()+TukeyHSD(), 두 변수의 관계는cor.test(), 정규성(normality)을 못 믿겠으면wilcox.test()·kruskal.test()같은 비모수(non-parametric) 검정으로 갑니다. 검정을 고르는 기준은 단 세 가지 — 그룹이 몇 개인가 (2 vs 3 이상), 짝지어졌는가 (대응 vs 독립), 정규분포를 따르는가 (모수 vs 비모수)입니다.이 글은 더미 발현 데이터를 직접 만들어, 가정 점검 (shapiro.test·var.test) → 2그룹 t-검정 (독립·대응·Welch) → wilcox.test → 일원분산분석 (ANOVA) + 사후검정 → kruskal.test → cor.test 순서로 복붙하면 도는 코드를 따라갑니다. 효과크기(effect size)와 다중비교 보정도 함께 짚습니다. 모두 base R 내장 함수라 따로 설치할 패키지가 없습니다. (코드는 R 4.5 기준, 출력은 실제 실행값입니다.)
🔗 관련 글 · 검정 결과의 p-값을 어떻게 다룰지 (다중검정 보정)는 p-value와 FDR, 다중검정 보정 · 결과를 그림으로 옮기려면 ggplot2 기초 · 차등발현 같은 대규모 검정은 DESeq2로 차등발현 분석

이 글에서 할 것
실험 데이터를 손에 쥐면 가장 먼저 떠오르는 질문이 "이 차이가 통계적으로 의미가 있나"입니다. 처리군이 대조군보다 발현이 높아 보이는데, 그게 진짜 효과인지 우연인지 가르는 일이죠. R에는 이걸 위한 검정 함수가 base에 거의 다 들어 있습니다 — t-검정 (t-test), 분산분석 (ANOVA), 상관(correlation), 비모수 검정까지 한 줄짜리 함수로 돌아갑니다.
문제는 함수를 모르는 게 아니라 어떤 검정을 언제 쓰는지를 헷갈리는 경우가 많다는 점입니다. 그래서 이 글은 검정을 고르는 흐름부터 잡고, 더미 데이터로 하나씩 실행해 결과를 읽습니다. 데모는 단순합니다. 대조군·처리군 발현값을 만들고 (재현 가능하도록 시드 고정), 가정을 점검한 뒤, 2그룹 비교 → 3그룹 분산분석 → 상관 분석을 차례로 돌립니다. 출력에 찍히는 t·F·r·p-값을 어떻게 해석하는지까지 정리합니다.
# base R만으로 — 설치할 패키지 없음
t.test(value ~ group, data = expr) # 두 그룹 평균 비교
aov(expr ~ geno, data = df3) # 세 그룹 이상 (분산분석)
cor.test(x, y, method = "pearson") # 두 변수의 상관
0. 검정 고르기 — 1분 흐름
검정을 고르는 결정은 세 갈래 질문으로 끝납니다. 순서대로 따라가면 됩니다.
- ① 그룹이 몇 개인가 — 두 그룹이면 t-검정 계열, 세 그룹 이상이면 분산분석 (ANOVA) 계열입니다. 세 그룹을 두 개씩 짝지어 t-검정을 반복하면 안 됩니다 (p-값이 부풀려지는 다중비교 문제 — 뒤에서 다룹니다).
- ② 짝지어졌는가 — 같은 개체를 처리 전후로 잰 값처럼 대응(paired) 표본이면 짝을 묶어 검정하고, 서로 다른 개체에서 잰 독립(independent) 표본이면 그냥 두 집단을 비교합니다. 이 구분을 놓치면 검정력이 떨어지거나 가정이 깨집니다.
- ③ 정규분포를 따르는가 — 데이터가 정규성(normality)을 만족하면 평균을 비교하는 모수 (parametric) 검정 (t-검정·ANOVA), 그렇지 않거나 표본이 작고 순위 자료면 비모수(non-parametric) 검정 (wilcox.test·kruskal.test)으로 갑니다. 비모수는 평균 대신 순위(rank)를 비교합니다.
1. 사전 준비 — 데이터 만들기
설치할 패키지가 없습니다. 검정 함수는 전부 base R (stats 패키지)에 들어 있으니 바로 시작합니다. 먼저 대조군·처리군 발현값을 만듭니다. 처리군 평균을 일부러 조금 높여, 검정이 차이를 잡아내는지 봅니다.
set.seed(2025) # 재현성 고정
n <- 8 # 군당 표본 수
ctrl <- rnorm(n, mean = 10.0, sd = 1.2) # 대조군 발현
trt <- rnorm(n, mean = 12.2, sd = 1.2) # 처리군 발현 (평균 높임)
expr <- data.frame(
value = c(ctrl, trt),
group = factor(rep(c("Ctrl", "Trt"), each = n)) # 그룹 라벨
)
head(expr, 4) # 구조 확인
value group
1 10.74491 Ctrl
2 10.04277 Ctrl
3 10.92779 Ctrl
4 11.52699 Ctrl
검정에 들어가는 데이터 형태는 보통 긴 형식(long format)입니다. 측정값 한 열 (value)과 그룹 라벨 한 열 (group)로 두면, value ~ group 같은 공식(formula)으로 함수에 그대로 넘길 수 있습니다. 데이터 정리가 필요하면 dplyr·tidyr 데이터 정리를 참고하세요.
2. 가정 점검 — 정규성·등분산
모수 검정 (t-검정·ANOVA)은 두 가지를 가정합니다 — 데이터가 정규분포를 따르고, 비교하는 집단의 분산이 비슷하다(등분산)는 것입니다. 검정을 돌리기 전에 이걸 확인합니다.
shapiro.test(ctrl) # 정규성 검정 (귀무가설: 정규분포다)
shapiro.test(trt)
var.test(value ~ group, data = expr) # 등분산 검정 (F-검정)
Shapiro-Wilk normality test
data: ctrl
W = 0.94679, p-value = 0.6789
F test to compare two variances
data: value by group
F = 0.28378, num df = 7, denom df = 7, p-value = 0.1185
shapiro.test() (Shapiro-Wilk 검정)는 귀무가설이 "데이터가 정규분포를 따른다"입니다. 그래서 p > 0.05면 정규성을 기각하지 못함 — 즉 정규분포로 봐도 무방하다는 뜻입니다 (여기선 p = 0.68이라 통과). var.test()는 두 집단의 분산이 같은지 보는 F-검정이고, 역시 p > 0.05면 등분산(equal variance)으로 봅니다 (여기선 p = 0.12). 셋 이상의 등분산은 Levene 검정 (car::leveneTest)이나 Bartlett 검정 (bartlett.test)을 씁니다.
shapiro.test만 맹신하기보다 히스토그램·Q-Q plot (qqnorm)으로 눈으로 함께 보는 편이 낫습니다. 애매하면 비모수 검정으로 가면 안전합니다.3. 2그룹 비교 — t.test()
가정을 통과했으니 두 그룹 평균을 비교합니다. R의 t.test()는 기본이 Welch t-검정으로, 두 집단의 분산이 다를 수 있다고 가정합니다 (var.equal = FALSE가 기본값). 그래서 등분산이 의심스러워도 안전한 게 기본 동작입니다.
t.test(value ~ group, data = expr) # Welch t-검정 (등분산 가정 안 함, 기본)
Welch Two Sample t-test
data: value by group
t = -3.7418, df = 10.677, p-value = 0.003426
alternative hypothesis: true difference in means between group Ctrl and group Trt is not equal to 0
95 percent confidence interval:
-2.5912808 -0.6673877
sample estimates:
mean in group Ctrl mean in group Trt
10.48409 12.11343
출력을 읽는 법은 이렇습니다. t = -3.74는 검정통계량 t-값 (두 평균 차이를 표준오차로 나눈 값), df = 10.677은 자유도 (Welch라 정수가 아닙니다), p-value = 0.0034는 두 평균이 같다는 귀무가설 아래 이 차이가 나올 확률입니다. p < 0.05이므로 두 군의 평균이 다르다고 봅니다. 95% 신뢰구간(confidence interval, CI) [-2.59, -0.67]이 0을 포함하지 않는 것도 같은 결론입니다 — 부호가 음수인 건 Ctrl − Trt라 대조군이 더 낮다는 뜻입니다.
등분산이 확실하면 고전적인 Student t-검정도 쓸 수 있습니다 (df가 정수로 바뀜).
t.test(value ~ group, data = expr, var.equal = TRUE) # Student t-검정 (등분산 가정)
# t = -3.7418, df = 14, p-value = 0.002189
같은 개체를 처리 전후로 잰 대응(paired) 표본이면 paired = TRUE를 켭니다. 짝을 묶어 차이를 검정하므로 개체 간 변동을 제거해 검정력이 올라갑니다.
# before/after는 같은 샘플을 두 번 잰 값 (순서·길이가 짝으로 맞아야 함)
t.test(after, before, paired = TRUE) # 대응 t-검정
# t = 7.1658, df = 9, p-value = 5.275e-05
paired를 헷갈리지 말 것 — 서로 다른 개체에서 잰 두 집단 (독립)에 paired = TRUE를 켜면 통계가 완전히 틀립니다. 대응은 "같은 대상의 전/후", 독립은 "다른 대상의 그룹 A/B"입니다. 또 대응이면 두 벡터의 순서가 짝으로 정확히 맞아야 합니다 (1번 샘플의 before와 after가 같은 위치).효과크기(effect size)도 함께 봅니다. p-값은 "차이가 있는가"만 말하지 "얼마나 큰가"는 말하지 않기 때문입니다. 두 그룹 비교의 표준 효과크기는 Cohen's d — 평균 차이를 합동 표준편차 (pooled SD)로 나눈 값입니다.
# Cohen's d = (평균 차) / 합동 표준편차
m1 <- mean(trt); m0 <- mean(ctrl)
s_p <- sqrt(((n-1)*var(trt) + (n-1)*var(ctrl)) / (2*n - 2))
(m1 - m0) / s_p # 1.87 → 큰 효과 (관행: 0.2 작음·0.5 중간·0.8 큼)
4. 비모수 2그룹 — wilcox.test()
정규성을 만족하지 못하거나 표본이 작고 이상치가 있으면 비모수 검정으로 갑니다. 두 그룹용은 wilcox.test() — 독립 표본이면 Mann-Whitney U 검정, 대응 표본 (paired = TRUE)이면 Wilcoxon 부호순위 검정입니다. 평균 대신 순위(rank)를 비교하므로 분포 가정이 느슨합니다.
wilcox.test(value ~ group, data = expr) # Mann-Whitney U (독립·비모수)
# paired = TRUE 면 Wilcoxon 부호순위 검정
Wilcoxon rank sum exact test
data: value by group
W = 5, p-value = 0.002953
출력의 W는 순위합 기반 검정통계량이고, 해석은 p-값으로 같습니다 (p < 0.05면 두 군의 분포 위치가 다름). 모수 t-검정과 결론이 대체로 일치하지만, 비모수는 평균이 아니라 분포의 위치 이동(location shift)을 봅니다. 동점(tie)이 있으면 정확 p-값 대신 정규근사를 쓴다는 경고가 뜰 수 있는데, 해석에는 지장이 없습니다.
5. 3그룹 이상 — aov() + TukeyHSD()
그룹이 셋 이상이면 t-검정을 짝마다 반복하는 대신 일원분산분석 (one-way ANOVA)으로 "적어도 한 군은 다른가"를 한 번에 검정합니다. R에서는 aov()로 모형을 적합하고 summary()로 표를 봅니다.
# 세 유전형: WT(야생형)·KO(녹아웃)·OE(과발현)
fit <- aov(expr ~ geno, data = df3) # 일원분산분석
summary(fit)
Df Sum Sq Mean Sq F value Pr(>F)
geno 2 29.52 14.760 19.51 1.63e-05 ***
Residuals 21 15.89 0.757
분산분석표에서 핵심은 F value = 19.51과 Pr(>F) = 1.63e-05입니다. F-값은 군 간 분산을 군 내 분산으로 나눈 비로, 클수록 군 차이가 두드러집니다. p < 0.05이므로 "세 군의 평균이 모두 같다"는 귀무가설을 기각합니다. 다만 ANOVA는 어느 군이 다른지는 말해 주지 않습니다 — 그래서 사후검정(post-hoc test)이 필요합니다.
TukeyHSD()는 모든 군 쌍을 비교하되, 다중비교에 따른 p-값 부풀림을 자체적으로 보정합니다 (Tukey의 HSD — honestly significant difference).
TukeyHSD(fit) # 모든 쌍 비교 + 다중비교 보정
$geno
diff lwr upr p adj
KO-WT 0.06274305 -1.033493 1.158979 0.9885979
OE-WT 2.38340203 1.287166 3.479638 0.0000558
OE-KO 2.32065899 1.224423 3.416895 0.0000779
표의 각 행이 한 쌍의 비교입니다. diff는 평균 차, lwr·upr는 그 차이의 95% 신뢰구간, p adj는 보정된 p-값입니다. 여기서는 OE-WT와 OE-KO가 p < 0.001로 유의하지만 KO-WT는 p = 0.99로 차이가 없습니다 — 즉 OE만 도드라지고 KO와 WT는 비슷하다는 뜻입니다. ANOVA의 전체 p-값만 봤다면 놓쳤을 구조를, 사후검정이 짚어 줍니다.
6. 비모수 3그룹 — kruskal.test()
세 군 이상인데 정규성을 못 믿겠으면 ANOVA의 비모수 대응인 kruskal.test() (Kruskal-Wallis 검정)를 씁니다. 역시 순위 기반으로 "적어도 한 군은 다른가"를 검정합니다.
kruskal.test(expr ~ geno, data = df3) # Kruskal-Wallis (비모수 ANOVA)
Kruskal-Wallis rank sum test
data: expr by geno
Kruskal-Wallis chi-squared = 15.365, df = 2, p-value = 0.0004608
p < 0.05이므로 군 간 차이가 있습니다. 어느 쌍이 다른지 보려면 사후검정으로 Dunn 검정(dunn.test::dunn.test 또는 FSA::dunnTest)을 쓰고, 거기서 다중비교 보정을 적용합니다 — TukeyHSD가 ANOVA의 짝이라면, Dunn 검정이 Kruskal-Wallis의 짝입니다.

7. 상관 — cor.test()
두 연속 변수가 함께 움직이는지 (예: 유전자 A 발현 ↔ 유전자 B 발현)는 상관(correlation)으로 봅니다. cor.test()는 상관계수 r과 그 p-값을 함께 줍니다.
cor.test(x, y, method = "pearson") # 피어슨: 선형 관계 (정규성 가정)
Pearson's product-moment correlation
data: x and y
t = 9.3529, df = 38, p-value = 2.119e-11
cor
0.8349581
cor = 0.83이 피어슨 상관계수 r입니다 (−1~+1, 0이면 무관, 부호는 방향). p < 0.05이므로 두 변수의 상관이 0이 아니라고 봅니다. 피어슨 (Pearson)은 선형(linear) 관계를 재고 정규성을 가정하므로, 관계가 곡선이거나 이상치가 있으면 순위 기반의 스피어만 (Spearman)으로 바꿉니다.
cor.test(x, y, method = "spearman") # 스피어만: 단조(순위) 관계, 비모수
# rho = 0.737, p-value = 3.419e-07
스피어만은 값 대신 순위로 상관을 재므로 (r 대신 ρ, rho), 단조(monotonic)이기만 하면 곡선 관계도 잡고 이상치에 덜 휘둘립니다. 관계의 모양을 모르겠으면 둘 다 보고 비교하면 됩니다. 관계를 식으로 모형화하려면 회귀(regression)로 넘어갑니다 — 한 줄이면 됩니다.
lm(y ~ x) # 선형회귀 (기울기·절편 추정)
summary(lm(y ~ x))$coefficients # 계수·표준오차·t·p

8. 검정 선택 표
| 상황 | 모수 검정 | 비모수 검정 | R 함수 |
|---|---|---|---|
| 2그룹·독립 | Student/Welch t-검정 | Mann-Whitney U | t.test() · wilcox.test() |
| 2그룹·대응 | 대응 t-검정 | Wilcoxon 부호순위 | t.test(paired=TRUE) |
| 3그룹 이상 | 일원분산분석 (ANOVA) | Kruskal-Wallis | aov() · kruskal.test() |
| ANOVA 사후검정 | TukeyHSD | Dunn 검정 | TukeyHSD() · dunn.test() |
| 두 변수 상관 | 피어슨 r | 스피어만 ρ | cor.test(method=...) |
| 정규성 점검 | — | — | shapiro.test() · qqnorm() |
| 등분산 점검 | — | — | var.test() · bartlett.test() |
9. 자주 발생하는 에러 & 오해
- 세 그룹을 t-검정으로 짝마다 비교 — 가장 흔한 실수입니다. 비교를 반복하면 위양성이 쌓이니, 3군 이상은
aov()+TukeyHSD()(또는kruskal.test()+ Dunn)로 한 번에 검정하고 사후 보정을 받으세요. - 독립 표본에
paired = TRUE— 서로 다른 개체인데 대응 검정을 켜면 결과가 무의미합니다. 같은 대상의 전/후일 때만 대응입니다. 반대로 진짜 대응 자료를 독립으로 검정하면 검정력을 버립니다. - 정규성·등분산을 점검하지 않고 t-검정 — 작은 표본에서 분포가 치우치면 t-검정의 p-값을 믿기 어렵습니다.
shapiro.test·qqnorm으로 보고, 애매하면wilcox.test로 가세요. R의t.test는 기본이 Welch라 등분산을 가정하지 않는 점은 다행입니다. - 다중비교 보정 누락 — 여러 유전자·여러 조건을 한꺼번에 검정하면 p-값을 그대로 쓰면 안 됩니다.
p.adjust(pvals, method = "BH")로 보정하세요 (BH = Benjamini-Hochberg, FDR 제어). 예:p.adjust(c(0.001, 0.008, 0.02, 0.04, 0.31), "BH")→0.005, 0.020, 0.033, 0.050, 0.310. 자세한 건 p-value와 FDR에서 다룹니다. - 상관을 인과로 해석 — r이 크고 p가 작아도 "A가 B를 일으킨다"는 뜻은 아닙니다. 상관은 동반 변화일 뿐, 인과는 실험 설계로 따로 보여야 합니다.
cor.test가 NA를 반환 — 입력에NA가 있으면 결과가 NA로 나옵니다.use = "complete.obs"를 주거나 결측을 먼저 제거하세요.
• (검정 다음 단계) p-value와 FDR, 다중검정 보정 — 여러 검정의 p-값을 어떻게 보정하나
• (결과 시각화) ggplot2 기초 · Volcano plot 시각화 — 검정 결과를 그림으로
• (대규모 검정) DESeq2로 차등발현 분석 — 수만 유전자를 한 번에 검정하는 모형
• (샘플 구조 먼저) PCA로 샘플 QC · dplyr·tidyr 데이터 정리
References
- R Core Team. (2024). R: A language and environment for statistical computing. (
t.test·aov·cor.test·wilcox.test·kruskal.test·p.adjust등stats패키지 기본 함수) - Student. (1908). The probable error of a mean. Biometrika, 6(1), 1–25. (t-검정의 원형)
- Welch, B. L. (1947). The generalization of "Student's" problem when several different population variances are involved. Biometrika, 34(1–2), 28–35. (Welch t-검정 —
t.test의 기본) - Benjamini, Y., Hochberg, Y. (1995). Controlling the false discovery rate. Journal of the Royal Statistical Society B, 57(1), 289–300. (
p.adjust(method = "BH")— 다중검정 보정) - Cohen, J. (1988). Statistical Power Analysis for the Behavioral Sciences (2nd ed.). Erlbaum. (Cohen's d — 효과크기 관행)
Pipette & Pipeline · A bio portfolio journal
'Code Bench > R Snippets' 카테고리의 다른 글
| pROC로 ROC 곡선·AUC 그리기 — 바이오마커·예측 모델 성능 평가 (R 실전) (0) | 2026.06.22 |
|---|---|
| GSVA로 샘플별 경로 활성 점수 매기기 — 유전자에서 경로로 (R 실전) (0) | 2026.06.19 |
| pheatmap으로 발현 히트맵 빠르게 그리기 — z-score·클러스터링·주석 (R 실전) (0) | 2026.06.18 |
| dplyr·tidyr로 생물 데이터 정리하기 — 분석 전 90%는 데이터 정리 (R 실전) (0) | 2026.06.18 |
| tximport로 salmon·kallisto 정량 불러오기 — RNA-seq 카운트의 출발점 (R 실전) (0) | 2026.06.17 |