유쾌한 R 통계학 부록 사이트
https://studysites.sagepub.com/dsur/study/articles.htm
SAGE - Student and Instructor Site for Discovering Statistics Using R
studysites.sagepub.com
{ 15.6 크러스컬-월리스 검정 : 여러 독립 그룹의 차이 }
[ `크러스컬-월리스 검정(Kruskal-Wallis test)`이란? ]
- 일원 독립 분산분석의 비모수적 버전
- 윌콕슨 순위합 검정 이론과 매우 유사 (Ch 15.4.1)
- 순위화된 자료에 기초
- 그룹을 하나로 모아, 점수를 오름차순으로 정렬
- 앞에서부터 순위 부여
- 값이 같을 경우, 해당 값들의 순위들의 평균값을 부여한다.
- 그룹별로 분리해서 각 그룹의 순위합 구하기
제 10장 : 일원 독립 분산 분석 요약글
- 일원 독립 분산 분석 : 여러 독립 그룹의 차이를 검정하는데 사용 (모수적 검정 가정들을 충족할 때)
- 모수적 검정의 가정들
- 각 실험 조건의 분산들이 등분산성을 가진다. → 레빈 검정으로 등분산성 검정
- 관측들이 서로 독립적
- 종속 변수가 적어도 구간 척도에서 측정된 것이어야 함
- 그룹 내 분포들이 정규 분포이어야 함
- 모수적 검정 가정들이 깨질 경우에는?
- 가장 먼저, 자료를 변환해본다.
- F 통계량 이용
- 가정들이 깨진 상황에서도 강건할 수 있음 (Ch 10.3)
- 검정이 강건하다 = 검정의 가정 몇 개가 깨져도 큰 문제가 되지 않는다.
- F 비는 강건하다.
- 분산의 동질성이 깨진 경우
- 웰치의 F (Welch's F) 사용
- 변형된 F 검정
- 분산의 동질성 가정 위반이 유발하는 문제를 극복하기 위해 F와 잔차 자유도를 수정한다.
- 웰치의 F (Welch's F) 사용
- 분포의 정규성 가정이 깨졌을 때
- 윌콕스가 R로 구현한 분산분석의 강건한 버전들 사용 (Ch 5.8.4)
- 자료의 변환으로도 해결이 안되거나, 변환 과정에서 문제가 생길 경우
- 비모수적 검정 사용 : 자료가 정규분포가 아니라는 가정으로 검정
- 단방향 독립 분산 분석 : 크러스컬-월리스 검정
- 정규성이 아니라 다른 가정이 위반된 자료일 경우 추천
- 강건한 검정 사용
- 컴퓨터 사용이 필요
- 작동 방식 : 절사평균, M 추정량
- 절사 평균 : 분포의 양극단에서 일정 비율의 점수를 제거한 후 평균 계산
- M 추정량 : 절사의 양을 실험적으로 결정하고 평균 등의 추정량을 구함
- 비모수적 검정 사용 : 자료가 정규분포가 아니라는 가정으로 검정
[ 검정 과정 ]
< 예시 자료 >
- 15장 과제 4 : 아동의 광대 공포증(coulrophobia)을 예방하는데 관심을 둔 연구
- 여러 아동 그룹 (각 15명) 에게 광대에 대한 서로 다른 형태의 긍정적 정보를 제공
- 그룹 1 (Advert) : 맥도널드 마스코드 광대를 보고 기뻐서 깡총거리는 아이들이 나오는 맥도널드 광고를 보여줌
- 그룹 2 (Story): 숲에서 길을 잃은 아이들을 도운 광대 이야기를 들려줌
- 그룹 3 (Exposure): 살아 있는 진짜 광대가 풍선으로 동물을 만들어서 아이들에게 줌
- 그룹 4 (nan) (대조군) : 아무것도 제공하지 않음
- 아이들에게 광대를 얼마나 무서워하는지 0점(전혀 무섭지 않음) ~ 5점(너무 무서움) 척도로 조사.
- 조사 결과 : coulrophobia.csv (교재 부록 참고)
- 교재 부록 사이트에는 .dat 파일로 올려져 있습니다. 이를csv 형식으로 바꿔서 파일을 만들었습니다.
- 여러 아동 그룹 (각 15명) 에게 광대에 대한 서로 다른 형태의 긍정적 정보를 제공
자료 coulrophobia.csv 불러오기
# 자료 coulrophobia.csv 불러오기
import pandas as pd
data4 = pd.read_csv("coulrophobia.csv")
data4
코드 실행결과
| infotype | beliefs | |
|---|---|---|
| 0 | Advert | 5 |
| 1 | Advert | 3 |
| 2 | Advert | 1 |
| 3 | Advert | 3 |
| 4 | Advert | 4 |
| 5 | Advert | 4 |
| 6 | Advert | 5 |
| 7 | Advert | 5 |
| 8 | Advert | 3 |
| 9 | Advert | 5 |
| 10 | Advert | 3 |
| 11 | Advert | 2 |
| 12 | Advert | 4 |
| 13 | Advert | 4 |
| 14 | Advert | 3 |
| 15 | Story | 4 |
| 16 | Story | 0 |
| 17 | Story | 1 |
| 18 | Story | 2 |
| 19 | Story | 4 |
| 20 | Story | 0 |
| 21 | Story | 1 |
| 22 | Story | 0 |
| 23 | Story | 2 |
| 24 | Story | 1 |
| 25 | Story | 1 |
| 26 | Story | 2 |
| 27 | Story | 2 |
| 28 | Story | 1 |
| 29 | Story | 4 |
| 30 | Exposure | 1 |
| 31 | Exposure | 3 |
| 32 | Exposure | 4 |
| 33 | Exposure | 0 |
| 34 | Exposure | 1 |
| 35 | Exposure | 2 |
| 36 | Exposure | 5 |
| 37 | Exposure | 0 |
| 38 | Exposure | 1 |
| 39 | Exposure | 0 |
| 40 | Exposure | 5 |
| 41 | Exposure | 1 |
| 42 | Exposure | 1 |
| 43 | Exposure | 2 |
| 44 | Exposure | 2 |
| 45 | NaN | 3 |
| 46 | NaN | 2 |
| 47 | NaN | 2 |
| 48 | NaN | 3 |
| 49 | NaN | 2 |
| 50 | NaN | 2 |
| 51 | NaN | 3 |
| 52 | NaN | 2 |
| 53 | NaN | 2 |
| 54 | NaN | 2 |
| 55 | NaN | 2 |
| 56 | NaN | 2 |
| 57 | NaN | 3 |
| 58 | NaN | 3 |
| 59 | NaN | 2 |
< 1. 자료 순위화하기 >
- 그룹을 하나로 모아, 점수를 오름차순으로 정렬
- 앞에서부터 순위 부여 (1,2,3,...)
- 그룹별로 분리해서 각 그룹의 $$R_{i} = SUM(순위)$$를 구한다.
beliefs 기준, 오름차순으로 정렬
# 자료 coulrophobia.csv
# beliefs 기준, 오름차순으로 정렬
data4_sorted = data4.sort_values(by= 'beliefs',ascending=True)
data4 = data4_sorted
data4
코드 실행결과
| infotype | beliefs | |
|---|---|---|
| 20 | Story | 0 |
| 22 | Story | 0 |
| 16 | Story | 0 |
| 33 | Exposure | 0 |
| 37 | Exposure | 0 |
| 39 | Exposure | 0 |
| 17 | Story | 1 |
| 2 | Advert | 1 |
| 24 | Story | 1 |
| 28 | Story | 1 |
| 41 | Exposure | 1 |
| 38 | Exposure | 1 |
| 34 | Exposure | 1 |
| 25 | Story | 1 |
| 30 | Exposure | 1 |
| 21 | Story | 1 |
| 42 | Exposure | 1 |
| 49 | NaN | 2 |
| 53 | NaN | 2 |
| 52 | NaN | 2 |
| 44 | Exposure | 2 |
| 43 | Exposure | 2 |
| 46 | NaN | 2 |
| 47 | NaN | 2 |
| 35 | Exposure | 2 |
| 50 | NaN | 2 |
| 27 | Story | 2 |
| 11 | Advert | 2 |
| 23 | Story | 2 |
| 18 | Story | 2 |
| 26 | Story | 2 |
| 54 | NaN | 2 |
| 59 | NaN | 2 |
| 55 | NaN | 2 |
| 56 | NaN | 2 |
| 3 | Advert | 3 |
| 48 | NaN | 3 |
| 51 | NaN | 3 |
| 57 | NaN | 3 |
| 45 | NaN | 3 |
| 58 | NaN | 3 |
| 10 | Advert | 3 |
| 8 | Advert | 3 |
| 31 | Exposure | 3 |
| 14 | Advert | 3 |
| 1 | Advert | 3 |
| 4 | Advert | 4 |
| 12 | Advert | 4 |
| 13 | Advert | 4 |
| 32 | Exposure | 4 |
| 15 | Story | 4 |
| 5 | Advert | 4 |
| 29 | Story | 4 |
| 19 | Story | 4 |
| 0 | Advert | 5 |
| 9 | Advert | 5 |
| 7 | Advert | 5 |
| 6 | Advert | 5 |
| 40 | Exposure | 5 |
| 36 | Exposure | 5 |
앞에서부터 순위 부여 (1,2,3,...)
# 순위 부여
data4['Rank1'] = data4_sorted['beliefs'].rank(method='first', ascending=True) # 1차 순서 부여
data4['동순위화'] = data4_sorted['beliefs'].rank(method='average', ascending=True) # 같은 점수들은 순위의 평균 점수로 동순위화
# 3.5, 12.0, 26.5, 41.0, 50.5, 57.5
data4 = data4.drop('Rank1', axis=1) # 1차로 부여했던 순서 열 제거
data4.head(10)
코드 실행결과
| infotype | beliefs | 동순위화 | |
|---|---|---|---|
| 20 | Story | 0 | 3.5 |
| 22 | Story | 0 | 3.5 |
| 16 | Story | 0 | 3.5 |
| 33 | Exposure | 0 | 3.5 |
| 37 | Exposure | 0 | 3.5 |
| 39 | Exposure | 0 | 3.5 |
| 17 | Story | 1 | 12.0 |
| 2 | Advert | 1 | 12.0 |
| 24 | Story | 1 | 12.0 |
| 28 | Story | 1 | 12.0 |
| 41 | Exposure | 1 | 12.0 |
| 38 | Exposure | 1 | 12.0 |
| 34 | Exposure | 1 | 12.0 |
| 25 | Story | 1 | 12.0 |
| 30 | Exposure | 1 | 12.0 |
| 21 | Story | 1 | 12.0 |
| 42 | Exposure | 1 | 12.0 |
| 49 | NaN | 2 | 26.5 |
| 53 | NaN | 2 | 26.5 |
| 52 | NaN | 2 | 26.5 |
| 44 | Exposure | 2 | 26.5 |
| 43 | Exposure | 2 | 26.5 |
| 46 | NaN | 2 | 26.5 |
| 47 | NaN | 2 | 26.5 |
| 35 | Exposure | 2 | 26.5 |
| 50 | NaN | 2 | 26.5 |
| 27 | Story | 2 | 26.5 |
| 11 | Advert | 2 | 26.5 |
| 23 | Story | 2 | 26.5 |
| 18 | Story | 2 | 26.5 |
| 26 | Story | 2 | 26.5 |
| 54 | NaN | 2 | 26.5 |
| 59 | NaN | 2 | 26.5 |
| 55 | NaN | 2 | 26.5 |
| 56 | NaN | 2 | 26.5 |
| 3 | Advert | 3 | 41.0 |
| 48 | NaN | 3 | 41.0 |
| 51 | NaN | 3 | 41.0 |
| 57 | NaN | 3 | 41.0 |
| 45 | NaN | 3 | 41.0 |
| 58 | NaN | 3 | 41.0 |
| 10 | Advert | 3 | 41.0 |
| 8 | Advert | 3 | 41.0 |
| 31 | Exposure | 3 | 41.0 |
| 14 | Advert | 3 | 41.0 |
| 1 | Advert | 3 | 41.0 |
| 4 | Advert | 4 | 50.5 |
| 12 | Advert | 4 | 50.5 |
| 13 | Advert | 4 | 50.5 |
| 32 | Exposure | 4 | 50.5 |
| 15 | Story | 4 | 50.5 |
| 5 | Advert | 4 | 50.5 |
| 29 | Story | 4 | 50.5 |
| 19 | Story | 4 | 50.5 |
| 0 | Advert | 5 | 57.5 |
| 9 | Advert | 5 | 57.5 |
| 7 | Advert | 5 | 57.5 |
| 6 | Advert | 5 | 57.5 |
| 40 | Exposure | 5 | 57.5 |
| 36 | Exposure | 5 | 57.5 |
그룹별 순위 분리
# 그룹별 순위 분리
group1 = data4[data4['infotype'] == 'Advert'] # 광고 보여준 그룹
group2 = data4[data4['infotype'] == 'Story'] # 이야기 들려준 그룹
group3 = data4[data4['infotype'] == 'Exposure'] # 직접 광대를 만난 그룹
group4 = data4[data4['infotype'].isna()] # 아무것도 안 한 그룹
group4['infotype'] = 'Nothing' # infotype의 결측치를 'Nothing' 문자열로 대체
데이터 최종본
# 데이터 최종본
import pandas as pd
# 1. 그룹 정의 및 필요한 열 추출
cols = [ 'infotype','beliefs', '동순위화']
# 인덱스 새로 부여
advert = group1[cols].reset_index(drop=True)
story = group2[cols].reset_index(drop=True)
exposure = group3[cols].reset_index(drop=True)
missing = group4[cols].reset_index(drop=True)
# 2. 계층적 열 정의(멀티 인덱스)
columns = pd.MultiIndex.from_product([
['Advert', 'Story', 'Exposure', 'Missing'],
cols
])
# 3. 열 방향 결합
data4 = pd.concat(
[advert, story, exposure, missing],
axis=1
)
data4.columns = columns
# 4. 결과 확인
data4
코드 실행결과
| Advert | Story | Exposure | Missing | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| infotype | beliefs | 동순위화 | infotype | beliefs | 동순위화 | infotype | beliefs | 동순위화 | infotype | beliefs | 동순위화 | |
| 0 | Advert | 1 | 12.0 | Story | 0 | 3.5 | Exposure | 0 | 3.5 | Nothing | 2 | 26.5 |
| 1 | Advert | 2 | 26.5 | Story | 0 | 3.5 | Exposure | 0 | 3.5 | Nothing | 2 | 26.5 |
| 2 | Advert | 3 | 41.0 | Story | 0 | 3.5 | Exposure | 0 | 3.5 | Nothing | 2 | 26.5 |
| 3 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 4 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 5 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 6 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 7 | Advert | 4 | 50.5 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 8 | Advert | 4 | 50.5 | Story | 2 | 26.5 | Exposure | 2 | 26.5 | Nothing | 2 | 26.5 |
| 9 | Advert | 4 | 50.5 | Story | 2 | 26.5 | Exposure | 2 | 26.5 | Nothing | 2 | 26.5 |
| 10 | Advert | 4 | 50.5 | Story | 2 | 26.5 | Exposure | 2 | 26.5 | Nothing | 3 | 41.0 |
| 11 | Advert | 5 | 57.5 | Story | 2 | 26.5 | Exposure | 3 | 41.0 | Nothing | 3 | 41.0 |
| 12 | Advert | 5 | 57.5 | Story | 4 | 50.5 | Exposure | 4 | 50.5 | Nothing | 3 | 41.0 |
| 13 | Advert | 5 | 57.5 | Story | 4 | 50.5 | Exposure | 5 | 57.5 | Nothing | 3 | 41.0 |
| 14 | Advert | 5 | 57.5 | Story | 4 | 50.5 | Exposure | 5 | 57.5 | Nothing | 3 | 41.0 |
순위합 구하기
# 순위합 구하기
# 1. 각 그룹의 '동순위화' 합 구하기
sums = {
group: data4[group]['동순위화'].sum()
for group in ['Advert', 'Story', 'Exposure', 'Missing']
}
# 2. 새로운 행 생성 (모든 값은 NaN으로 시작)
sum_row = pd.DataFrame([[None]*data4.shape[1]], columns=data4.columns)
# 3. 그룹별로 '동순위화' 위치에 합계 값 채우기
for i, group in enumerate(['Advert', 'Story', 'Exposure', 'Missing']):
sum_row.iloc[0, i*3 + 2] = sums[group] # 3개씩 묶이므로 i*3 + 2가 '동순위화'
# 4. 합계 행의 인덱스를 '합계'로 지정하고, 원본 아래에 추가
sum_row.index = ['합계']
data4_with_sum = pd.concat([data4, sum_row])
data4_with_sum
코드 실행결과
| Advert | Story | Exposure | Missing | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| infotype | beliefs | 동순위화 | infotype | beliefs | 동순위화 | infotype | beliefs | 동순위화 | infotype | beliefs | 동순위화 | |
| 0 | Advert | 1 | 12.0 | Story | 0 | 3.5 | Exposure | 0 | 3.5 | Nothing | 2 | 26.5 |
| 1 | Advert | 2 | 26.5 | Story | 0 | 3.5 | Exposure | 0 | 3.5 | Nothing | 2 | 26.5 |
| 2 | Advert | 3 | 41.0 | Story | 0 | 3.5 | Exposure | 0 | 3.5 | Nothing | 2 | 26.5 |
| 3 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 4 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 5 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 6 | Advert | 3 | 41.0 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 7 | Advert | 4 | 50.5 | Story | 1 | 12.0 | Exposure | 1 | 12.0 | Nothing | 2 | 26.5 |
| 8 | Advert | 4 | 50.5 | Story | 2 | 26.5 | Exposure | 2 | 26.5 | Nothing | 2 | 26.5 |
| 9 | Advert | 4 | 50.5 | Story | 2 | 26.5 | Exposure | 2 | 26.5 | Nothing | 2 | 26.5 |
| 10 | Advert | 4 | 50.5 | Story | 2 | 26.5 | Exposure | 2 | 26.5 | Nothing | 3 | 41.0 |
| 11 | Advert | 5 | 57.5 | Story | 2 | 26.5 | Exposure | 3 | 41.0 | Nothing | 3 | 41.0 |
| 12 | Advert | 5 | 57.5 | Story | 4 | 50.5 | Exposure | 4 | 50.5 | Nothing | 3 | 41.0 |
| 13 | Advert | 5 | 57.5 | Story | 4 | 50.5 | Exposure | 5 | 57.5 | Nothing | 3 | 41.0 |
| 14 | Advert | 5 | 57.5 | Story | 4 | 50.5 | Exposure | 5 | 57.5 | Nothing | 3 | 41.0 |
| 합계 | None | None | 675.5 | None | None | 328.0 | None | None | 356.5 | None | None | 470.0 |
< 2. 탐색적 분석 (정규성, 등분산성) >
- 정규 분포 관련 통계량 분석 : 정규 분포 여부
- 레빈 검정 : 분산의 동질성 여부
검정에 필요한 열만 추출
# 정규성 검정, 검정에 필요한 열만 가져오기
grouped = []
for i in data4.columns:
if '동순위화' in i:
grouped.append(i)
test_data = data4[grouped].astype('float64')
test_data
코드 실행결과
| Advert | Story | Exposure | Missing | |
|---|---|---|---|---|
| 동순위화 | 동순위화 | 동순위화 | 동순위화 | |
| 0 | 12.0 | 3.5 | 3.5 | 26.5 |
| 1 | 26.5 | 3.5 | 3.5 | 26.5 |
| 2 | 41.0 | 3.5 | 3.5 | 26.5 |
| 3 | 41.0 | 12.0 | 12.0 | 26.5 |
| 4 | 41.0 | 12.0 | 12.0 | 26.5 |
| 5 | 41.0 | 12.0 | 12.0 | 26.5 |
| 6 | 41.0 | 12.0 | 12.0 | 26.5 |
| 7 | 50.5 | 12.0 | 12.0 | 26.5 |
| 8 | 50.5 | 26.5 | 26.5 | 26.5 |
| 9 | 50.5 | 26.5 | 26.5 | 26.5 |
| 10 | 50.5 | 26.5 | 26.5 | 41.0 |
| 11 | 57.5 | 26.5 | 41.0 | 41.0 |
| 12 | 57.5 | 50.5 | 50.5 | 41.0 |
| 13 | 57.5 | 50.5 | 57.5 | 41.0 |
| 14 | 57.5 | 50.5 | 57.5 | 41.0 |
정규 분포 관련 통계량 분석 : 정규 분포 여부
import pandas as pd
from scipy.stats import shapiro
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
import matplotlib.font_manager as fm
import matplotlib
# 한글 폰트 설정 (예: Windows의 'Malgun Gothic')
plt.rcParams['font.family'] = 'Malgun Gothic'
# 마이너스 기호 깨짐 방지
plt.rcParams['axes.unicode_minus'] = False
# 예시 데이터프레임 (data4의 한 그룹의 '동순위화' 열이라고 가정)
# 예: data4[('Advert', '동순위화')]
for col, data in test_data:
normtest_data = test_data[(col,data)].astype("float64")
# 정규성
stat, p_value = shapiro(normtest_data)
print(f"shapiro-Wilk 통계량 (W) : {stat}")
print(f"p-value : {p_value}")
alpha = 0.05
if p_value > alpha:
print("(^_^) 정규성을 만족합니다. (p > 0.05) ")
else:
print("(ㅠ_ㅠ) 정규성을 만족하지 않습니다. (p ≤ 0.05)")
print()
코드 실행결과
shapiro-Wilk 통계량 (W) : 0.8358006490595213 p-value : 0.011000439539294117 (ㅠ_ㅠ) 정규성을 만족하지 않습니다. (p ≤ 0.05) shapiro-Wilk 통계량 (W) : 0.8333824479335998 p-value : 0.010190272733535321 (ㅠ_ㅠ) 정규성을 만족하지 않습니다. (p ≤ 0.05) shapiro-Wilk 통계량 (W) : 0.8499346877304855 p-value : 0.01732896562600641 (ㅠ_ㅠ) 정규성을 만족하지 않습니다. (p ≤ 0.05) shapiro-Wilk 통계량 (W) : 0.603427100678753 p-value : 2.738197758845093e-05 (ㅠ_ㅠ) 정규성을 만족하지 않습니다. (p ≤ 0.05)
시각화 [더보기 클릭]
for col, data in grouped:
normtest_data = test_data[(col,data)].astype("float64")
# 시각화
fig, axes = plt.subplots(1,2, figsize=(8,4))
fig.suptitle(f"{col} 그룹의 정규성 시각화", fontsize=14)
# 히스토그램
sns.histplot(normtest_data, kde=True, ax=axes[0], color='skyblue')
plt.title(f"{group} 그룹 히스토그램")
plt.xlabel("순위 (Rank)")
plt.ylabel("빈도 (Frequency)")
# QQ plot
plt.subplot(1, 2, 2)
stats.probplot(normtest_data, dist="norm", plot=plt)
plt.title("QQ Plot")
plt.tight_layout()
plt.subplots_adjust(top=0.85)
plt.show()




레빈 검정 : 분산의 동질성 여부
여러 그룹의 분산이 동일한가? (등분산인가?)
- 2개 이상의 독립된 그룹의 분산이 같다는 귀무가설을 검정하는데 사용
from scipy.stats import levene
# test_data의 컬럼들이 모두 동일한 그룹의 '동순위화' 값이라고 가정
# 각 컬럼별 실제 데이터를 리스트에 담아야 함
from scipy.stats import levene
# Advert, Story, Exposure, Missing 그룹의 '동순위화' 값 추출
adv = test_data[('Advert', '동순위화')].dropna()
sto = test_data[('Story', '동순위화')].dropna()
exp = test_data[('Exposure', '동순위화')].dropna()
mis = test_data[('Missing', '동순위화')].dropna()
# 레빈 검정
stat, p = levene(adv, sto, exp, mis)
print("Levene 검정 통계량 :", stat)
print("p-value :", p)
if p > 0.05:
print("결과 : (^_^) 등분산성 가정을 만족합니다 (p > 0.05)")
else:
print("결과: (ㅠ_ㅠ) 등분산성 가정을 만족하지 않습니다 (p ≤ 0.05)")
코드 실행결과
Levene 검정 통계량 : 1.9961734246094438 p-value : 0.12499772715931731 결과 : (^_^) 등분산성 가정을 만족합니다 (p > 0.05)
시각화 [더보기 클릭]
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# 시각화를 위한 데이터프레임 생성
plot_df = pd.DataFrame({
"Advert": adv,
"Story": sto,
"Exposure": exp,
"Missing": mis
})
# 길게 (long-form) 변형
long_df = plot_df.melt(var_name='Group', value_name='동순위화')
# 시각화
plt.figure(figsize=(12, 6))
# 1. Boxplot
plt.subplot(1, 2, 1)
sns.boxplot(data=long_df, x='Group', y='동순위화', palette='pastel')
plt.title("그룹별 Boxplot")
# 2. Violin plot
plt.subplot(1, 2, 2)
sns.violinplot(data=long_df, x='Group', y='동순위화', palette='Set2')
plt.title("그룹별 Violin Plot")
plt.tight_layout()
plt.show()

정규성을 만족하지 않음 => 비모수적 검정 시행
< 3. 검정통계량 H 계산 >
검정 통계량 H 계산식은 다음과 같음.
$$ H = \frac{12}{N(N+1)} \Sigma\frac{R^2_{i}}{n_{i}}-3(N+1)$$
+ Ri : 각 그룹의 순위합
+ N : 전체 표본의 크기(예:60명)
+ ni: 각 그룹의 표본 수(예: 15명)
+ SUM((순위합의 제곱)/그룹의 크기)
+ 예시의 검정 통계량 H

- 크러스컬-월리스 검정통계량 H = 16.37일 때, 자유도 3에서의 p-value는 0.00095
- 일원 분산분석처럼 이 검정은 단지 그룹들 사이에 차이가 있는지 아닌지만 말해줌
- 구체적으로 어디서 차이가 나는지 알려주지 않음
- 구체적인 차이를 보고 싶다면? ==> 평균 순위 or 상자그림 확인!
- chi-squared 분포를 따름.
- 자유도 = 그룹 수 - 1
파이썬 코드로 구현한 크러스컬-월리스 검정
from scipy.stats import kruskal
# k_data = data4.drop('동순위화',axis=1)
one = group1['beliefs']
two = group2['beliefs']
three = group3['beliefs']
four = group4['beliefs']
stat, p = kruskal(one, two, three, four)
print("크러스컬-월리스 검정 통계량 :", stat)
print("p-value :", p)
# 해석
alpha = 0.05
if p > alpha:
print("✔️ 그룹 간 순위의 차이가 통계적으로 유의하지 않습니다. (p > 0.05)")
else:
print("❗ 그룹 간 순위의 차이가 통계적으로 유의합니다. (p ≤ 0.05)")
코드 실행결과
크러스컬-월리스 검정 통계량 : 17.058078446180804 p-value : 0.0006875678653645978 ❗ 그룹 간 순위의 차이가 통계적으로 유의합니다. (p ≤ 0.05)
- 수식을 이용한 검정 통계량 H(16.37)와 파이썬 코드로 구한 검정 통계량 H(17.06)이 값이 다른 이유?
scipy.stats.kruskal()은 동순위가 있는 경우 tie correction(tie 보정)을 자동으로 적용한다.
tie correction : 순위값이 동일한(동순위인) 값들이 있는 경우, 그로 인해 생기는 검정통계량 H의 왜곡을 보정하는 과정.
동순위 처리는 순위합에 영향을 주고, 검정 통계량 H의 분산이 달라진다.
수식 계산은 왜곡을 보정하지 않았고, scipy.stats.kruskal()는 이를 보정하는 과정이 포함되어 있기 때문에 값이 다르게 나온 것이다. (수식 검토 필요)

< 4. 검정 통계량 H 해석 >
유의수준 0.05보다 작기 때문에, 통계적으로 유의한 차이가 있다고 해석할 수 있다.
즉, 아이들에게 제시된 광대에 대한 정보 유형은 공포 점수에 유의미한 영향을 미쳤다고 볼 수 있다.
구체적으로 어떤 차이가 있는지 확인
평균 순위, boxplot
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats as stats
# 데이터프레임 합치기
combined = pd.concat([group1, group2, group3, group4], axis=0).sort_values('beliefs')
combined
avg_ranks = combined.groupby('infotype')['동순위화'].mean()
print("📌 그룹별 평균 순위:\n", avg_ranks)
코드 실행결과
📌 그룹별 평균 순위: infotype Advert 45.033333 Exposure 23.766667 Nothing 31.333333 Story 21.866667 Name: 동순위화, dtype: float64
plt.figure(figsize=(10, 6))
sns.boxplot(x='infotype', y='동순위화', data=combined, palette='Set2')
plt.title("그룹별 순위 분포 (중앙값과 IQR)")
plt.xlabel("그룹")
plt.ylabel("순위 점수")
plt.grid(axis='y')
plt.show()

해석 (보고 방식)
H = 17.06은 카이제곱 분포에서 자유도 3일 때 네 그룹(Advert, Story, Exposure, Missing)의 중앙값 순위가 서로 유의하게 다르다는 것을 나타낸다. p-value = 0.00069는 0.05보다 매우 작기 때문에, 귀무가설을 기각한다.
아이들에게 제시된 광대(clown)에 대한 정보 유형은 광대에 대한 공포 점수에 유의미한 영향을 미쳤다고 할 수 있다.
차이를 좀 더 자세히 서술하자면, 각 그룹의 평균 순위는 다음과 같다. (내림차순)
- Advert 그룹: 45.03
- None 그룹: 31.33
- Exposure 그룹: 23.77
- Story 그룹: 21.87
즉, Advert 그룹이 평균 순위가 가장 높아 공포 점수가 전반적으로 높았고, Story 그룹이 가장 낮아 공포 점수가 가장 낮았다는 것을 의미한다.
이는 광고를 통해 광대를 접한 아이들이 가장 광대를 두려워했다는 해석이 가능하다.
< 5. 사후 검정 : 윌콕슨 순위합 검정 수행 >
2가지 방법
1. 모든 가능한 비교에 대해 윌콕슨 순위합 검정 수행한다.
- 서로 다른 그룹의 평균 순위 차이 >= 임계값
- 부등식의 판정 결과가 이 검정의 유의성이다.
- '평균 순위 차이 >= 임계값' : 두 그룹의 차이는 유의함
- 부등식의 판정 결과가 이 검정의 유의성이다.

- 좌변 : 비교하는 두 그룹의 평균 순위의 차이 크기
- 우변 : 임계 차이
- k : 그룹 수 (예제에서는 4)
- N : 전체 표본 크기(예제에서는 60)
- nu : 비교하는 첫 그룹의 참가자 수
- nv : 둘째 그룹의 참가자 수
- alpha : 관례 상 0.05로
- 모든 그룹의 표본 크기가 동일하다면, 구한 임계 차이(우변)를 모든 그룹에 비교 사용 가능
- 표본 크기가 다르다면, 비교마다 따로 임계 차이(우변)를 구해야 함.
- 모든 그룹을 다른 모든 그룹과 비교할 때는 차이들의 유의성이 너무 엄격하게 판정한다.
- why?
- 다중 비교를 하면 제 1종 오류 확률이 상승하기 때문. ( 1종 오류 : 차이가 없는데 통계적으로 차이가 있다고 잘못 판단하는 오류) - 상세 설명은 [더보기 클릭]
예를 들어 그룹 A, B, C, D가 있다면
A-B, A-C, A-D, B-C, B-D, C-D
이렇게 총 6개의 쌍을 서로 비교하게 됨.
각 비교마다 별도로 유의수준(예: α=0.05)을 적용하면 오류가 누적되어 실제로는 아무 차이도 없는데 우연히 차이나는 것처럼 나오는 확률이 커짐.
한 번만 검정하면 α = 0.05 (즉, 5%)의 확률로 이런 실수를 하게 되지만 여러 번 검정하면?
6쌍을 α=0.05로 각각 비교하면, 5%씩 6번의 기회가 생겨 실제 오류 확률은 1 - (1 - 0.05)^6 ≈ 26.5% 가 됨. 즉, 거짓 양성(false positive) 위험이 급격히 커짐.
그래서 엄격히 판정.
유의성을 확인하려면 더 큰 차이가 있어야 하고, p-value 기준도 더 낮게 잡음.
유의성 판정을 조금 완화하기 위해 다음과 같이 사후검정을 하기도 한다.
2. 기준 그룹(대조군)과 다른 그룹을 비교
비교 항목:
- 테스트 1: Advert vs. None
- 테스트 2: Exposure vs. None
- 테스트 3: Story vs. None
파이썬에서 Kruskal-Wallis 검정 후 Dunn's test나 pairwise comparisons를 수행함으로써 사후검정을 한다.
import pandas as pd
import scikit_posthocs as sp
from scipy.stats import kruskal
import matplotlib.pyplot as plt
import seaborn as sns
# 데이터 준비: beliefs + group
df = pd.concat([
pd.DataFrame({'beliefs': group1['beliefs'], 'group': 'Advert'}),
pd.DataFrame({'beliefs': group2['beliefs'], 'group': 'Story'}),
pd.DataFrame({'beliefs': group3['beliefs'], 'group': 'Exposure'}),
pd.DataFrame({'beliefs': group4['beliefs'], 'group': 'None'})
])
# ✅ Kruskal-Wallis 검정
stat, p = kruskal(
df[df['group'] == 'Advert']['beliefs'],
df[df['group'] == 'Story']['beliefs'],
df[df['group'] == 'Exposure']['beliefs'],
df[df['group'] == 'None']['beliefs']
)
print("📊 Kruskal-Wallis 검정 통계량:", stat)
print("p-value:", p)
# ✅ Dunn's test (Holm 보정, 양측 검정)
posthoc = sp.posthoc_dunn(df, val_col='beliefs', group_col='group', p_adjust='holm')
print("\n🔍 Dunn's 사후검정 결과 (p-value):\n")
print(posthoc)
코드 실행결과
📊 Kruskal-Wallis 검정 통계량: 17.058078446180804
p-value: 0.0006875678653645978
🔍 Dunn's 사후검정 결과 (p-value):
Advert Exposure None Story
Advert 1.000000 0.003251 0.112208 0.001222
Exposure 0.003251 1.000000 0.450119 0.760646
None 0.112208 0.450119 1.000000 0.387182
Story 0.001222 0.760646 0.387182 1.000000
| 비교쌍 | p-value | 유의 여부 (α=0.05) | 해석 |
|---|---|---|---|
| Advert vs Exposure | 0.0033 | ✅ 유의함 | 차이 있음 |
| Advert vs Story | 0.0012 | ✅ 유의함 | 차이 있음 |
| Advert vs None | 0.1122 | ❌ 유의하지 않음 | 차이 없음 |
| Exposure vs None | 0.4501 | ❌ 유의하지 않음 | 차이 없음 |
| Exposure vs Story | 0.7606 | ❌ 유의하지 않음 | 차이 없음 |
| Story vs None | 0.3872 | ❌ 유의하지 않음 | 차이 없음 |
결론 해석
- 정보 제공 방식은 아동의 공포 신념 형성에 영향을 미친다.
- 특히 광고와 비교했을 때, 노출과 이야기는 공포 수준을 유의하게 낮추는 효과를 보였다.
- 반면, 대조군(None) 과 다른 조건들과의 비교에서는 유의한 차이가 나타나지 않았다.
- 따라서 긍정적인 광고는 오히려 아동의 공포 인식을 강화할 수 있으며, 이야기나 실제 노출이 효과적인 공포 완화 전략이 될 수 있다.
< 6. 추세 검정 : 용크헤이러-테르프스트라 검정(Jonckheere Terpstra test) >
- 말 그대로 추세 확인
- 연구에서는 그룹들의 차이에 어떠한 추세가 존재한다는 가설을 세우기도 한다.
- 비교 그룹들의 중앙값들에 어떤 순서 있는 패턴이 존재하는지를 검사
- 중앙값들이 차이 비교 + 그룹들의 순서가 의미가 있는지(오름차순 or 내림차순)
광대 공포증에 대한 예시는 측정 조건끼리의 순서가 중요하지 않은 자료이다.
추세 검정에 대한 설명을 위해 교재의 예시로 설명하면 다음과 같다.
- 기대 : 콩 식품을 많이 먹을수록 정자 수가 줄어들 것
- + 주 0회 > 1회 > 4회 > 7회 순서일 것이다.(중앙값이 내림차순일 것이다.)

- JT : 검정 통계량
- 표본이 클 때는 JT의 표집 분포가 정규 분포이므로 그 평균과 표준편차를 쉽게 정의하고 계산 가능
- JT는 912이고,평균은 1,200, 표준편차는 116.33, p-value는 0.013
- 통계적으로 유의한 추세가 존재한다.
- 추세가 오름차순인지 내림차순인지는 평균 순위들로 알 수 있음
- 콩 식품 섭취량이 증가할수록 정자 수 감소 => 내림차순(감소추세)에 해당함
< 7. 효과 크기 r 계산 >
- 자유도가 2 이상이 경우, 카이제곱 통계량을 효과 크기 r로 변환하는 방법은 x
- 크러스컬-월리스 검정의 유의확률로 정규분포 확률표를 통해 z점수를 구하고 r을 구하면 되지만, 효과 크기가 유용한 경우는 드물다.
< 8. 최종 결과 보고 >
- 필수 :검정 통계량 H, 자유도, 유의확률
- 권장 : 사후 검정 결과, 추세 검정 결과
<보고서>
아동이 가진 광대에 대한 공포 신념은 어떤 방식으로 정보를 전달받았느냐(예: 광고, 노출, 이야기, 아무것도 아님)에 따라 유의미하게 차이가 있었습니다.검정 통계량 H는 17.06이며, 자유도는 3이고, 유의확률(p)은 0.05보다 작으므로 통계적으로 유의합니다.
광고 조건을 기준으로 비교했을 때, 이야기나 노출 조건에서는 공포 수준이 현저히 줄어들었으며, 이 차이는 통계적으로 유의미했습니다.하지만 아무 정보도 받지 않은 대조군과 광고 조건 간의 차이는 유의하지 않았습니다. 대조군을 기준으로 다시 비교해보면, 그 어떤 다른 정보 조건(광고, 노출, 이야기)도 공포 신념에 있어 유의한 차이를 보이지 않았습니다.
{ 15.7 프리드먼 분산 분석 : 연관된 여러 그룹의 비교 }
[ 프리드먼 분산분석이란? ]
일원 반복 측정 분산분석에서 가정 위반에 대안들로 강건한 버전들이 소개되었다. (제 13장)
강건한 버전 중 하나가 프리드먼 분산분석이다.
- 조건이 둘 이상이고 모든 조건에서 같은 참가자들이 쓰인 상황에서 조건들의 차이를 비교하는데 쓰임
- 각 사례가 자료에 여러 개의 점수를 기여하는 상황
- 모수적 검정의 일부 가정이 깨졌을 때, 이 검정을 사용하면 문제 우회 가능
- 순위화된 자료에 기초
- 각 조건의 점수들을 각각 다른 열에 넣는다 ( 조건이 3개일 경우, 열이 3개 )
[ 검정 과정 ]
< 예시 자료 >
- 교재 본문 예시
- 저자는 앤디 킨스 다이어트라는 살 빼기 식이요법을 고안해 냈다. 새 다이어트법의 효험을 시험하기 위해,저자는 살을 빼야 한다고 믿는 여성 10명을 모아서 두 달간 이 다이어트를 실행하게 했다.
- 다이어트 시작 시점
- 1개월 후
- 2개월 후
- 이후, 각각 참가자들의 몸무게(킬로그램 단위)를 측정
import pandas as pd
diet_data = pd.read_csv("Diet.csv")
diet_data
코드 실행결과
| Start | Month1 | Month2 | |
|---|---|---|---|
| 0 | 63.745621 | 65.383694 | 81.340061 |
| 1 | 62.982845 | 66.244556 | 69.310398 |
| 2 | 65.984890 | 67.698470 | 77.893195 |
| 3 | 107.267579 | 102.721548 | 91.325644 |
| 4 | 66.583885 | 69.447077 | 72.869755 |
| 5 | 120.464449 | 119.963757 | 114.258942 |
| 6 | 62.011088 | 66.092468 | 68.010168 |
| 7 | 71.873758 | 73.617196 | 55.431306 |
| 8 | 83.005351 | 75.810791 | 71.628932 |
| 9 | 76.623967 | 67.655458 | 68.600000 |
< 1. 자료 순위화하기 >
- 한 개체를 여러 조건으로 평가하고 수치를 기록했을 것
- 한 개체에 대해 조건에 대한 수치들에 오름차순으로 순위를 부여(동순위화 적용)
- 여러 조건에 대해 순위합을 구함
- 모든 개체에 적용

참가자 1를 예를 들면,
참가자 1의 시작 몸무게, 1개월 후 몸무게, 2개월 후의 몸무게를 보고
그 중 가장 작은 수치를 1, 그 다음을 2, 가장 큰 수치를 3으로 순위화한다.(같을 경우 동순위화)
모든 참가자들에 대해 이러한 방식을 취한다.
각 조건의 순위 (시작, 1개월, 2개월) 를 합해서 조건별 순위합을 구한다.
< 2. 탐색적 분석 (정규성) >
교재 내용

import pandas as pd
import numpy as np
from scipy import stats
# CSV 파일 불러오기
df = pd.read_csv("Diet.csv")
# 결과 저장
results = {}
# 각 열에 대해 통계 계산
for col in df.columns:
x = df[col].dropna()
n = len(x)
mean = np.mean(x)
median = np.median(x)
se_mean = stats.sem(x)
ci_low, ci_high = stats.t.interval(0.95, n-1, loc=mean, scale=se_mean)
var = np.var(x, ddof=1)
std = np.std(x, ddof=1)
coef_var = std / mean
skew = stats.skew(x)
kurt = stats.kurtosis(x, fisher=True)
skew_se = np.sqrt(6/n)
kurt_se = np.sqrt(24/n)
skew_sig = abs(skew) > 2 * skew_se
kurt_sig = abs(kurt) > 2 * kurt_se
w_stat, p_value = stats.shapiro(x)
results[col] = {
"median": median,
"mean": mean,
"SE.mean": se_mean,
"CI.mean.0.95": (ci_low, ci_high),
"var": var,
"std.dev": std,
"coef.var": coef_var,
"skewness": skew,
"skew.2SE": skew_sig,
"kurtosis": kurt,
"krt.2SE": kurt_sig,
"normtest.W": w_stat,
"normtest.p": p_value
}
# 데이터프레임 변환
summary_df = pd.DataFrame(results).round(3)
# 출력
summary_df
코드 실행결과
| Start | Month1 | Month2 | |
|---|---|---|---|
| median | 69.228822 | 68.572773 | 72.249343 |
| mean | 78.054343 | 77.463501 | 77.06684 |
| SE.mean | 6.397315 | 5.886585 | 5.093204 |
| CI.mean.0.95 | (63.582612584950525, 92.5260742276739) | (64.14712060956963, 90.7798822633796) | (65.54521314898845, 88.58846692940499) |
| var | 409.256328 | 346.518851 | 259.407224 |
| std.dev | 20.230085 | 18.615017 | 16.106124 |
| coef.var | 0.259179 | 0.240307 | 0.208989 |
| skewness | 1.235583 | 1.557597 | 1.184235 |
| skew.2SE | False | True | False |
| kurtosis | 0.069044 | 0.851676 | 1.019408 |
| krt.2SE | False | False | False |
| normtest.W | 0.784385 | 0.684879 | 0.877213 |
| normtest.p | 0.009362 | 0.000578 | 0.121203 |
< 정규성 검정 결과 >
- 기저 범주(다이어트 시작 시점) : W(10) = 0.78, p = .009
- 1개월 후 : W(10) =0.68, p < .001
- 시작 시점과 1개월 후 변수의 점수들은 정규분포에서 유의하게 벗어남
- 2개월 후 : W(10) = 0.87, p = .121
- 정규분포라고 봐도 됨
< 3. 검정 통계량 Fr 계산 >
- 참가자들이 많으면(10명 이상) 카이제곱 분포를 따름
- 카이제곱 분포의 자유도 : k-1

- Ri : 각 그룹의 순위합
- N : 전체 표본 크기(예에서는 10)
- k는 조건 개수 (예에서는 3)


파이썬으로 프리드먼 분산분석을 실행하는 코드는 다음과 같다.
import pandas as pd
from scipy.stats import friedmanchisquare
# CSV 파일 읽기
df = pd.read_csv("Diet.csv")
# 각 시점별 체중 데이터를 열 단위로 분리
start = df["Start"]
month1 = df["Month1"]
month2 = df["Month2"]
# 프리드먼 검정 실행
statistic, p_value = friedmanchisquare(start, month1, month2)
# 결과 출력
print("프리드먼 분산분석 결과:")
print(f"검정 통계량 (Friedman chi-square) = {statistic:.4f}")
print(f"p-value = {p_value:.4f}")
# 유의성 판정
alpha = 0.05
if p_value < alpha:
print("[ O ] 시점 간 유의한 차이가 있습니다.")
else:
print("[ X ] 시점 간 유의한 차이가 없습니다.")
코드 실행결과
프리드먼 분산분석 결과: 검정 통계량 (Friedman chi-square) = 0.2000 p-value = 0.9048 [ X ] 시점 간 유의한 차이가 없습니다.
- 검정 통계량 : 0.2
- 자유도 : 3-1 = 2
- 유의확률 : 0.905
- 앤디 킨스 다이어트에 효험이 있다는 증거를 발견하지 못했다. 다이어트 기간의 몸무게 차이가 유의하지 않다.
< 4. 사후 검정 >
- 프리드먼 분산분석이 유의하지 않게 나왔다면? => 사후검정은 수행하지 않는다.
- 사후 검정은 주 효과가 유의할 때 그것을 좀 더 구체적으로 파악하기 위한 것이기 때문
- 프리드먼 분산분석이 유의한 결과를 내놨을 경우
- R에서는 ` friedmanmc() ` 함수를 사용한다.
- 행렬을 요구
- 모든 그룹 VS 모든 그룹
- 대조군 그룹 VS 모든 그룹
- 둘 다 가능
- 행렬을 요구
- R에서는 ` friedmanmc() ` 함수를 사용한다.

- 파이썬에는 자체 구현하거나 라이브러리를 활용한 쌍별 비교를 통해 처리
- 자체 구현 코드
import pandas as pd
import numpy as np
import itertools
from scipy.stats import friedmanchisquare, chi2
# CSV 파일 읽기
df = pd.read_csv("Diet.csv")
# 사후검정을 위한 순위화
ranked_df = df.rank(axis=1) # 각 행(참가자별) 기준 순위화
# 각 조건의 평균 순위 계산
mean_ranks = ranked_df.mean()
# 조건 쌍 조합
pairs = list(itertools.combinations(ranked_df.columns, 2))
# 관측된 순위 차이(obs.dif) 계산
obs_dif = []
for i, j in pairs:
diff = abs(mean_ranks[i] - mean_ranks[j])
obs_dif.append(diff)
# 임계값(critical.dif) 계산
n = df.shape[0] # 참가자 수
k = df.shape[1] # 조건 수
df_chi2 = k - 1
chi2_critical = chi2.ppf(1 - alpha, df=df_chi2)
critical_diff = np.sqrt((k * (k + 1)) / (6 * n)) * np.sqrt(chi2_critical)
# 유의성 판단
difference = [od > critical_diff for od in obs_dif]
# 결과 정리
results = pd.DataFrame({
"Comparison": [f"{i} vs {j}" for i, j in pairs],
"obs.dif": obs_dif,
"critical.dif": [critical_diff] * len(pairs),
"difference": difference
})
# 결과 출력
results
코드 실행결과
| Comparison | obs.dif | critical.dif | difference | |
|---|---|---|---|---|
| 0 | Start vs Month1 | 0.1 | 1.094666 | False |
| 1 | Start vs Month2 | 0.2 | 1.094666 | False |
| 2 | Month1 vs Month2 | 0.1 | 1.094666 | False |
- 사후 검정 (Wilcoxon + Bonferroni 보정)
from scipy.stats import wilcoxon
from statsmodels.stats.multitest import multipletests
import itertools
import numpy as np
import pandas as pd
# 1. CSV 파일 로딩
df = pd.read_csv("Diet.csv")
# 2. 각 시점별 데이터 분리
start = df["Start"]
month1 = df["Month1"]
month2 = df["Month2"]
# 3. 데이터 결합 (행: 참가자, 열: 조건)
data = df[['Start', 'Month1', 'Month2']].to_numpy()
# 4. 조건 쌍 조합
pairs = list(itertools.combinations(range(data.shape[1]), 2))
p_values = []
comparisons = []
# 5. 쌍별 Wilcoxon 검정 수행
for i, j in pairs:
stat, p = wilcoxon(data[:, i], data[:, j])
p_values.append(p)
comparisons.append(f"{df.columns[i]} vs {df.columns[j]}")
# 6. Bonferroni 보정
reject, pvals_corrected, _, _ = multipletests(p_values, method='bonferroni')
# 7. 결과 정리
results = pd.DataFrame({
"Comparison": comparisons,
"Original p-value": p_values,
"Corrected p-value (Bonferroni)": pvals_corrected,
"Significant (α=0.05)": reject
})
# 8. 결과 출력
results
코드 실행결과
| Comparison | Original p-value | Corrected p-value (Bonferroni) | Significant (α=0.05) | |
|---|---|---|---|---|
| 0 | Start vs Month1 | 1.000000 | 1.0 | False |
| 1 | Start vs Month2 | 0.845703 | 1.0 | False |
| 2 | Month1 vs Month2 | 0.921875 | 1.0 | False |
< 5. 효과 크기 r 계산 >
- 자유도가 2 이상인 카이제곱 통계량을 손쉽게 효과크기 r로 변환하는 방법 없음
- 프리드먼 분산분석으로 검사하는 전반적인 효과의 효과크기는 그리 유용하지 않음
< 6. 결과 보고 >
- 필수 : 검정 통계량 Fr (=chi-squared), 자유도, 유의확률
- 권장 : 사후검정
<보고서>
2개월간의 다이어트 프로그램이 참가자들의 몸무게에 유의미한 영향을 미쳤는지 확인하기 위해 프리드먼 분산분석을 실시하였다. 분석 결과, 다이어트 시작 시점, 1개월 후, 2개월 후 사이의 몸무게 변화는 통계적으로 유의하지 않았다(카이제곱(2) = 0.20, p > .05). 이에 따라 주 효과가 유의하지 않으므로, 일반적으로 사후검정은 보고하지 않는다.
그러나 설명을 위해 사후검정을 수행한 결과는 다음과 같다. 본페로니 수정(Bonferroni correction)을 적용한 윌콕슨 부호순위 검정(Wilcoxon signed-rank test) 및 관측된 평균 순위 차이와 임계 차이(critical difference)를 기반으로 비교했을 때, 다음의 모든 비교에서 통계적으로 유의미한 차이는 나타나지 않았다:
- 다이어트 시작 시점 vs 1개월 후
- 다이어트 시작 시점 vs 2개월 후
- 1개월 후 vs 2개월 후
모든 비교에서 관측된 순위 차이는 임계 차이 1.09 (α = 0.05)보다 작았다. 따라서 본 분석 결과에 따르면, 2개월간의 다이어트는 참가자들의 몸무게에 통계적으로 유의한 변화를 유도하지 않은 것으로 해석할 수 있다.
{ 정리 }
- 정규성, 등분산성 + alpha의 이유로 모수적 가정이 깨졌을 때 => 비모수적 검정 시행
- 자료의 순위화(ranking)에 기초함
- 대표적인 비모수적 검정 절차들
- 윌콕슨 순위합 검정(만-위트니 검정)
- 독립적인 두 조건의 비교 : 서로 다른 참가자들로 측정한 두 조건의 차이를 검사할 때 사용 (엑시터시(일요일)-우울증(일요일), 엑시터시(수요일)-우울증(수요일)
- 윌콕슨 부호 순위 검정
- 연관된 두 조건의 비교 : 참가자의 변화에 집중 (엑시터시(일요일)-엑시터시(수요일), 우울증(일요일)-우울증(수요일))
- 크러스컬-월리스 검정
- 여러 독립 그룹의 차이 : 일원 독립 분산 분석의 비모수적 버전
- 프리드먼 검정
- 연관된 여러 그룹의 비교 : 일원 반복 측정 분산분석의 비모수적 버전
- 윌콕슨 순위합 검정(만-위트니 검정)
파이썬 패키지
pip install pandas seaborn matplotlib statsmodels pingouin plotnine
코드 실행결과
| R 패키지 | 주요 기능 | Python 라이브러리 | 설치 명령어 |
|---|---|---|---|
stats (기본 내장) |
t검정, ANOVA, 상관 등 | scipy.stats |
✅ 기본 포함 |
clinfun |
순위 기반 검정 (비모수) | scipy.stats, pingouin |
pip install pingouin |
pastecs |
기술통계량 요약 | pandas, pingouin |
pip install pingouin |
pgirmess |
사후 검정 (Tukey 등) | statsmodels |
pip install statsmodels |
car |
레빈 검정, 회귀 진단 | scipy.stats, pingouin |
✅ 함께 사용 |
ggplot2 |
고급 시각화 | seaborn, matplotlib, plotnine |
pip install seaborn matplotlib plotnine |
'부트캠프 > LG U+ Why not SW Camp 6기' 카테고리의 다른 글
| [LG U+ why not SW camp 6기] Python 프로그래밍 기초 (0) | 2026.01.30 |
|---|---|
| [LG U+ why not SW camp 6기] 머신 러닝 & 딥러닝 개요 (0) | 2026.01.29 |
| LG U+ why not SW camp 상단 캘린더 형식 (0) | 2025.06.04 |
| [LG U+ why not SW camp 6기] python 데이터 분석 2 (5.16.금) (0) | 2025.05.17 |
| [LG U+ why not SW camp 6기] python 데이터 분석 2 (5.15.목) (0) | 2025.05.16 |