Python으로 실험 통계 분석하기
이 토픽을 마치면
Python으로 두 그룹의 차이를 t-test로 검정하고, 세 그룹 이상은 ANOVA로 비교하고, p-value를 올바르게 해석할 수 있습니다.
왜 통계 검정이 필요한가
약물 처리 그룹의 세포 생존율 평균이 80%, 대조군이 85%입니다. "약물이 효과가 있다"고 결론 내릴 수 있을까요?
평균이 다르다고 해서 반드시 "차이가 있다"는 아닙니다. 실험을 반복하면 매번 다른 값이 나옵니다 — 이것이 **변이(variation)**입니다. 5% 차이가 약물 효과인지, 단순히 실험 오차인지 구분해야 합니다.
이것을 판단하는 도구가 **통계 검정(statistical test)**이고, 그 결과가 p-value입니다.
p-value란
p-value는 "차이가 없다고 가정했을 때, 이 정도 차이가 우연히 나올 확률"입니다.
p-value = 0.03 → "차이가 없는데 이 정도 차이가 우연히 나올 확률이 3%"
→ 우연이라 보기 어렵다 → 차이가 있다고 판단
p-value = 0.42 → "차이가 없는데 이 정도 차이가 우연히 나올 확률이 42%"
→ 충분히 우연일 수 있다 → 차이가 있다고 말하기 어렵다관례적으로 p < 0.05면 "통계적으로 유의하다(statistically significant)"고 합니다. 하지만 0.05는 절대적 기준이 아니라 관례입니다.
t-test: 두 그룹 비교
Independent t-test — 두 독립적인 그룹의 평균을 비교합니다.
import numpy as npfrom scipy import stats
# 약물 처리 그룹의 세포 생존율 (%)drug = np.array([78, 82, 75, 80, 77, 83, 79, 76])
# 대조군의 세포 생존율 (%)control = np.array([85, 88, 82, 86, 90, 84, 87, 89])
t_stat, p_value = stats.ttest_ind(drug, control)
print(f"Drug 평균: {drug.mean():.1f}%")print(f"Control 평균: {control.mean():.1f}%")print(f"t-statistic: {t_stat:.3f}")print(f"p-value: {p_value:.4f}")
if p_value < 0.05: print("→ 통계적으로 유의한 차이 (p < 0.05)")else: print("→ 유의한 차이 없음")
assert p_value < 0.05stats.ttest_ind() — independent (독립) t-test. 두 그룹이 서로 다른 시료일 때 사용합니다.
Paired t-test: 같은 시료의 전후 비교
같은 환자에서 치료 전/후를 비교할 때는 paired t-test를 씁니다:
from scipy import statsimport numpy as np
before = np.array([120, 135, 128, 142, 138, 125])after = np.array([115, 128, 122, 130, 132, 118])
t_stat, p_value = stats.ttest_rel(before, after)
print(f"치료 전 평균: {before.mean():.1f}")print(f"치료 후 평균: {after.mean():.1f}")print(f"p-value: {p_value:.4f}")ttest_rel() — related (관련된, 대응하는) t-test. 같은 대상의 전/후 측정값을 비교합니다.
| 상황 | 검정 방법 | scipy 함수 |
|---|---|---|
| 서로 다른 두 그룹 비교 | Independent t-test | stats.ttest_ind() |
| 같은 대상의 전/후 비교 | Paired t-test | stats.ttest_rel() |
ANOVA: 세 그룹 이상 비교
세 가지 배지(DMEM, RPMI, MEM)에서 세포 증식률을 비교하려면? t-test는 두 그룹만 비교할 수 있으므로, **ANOVA(Analysis of Variance)**를 씁니다.
from scipy import statsimport numpy as np
dmem = np.array([1.2, 1.4, 1.3, 1.5, 1.1])rpmi = np.array([1.8, 1.7, 1.9, 2.0, 1.6])mem = np.array([1.0, 1.1, 0.9, 1.2, 1.0])
f_stat, p_value = stats.f_oneway(dmem, rpmi, mem)
print(f"DMEM 평균: {dmem.mean():.2f}")print(f"RPMI 평균: {rpmi.mean():.2f}")print(f"MEM 평균: {mem.mean():.2f}")print(f"F-statistic: {f_stat:.3f}")print(f"p-value: {p_value:.6f}")
assert p_value < 0.05ANOVA의 p-value가 유의하면 "세 그룹 중 적어도 하나는 다르다"는 의미입니다. 어떤 그룹이 다른지는 사후 검정(post-hoc test)으로 확인합니다.
결과 시각화: 박스플롯
숫자만으로는 분포를 파악하기 어렵습니다. 박스플롯으로 시각화합니다:
import matplotlib.pyplot as pltimport numpy as np
drug = np.array([78, 82, 75, 80, 77, 83, 79, 76])control = np.array([85, 88, 82, 86, 90, 84, 87, 89])
fig, ax = plt.subplots(figsize=(6, 4))ax.boxplot([drug, control], labels=["Drug", "Control"])ax.set_ylabel("Cell Viability (%)")ax.set_title("Drug vs Control")plt.tight_layout()plt.savefig("drug_vs_control.png", dpi=150)plt.show()박스플롯은 중앙값, 사분위 범위, 이상치를 한눈에 보여줍니다. 논문 Figure에서 가장 흔한 그래프 유형 중 하나입니다.
주의사항: p-value의 함정
p-value를 해석할 때 주의할 점:
1. p < 0.05라고 해서 "중요한" 차이는 아닙니다
통계적 유의성(statistical significance)과 실질적 유의성(practical significance)은 다릅니다. 시료 수가 매우 많으면 아주 작은 차이도 p < 0.05가 될 수 있습니다. "이 차이가 생물학적으로 의미가 있는가?"를 항상 함께 생각하세요.
2. p > 0.05라고 해서 "차이가 없다"는 아닙니다
"차이가 있다는 증거를 찾지 못했다"는 의미입니다. 시료 수가 너무 적으면 실제 차이가 있어도 감지하지 못합니다.
3. 다중 비교 문제
유전자 20,000개를 한꺼번에 검정하면, 5%인 1,000개가 우연히 p < 0.05가 됩니다. 이 경우 Bonferroni 보정 또는 FDR(False Discovery Rate) 보정이 필요합니다.
직접 해보기 (Faded Example)
아래 빈칸을 채워 두 그룹의 t-test를 수행하세요.
from import statsimport numpy as nptreated = np.array([4.2, 3.8, 4.5, 4.1, 3.9])control = np.array([5.1, 5.3, 4.9, 5.0, 5.2])t_stat, p_value = stats.ind(treated, control)print(f"p-value: {p_value:.4f}")if p_value < :print("유의한 차이")
흔한 에러 & 해결법
Q: ModuleNotFoundError: No module named 'scipy'
pip install scipy로 설치하세요. Google Colab에서는 이미 설치되어 있습니다.
Q: t-test와 ANOVA 중 어떤 것을 써야 하나요?
비교할 그룹이 2개면 t-test, 3개 이상이면 ANOVA입니다. 3개 그룹을 t-test로 3번 비교(A-B, A-C, B-C)하면 다중 비교 문제가 생기므로 ANOVA를 써야 합니다.
Q: 데이터가 정규분포가 아니면 어떻게 하나요?
t-test와 ANOVA는 데이터가 대략 정규분포를 따른다고 가정합니다. 정규분포가 아닌 경우 비모수 검정(non-parametric test)을 씁니다: Mann-Whitney U test (stats.mannwhitneyu()), Kruskal-Wallis test (stats.kruskal()).
Q: nan이 결과에 나옵니다
데이터에 결측값(NaN)이 포함되어 있을 수 있습니다. np.nanmean(data)처럼 nan을 무시하는 함수를 쓰거나, Pandas에서 df.dropna()로 결측값을 제거한 후 검정하세요.