Part 0: 환경 설정¶
In [23]:
!pip install openpyxl -q
import numpy as np
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
# 한글 폰트 설정
import platform
if platform.system() == 'Darwin':
plt.rcParams['font.family'] = 'AppleGothic'
elif platform.system() == 'Windows':
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['figure.dpi'] = 100
# ──────────────────────────────────────────────
# [Google Colab 사용 시] 아래 코드를 실행하세요
# ──────────────────────────────────────────────
# !apt-get -qq install fonts-nanum
# import matplotlib.font_manager as fm
# font_path = '/usr/share/fonts/truetype/nanum/NanumGothic.ttf'
# fm.fontManager.addfont(font_path)
# plt.rcParams['font.family'] = 'NanumGothic'
# plt.rcParams['axes.unicode_minus'] = False
# ──────────────────────────────────────────────
print("환경 설정 완료!")
환경 설정 완료!
In [24]:
# ── 데이터 1: 축구 선수 체지방률 (단일표본용) ──
# 리그 평균 = 12%
# pd.read_excel(파일경로, sheet_name=시트이름)
# - 엑셀 파일을 DataFrame으로 읽어오는 함수
# - sheet_name: 읽을 시트 이름 (생략하면 첫 번째 시트)
df_fat = pd.read_excel('t검정_실습데이터.xlsx', sheet_name='단일표본_체지방률')
# df['열이름'].values → 해당 열을 numpy 배열로 변환
body_fat = df_fat['체지방률(%)'].values
print("=== 데이터 1: 체지방률 (단일표본) ===")
print(df_fat)
print()
# .mean() → 평균, .std(ddof=1) → 표본 표준편차 (n-1로 나눔)
print(f"표본 크기: {len(body_fat)}")
print(f"표본 평균: {body_fat.mean():.2f}%")
print(f"표본 표준편차: {body_fat.std(ddof=1):.2f}%")
print(f"리그 평균 기준값: 12%")
=== 데이터 1: 체지방률 (단일표본) ===
선수 포지션 체지방률(%)
0 선수1 FW 10.2
1 선수2 MF 11.5
2 선수3 DF 9.8
3 선수4 FW 12.1
4 선수5 MF 10.7
5 선수6 DF 11.3
6 선수7 FW 10.0
7 선수8 MF 11.8
8 선수9 DF 9.5
9 선수10 FW 10.9
표본 크기: 10
표본 평균: 10.78%
표본 표준편차: 0.89%
리그 평균 기준값: 12%
In [25]:
# ── 데이터 2: A팀 vs B팀 스프린트 속도 (독립표본용) ──
df_sprint = pd.read_excel('t검정_실습데이터.xlsx', sheet_name='독립표본_스프린트속도')
# .dropna() → NaN(빈 값)을 제거 (A팀 12명, B팀 10명이라 빈 칸이 있음)
# .values → numpy 배열로 변환
team_a = df_sprint['A팀_스프린트속도(km/h)'].dropna().values
team_b = df_sprint['B팀_스프린트속도(km/h)'].dropna().values
print("=== 데이터 2: 스프린트 속도 (독립표본) ===")
print(df_sprint)
print()
print(f"A팀 (n={len(team_a)}): 평균 = {team_a.mean():.2f} km/h, 표준편차 = {team_a.std(ddof=1):.2f}")
print(f"B팀 (n={len(team_b)}): 평균 = {team_b.mean():.2f} km/h, 표준편차 = {team_b.std(ddof=1):.2f}")
=== 데이터 2: 스프린트 속도 (독립표본) === A팀_선수 A팀_스프린트속도(km/h) B팀_선수 B팀_스프린트속도(km/h) 0 A1 33.2 B1 31.5 1 A2 34.1 B2 32.0 2 A3 32.8 B3 31.8 3 A4 33.5 B4 32.5 4 A5 34.5 B5 31.2 5 A6 33.0 B6 32.8 6 A7 34.2 B7 31.0 7 A8 33.8 B8 32.3 8 A9 32.5 B9 31.7 9 A10 34.0 B10 32.1 10 A11 33.7 NaN NaN 11 A12 34.3 NaN NaN A팀 (n=12): 평균 = 33.63 km/h, 표준편차 = 0.64 B팀 (n=10): 평균 = 31.89 km/h, 표준편차 = 0.57
In [26]:
# ── 데이터 3: 훈련 전후 민첩성 테스트 (대응표본용) ──
df_agility = pd.read_excel('t검정_실습데이터.xlsx', sheet_name='대응표본_민첩성')
before = df_agility['훈련전_민첩성(초)'].values
after = df_agility['훈련후_민첩성(초)'].values
# 차이 열 추가
df_agility['차이(후-전)'] = after - before
print("=== 데이터 3: 민첩성 테스트 (대응표본) ===")
print(df_agility)
print()
print(f"훈련 전 (n={len(before)}): 평균 = {before.mean():.2f}초")
print(f"훈련 후 (n={len(after)}): 평균 = {after.mean():.2f}초")
print(f"평균 차이 (후-전): {(after - before).mean():.2f}초")
=== 데이터 3: 민첩성 테스트 (대응표본) ===
선수 훈련전_민첩성(초) 훈련후_민첩성(초) 차이(후-전)
0 선수1 11.2 10.5 -0.7
1 선수2 10.8 10.2 -0.6
2 선수3 11.5 10.8 -0.7
3 선수4 10.5 10.1 -0.4
4 선수5 11.0 10.3 -0.7
5 선수6 11.3 10.7 -0.6
6 선수7 10.9 10.4 -0.5
7 선수8 11.1 10.6 -0.5
훈련 전 (n=8): 평균 = 11.04초
훈련 후 (n=8): 평균 = 10.45초
평균 차이 (후-전): -0.59초
Part 2: 전제조건 검정¶
2-1. 정규성 검정 (Shapiro-Wilk)¶
- t검정의 전제조건: 데이터가 정규분포를 따라야 함
- H₀: 데이터가 정규분포를 따른다
- H₁: 데이터가 정규분포를 따르지 않는다
- p ≥ 0.05 → 정규분포 가정 충족
In [27]:
# ── 정규성 검정: Shapiro-Wilk ──
print("=== 정규성 검정 (Shapiro-Wilk) ===")
print()
# stats.shapiro(데이터)
# - 데이터가 정규분포를 따르는지 검정하는 함수
# - 반환값: (W통계량, p값)
# - W: 1에 가까울수록 정규분포, p ≥ 0.05이면 정규분포로 판단
# 데이터 1: 체지방률
stat1, p1 = stats.shapiro(body_fat)
print(f"[체지방률] W = {stat1:.4f}, p = {p1:.4f} → {'정규분포 ✓' if p1 >= 0.05 else '비정규 ✗'}")
# 데이터 2: A팀, B팀
stat2a, p2a = stats.shapiro(team_a)
stat2b, p2b = stats.shapiro(team_b)
print(f"[A팀 속도] W = {stat2a:.4f}, p = {p2a:.4f} → {'정규분포 ✓' if p2a >= 0.05 else '비정규 ✗'}")
print(f"[B팀 속도] W = {stat2b:.4f}, p = {p2b:.4f} → {'정규분포 ✓' if p2b >= 0.05 else '비정규 ✗'}")
# 데이터 3: 대응표본에서는 "차이값"의 정규성을 검정
diff = after - before
stat3, p3 = stats.shapiro(diff)
print(f"[전후 차이] W = {stat3:.4f}, p = {p3:.4f} → {'정규분포 ✓' if p3 >= 0.05 else '비정규 ✗'}")
=== 정규성 검정 (Shapiro-Wilk) === [체지방률] W = 0.9577, p = 0.7593 → 정규분포 ✓ [A팀 속도] W = 0.9526, p = 0.6757 → 정규분포 ✓ [B팀 속도] W = 0.9874, p = 0.9925 → 정규분포 ✓ [전후 차이] W = 0.8821, p = 0.1973 → 정규분포 ✓
── Q-Q Plot으로 정규성 시각적 확인 ──¶
Q-Q Plot 해석¶
- X축 (Theoretical Quantiles) = 정규분포라면 있어야 할 이론적 위치 (z-score)
- Y축 (Ordered Values) = 실제 데이터 값 (작은 순서대로 정렬)
- 빨간 직선 = 완벽한 정규분포 기준선
판단 방법
- 점들이 직선에 가까움 → 이론적 위치와 실제 값이 일치 → 정규분포
- 점들이 직선에서 벗어남 → 이론과 실제가 불일치 → 비정규분포
위 3개 그래프 모두 점들이 직선 근처에 있으므로 → 정규분포 가정 충족
In [28]:
# ── Q-Q Plot으로 정규성 시각적 확인 ──
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
datasets = [body_fat, team_a, diff]
titles = ['체지방률', 'A팀 스프린트 속도', '훈련 전후 차이']
for ax, data, title in zip(axes, datasets, titles):
# stats.probplot(데이터, dist=비교할분포, plot=그릴축)
# - 데이터의 분위수와 이론적 분위수를 비교하는 Q-Q Plot 생성
# - dist='norm': 정규분포와 비교
# - plot=ax: 해당 축(ax)에 그래프를 그림
stats.probplot(data, dist='norm', plot=ax)
ax.set_title(f'Q-Q Plot: {title}', fontweight='bold')
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()
2-2. 등분산 검정 (Levene's Test)¶
- 독립표본 t검정에서만 필요
- H₀: 두 집단의 분산이 같다 (등분산)
- H₁: 두 집단의 분산이 다르다 (이분산)
- p ≥ 0.05 → 등분산 → Student's t-test
- p < 0.05 → 이분산 → Welch's t-test
In [29]:
# ── 등분산 검정: Levene's Test ──
print("=== 등분산 검정 (Levene's Test) ===")
print()
# stats.levene(집단1, 집단2)
# - 두 집단의 분산이 같은지 검정하는 함수
# - 반환값: (검정통계량, p값)
# - p ≥ 0.05 → 등분산 (Student's t), p < 0.05 → 이분산 (Welch's t)
lev_stat, lev_p = stats.levene(team_a, team_b)
# .var(ddof=1) → 표본 분산 (n-1로 나눔)
print(f"A팀 분산: {team_a.var(ddof=1):.4f}")
print(f"B팀 분산: {team_b.var(ddof=1):.4f}")
print(f"Levene 통계량 = {lev_stat:.4f}, p = {lev_p:.4f}")
print()
if lev_p >= 0.05:
print("→ p ≥ 0.05: 등분산 가정 충족 → Student's t-test 사용")
else:
print("→ p < 0.05: 이분산 → Welch's t-test 사용")
=== 등분산 검정 (Levene's Test) === A팀 분산: 0.4079 B팀 분산: 0.3210 Levene 통계량 = 0.2097, p = 0.6519 → p ≥ 0.05: 등분산 가정 충족 → Student's t-test 사용
In [30]:
# ── 분산 비교 시각화 ──
fig, ax = plt.subplots(figsize=(7, 5))
bp = ax.boxplot([team_a, team_b], labels=['A팀', 'B팀'], patch_artist=True,
medianprops=dict(color='red', linewidth=2))
bp['boxes'][0].set_facecolor('#42A5F5')
bp['boxes'][0].set_alpha(0.6)
bp['boxes'][1].set_facecolor('#EF5350')
bp['boxes'][1].set_alpha(0.6)
ax.set_title('A팀 vs B팀 스프린트 속도 분포', fontsize=14, fontweight='bold')
ax.set_ylabel('최대 스프린트 속도 (km/h)')
ax.grid(axis='y', alpha=0.3)
plt.show()
print("해석: 상자의 크기(IQR)가 비슷하면 분산이 비슷한 것")
/var/folders/_f/8h1v3dgs7396l2lgm0gb6d_h0000gn/T/ipykernel_54167/3167626623.py:4: MatplotlibDeprecationWarning: The 'labels' parameter of boxplot() has been renamed 'tick_labels' since Matplotlib 3.9; support for the old name will be dropped in 3.11. bp = ax.boxplot([team_a, team_b], labels=['A팀', 'B팀'], patch_artist=True,
해석: 상자의 크기(IQR)가 비슷하면 분산이 비슷한 것
Part 3: 단일표본 t검정 (One-sample t-test)¶
- 질문: 우리 팀 선수 체지방률이 리그 평균(12%)과 다른가?
- H₀: μ = 12 (리그 평균과 같다)
- H₁: μ ≠ 12 (리그 평균과 다르다)
3-1. 수작업 계산¶
In [31]:
# ── 단일표본 t검정: 수작업 계산 ──
print("=== 단일표본 t검정 (수작업) ===")
print()
mu_0 = 12 # 기준값 (리그 평균)
n = len(body_fat) # 표본 크기
x_bar = body_fat.mean() # 표본 평균
# .std(ddof=1) → 표본 표준편차
# - ddof=0: 모집단 표준편차 (n으로 나눔)
# - ddof=1: 표본 표준편차 (n-1로 나눔, 불편추정량)
s = body_fat.std(ddof=1)
# np.sqrt(n) → n의 제곱근 (√n)
se = s / np.sqrt(n) # 표준오차 = s / √n
print(f"기준값 (μ₀): {mu_0}")
print(f"표본 크기 (n): {n}")
print(f"표본 평균 (X̄): {x_bar:.4f}")
print(f"표본 표준편차 (s): {s:.4f}")
print(f"표준오차 (SE): {se:.4f}")
print()
# t값 계산: t = (X̄ - μ₀) / (s / √n)
t_manual = (x_bar - mu_0) / se
df = n - 1 # 자유도
print(f"t = (X̄ - μ₀) / SE")
print(f"t = ({x_bar:.4f} - {mu_0}) / {se:.4f}")
print(f"t = {t_manual:.4f}")
print(f"자유도 (df) = n - 1 = {df}")
print()
# stats.t.sf(t값, 자유도)
# - t분포의 생존함수 (Survival Function)
# - "t값보다 큰 값이 나올 확률" = 오른쪽 꼬리 확률
# - abs(): 절대값 (음수여도 양수로 바꿔서 오른쪽 꼬리 계산)
# - 2 * : 양측검정이므로 양쪽 꼬리 확률을 합산
p_manual = 2 * stats.t.sf(abs(t_manual), df)
print(f"p값 (양측) = {p_manual:.4f}")
print()
if p_manual < 0.05:
print(f"→ p = {p_manual:.4f} < 0.05: 귀무가설 기각")
print("→ 우리 팀 체지방률은 리그 평균과 유의한 차이가 있다")
else:
print(f"→ p = {p_manual:.4f} ≥ 0.05: 귀무가설 채택")
print("→ 우리 팀 체지방률은 리그 평균과 유의한 차이가 없다")
=== 단일표본 t검정 (수작업) === 기준값 (μ₀): 12 표본 크기 (n): 10 표본 평균 (X̄): 10.7800 표본 표준편차 (s): 0.8904 표준오차 (SE): 0.2816 t = (X̄ - μ₀) / SE t = (10.7800 - 12) / 0.2816 t = -4.3327 자유도 (df) = n - 1 = 9 p값 (양측) = 0.0019 → p = 0.0019 < 0.05: 귀무가설 기각 → 우리 팀 체지방률은 리그 평균과 유의한 차이가 있다
3-2. 라이브러리 사용¶
In [32]:
# ── 단일표본 t검정: scipy 라이브러리 ──
print("=== 단일표본 t검정 (scipy) ===")
print()
# stats.ttest_1samp(데이터, popmean=기준값)
# - 단일표본 t검정 함수
# - 데이터: 검정할 표본 데이터 (배열)
# - popmean: 비교할 모집단 평균 (기준값)
# - 반환값: (t통계량, p값) — 양측검정 결과
t_stat, p_value = stats.ttest_1samp(body_fat, popmean=12)
print(f"t = {t_stat:.4f}")
print(f"p = {p_value:.4f}")
print()
print("→ 수작업 결과와 동일한지 확인!")
print()
# 효과 크기 (Cohen's d) = (표본평균 - 기준값) / 표본 표준편차
d = (body_fat.mean() - 12) / body_fat.std(ddof=1)
print(f"Cohen's d = {d:.4f}")
print(f"→ 효과 크기: {'작음' if abs(d) < 0.5 else '중간' if abs(d) < 0.8 else '큼'}")
print()
print(f"APA 보고: t({n-1}) = {t_stat:.2f}, p = {p_value:.3f}, d = {abs(d):.2f}")
=== 단일표본 t검정 (scipy) === t = -4.3327 p = 0.0019 → 수작업 결과와 동일한지 확인! Cohen's d = -1.3701 → 효과 크기: 큼 APA 보고: t(9) = -4.33, p = 0.002, d = 1.37
Part 4: 독립표본 t검정 (Independent two-sample t-test)¶
- 질문: A팀과 B팀의 스프린트 속도에 유의한 차이가 있는가?
- H₀: μ_A = μ_B (두 팀 평균이 같다)
- H₁: μ_A ≠ μ_B (두 팀 평균이 다르다)
4-1. 수작업 계산 (등분산 가정 — Student's t-test)¶
In [33]:
# ── 독립표본 t검정: 수작업 계산 (등분산) ──
print("=== 독립표본 t검정 (수작업 — 등분산) ===")
print()
n1 = len(team_a)
n2 = len(team_b)
x1 = team_a.mean()
x2 = team_b.mean()
s1 = team_a.std(ddof=1)
s2 = team_b.std(ddof=1)
print(f"A팀: n₁={n1}, X̄₁={x1:.4f}, s₁={s1:.4f}")
print(f"B팀: n₂={n2}, X̄₂={x2:.4f}, s₂={s2:.4f}")
print()
# 합동분산 (Pooled variance)
sp2 = ((n1-1)*s1**2 + (n2-1)*s2**2) / (n1 + n2 - 2)
sp = np.sqrt(sp2)
print(f"합동분산 (Sp²) = ((n₁-1)s₁² + (n₂-1)s₂²) / (n₁+n₂-2)")
print(f"Sp² = (({n1}-1)×{s1:.4f}² + ({n2}-1)×{s2:.4f}²) / ({n1}+{n2}-2)")
print(f"Sp² = {sp2:.4f}")
print(f"Sp = {sp:.4f}")
print()
# t값 계산
t_manual = (x1 - x2) / (sp * np.sqrt(1/n1 + 1/n2))
df = n1 + n2 - 2
print(f"t = (X̄₁ - X̄₂) / (Sp × √(1/n₁ + 1/n₂))")
print(f"t = ({x1:.4f} - {x2:.4f}) / ({sp:.4f} × √(1/{n1} + 1/{n2}))")
print(f"t = {t_manual:.4f}")
print(f"자유도 (df) = n₁ + n₂ - 2 = {df}")
print()
# p값 계산
p_manual = 2 * stats.t.sf(abs(t_manual), df)
print(f"p값 (양측) = {p_manual:.6f}")
print()
if p_manual < 0.05:
print(f"→ p = {p_manual:.6f} < 0.05: 귀무가설 기각")
print("→ A팀과 B팀의 스프린트 속도에 유의한 차이가 있다")
else:
print(f"→ p = {p_manual:.6f} ≥ 0.05: 귀무가설 채택")
print("→ A팀과 B팀의 스프린트 속도에 유의한 차이가 없다")
=== 독립표본 t검정 (수작업 — 등분산) === A팀: n₁=12, X̄₁=33.6333, s₁=0.6387 B팀: n₂=10, X̄₂=31.8900, s₂=0.5666 합동분산 (Sp²) = ((n₁-1)s₁² + (n₂-1)s₂²) / (n₁+n₂-2) Sp² = ((12-1)×0.6387² + (10-1)×0.5666²) / (12+10-2) Sp² = 0.3688 Sp = 0.6073 t = (X̄₁ - X̄₂) / (Sp × √(1/n₁ + 1/n₂)) t = (33.6333 - 31.8900) / (0.6073 × √(1/12 + 1/10)) t = 6.7046 자유도 (df) = n₁ + n₂ - 2 = 20 p값 (양측) = 0.000002 → p = 0.000002 < 0.05: 귀무가설 기각 → A팀과 B팀의 스프린트 속도에 유의한 차이가 있다
4-2. 수작업 계산 (이분산 가정 — Welch's t-test)¶
In [34]:
# ── 독립표본 t검정: 수작업 계산 (이분산 — Welch) ──
print("=== 독립표본 t검정 (수작업 — Welch) ===")
print()
# Welch's t값 계산
se_welch = np.sqrt(s1**2/n1 + s2**2/n2)
t_welch = (x1 - x2) / se_welch
print(f"t = (X̄₁ - X̄₂) / √(s₁²/n₁ + s₂²/n₂)")
print(f"t = ({x1:.4f} - {x2:.4f}) / √({s1:.4f}²/{n1} + {s2:.4f}²/{n2})")
print(f"t = {t_welch:.4f}")
print()
# Welch-Satterthwaite 자유도
df_welch = (s1**2/n1 + s2**2/n2)**2 / ((s1**2/n1)**2/(n1-1) + (s2**2/n2)**2/(n2-1))
print(f"Welch 자유도 (df) = {df_welch:.2f}")
print()
# p값
p_welch = 2 * stats.t.sf(abs(t_welch), df_welch)
print(f"p값 (양측) = {p_welch:.6f}")
print()
print("참고: 등분산 결과와 비교해보세요. 등분산이면 두 결과가 비슷합니다.")
=== 독립표본 t검정 (수작업 — Welch) === t = (X̄₁ - X̄₂) / √(s₁²/n₁ + s₂²/n₂) t = (33.6333 - 31.8900) / √(0.6387²/12 + 0.5666²/10) t = 6.7813 Welch 자유도 (df) = 19.90 p값 (양측) = 0.000001 참고: 등분산 결과와 비교해보세요. 등분산이면 두 결과가 비슷합니다.
4-3. 라이브러리 사용¶
In [35]:
# ── 독립표본 t검정: scipy 라이브러리 ──
print("=== 독립표본 t검정 (scipy) ===")
print()
# stats.ttest_ind(집단1, 집단2, equal_var=등분산여부)
# - 독립표본 t검정 함수 (두 개의 독립된 집단 비교)
# - 집단1, 집단2: 비교할 두 집단의 데이터 (배열)
# - equal_var=True: 등분산 가정 → Student's t-test
# - equal_var=False: 이분산 가정 → Welch's t-test
# - 반환값: (t통계량, p값)
# Student's t-test (등분산 가정)
t_student, p_student = stats.ttest_ind(team_a, team_b, equal_var=True)
print(f"[Student's] t = {t_student:.4f}, p = {p_student:.6f}")
# Welch's t-test (이분산 가정)
t_welch_lib, p_welch_lib = stats.ttest_ind(team_a, team_b, equal_var=False)
print(f"[Welch's] t = {t_welch_lib:.4f}, p = {p_welch_lib:.6f}")
print()
print("→ 수작업 결과와 동일한지 확인!")
print()
# 효과 크기 (Cohen's d) = (평균1 - 평균2) / 합동표준편차
sp_cohen = np.sqrt(((n1-1)*s1**2 + (n2-1)*s2**2) / (n1+n2-2))
d = (x1 - x2) / sp_cohen
print(f"Cohen's d = {d:.4f}")
print(f"→ 효과 크기: {'작음' if abs(d) < 0.5 else '중간' if abs(d) < 0.8 else '큼'}")
print()
print(f"APA 보고: t({n1+n2-2}) = {t_student:.2f}, p < .001, d = {abs(d):.2f}")
=== 독립표본 t검정 (scipy) === [Student's] t = 6.7046, p = 0.000002 [Welch's] t = 6.7813, p = 0.000001 → 수작업 결과와 동일한지 확인! Cohen's d = 2.8707 → 효과 크기: 큼 APA 보고: t(20) = 6.70, p < .001, d = 2.87
Part 5: 대응표본 t검정 (Paired t-test)¶
- 질문: 6주 훈련 프로그램이 민첩성 향상에 효과가 있는가?
- H₀: μ_D = 0 (훈련 전후 차이가 없다)
- H₁: μ_D ≠ 0 (훈련 전후 차이가 있다)
5-1. 수작업 계산¶
In [36]:
# ── 대응표본 t검정: 수작업 계산 ──
print("=== 대응표본 t검정 (수작업) ===")
print()
# 차이값 계산 (후 - 전)
diff = after - before
n_d = len(diff)
d_bar = diff.mean() # 차이값의 평균
s_d = diff.std(ddof=1) # 차이값의 표준편차
se_d = s_d / np.sqrt(n_d) # 차이값의 표준오차
print("각 선수의 차이값 (후 - 전):")
for i in range(n_d):
print(f" 선수{i+1}: {after[i]:.1f} - {before[i]:.1f} = {diff[i]:.1f}")
print()
print(f"차이값 개수 (n): {n_d}")
print(f"차이값 평균 (D̄): {d_bar:.4f}")
print(f"차이값 표준편차 (s_D): {s_d:.4f}")
print(f"차이값 표준오차 (SE_D): {se_d:.4f}")
print()
# t값 계산: t = D̄ / (s_D / √n)
t_manual = d_bar / se_d
df = n_d - 1
print(f"t = D̄ / (s_D / √n)")
print(f"t = {d_bar:.4f} / {se_d:.4f}")
print(f"t = {t_manual:.4f}")
print(f"자유도 (df) = n - 1 = {df}")
print()
# p값 계산
p_manual = 2 * stats.t.sf(abs(t_manual), df)
print(f"p값 (양측) = {p_manual:.4f}")
print()
if p_manual < 0.05:
print(f"→ p = {p_manual:.4f} < 0.05: 귀무가설 기각")
print("→ 훈련 프로그램이 민첩성 향상에 유의한 효과가 있다")
else:
print(f"→ p = {p_manual:.4f} ≥ 0.05: 귀무가설 채택")
print("→ 훈련 프로그램이 민첩성 향상에 유의한 효과가 없다")
=== 대응표본 t검정 (수작업) === 각 선수의 차이값 (후 - 전): 선수1: 10.5 - 11.2 = -0.7 선수2: 10.2 - 10.8 = -0.6 선수3: 10.8 - 11.5 = -0.7 선수4: 10.1 - 10.5 = -0.4 선수5: 10.3 - 11.0 = -0.7 선수6: 10.7 - 11.3 = -0.6 선수7: 10.4 - 10.9 = -0.5 선수8: 10.6 - 11.1 = -0.5 차이값 개수 (n): 8 차이값 평균 (D̄): -0.5875 차이값 표준편차 (s_D): 0.1126 차이값 표준오차 (SE_D): 0.0398 t = D̄ / (s_D / √n) t = -0.5875 / 0.0398 t = -14.7577 자유도 (df) = n - 1 = 7 p값 (양측) = 0.0000 → p = 0.0000 < 0.05: 귀무가설 기각 → 훈련 프로그램이 민첩성 향상에 유의한 효과가 있다
5-2. 라이브러리 사용¶
In [37]:
# ── 대응표본 t검정: scipy 라이브러리 ──
print("=== 대응표본 t검정 (scipy) ===")
print()
# stats.ttest_rel(데이터1, 데이터2)
# - 대응표본 t검정 함수 (동일 대상의 전/후 비교)
# - 데이터1: 사후 측정값, 데이터2: 사전 측정값 (순서 주의)
# - 내부적으로 (데이터1 - 데이터2)의 차이값에 대해 단일표본 t검정 수행
# - 반환값: (t통계량, p값)
t_stat, p_value = stats.ttest_rel(after, before)
print(f"t = {t_stat:.4f}")
print(f"p = {p_value:.4f}")
print()
print("→ 수작업 결과와 동일한지 확인!")
print()
# 효과 크기 (Cohen's d for paired) = 차이값 평균 / 차이값 표준편차
d_cohen = diff.mean() / diff.std(ddof=1)
print(f"Cohen's d = {d_cohen:.4f}")
print(f"→ 효과 크기: {'작음' if abs(d_cohen) < 0.5 else '중간' if abs(d_cohen) < 0.8 else '큼'}")
print()
print(f"APA 보고: t({n_d-1}) = {t_stat:.2f}, p = {p_value:.3f}, d = {abs(d_cohen):.2f}")
=== 대응표본 t검정 (scipy) === t = -14.7577 p = 0.0000 → 수작업 결과와 동일한지 확인! Cohen's d = -5.2176 → 효과 크기: 큼 APA 보고: t(7) = -14.76, p = 0.000, d = 5.22
Part 6: 결과 시각화¶
In [38]:
# ── 단일표본: 체지방률 분포 + 리그 평균 기준선 ──
fig, ax = plt.subplots(figsize=(8, 5))
# ax.hist(데이터, bins=구간수, color=색, edgecolor=테두리색, alpha=투명도)
# - 히스토그램 — 데이터의 분포(빈도)를 막대로 표시
# - bins: 막대 개수 (구간을 몇 개로 나눌지)
# - alpha: 투명도 (0=투명, 1=불투명)
ax.hist(body_fat, bins=8, color='#42A5F5', edgecolor='white', alpha=0.8, label='선수 체지방률')
# ax.axvline(x위치, color=색, linestyle=선스타일, linewidth=두께)
# - 세로 기준선을 그래프에 추가
# - linestyle: '--'=점선, '-'=실선
ax.axvline(12, color='red', linestyle='--', linewidth=2, label='리그 평균 (12%)')
ax.axvline(body_fat.mean(), color='blue', linestyle='-', linewidth=2, label=f'팀 평균 ({body_fat.mean():.1f}%)')
ax.set_title('단일표본 t검정: 체지방률 vs 리그 평균', fontsize=14, fontweight='bold')
ax.set_xlabel('체지방률 (%)')
ax.set_ylabel('선수 수')
ax.legend() # 범례 표시
plt.show()
In [39]:
# ── 독립표본: A팀 vs B팀 비교 ──
# plt.subplots(행, 열, figsize=(가로인치, 세로인치))
# - 여러 그래프를 한 화면에 배치하는 함수
# - 반환값: (fig=전체 그림, axes=각 칸의 축 배열)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# ax.boxplot(데이터리스트, labels=라벨, patch_artist=색칠여부)
# - 박스플롯 (상자 그림) — 분포를 중앙값, 사분위수, 이상치로 요약
# - patch_artist=True: 상자 안을 색으로 채울 수 있게 함
# - medianprops: 중앙값 선의 스타일 지정
bp = axes[0].boxplot([team_a, team_b], labels=['A팀', 'B팀'], patch_artist=True,
medianprops=dict(color='red', linewidth=2))
bp['boxes'][0].set_facecolor('#42A5F5')
bp['boxes'][0].set_alpha(0.6)
bp['boxes'][1].set_facecolor('#EF5350')
bp['boxes'][1].set_alpha(0.6)
axes[0].set_title('A팀 vs B팀 스프린트 속도', fontsize=13, fontweight='bold')
axes[0].set_ylabel('속도 (km/h)')
axes[0].grid(axis='y', alpha=0.3)
# stats.sem(데이터) → 표준오차 (Standard Error of Mean)
# - 표준편차를 √n으로 나눈 값 — 오차 막대(error bar)에 사용
means = [team_a.mean(), team_b.mean()]
sems = [stats.sem(team_a), stats.sem(team_b)]
# ax.bar(x, 높이, yerr=오차값, capsize=오차막대꺾임크기)
# - 막대 그래프 + 오차 막대
# - yerr: 각 막대 위아래로 표시할 오차 크기 (여기서는 SEM)
# - capsize: 오차 막대 끝의 가로선 길이 (픽셀)
bars = axes[1].bar(['A팀', 'B팀'], means, yerr=sems, capsize=10,
color=['#42A5F5', '#EF5350'], edgecolor='white', alpha=0.8)
for bar, m in zip(bars, means):
axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.15,
f'{m:.1f}', ha='center', fontweight='bold', fontsize=12)
axes[1].set_title('평균 스프린트 속도 (±SEM)', fontsize=13, fontweight='bold')
axes[1].set_ylabel('속도 (km/h)')
axes[1].set_ylim(28, 36)
# 유의성 표시 (bracket + asterisk)
axes[1].plot([0, 0, 1, 1], [35, 35.3, 35.3, 35], color='black', linewidth=1.5)
axes[1].text(0.5, 35.4, '***', ha='center', fontsize=14)
plt.tight_layout()
plt.show()
print("해석: A팀이 B팀보다 평균 스프린트 속도가 유의하게 빠름 (*** p < .001)")
/var/folders/_f/8h1v3dgs7396l2lgm0gb6d_h0000gn/T/ipykernel_54167/3203606218.py:11: MatplotlibDeprecationWarning: The 'labels' parameter of boxplot() has been renamed 'tick_labels' since Matplotlib 3.9; support for the old name will be dropped in 3.11. bp = axes[0].boxplot([team_a, team_b], labels=['A팀', 'B팀'], patch_artist=True,
해석: A팀이 B팀보다 평균 스프린트 속도가 유의하게 빠름 (*** p < .001)
In [40]:
# ── 대응표본: 훈련 전후 비교 ──
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 선수별 전후 연결선 그래프 (Paired Plot / Spaghetti Plot)
# - 같은 선수의 전/후 값을 선으로 연결 → 개별 변화 방향을 시각화
x_pos = [0, 1]
for i in range(len(before)):
# 'o-' → 점(o) + 실선(-) 스타일
axes[0].plot(x_pos, [before[i], after[i]], 'o-', color='#666666', alpha=0.5)
# 's-' → 사각점(s) + 실선(-), zorder=5 → 다른 선 위에 그림
axes[0].plot(x_pos, [before.mean(), after.mean()], 's-', color='red',
linewidth=3, markersize=10, label=f'평균', zorder=5)
axes[0].set_xticks(x_pos)
axes[0].set_xticklabels(['훈련 전', '훈련 후'])
axes[0].set_title('선수별 민첩성 변화', fontsize=13, fontweight='bold')
axes[0].set_ylabel('민첩성 테스트 기록 (초)')
axes[0].legend()
axes[0].grid(axis='y', alpha=0.3)
# 차이값 히스토그램 — 차이(후-전)의 분포 확인
axes[1].hist(diff, bins=6, color='#66BB6A', edgecolor='white', alpha=0.8)
axes[1].axvline(0, color='red', linestyle='--', linewidth=2, label='차이 없음 (0)')
axes[1].axvline(diff.mean(), color='blue', linestyle='-', linewidth=2,
label=f'평균 차이 ({diff.mean():.2f}초)')
axes[1].set_title('전후 차이값 분포', fontsize=13, fontweight='bold')
axes[1].set_xlabel('차이 (후 - 전, 초)')
axes[1].set_ylabel('빈도')
axes[1].legend()
# plt.tight_layout() → 그래프 간 여백을 자동 조정하여 겹침 방지
plt.tight_layout()
plt.show()
print("해석: 모든 선수의 기록이 감소(개선)함. 파란 선(평균 차이)이 0보다 왼쪽에 위치.")
해석: 모든 선수의 기록이 감소(개선)함. 파란 선(평균 차이)이 0보다 왼쪽에 위치.
Part 7: 전체 요약¶
t검정 종류 선택 가이드¶
┌─────────────────────────────────────────────────────────────┐
│ 비교 상황이... → 이 검정을 쓰자! │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1개 집단 vs 기준값 │
│ └─ 단일표본 t검정 stats.ttest_1samp(data, popmean) │
│ │
│ 2개 서로 다른 집단 │
│ ├─ 등분산 → Student stats.ttest_ind(a, b, equal_var=T) │
│ └─ 이분산 → Welch stats.ttest_ind(a, b, equal_var=F) │
│ │
│ 동일 대상 전/후 │
│ └─ 대응표본 t검정 stats.ttest_rel(after, before) │
│ │
├─────────────────────────────────────────────────────────────┤
│ 사전 검정 │
│ ├─ 정규성 stats.shapiro(data) │
│ └─ 등분산 stats.levene(a, b) │
└─────────────────────────────────────────────────────────────┘
판단 기준 요약¶
| 검정 | p ≥ 0.05 | p < 0.05 |
|---|---|---|
| 정규성 | 정규분포 ✓ → t검정 가능 | 비정규 ✗ → 비모수 검정 |
| 등분산 | 등분산 ✓ → Student's t | 이분산 ✗ → Welch's t |
| t검정 | 차이 없음 (H₀ 채택) | 유의한 차이 있음 (H₀ 기각) |
Part 8: 논문 결과 보고¶
8-1. APA 스타일 본문 보고 형식¶
t검정 결과를 논문 본문에 기술할 때의 형식:
t(자유도) = t값, p = p값, d = Cohen's d
작성 규칙¶
- t는 이탤릭 처리
- 자유도는 괄호 안에 정수로
- t값은 소수점 2자리
- p값은 소수점 3자리 (p < .001이면 "p < .001"로 표기)
- p값 앞에 0을 붙이지 않음 (0.037이 아니라 .037)
- Cohen's d는 소수점 2자리
예시 (본 실습 결과)¶
단일표본:
선수들의 체지방률(M = 10.78, SD = 0.89)은 리그 평균(12%)과 유의한 차이가 있었다, t(9) = -4.33, p = .002, d = 1.37.
독립표본:
A팀(M = 33.63, SD = 0.64)은 B팀(M = 31.89, SD = 0.57)보다 최대 스프린트 속도가 유의하게 빨랐다, t(20) = 6.70, p < .001, d = 2.87.
대응표본:
6주 훈련 프로그램 후 민첩성 테스트 기록이 유의하게 감소하였다(M_pre = 11.04, SD_pre = 0.30; M_post = 10.45, SD_post = 0.23), t(7) = -14.76, p < .001, d = 5.22.
8-2. 논문용 표 (Table) 구성¶
독립표본 t검정 표 형식¶
| 변인 | 집단 | n | M | SD | t | df | p | d |
|---|---|---|---|---|---|---|---|---|
| 스프린트 속도(km/h) | A팀 | 12 | 33.63 | 0.64 | 6.70 | 20 | <.001 | 2.87 |
| B팀 | 10 | 31.89 | 0.57 |
대응표본 t검정 표 형식¶
| 변인 | 시점 | n | M | SD | t | df | p | d |
|---|---|---|---|---|---|---|---|---|
| 민첩성(초) | 훈련 전 | 8 | 11.04 | 0.30 | -14.76 | 7 | <.001 | 5.22 |
| 훈련 후 | 8 | 10.45 | 0.23 |
참고: M = 평균 (Mean), SD = 표준편차 (Standard Deviation), d = Cohen's d
8-3. pandas로 결과 표 만들기¶
In [41]:
# ── 단일표본 t검정 결과표 ──
t1, p1 = stats.ttest_1samp(body_fat, popmean=12)
d1 = (body_fat.mean() - 12) / body_fat.std(ddof=1)
# p값을 논문 형식으로 변환하는 함수
def format_p(p):
"""p값을 APA 형식으로 변환 (0 생략, <.001 표기)"""
if p < 0.001:
return '<.001'
else:
return f'{p:.3f}'.lstrip('0') # 앞의 0 제거 → ".037"
df_result1 = pd.DataFrame({
'검정 종류': ['단일표본'],
'변인': ['체지방률(%)'],
'집단/조건': ['선수 10명'],
'n': [len(body_fat)],
'M': [f'{body_fat.mean():.2f}'],
'SD': [f'{body_fat.std(ddof=1):.2f}'],
'기준값': [12],
't': [f'{t1:.2f}'],
'df': [len(body_fat) - 1],
'p': [format_p(p1)],
"Cohen's d": [f'{abs(d1):.2f}']
})
print("=== Table 1. 단일표본 t검정 결과 ===")
print()
df_result1
=== Table 1. 단일표본 t검정 결과 ===
Out[41]:
| 검정 종류 | 변인 | 집단/조건 | n | M | SD | 기준값 | t | df | p | Cohen's d | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 단일표본 | 체지방률(%) | 선수 10명 | 10 | 10.78 | 0.89 | 12 | -4.33 | 9 | .002 | 1.37 |
In [42]:
# ── 독립표본 t검정 결과표 ──
t2, p2 = stats.ttest_ind(team_a, team_b, equal_var=True)
sp_d = np.sqrt(((len(team_a)-1)*team_a.std(ddof=1)**2 + (len(team_b)-1)*team_b.std(ddof=1)**2) / (len(team_a)+len(team_b)-2))
d2 = (team_a.mean() - team_b.mean()) / sp_d
# pd.DataFrame(리스트) → 여러 행을 한번에 생성
# - 각 행을 딕셔너리로 만들어 리스트에 넣으면 여러 줄 표 생성
df_result2 = pd.DataFrame([
{
'변인': '스프린트 속도(km/h)',
'집단': 'A팀',
'n': len(team_a),
'M': f'{team_a.mean():.2f}',
'SD': f'{team_a.std(ddof=1):.2f}',
't': f'{t2:.2f}',
'df': len(team_a) + len(team_b) - 2,
'p': format_p(p2),
"Cohen's d": f'{abs(d2):.2f}'
},
{
'변인': '',
'집단': 'B팀',
'n': len(team_b),
'M': f'{team_b.mean():.2f}',
'SD': f'{team_b.std(ddof=1):.2f}',
't': '', 'df': '', 'p': '', "Cohen's d": ''
}
])
print("=== Table 2. 독립표본 t검정 결과 ===")
print()
df_result2
=== Table 2. 독립표본 t검정 결과 ===
Out[42]:
| 변인 | 집단 | n | M | SD | t | df | p | Cohen's d | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 스프린트 속도(km/h) | A팀 | 12 | 33.63 | 0.64 | 6.70 | 20 | <.001 | 2.87 |
| 1 | B팀 | 10 | 31.89 | 0.57 |
In [43]:
# ── 대응표본 t검정 결과표 ──
t3, p3 = stats.ttest_rel(after, before)
diff = after - before
d3 = diff.mean() / diff.std(ddof=1)
df_result3 = pd.DataFrame([
{
'변인': '민첩성(초)',
'시점': '훈련 전',
'n': len(before),
'M': f'{before.mean():.2f}',
'SD': f'{before.std(ddof=1):.2f}',
't': f'{t3:.2f}',
'df': len(before) - 1,
'p': format_p(p3),
"Cohen's d": f'{abs(d3):.2f}'
},
{
'변인': '',
'시점': '훈련 후',
'n': len(after),
'M': f'{after.mean():.2f}',
'SD': f'{after.std(ddof=1):.2f}',
't': '', 'df': '', 'p': '', "Cohen's d": ''
}
])
print("=== Table 3. 대응표본 t검정 결과 ===")
print()
df_result3
=== Table 3. 대응표본 t검정 결과 ===
Out[43]:
| 변인 | 시점 | n | M | SD | t | df | p | Cohen's d | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 민첩성(초) | 훈련 전 | 8 | 11.04 | 0.31 | -14.76 | 7 | <.001 | 5.22 |
| 1 | 훈련 후 | 8 | 10.45 | 0.24 |
In [44]:
# ── 결과표를 엑셀 파일로 내보내기 ──
# pd.ExcelWriter(파일경로, engine=엔진)
# - 여러 시트를 하나의 엑셀 파일에 저장할 때 사용
# - with문과 함께 사용하면 자동으로 파일을 닫아줌
# df.to_excel(writer, sheet_name=시트이름, index=인덱스포함여부)
# - index=False: 행 번호(0,1,2...)를 엑셀에 포함하지 않음
with pd.ExcelWriter('t검정_결과표.xlsx', engine='openpyxl') as writer:
df_result1.to_excel(writer, sheet_name='단일표본', index=False)
df_result2.to_excel(writer, sheet_name='독립표본', index=False)
df_result3.to_excel(writer, sheet_name='대응표본', index=False)
print("t검정_결과표.xlsx 저장 완료!")
print("→ 논문 작성 시 이 표를 복사하여 사용하세요")
t검정_결과표.xlsx 저장 완료! → 논문 작성 시 이 표를 복사하여 사용하세요