Sparta/Theory

[251002] 통계검정 실습 01 - t-test

junecho 2025. 10. 2. 23:38

✅ t-test                                                                      

두 그룹 간 평균의 차이가 통계적으로 유의미한지를 검정하는 방법

실제로 차이가 있는지, 우연인지 판단

 

🔸 이론                                                                                                                        

🔎 수행 단계                                                                                                                                  

1️⃣ 표본 크기에 따른 정규성 확인

  • n < 30 : Shapiro-Wilk test 필수 ⇒ p > 0.05 일시 t-test
  • 30 ≤ n < 100 : 왜도/첨도 확인 ⇒ 왜도<1, 첨도<2 일시 t-test
  • n ≥ 100 : 중심극한정리 적용 ⇒ 왜도 < 2 일시 t-test

2️⃣ 정규성 검정 stats.shapiro()

  • p > 0.05 : 정규분포 ⇒ t-test
  • p ≤ 0.05 : 비정규분포 ⇒ 비모수검정

3️⃣ 등분산성 검정 (독립표본만) stats.levene

  • Levene's test p > 0.05 ⇒ equal_var=True (Student’s t-test)
  • Levene's test p ≤ 0.05 ⇒ equal_var=False (Welch's t-test)

4️⃣ 결과해석

  • p-value < 0.05 ⇒ 유의한 차이 있음
  • Cohen's d로 효과 크기 확인 (0.2: 작음, 0.5: 중간, 0.8: 큼)

 

🔎 종류                                                                                                                                                

종류 사용처 함수 예시
독립표본 t-test 두 독립 그룹 비교 ttest_ind() 와인 종류별 알코올 도수
대응표본 t-test 같은 대상의 전후 비교 ttest_rel() 치료 전후 혈당 수치
단일표본 t-test 한 그룹과 기준값 비교 ttest_1samp() 평균 알코올 도수 13도인지?



🔎 정규성 확인을 위한 Q-Q Plot 이해와 해석                                                                              

Quantile-Quantile Plot : 데이터가 정규분포를 따르는지 시각적으로 확인하는 도구

  • X축 : 이론적 정규분포 분위수 (정규분포 기준)
  • Y축 : 실제 데이터 분위수
  • 핵심 : 데이터가 정규분포를 따르면 점들이 직선에 가깝게 배열
  • 왜도 (Skewness)
    • : 데이터 분포의 비대칭 정도를 나타내는 통계량
    • 0에 가까울 수록 분포가 대칭적(정규분포)
    • ∩ 모양 → Left-skew 왼쪽 긴 꼬리
    • ∪ 모양 → Right-skew 오른쪽 긴 꼬리
  • 첨도 (Kurtosis)
    • : 데이터 분포의 꼬리 두께와 중심부의 뾰족함 정도를 나타내는 통계량
    • 정규 분포의 첨도 : 일반적으로 3.
    • 양 끝 ↓ → Light Tailed (극단값 ↓)
    • 양 끝 ↑ → Heavy Tailed (극단값 ↑)

 

패턴명 Q-Q Plot 모양 의미 대응방법
Normal 직선 정규분포 t-test
Light Tail 가벼운 꼬리 S자 극단값 ↓ 큰 문제 ❌
Heavy Tail 무거운 꼬리 역S자 극단값 ↑ 비모수 검정 고려
Left-skew ∩ 모양 음의 왜도 제곱 변환 고려
Right-skew ∪ 모양 양의 왜도 로그 변환 고려
Bimodal 계단 모양 이산형 데이터 비모수 검정 권장

 

https://jtr13.github.io/EDAVold/qqplot.html

🔎 p-value 🆚 Cohen’s d                                                                                                               

구분  p-value (통계적 유의성) Cohen’s d (실질적 유의성)
핵심 질문 “이 결과가 우연인가?” “이 차이가 실무적으로 중요한가?”
특징 표본 크기에 민감 표본 크기와 무관한 표준화 지표
장단점 차이의 크기를 알 수 ❌ 실제 영향력의 크기를 보여줌

 

🔎 비모수 검정 대안                                                                                                                        

종류 사용처 함수
독립표본 t-test Mann-Whitney U test mannwhitneyu()
대응표본 t-test Wilcoxon signed-rank test wilcoxon()
단일표본 t-test One-sample Wilcoxon test
wilcoxon(data-기준값)

 

💥 실무 팁                                                                                                                                          

1️⃣ 시각화 우선 : 데이터 분포를 먼저 확인 (박스플롯, 히스토그램, Q-Q Plot)

2️⃣ Q-Q Plot + Shapiro-Wilk 조합

  • : 정량적 검정과 시각적 확인을 함께함. stats.shapiro()
  • 불확실하면 Welch’s t-test : stats.ttest_ind(equal_var=False) 가 더 안전

3️⃣ p-value + 효과 크기 : 통계적 유의성과 실제적 중요성을 함께 평가

4️⃣ 애매하면 비모수 : 정규성이 의심스러우면 비모수 검정이 안전

 

 

🔸 코드                                                                                              

# 필수 라이브러리 Import
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from scipy.stats import shapiro, levene, ttest_ind, ttest_rel, ttest_1samp
from scipy.stats import mannwhitneyu, wilcoxon
from sklearn.datasets import load_wine, load_iris, load_diabetes
import warnings
import platform

warnings.filterwarnings('ignore')

# 운영체제별 한글 폰트 설정
if platform.system() == 'Windows':
    plt.rcParams['font.family'] = 'Malgun Gothic'
elif platform.system() == 'Darwin':  # macOS
    plt.rcParams['font.family'] = 'AppleGothic'
else:  # Linux
    plt.rcParams['font.family'] = 'NanumGothic'

# 마이너스 기호 깨짐 방지
plt.rcParams['axes.unicode_minus'] = False

# 시각화 기본 설정
plt.rcParams['figure.figsize'] = (12, 4)

# 전역 시드 설정 (재현성을 위해)
np.random.seed(42)

print("="*50)
print("라이브러리 로드 완료!")
print("한글 폰트 설정 완료!")
print("="*50)

 

🔎 정규성 판단 도우미 함수                                                                                                          

# 정규성 판단 도우미 함수
def check_normality_simple(data, name="데이터"):
    # NaN 체크
    if pd.isna(data).any():
        print(f"⚠️ 경고: {name}에 NaN 값이 {pd.isna(data).sum()}개 포함됨")
        data = data.dropna()
        print(f"   → NaN 제거 후 n={len(data)}")
    
    n = len(data)
    
    print(f"\\n[{name} 정규성 검정] n={n}")
    print("-"*40)
    
    # 왜도와 첨도
    skew = stats.skew(data)
    kurt = stats.kurtosis(data, fisher=True)
    print(f"왜도(Skewness): {skew:.3f}")
    print(f"첨도(Kurtosis): {kurt:.3f}")
    
    # 표본 크기에 따른 판단
    if n < 30:
        stat, p = shapiro(data)
        print(f"Shapiro-Wilk p-value: {p:.4f}")
        is_normal = p > 0.05
        reason = f"Shapiro p={'>' if is_normal else '≤'}0.05"
    elif n < 100:
        if abs(skew) < 1 and abs(kurt) < 2:
            is_normal = True
            reason = "|왜도|<1, |첨도|<2"
        else:
            stat, p = shapiro(data)
            print(f"추가 Shapiro-Wilk p-value: {p:.4f}")
            is_normal = p > 0.05
            reason = f"Shapiro p={'>' if is_normal else '≤'}0.05"
    else:
        is_normal = abs(skew) < 2
        reason = f"|왜도|{'<' if is_normal else '≥'}2 (중심극한정리)"
    
    print(f"결과: {'✅ 정규분포 가정 충족' if is_normal else '❌ 정규분포 가정 위반'} ({reason})")
    return is_normal
  • Parameters
    • data : 정규성을 검정할 데이터 (Null 자동 제거)
    • name : 출력 시 표시될 데이터 이름. str, default=”데이터”
  • bool
    • True : 정규분포 가정 가능 (모수 검정)
    • False : 정규분포 가정 위반 (비모수 검정)
  • 검정 기준
    • n < 30 : Shapiro-Wilk 검정 (p > 0.05)
    • 30 ≤ n < 100 : 왜도/첨도 우선, 필요시 Shapiro-Wilk
    • n ≥ 100 : 왜도 기준 ( ㅣ왜도ㅣ< 2, 중심극한정리)

 

🔎 실습 01 : Wine 데이터로 독립표본 t-test                                                                                

❓ 클래스 0과 클래스1 와인의 알코올 도수에 차이 있는가 ❓

print("\\n" + "="*60)
print("실습 1: Wine 데이터 - 독립표본 t-test")
print("="*60)

# Wine 데이터 로드
wine = load_wine()
wine_df = pd.DataFrame(wine.data, columns=wine.feature_names)
wine_df['class'] = wine.target

print(f"\\n데이터 크기: {wine_df.shape}")
print(f"클래스: {wine.target_names.tolist()}")
print("\\n특징 변수 (처음 5개):")
for i, feature in enumerate(wine.feature_names[:5]):
    print(f"  {i+1}. {feature}")

# 클래스 0과 1의 알코올 도수 비교
class0_alcohol = wine_df[wine_df['class'] == 0]['alcohol']
class1_alcohol = wine_df[wine_df['class'] == 1]['alcohol']

# 기초 통계량 테이블
stats_table = pd.DataFrame({
    '구분': ['Class 0', 'Class 1'],
    '샘플수': [len(class0_alcohol), len(class1_alcohol)],
    '평균': [class0_alcohol.mean(), class1_alcohol.mean()],
    '표준편차': [class0_alcohol.std(), class1_alcohol.std()],
    '최소값': [class0_alcohol.min(), class1_alcohol.min()],
    '최대값': [class0_alcohol.max(), class1_alcohol.max()]
})

print("\\n[알코올 도수 기초 통계량]")
display(stats_table.round(2))

 

# 시각화
fig, axes = plt.subplots(1, 3, figsize=(14, 5))

# 박스플롯
bp = axes[0].boxplot([class0_alcohol, class1_alcohol], 
                      labels=['Class 0', 'Class 1'],
                      patch_artist=True)
bp['boxes'][0].set_facecolor("#aed6df")
bp['boxes'][1].set_facecolor("#fea188")
axes[0].set_ylabel('알코올 도수')
axes[0].set_title('알코올 도수 분포')
axes[0].grid(True, alpha=0.3)

# 히스토그램
axes[1].hist(class0_alcohol, bins=10, alpha=0.6, label='Class 0', 
             color="#0063b2", density=True, edgecolor='black')
axes[1].hist(class1_alcohol, bins=10, alpha=0.6, label='Class 1', 
             color="#e94b3c", density=True, edgecolor='black')
axes[1].set_xlabel('알코올 도수')
axes[1].set_ylabel('밀도')
axes[1].set_title('알코올 도수 분포 비교')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# Q-Q plot (Class 0)
stats.probplot(class0_alcohol, dist="norm", plot=axes[2])
axes[2].set_title('Q-Q Plot (Class 0)')
axes[2].grid(True, alpha=0.3)

# stats.probplot() 함수는 직접적으로 색상 변경 옵션 제공X. SO, 반환된 결과를 사용해 수정해야함
(osm, osr), (slope, intercept, r) = stats.probplot(class0_alcohol, dist="norm", fit=True)
line = axes[2].get_lines()[0]  # 기존 라인 가져오기
axes[2].clear()  # 기존 플롯 지우기

# 새로운 색상으로 마커와 라인 그리기
axes[2].scatter(osm, osr, color='#0063b2', marker='o', alpha=0.7)  # 마커 색상 변경
axes[2].plot(osm, slope * osm + intercept, color='#e94b3c', linewidth=2)  # 선 색상 변경

axes[2].set_title('Q-Q Plot (Class 0)')
axes[2].grid(True, alpha=0.3)
plt.show()

 

1️⃣ 표본 크기에 따른 정규성 확인

2️⃣ 정규성 검정

print("\\n" + "="*50)
print("가설검정 프로세스")
print("="*50)

# Step 1: 정규성 검정
is_normal_0 = check_normality_simple(class0_alcohol, "Class 0 알코올")
is_normal_1 = check_normality_simple(class1_alcohol, "Class 1 알코올")

 

3️⃣ 등분산성 검정 (독립표본만) stats.levene

# Step 2: 등분산성 검정
print("\\n[등분산성 검정]")
print("-"*40)
stat, p_levene = levene(class0_alcohol, class1_alcohol)
print(f"Levene's test p-value: {p_levene:.4f}")
equal_var = p_levene > 0.05
print(f"결과: {'✅ 등분산 가정 충족' if equal_var else '❌ 이분산 → Welch t-test 사용'}")

 

4️⃣ 가설검정 ttest_ind(equal_var=equal_var)

5️⃣ 결론 도출

# =============================================================================
# Step 3: 가설검정
# =============================================================================
print("\\n[가설검정]")
print("-"*40)

# 가설 설정
print("H₀: μ₀ = μ₁ (두 클래스의 알코올 도수가 같다)")
print("H₁: μ₀ ≠ μ₁ (두 클래스의 알코올 도수가 다르다)")
print("유의수준: α = 0.05")

# -----------------------------------------------------------------------------
# 3-1. 검정 방법 선택 및 실행
# -----------------------------------------------------------------------------
# 정규성 검정 결과에 따라 모수/비모수 검정 선택
if is_normal_0 and is_normal_1:
    # 모수 검정: 독립표본 t-검정 (두 그룹 모두 정규분포)
    t_stat, p_value = ttest_ind(class0_alcohol, class1_alcohol, equal_var=equal_var)
    test_name = "Student's t-test" if equal_var else "Welch's t-test"
    print(f"\\n{test_name} 결과:")
    print(f"t = {t_stat:.4f}, p = {p_value:.4f}")
    
    # Cohen's d 효과 크기 계산 (표준화된 평균 차이)
    # d = (평균1 - 평균2) / 합동표준편차
    pooled_std = np.sqrt((class0_alcohol.var() + class1_alcohol.var()) / 2)
    cohens_d = (class0_alcohol.mean() - class1_alcohol.mean()) / pooled_std
    abs_d = abs(cohens_d)
    
    # Cohen's d 해석 기준
    if abs_d < 0.2:
        effect = "매우 작은 효과"
    elif abs_d < 0.5:
        effect = "작은 효과"
    elif abs_d < 0.8:
        effect = "중간 효과"
    else:
        effect = "큰 효과"
    
    print(f"Cohen's d = {cohens_d:.3f} ({effect})")

else:
    # 비모수 검정: Mann-Whitney U 검정 (정규성 가정 위반)
    # 중앙값 차이를 검정 (순위 기반)
    u_stat, p_value = mannwhitneyu(class0_alcohol, class1_alcohol, alternative='two-sided')
    print(f"\\nMann-Whitney U test 결과:")
    print(f"U = {u_stat:.4f}, p = {p_value:.4f}")

# -----------------------------------------------------------------------------
# 3-2. 통계적 결론 도출
# -----------------------------------------------------------------------------
print(f"\\n[결론]")

# p-value를 유의수준(α=0.05)과 비교하여 가설 채택/기각 결정
if p_value < 0.05:
    print(f"✅ p-value({p_value:.4f}) < 0.05 → 귀무가설 기각")
    print(f"   두 클래스의 알코올 도수에 유의한 차이가 있음")
    print(f"   (통계적으로 의미있는 차이 존재)")
else:
    print(f"❌ p-value({p_value:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"   두 클래스의 알코올 도수에 유의한 차이가 없음")
    print(f"   (관측된 차이는 우연에 의한 것일 수 있음)")

 

 

🔎 실습 02 : Diabetes 데이터로 대응표본 t-test

❓ 치료 전후 혈당 수치 변화 있는가 ❓

print("\\n" + "="*60)
print("실습 2: Diabetes 데이터 - 대응표본 t-test")
print("="*60)

# Diabetes 데이터 로드 및 가상의 전후 데이터 생성
diabetes = load_diabetes()

# 30명 환자의 치료 전 혈당 (표준화된 값)
n_patients = 30
before_glucose = diabetes.target[:n_patients]

# 치료 후 혈당 (평균적으로 감소하는 가상의 데이터 생성)
treatment_effect = np.random.normal(-15, 5, n_patients)  # 평균 15 감소
after_glucose = before_glucose + treatment_effect

# DataFrame 생성
treatment_df = pd.DataFrame({
    '환자ID': [f'P{i:03d}' for i in range(1, n_patients+1)],
    '치료전': before_glucose,
    '치료후': after_glucose,
    '변화량': after_glucose - before_glucose
})

print("\\n[데이터 샘플 (처음 5명)]")
display(treatment_df.head())

print("\\n[기초 통계량]")
stats_summary = pd.DataFrame({
    '구분': ['치료 전', '치료 후', '변화량'],
    '평균': [treatment_df['치료전'].mean(), 
            treatment_df['치료후'].mean(),
            treatment_df['변화량'].mean()],
    '표준편차': [treatment_df['치료전'].std(),
                treatment_df['치료후'].std(),
                treatment_df['변화량'].std()]
})
display(stats_summary.round(2))

 

# 시각화
fig, axes = plt.subplots(1, 3, figsize=(14, 5))

# Before-After 연결선 그래프
for i in range(len(treatment_df)):
    axes[0].plot([0, 1], [treatment_df.iloc[i]['치료전'], treatment_df.iloc[i]['치료후']], 
                'gray', alpha=0.4, linewidth=0.8)
axes[0].plot([0, 1], [treatment_df['치료전'].mean(), treatment_df['치료후'].mean()], 
            'red', linewidth=3, marker='o', markersize=8, label='평균')
axes[0].set_xticks([0, 1])
axes[0].set_xticklabels(['치료 전', '치료 후'])
axes[0].set_ylabel('혈당 수치')
axes[0].set_title('개인별 변화')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 박스플롯
bp = axes[1].boxplot([treatment_df['치료전'], treatment_df['치료후']], 
                     labels=['치료 전', '치료 후'],
                     patch_artist=True)
bp['boxes'][0].set_facecolor("#ead98b")
bp['boxes'][1].set_facecolor("#7dd0b6")
axes[1].set_ylabel('혈당 수치')
axes[1].set_title('혈당 분포')
axes[1].grid(True, alpha=0.3)

# 변화량 히스토그램
axes[2].hist(treatment_df['변화량'], bins=10, edgecolor='black', alpha=0.7, color="#93c763")
axes[2].axvline(0, color='red', linestyle='--', linewidth=2, label='변화 없음')
axes[2].axvline(treatment_df['변화량'].mean(), color='blue', linestyle='--', 
               linewidth=2, label=f'평균: {treatment_df["변화량"].mean():.1f}')
axes[2].set_xlabel('혈당 변화량')
axes[2].set_ylabel('빈도')
axes[2].set_title('변화량 분포')
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

 

1️⃣ 표본 크기에 따른 정규성 확인

2️⃣ 정규성 검정

print("\\n" + "="*50)
print("가설검정 프로세스")
print("="*50)

# Step 1: 변화량의 정규성 검정
is_normal_diff = check_normality_simple(treatment_df['변화량'], "변화량")

 

3️⃣ 가설검정 ttest_rel()

4️⃣ 결론 도출

# =============================================================================
# Step 2: 가설검정
# =============================================================================
print("\\n[가설검정]")
print("-"*40)

# 가설 설정 (대응표본 검정)
print("H₀: μ_before = μ_after (치료 효과 없음)")
print("H₁: μ_before ≠ μ_after (치료 효과 있음)")
print("유의수준: α = 0.05")

# -----------------------------------------------------------------------------
# 2-1. 검정 방법 선택 및 실행
# -----------------------------------------------------------------------------
# 차이값의 정규성에 따라 모수/비모수 검정 선택
if is_normal_diff:
    # 모수 검정: 대응표본 t-검정 (차이값이 정규분포)
    # 동일 대상의 전후 비교이므로 paired t-test 사용
    t_stat, p_value = ttest_rel(treatment_df['치료전'], treatment_df['치료후'])
    print(f"\\nPaired t-test 결과:")
    print(f"t = {t_stat:.4f}, p = {p_value:.4f}")
    
    # -------------------------------------------------------------------------
    # 2-2. 효과 크기 계산 (Cohen's d for paired samples)
    # -------------------------------------------------------------------------
    # 대응표본의 Cohen's d = 평균 변화량 / 변화량의 표준편차
    cohens_d = treatment_df['변화량'].mean() / treatment_df['변화량'].std()
    abs_d = abs(cohens_d)
    
    # Cohen's d 해석 기준 (대응표본)
    if abs_d < 0.2:
        effect = "매우 작은 효과"
    elif abs_d < 0.5:
        effect = "작은 효과"  
    elif abs_d < 0.8:
        effect = "중간 효과"
    else:
        effect = "큰 효과"
    
    print(f"Cohen's d = {cohens_d:.3f} ({effect})")
    
    # -------------------------------------------------------------------------
    # 2-3. 신뢰구간 계산
    # -------------------------------------------------------------------------
    # 평균 변화량의 95% 신뢰구간 추정
    # CI = 평균 ± t(α/2, df) × SE
    confidence = 0.95  # 신뢰수준
    n = len(treatment_df)  # 표본 크기
    mean_diff = treatment_df['변화량'].mean()  # 평균 변화량
    se_diff = stats.sem(treatment_df['변화량'])  # 표준오차
    
    # t-분포 기반 신뢰구간 (자유도 = n-1)
    ci = stats.t.interval(confidence, n-1, loc=mean_diff, scale=se_diff)
    print(f"평균 변화의 95% CI: [{ci[0]:.2f}, {ci[1]:.2f}]")
    
else:
    # 비모수 검정: Wilcoxon 부호순위 검정 (정규성 가정 위반)
    # 중앙값 차이를 검정 (순위와 부호 기반)
    w_stat, p_value = wilcoxon(treatment_df['치료전'], treatment_df['치료후'])
    print(f"\\nWilcoxon signed-rank test 결과:")
    print(f"W = {w_stat:.4f}, p = {p_value:.4f}")

# -----------------------------------------------------------------------------
# 2-4. 통계적 결론 도출
# -----------------------------------------------------------------------------
print(f"\\n[결론]")

# p-value를 유의수준(α=0.05)과 비교하여 가설 채택/기각 결정
if p_value < 0.05:
    print(f"✅ p-value({p_value:.4f}) < 0.05 → 귀무가설 기각")
    print(f"   치료가 효과가 있음 (평균 {abs(treatment_df['변화량'].mean()):.1f} 감소)")
    print(f"   (통계적으로 유의한 개선 효과)")
else:
    print(f"❌ p-value({p_value:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"   치료 효과가 유의하지 않음")
    print(f"   (관측된 변화는 우연에 의한 것일 수 있음)")

 

🔎 실습 03 : Iris 데이터로 단일표본 t-test                                                                                  

❓ Setosa 종의 평균 꽃받침 길이가 5.0cm인가 검정 ❓

print("\\n" + "="*60)
print("실습 3: Iris 데이터 - 단일표본 t-test")
print("="*60)

# Iris 데이터 로드
iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
iris_df['species'] = iris.target

# Setosa 종의 꽃받침 길이
setosa_sepal = iris_df[iris_df['species'] == 0]['sepal length (cm)']
target_value = 5.0  # 검정할 기준값

print(f"[Setosa 꽃받침 길이 정보]")
print(f"샘플 수: {len(setosa_sepal)}")
print(f"평균: {setosa_sepal.mean():.3f}cm")
print(f"표준편차: {setosa_sepal.std():.3f}cm")
print(f"중앙값: {setosa_sepal.median():.3f}cm")
print(f"검정 기준값: {target_value}cm")

 

# 시각화
fig, axes = plt.subplots(1, 3, figsize=(14, 5))

# 히스토그램
axes[0].hist(setosa_sepal, bins=12, edgecolor='black', alpha=0.7, color="#aed6df")
axes[0].axvline(target_value, color='red', linestyle='--', linewidth=2, label=f'기준값: {target_value}cm')
axes[0].axvline(setosa_sepal.mean(), color='green', linestyle='--', linewidth=2, 
                label=f'평균: {setosa_sepal.mean():.2f}cm')
axes[0].set_xlabel('꽃받침 길이 (cm)')
axes[0].set_ylabel('빈도')
axes[0].set_title('Setosa 꽃받침 길이 분포')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 박스플롯
bp = axes[1].boxplot(setosa_sepal, patch_artist=True)
bp['boxes'][0].set_facecolor("#aed6df")
axes[1].axhline(target_value, color='red', linestyle='--', linewidth=2)
axes[1].set_ylabel('꽃받침 길이 (cm)')
axes[1].set_title('박스플롯')
axes[1].text(1.1, target_value+0.05, f'기준값: {target_value}', color='red')
axes[1].grid(True, alpha=0.3)

# Q-Q plot
stats.probplot(setosa_sepal, dist="norm", plot=axes[2])
axes[2].set_title('Q-Q Plot')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

 

1️⃣ 표본 크기에 따른 정규성 확인

2️⃣ 정규성 검정

print("\\n" + "="*50)
print("가설검정 프로세스")
print("="*50)

# Step 1: 정규성 검정
is_normal = check_normality_simple(setosa_sepal, "Setosa 꽃받침 길이")

 

3️⃣ 가설검정 ttest_1smap

4️⃣ 결론 도출

# =============================================================================
# Step 2: 가설검정
# =============================================================================
print("\\n[가설검정]")
print("-"*40)

# 가설 설정 (단일표본 검정)
# 표본 평균이 특정 값(target_value)과 같은지 검정
print(f"H₀: μ = {target_value}cm (평균이 {target_value}cm)")
print(f"H₁: μ ≠ {target_value}cm (평균이 {target_value}cm가 아님)")
print("유의수준: α = 0.05")

# -----------------------------------------------------------------------------
# 2-1. 검정 방법 선택 및 실행
# -----------------------------------------------------------------------------
# 정규성 검정 결과에 따라 모수/비모수 검정 선택
if is_normal:
    # 모수 검정: 단일표본 t-검정 (데이터가 정규분포)
    # 표본 평균과 모집단 평균(target_value)을 비교
    t_stat, p_value = ttest_1samp(setosa_sepal, target_value)
    print(f"\\nOne-sample t-test 결과:")
    print(f"t = {t_stat:.4f}, p = {p_value:.4f}")
    
    # -------------------------------------------------------------------------
    # 2-2. 신뢰구간 계산 및 해석
    # -------------------------------------------------------------------------
    # 모평균의 95% 신뢰구간 추정
    # CI = 표본평균 ± t(α/2, df) × SE
    confidence = 0.95  # 신뢰수준
    n = len(setosa_sepal)  # 표본 크기
    mean = setosa_sepal.mean()  # 표본 평균
    se = stats.sem(setosa_sepal)  # 표준오차 (SE = s/√n)
    
    # t-분포 기반 신뢰구간 (자유도 = n-1)
    ci = stats.t.interval(confidence, n-1, loc=mean, scale=se)
    print(f"평균의 95% CI: [{ci[0]:.3f}, {ci[1]:.3f}]cm")
    
    # 신뢰구간과 목표값 비교
    # 목표값이 신뢰구간 내에 있으면 H₀를 기각할 수 없음
    if ci[0] <= target_value <= ci[1]:
        print(f"→ {target_value}cm가 신뢰구간 내에 있음")
    else:
        print(f"→ {target_value}cm가 신뢰구간 밖에 있음")
        
else:
    # 비모수 검정: Wilcoxon 부호순위 검정 (정규성 가정 위반)
    # 중앙값이 목표값과 같은지 검정 (순위와 부호 기반)
    
    # 각 관측값과 목표값의 차이 계산
    differences = setosa_sepal - target_value
    
    # Wilcoxon 부호순위 검정 실행
    # 차이의 절댓값에 순위를 매기고, 부호를 고려하여 검정
    w_stat, p_value = wilcoxon(differences)
    print(f"\\nWilcoxon signed-rank test 결과:")
    print(f"W = {w_stat:.4f}, p = {p_value:.4f}")

# -----------------------------------------------------------------------------
# 2-3. 통계적 결론 도출
# -----------------------------------------------------------------------------
print(f"\\n[결론]")

# p-value를 유의수준(α=0.05)과 비교하여 가설 채택/기각 결정
if p_value < 0.05:
    print(f"✅ p-value({p_value:.4f}) < 0.05 → 귀무가설 기각")
    
    # 차이의 방향 확인 (평균이 목표값보다 높은지 낮은지)
    diff = setosa_sepal.mean() - target_value
    if diff > 0:
        print(f"   평균({setosa_sepal.mean():.3f}cm)이 {target_value}cm보다 유의하게 높음")
    else:
        print(f"   평균({setosa_sepal.mean():.3f}cm)이 {target_value}cm보다 유의하게 낮음")
    print(f"   (통계적으로 의미있는 차이 존재)")
    
else:
    print(f"❌ p-value({p_value:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"   평균이 {target_value}cm와 유의한 차이가 없음")
    print(f"   (관측된 차이는 우연에 의한 것일 수 있음)")

 

🔎 실습 04 : Wine 데이터 다중 비교                                                                                             

# =============================================================================
# 추가 실습: Wine 데이터에서 여러 특징 비교
# =============================================================================
print("\\n" + "="*60)
print("추가 실습: Wine 데이터에서 여러 특징 비교")
print("="*60)

# -----------------------------------------------------------------------------
# 1. 비교할 특징 설정 및 반복 검정
# -----------------------------------------------------------------------------
# Class 0과 Class 1 간 여러 특징을 한 번에 비교
features_to_compare = ['malic_acid', 'ash', 'total_phenols', 'flavanoids']

# 결과를 저장할 리스트
results = []

# 각 특징에 대해 독립표본 t-검정 수행
for feature in features_to_compare:
    # -------------------------------------------------------------------------
    # 1-1. 데이터 추출
    # -------------------------------------------------------------------------
    class0_data = wine_df[wine_df['class'] == 0][feature]
    class1_data = wine_df[wine_df['class'] == 1][feature]
    
    # -------------------------------------------------------------------------
    # 1-2. 등분산 검정 (Levene's test)
    # -------------------------------------------------------------------------
    # 두 그룹의 분산이 같은지 검정하여 적절한 t-test 선택
    _, p_levene = levene(class0_data, class1_data)
    equal_var = p_levene > 0.05  # p > 0.05면 등분산 가정
    
    # -------------------------------------------------------------------------
    # 1-3. 독립표본 t-검정 수행
    # -------------------------------------------------------------------------
    # equal_var에 따라 Student's t-test 또는 Welch's t-test 실행
    t_stat, p_value = ttest_ind(class0_data, class1_data, equal_var=equal_var)
    
    # -------------------------------------------------------------------------
    # 1-4. 효과 크기 계산 (Cohen's d)
    # -------------------------------------------------------------------------
    # 표준화된 평균 차이 = (평균1 - 평균2) / 합동표준편차
    pooled_std = np.sqrt((class0_data.var() + class1_data.var()) / 2)
    cohens_d = (class0_data.mean() - class1_data.mean()) / pooled_std
    
    # Cohen's d 해석 기준
    abs_d = abs(cohens_d)
    if abs_d < 0.2:
        effect = "매우 작음"
    elif abs_d < 0.5:
        effect = "작음"
    elif abs_d < 0.8:
        effect = "중간"
    else:
        effect = "큼"
    
    # -------------------------------------------------------------------------
    # 1-5. 결과 저장
    # -------------------------------------------------------------------------
    results.append({
        '특징': feature,
        'Class0 평균': class0_data.mean(),
        'Class1 평균': class1_data.mean(),
        '차이': class0_data.mean() - class1_data.mean(),
        't값': t_stat,
        'p-value': p_value,
        "Cohen's d": cohens_d,
        '효과크기': effect,
        '유의성': '유의함' if p_value < 0.05 else '유의하지 않음'
    })

# -----------------------------------------------------------------------------
# 2. 결과 정리 및 출력
# -----------------------------------------------------------------------------
# 결과를 DataFrame으로 변환하여 가독성 향상
results_df = pd.DataFrame(results)
results_df = results_df.round(4)  # 소수점 4자리로 반올림

print("\\n[Class 0 vs Class 1 비교 결과]")
# 핵심 정보만 선택하여 표시
display(results_df[['특징', '차이', 'p-value', "Cohen's d", '효과크기', '유의성']])

# -----------------------------------------------------------------------------
# 3. 시각화: p-value 및 효과 크기 비교
# -----------------------------------------------------------------------------
fig, ax = plt.subplots(figsize=(10, 6))

# -------------------------------------------------------------------------
# 3-1. p-value 막대 그래프
# -------------------------------------------------------------------------
# 유의한 결과(p<0.05)는 빨간색, 그렇지 않으면 회색으로 표시
colors = ['red' if p < 0.05 else 'gray' for p in results_df['p-value']]
bars = ax.bar(range(len(results_df)), results_df['p-value'], color=colors)

# 유의수준 기준선 (α = 0.05)
ax.axhline(0.05, color='black', linestyle='--', label='p=0.05')

# x축 레이블 설정
ax.set_xticks(range(len(results_df)))
ax.set_xticklabels(results_df['특징'], rotation=45)
ax.set_ylabel('p-value')
ax.set_title('각 특징별 유의성 검정 결과')
ax.legend()

# -------------------------------------------------------------------------
# 3-2. 막대 위에 Cohen's d 값 표시
# -------------------------------------------------------------------------
# 각 막대 위에 효과 크기를 텍스트로 추가
for i, (bar, d) in enumerate(zip(bars, results_df["Cohen's d"])):
    ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.01,
            f'd={d:.2f}', ha='center', va='bottom')

plt.tight_layout()
plt.show()

 

 

🔸 self 실습                                                                                                                    

🔎 self 실습 01 : 레스토랑 매출 비교                                                                                            

❓ 주말(토,일)과 평일(월~금)의 일일 평균 매출에 차이가 있는가? ❓

⇒ 두 독립 그룹의 비교니까 ttest_ind() 사용하기?

# 가상의 레스토랑 매출 데이터 생성
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from scipy.stats import shapiro, levene, ttest_ind

np.random.seed(42)

# 4주간의 데이터 (28일)
days = ['월', '화', '수', '목', '금', '토', '일'] * 4
dates = pd.date_range('2024-01-01', periods=28)

# 평일은 평균 100만원, 주말은 평균 130만원 매출 (단위: 만원)
daily_sales = []
for day in days:
    if day in ['토', '일']:
        # 주말: 평균 130, 표준편차 20
        sale = np.random.normal(130, 20)
    else:
        # 평일: 평균 100, 표준편차 15
        sale = np.random.normal(100, 15)
    daily_sales.append(sale)

# DataFrame 생성
sales_df = pd.DataFrame({
    '날짜': dates,
    '요일': days,
    '매출액': daily_sales,
    '주말여부': ['주말' if d in ['토', '일'] else '평일' for d in days]
})

# 주말과 평일 데이터 분리
weekend_sales = sales_df[sales_df['주말여부'] == '주말']['매출액']
weekday_sales = sales_df[sales_df['주말여부'] == '평일']['매출액']

print("="*60)
print("실습 문제 1: 레스토랑 매출 비교")
print("="*60)
print(f"\\n데이터 기간: 4주 (28일)")
print(f"평일(월~금) 데이터: n={len(weekday_sales)}")
print(f"주말(토,일) 데이터: n={len(weekend_sales)}")
print("\\n[데이터 미리보기]")
display(sales_df.head(7))  # 첫 주 데이터

 

dd = sales_df.groupby(["주말여부"])["매출액"].describe()
dd

 

cond0 = sales_df[sales_df["주말여부"] == "평일"]["매출액"]
cond1 = sales_df[sales_df["주말여부"] == "주말"]["매출액"]

cond1

 

# 시각화
fig, axes = plt.subplots(1, 4, figsize=(20, 6))

# 박스플롯
bp = axes[0].boxplot([cond0, cond1], labels=["평일", "주말"], patch_artist=True)
bp["boxes"][0].set_facecolor("#0063b2")
bp["boxes"][1].set_facecolor("#e94b3c")
axes[0].set_title("주말 여부 매출액 차이")
axes[0].grid(axis="y", alpha=0.5)

# 히스토그램
axes[1].hist(cond0, bins=5, alpha=0.6, label="평일", color="#0063b2",
             edgecolor="black", density=True)
axes[1].hist(cond1, bins=5, alpha=0.6, label="평일", color="#e94b3c",
             edgecolor="black", density=True)
axes[1].set_xlabel("매출액")
axes[1].set_title("주말 여부 매출액 차이")
axes[1].grid(axis="y", alpha=0.5)

# Q-Q Plot
stats.probplot(cond0, dist="norm", plot=axes[2])
axes[2].set_title('Q-Q Plot (Class 0)')
axes[2].grid(True, alpha=0.3)

stats.probplot(cond1, dist="norm", plot=axes[3])
axes[3].set_title('Q-Q Plot (Class 0)')
axes[3].grid(True, alpha=0.3)

 

1️⃣ 표본 크기에 따른 정규성 확인

2️⃣ 정규성 검정

print("\\n" + "="*50)
print("가설검정 프로세스")
print("="*50)

# Step 1: 정규성 검정
is_normal_0 = check_normality_simple(cond0, "평일 매출")
is_normal_1 = check_normality_simple(cond1, "주말 매출")

 

3️⃣ 등분산성 검정 (독립표본만) stats.levene

# Step 2: 등분산성 검정
print("\\n[등분산성 검정]")
print("-"*40)
stat, p_levene = levene(cond0, cond1)
print(f"Levene's test p-value: {p_levene:.4f}")
equal_var = p_levene > 0.05
print(f"결과: {'✅ 등분산 가정 충족' if equal_var else '❌ 이분산 → Welch t-test 사용'}")

 

4️⃣ Welch t-test ttest_ind(equal_var=False)

# Step 3: Welch t-test 사용
print("\\n[Welch t-test]")
print("-"*40)
tstat, pval = stats.ttest_ind(cond0, cond1, equal_var=False)
print("평일 vs 주말")
print(f"주말-평일 매출액 : {tstat:.4f}", f"\\np-value : {pval:.4f}")
print("-"*40)

if pval < 0.05:
    print(f"✅ p-value({pval:.4f}) < 0.05 → 귀무가설 기각")
    print(f"    평일 매출액과 주말 매출액은 유의한 차이가 있음")
    print(f"    (통계적으로 의미있는 차이 존재)")
else:
    print(f"❌ p-value({pval:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"    평일 매출액과 주말 매출액은 유의한 차이가 없음")
    print(f"    (관측된 차이는 우연에 의한 것일 수 있음)")

 

5️⃣ 가설검정

6️⃣ 결론 도출

# =============================================================================
# Step 4: 가설검정
# =============================================================================
print("\\n[가설검정]")
print("-"*40)

# 가설 설정
print("H₀: μ₀ = μ₁ (평일 매출액과 주말 매출액은 유의한 차이가 없다)")
print("H₁: μ₀ ≠ μ₁ (평일 매출액과 주말 매출액은 유의한 차이가 있다)")
print("유의수준: α = 0.05")

# -----------------------------------------------------------------------------
# 4-1. 검정 방법 선택 및 실행
# -----------------------------------------------------------------------------
# 정규성 검정 결과에 따라 모수/비모수 검정 선택
if is_normal_0 and is_normal_1:
    # 모수 검정: 독립표본 t-검정 (두 그룹 모두 정규분포)
    t_stat, p_value = ttest_ind(cond0, cond1, equal_var=equal_var)
    test_name = "Student's t-test" if equal_var else "Welch's t-test"
    print(f"\\n{test_name} 결과:")
    print(f"t = {t_stat:.4f}, p = {p_value:.4f}")
    
    # Cohen's d 효과 크기 계산 (표준화된 평균 차이)
    # d = (평균1 - 평균2) / 합동표준편차
    pooled_std = np.sqrt((cond0.var() + cond1.var()) / 2)
    cohens_d = (cond0.mean() - cond1.mean()) / pooled_std
    abs_d = abs(cohens_d)
    
    # Cohen's d 해석 기준
    if abs_d < 0.2:
        effect = "매우 작은 효과"
    elif abs_d < 0.5:
        effect = "작은 효과"
    elif abs_d < 0.8:
        effect = "중간 효과"
    else:
        effect = "큰 효과"
    
    print(f"Cohen's d = {cohens_d:.3f} ({effect})")

else:
    # 비모수 검정: Mann-Whitney U 검정 (정규성 가정 위반)
    # 중앙값 차이를 검정 (순위 기반)
    u_stat, p_value = mannwhitneyu(cond0, cond1, alternative='two-sided')
    print(f"\\nMann-Whitney U test 결과:")
    print(f"U = {u_stat:.4f}, p = {p_value:.4f}")

# -----------------------------------------------------------------------------
# 4-2. 통계적 결론 도출
# -----------------------------------------------------------------------------
print(f"\\n[결론]")

# p-value를 유의수준(α=0.05)과 비교하여 가설 채택/기각 결정
if p_value < 0.05:
    print(f"✅ p-value({p_value:.4f}) < 0.05 → 귀무가설 기각")
    print(f"   두 클래스의 알코올 도수에 유의한 차이가 있음")
    print(f"   (통계적으로 의미있는 차이 존재)")
else:
    print(f"❌ p-value({p_value:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"   두 클래스의 알코올 도수에 유의한 차이가 없음")
    print(f"   (관측된 차이는 우연에 의한 것일 수 있음)")

 

🔎 self 실습 02 : 운동 프로그램 효과 평가                                                                                  

❓ 8주 운동 프로그램이 체지방률 감소에 효과가 있는가❓

⇒ 같은 대상의 전후 비교니까 ttest_rel() 사용하기?

# 가상의 데이터 생성 (25명 참가자)
np.random.seed(123)
n_participants = 25

# 프로그램 전 체지방률 (%)
before_fat = np.random.normal(28, 5, n_participants)
before_fat = np.clip(before_fat, 15, 40)  # 현실적인 범위로 제한

# 프로그램 후 체지방률 (평균적으로 2% 감소, 개인차 존재)
reduction = np.random.gamma(2, 1, n_participants)  # 감소량은 양수
after_fat = before_fat - reduction
after_fat = np.clip(after_fat, 10, 39)  # 현실적인 범위로 제한

# DataFrame 생성
fitness_df = pd.DataFrame({
    '참가자ID': [f'ID{i:03d}' for i in range(1, n_participants+1)],
    '운동전_체지방률': before_fat,
    '운동후_체지방률': after_fat,
    '체지방_감소량': before_fat - after_fat
})

print("="*60)
print("실습 문제 2: 운동 프로그램 효과 평가")
print("="*60)
print(f"\\n참가자 수: {n_participants}명")
print("\\n[데이터 샘플]")
display(fitness_df.head())

 

# 시각화
fig, axes = plt.subplots(1, 3, figsize=(14, 5))

# Before-After 연결선 그래프
for i in range(len(fitness_df)):
    axes[0].plot([0, 1], [fitness_df.iloc[i]["운동전_체지방률"], fitness_df.iloc[i]["운동후_체지방률"]], 
                'gray', alpha=0.4, linewidth=0.8)
axes[0].plot([0, 1], [fitness_df["운동전_체지방률"].mean(), fitness_df["운동후_체지방률"].mean()], 
            'red', linewidth=3, marker='o', markersize=8, label='평균')
axes[0].set_xticks([0, 1])
axes[0].set_xticklabels(["운동 전", "운동 후"])
axes[0].set_ylabel("체지방률")
axes[0].set_title('개인별 변화')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 박스플롯
bp = axes[1].boxplot([fitness_df["운동전_체지방률"], fitness_df["운동후_체지방률"]], 
                     labels=["운동 전", "운동 후"], patch_artist=True)
bp['boxes'][0].set_facecolor("#ead98b")
bp['boxes'][1].set_facecolor("#7dd0b6")
axes[1].set_ylabel("체지방률")
axes[1].set_title("체지방률 변화")
axes[1].grid(True, alpha=0.3)

# 변화량 히스토그램
axes[2].hist(fitness_df["체지방_감소량"], bins=10, edgecolor='black', alpha=0.7, color="#93c763")
axes[2].axvline(0, color='red', linestyle='--', linewidth=2, label='변화 없음')
axes[2].axvline(fitness_df["체지방_감소량"].mean(), color='blue', linestyle='--', 
               linewidth=2, label=f'평균: {fitness_df["체지방_감소량"].mean():.1f}')
axes[2].set_xlabel("체지방 감소량")
axes[2].set_ylabel("빈도")
axes[2].set_title("체지방 감소량 분포")
axes[2].legend()
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

 

1️⃣ 표본 크기에 따른 정규성 확인

2️⃣ 정규성 검정

print("\\n" + "="*50)
print("가설검정 프로세스")
print("="*50)

# Step 1: 변화량의 정규성 검정
is_normal_diff = check_normality_simple(fitness_df["체지방_감소량"], "체지방_감소량")

⇒ 비모수검정 - 윌콕슨 사용해야 함

 

3️⃣ 가설검정

4️⃣ 윌콕슨

5️⃣ 결론 도출

# =============================================================================
# Step 2: 가설검정
# =============================================================================
print("\\n[가설검정]")
print("-"*40)

# 가설 설정 (대응표본 검정)
print("H₀: μ_before = μ_after (운동 효과 없음)")
print("H₁: μ_before ≠ μ_after (운동 효과 있음)")
print("유의수준: α = 0.05")
alpha = 0.05
# -----------------------------------------------------------------------------
# 2-1. 검정 방법 선택 및 실행
# -----------------------------------------------------------------------------
# 차이값의 정규성에 따라 모수/비모수 검정 선택
if is_normal_diff:
    # 모수 검정: 대응표본 t-검정 (차이값이 정규분포)
    # 동일 대상의 전후 비교이므로 paired t-test 사용
    t_stat, p_value = ttest_rel(fitness_df["운동전_체지방률"], fitness_df["운동후_체지방률"])
    print(f"\\nPaired t-test 결과:")
    print(f"t = {t_stat:.4f}, p = {p_value:.4f}")
    
    # -------------------------------------------------------------------------
    # 2-2. 효과 크기 계산 (Cohen's d for paired samples)
    # -------------------------------------------------------------------------
    # 대응표본의 Cohen's d = 평균 변화량 / 변화량의 표준편차
    cohens_d = fitness_df["체지방_감소량"].mean() / fitness_df["체지방_감소량"].std()
    abs_d = abs(cohens_d)
    
    # Cohen's d 해석 기준 (대응표본)
    if abs_d < 0.2:
        effect = "매우 작은 효과"
    elif abs_d < 0.5:
        effect = "작은 효과"  
    elif abs_d < 0.8:
        effect = "중간 효과"
    else:
        effect = "큰 효과"
    
    print(f"Cohen's d = {cohens_d:.3f} ({effect})")
    
    # -------------------------------------------------------------------------
    # 2-3. 신뢰구간 계산
    # -------------------------------------------------------------------------
    # 평균 변화량의 95% 신뢰구간 추정
    # CI = 평균 ± t(α/2, df) × SE
    confidence = 0.95  # 신뢰수준
    n = len(treatment_df)  # 표본 크기
    mean_diff = fitness_df["체지방_감소량"].mean()  # 평균 변화량
    se_diff = stats.sem(fitness_df["체지방_감소량"])  # 표준오차
    
    # t-분포 기반 신뢰구간 (자유도 = n-1)
    ci = stats.t.interval(confidence, n-1, loc=mean_diff, scale=se_diff)
    print(f"평균 변화의 95% CI: [{ci[0]:.2f}, {ci[1]:.2f}]")
    
else:
# Step 2: 비모수검정 - 윌콕슨 (정규성 가정 위반)
    # 중앙값 차이를 검정 (순위와 부호 기반)
    w_stat, p_value = wilcoxon(fitness_df["운동전_체지방률"], fitness_df["운동후_체지방률"])
    print(f"\\nWilcoxon signed-rank test 결과 :")
    print(f"W = {w_stat:.7f}, p = {p_value:.7f}")

# -----------------------------------------------------------------------------
# 2-4. 통계적 결론 도출
# -----------------------------------------------------------------------------
print(f"\\n[결론]")

# p-value를 유의수준(α=0.05)과 비교하여 가설 채택/기각 결정
if p_value < alpha:
    print(f"✅ p-value({p_value:.4f}) < 0.05 → 귀무가설 기각")
    print(f"   운동 효과가 있음 (평균 {abs(fitness_df['체지방_감소량'].mean()):.1f} 감소)")
    print(f"   (통계적으로 유의한 개선 효과)")
else:
    print(f"❌ p-value({p_value:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"   운동 유의하지 않음")
    print(f"   (관측된 변화는 우연에 의한 것일 수 있음)")

 

🔎 self 실습 03 : 제품 품질 검사                                                                                                  

❓ 생산된 음료의 실제 용량이 표기 용량(500ml)과 일치하는가❓

⇒ 한 그룹과 기준값의 비교니까 ttest_1samp() 사용하기 ?

# 가상의 데이터 생성 (40개 샘플)
np.random.seed(456)
n_samples = 40

# 실제 용량 (평균 498ml, 표준편차 3ml로 약간 부족한 상황 시뮬레이션)
actual_volume = np.random.normal(498, 3, n_samples)

# DataFrame 생성
quality_df = pd.DataFrame({
    '샘플번호': [f'S{i:03d}' for i in range(1, n_samples+1)],
    '실제용량': actual_volume,
    '표기용량과의_차이': actual_volume - 500
})

print("="*60)
print("실습 문제 3: 제품 품질 검사")
print("="*60)
print(f"\\n샘플 수: {n_samples}개")
print(f"표기 용량: 500ml")
print("\\n[데이터 정보]")
print(f"실제 용량 평균: {actual_volume.mean():.2f}ml")
print(f"실제 용량 표준편차: {actual_volume.std():.2f}ml")

 

quality_df

 

# 시각화
fig, axes = plt.subplots(1, 3, figsize=(14, 5))
target_value = 500

# 히스토그램
axes[0].hist(quality_df["실제용량"], bins=12, edgecolor='black', alpha=0.7, color="#aed6df")
axes[0].axvline(target_value, color='red', linestyle='--', linewidth=2, label=f'기준값: {target_value}cm')
axes[0].axvline(quality_df["실제용량"].mean(), color='green', linestyle='--', linewidth=2, 
                label=f'평균: {quality_df["실제용량"].mean():.2f}cm')
axes[0].set_xlabel('음료 실제 용량 (ml)')
axes[0].set_ylabel('빈도')
axes[0].set_title('음료 실제 용량')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# 박스플롯
bp = axes[1].boxplot(quality_df["실제용량"], patch_artist=True)
bp['boxes'][0].set_facecolor("#aed6df")
axes[1].axhline(target_value, color='red', linestyle='--', linewidth=2)
axes[1].set_ylabel('음료 실제 용량 (ml)')
axes[1].set_title('박스플롯')
axes[1].text(1.1, target_value+0.05, f'기준값: {target_value}', color='red')
axes[1].grid(True, alpha=0.3)

# Q-Q plot
stats.probplot(quality_df["실제용량"], dist="norm", plot=axes[2])
axes[2].set_title('Q-Q Plot')
axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

 

1️⃣ 표본 크기에 따른 정규성 확인

2️⃣ 정규성 검정

print("\\n" + "="*50)
print("가설검정 프로세스")
print("="*50)

# Step 1: 정규성 검정
is_normal = check_normality_simple(quality_df["실제용량"], "음료 실제 용량")

 

3️⃣ 가설검정

4️⃣ 결론 도출

# =============================================================================
# Step 2: 가설검정
# =============================================================================
print("\\n[가설검정]")
print("-"*40)

# 가설 설정 (단일표본 검정)
# 표본 평균이 특정 값(target_value)과 같은지 검정
print(f"H₀: μ = {target_value}ml (평균이 {target_value}ml)")
print(f"H₁: μ ≠ {target_value}ml (평균이 {target_value}ml가 아님)")
print("유의수준: α = 0.05")

# -----------------------------------------------------------------------------
# 2-1. 검정 방법 선택 및 실행
# -----------------------------------------------------------------------------
# 정규성 검정 결과에 따라 모수/비모수 검정 선택
if is_normal:
    # 모수 검정: 단일표본 t-검정 (데이터가 정규분포)
    # 표본 평균과 모집단 평균(target_value)을 비교
    t_stat, p_value = ttest_1samp(quality_df["실제용량"], target_value)
    print(f"\\nOne-sample t-test 결과:")
    print(f"t = {t_stat:.4f}, p = {p_value:.4f}")
    
    # -------------------------------------------------------------------------
    # 2-2. 신뢰구간 계산 및 해석
    # -------------------------------------------------------------------------
    # 모평균의 95% 신뢰구간 추정
    # CI = 표본평균 ± t(α/2, df) × SE
    confidence = 0.95  # 신뢰수준
    n = len(quality_df["실제용량"])  # 표본 크기
    mean = quality_df["실제용량"].mean()  # 표본 평균
    se = stats.sem(quality_df["실제용량"])  # 표준오차 (SE = s/√n)
    
    # t-분포 기반 신뢰구간 (자유도 = n-1)
    ci = stats.t.interval(confidence, n-1, loc=mean, scale=se)
    print(f"평균의 95% CI: [{ci[0]:.3f}, {ci[1]:.3f}]cm")
    
    # 신뢰구간과 목표값 비교
    # 목표값이 신뢰구간 내에 있으면 H₀를 기각할 수 없음
    if ci[0] <= target_value <= ci[1]:
        print(f"→ {target_value}ml가 신뢰구간 내에 있음")
    else:
        print(f"→ {target_value}ml가 신뢰구간 밖에 있음")
        
else:
    # 비모수 검정: Wilcoxon 부호순위 검정 (정규성 가정 위반)
    # 중앙값이 목표값과 같은지 검정 (순위와 부호 기반)
    
    # 각 관측값과 목표값의 차이 계산
    differences = quality_df["실제용량"] - target_value
    
    # Wilcoxon 부호순위 검정 실행
    # 차이의 절댓값에 순위를 매기고, 부호를 고려하여 검정
    w_stat, p_value = wilcoxon(differences)
    print(f"\\nWilcoxon signed-rank test 결과:")
    print(f"W = {w_stat:.4f}, p = {p_value:.4f}")

# -----------------------------------------------------------------------------
# 2-3. 통계적 결론 도출
# -----------------------------------------------------------------------------
print(f"\\n[결론]")

# p-value를 유의수준(α=0.05)과 비교하여 가설 채택/기각 결정
if p_value < 0.05:
    print(f"✅ p-value({p_value:.4f}) < 0.05 → 귀무가설 기각")
    
    # 차이의 방향 확인 (평균이 목표값보다 높은지 낮은지)
    diff = quality_df["실제용량"].mean() - target_value
    if diff > 0:
        print(f"   평균({quality_df['실제용량'].mean():.3f}cm)이 {target_value}cm보다 유의하게 높음")
    else:
        print(f"   평균({quality_df['실제용량'].mean():.3f}cm)이 {target_value}cm보다 유의하게 낮음")
    print(f"   (통계적으로 의미있는 차이 존재)")
    
else:
    print(f"❌ p-value({p_value:.4f}) ≥ 0.05 → 귀무가설 채택")
    print(f"   평균이 {target_value}cm와 유의한 차이가 없음")
    print(f"   (관측된 차이는 우연에 의한 것일 수 있음)")

'Sparta > Theory' 카테고리의 다른 글

[251001] 머신러닝 04 - 분류  (0) 2025.10.01
[250930] 머신러닝 03 - 회귀  (0) 2025.09.30
[250929] 머신러닝 02  (0) 2025.09.29
[250929] 머신러닝 01  (0) 2025.09.29
[250925] 스파르타코딩 본캠프 38일차  (0) 2025.09.25