아이티윌_데이터 분석 55기/강의내용 필기_분석(Python)

#7 7일차_독립성 검정, 적합도 검정, 클래스 불균등 처리

ecosso 2026. 6. 11. 16:20

01 독립성 검정

02 적합도 검정

03 클래스 불균등 처리

 -01 언더샘플링

 -02 오버샘플링

 -03 가중치 조절

 -04 임계값 이동


01 독립성 검정

 - 범주형 자료 분석의 일종 (빈도분석 = 교차분석 = 카이제곱 검정)

 - 멘델의 유전자 법칙은 적합도 검정~

 - 두 범주형 자료가 독립적인지에 대한 여부를 확인하는 검정 형태

   ex) 성별별 통신사 선호도가 존재하는지에 대한 여부, 타이타닉 데이터에서 성별별 생존 여부가 다른지에 대한 가설검정 등

 

 - 가설의 형태

  H0 : 두 변수가 서로 독립적이다. (연관성이 없다)

  H1 : 두 변수는 서로 독립적이지 않다. (연관성이 있다)

 

 - 카이제곱 통계량이 클수록 (유의확률이 작을수록) 두 변수의 연관성 강도가 커진다. ( ★ ★ ★ )

유의성 정도는 검정통계량의 크기로 비교한다.

검정통계량이 커질수록 유의확률은 감소한다.

가장 유의한 변수 찾기 -> 유의확률이 가장 작음 혹은 검정통계량이 가장 큼

 

따라서 가장 연관성이 있는 변수를 찾기 위해서는 카이제곱 검정의 검정통계량 / 유의확률을 확인하면 된다.


[ 예제 - 타이타닉 데이터에서 여러 변수와 생존여부 (target)의 연관성 확인 ]
import pandas as pd
df = pd.read_csv('titanic.csv')

df.columns
df.info()

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB

 

## 1. 성별에 따른 생존여부 연관성 확인하기

# 성별, 생존 여부 교차표 생성
tab1 = pd.crosstab(df['Sex'], df['Survived'])

pd.crosstab(df['Sex'], df['Survived'])
Out[13]: 
Survived    0    1
Sex               
female     81  233
male      468  109

 

# 카이제곱 검정
from scipy.stats import chi2_contingency
import scipy.stats
dir(scipy.stats)

chi_value1, pvalue1, _,  _ = chi2_contingency(tab1)
# 4개를 지정해야 하는데, 2개의 값만 필요한 경우 이름을 지정하지 않고 _ 로 물리적인 그릇을 만들어준다.
# _ 는 버리는 이름이라고 생각하면 된다.

print('카이제곱통계량:', chi_value1)

print('카이제곱통계량:', chi_value1)
카이제곱통계량: 260.71702016732104

 

print('유의확률:', pvalue1)

print('유의확률:', pvalue1)
유의확률: 1.1973570627755645e-58

 

# 결론) 유의확률이 0.05보다 매우 작기 때문에 영가설을 기각한다.

   -> 성별별 생존여부가 유의한 차이를 가진다.

 

 

유의성 정도는 검정통계량의 크기로 비교한다.

검정통계량이 커질수록 유의확률은 감소한다.

가장 유의한 변수 찾기 -> 유의확률이 가장 작음 혹은 검정통계량이 가장 큼

 

따라서 가장 연관성이 있는 변수를 찾기 위해서는 카이제곱 검정의 검정통계량 / 유의확률을 확인하면 된다.


## 2. 객실등급 (Pclass) 과 생존 여부 교차표 생성
# 객실등급, 생존 여부 교차표 생성
tab2 = pd.crosstab(df['Pclass'], df['Survived'])

# 카이제곱 검정
dir(scipy.stats)

chi_value2, pvalue2, _,  _ = chi2_contingency(tab2)

print('카이제곱통계량:', chi_value2)

print('카이제곱통계량:', chi_value2)
카이제곱통계량: 102.88898875696056


print('유의확률:', pvalue2)

print('유의확률:', pvalue2)
유의확률: 4.549251711298793e-23

# 결론) 유의확률이 0.05보다 매우 작기 때문에 영가설을 기각한다.

   -> 객실등급별 생존여부가 유의한 차이를 가진다. (등급이 높을수록 생존자가 많아진다.)

 


## 3. Pclass vs 생존여부

chi_value1 > chi_value2 

# 자유도에 따라 해석이 달라질 수 있다.

chi_value1 > chi_value2
Out[61]: np.True_


pvalue1 > pvalue2

# 자유도가 다른 경우 유의확률로 비교하는 것이 더 적절하다.

pvalue1 > pvalue2
Out[62]: np.False_

[ 연습문제 (빅분기 실기 대비) - 타이타닉 범주형 변수들 중 생존 여부와 가장 유의한 변수 찾기 ] 
- 타이타닉 데이털르 사용하여 범주형인 독립변수와 종속변수 (생존여부)와의 독립성 검정 수행
- 그 결과 가장 유의(유의확률)한 변수의 검정통계량을 소숫점 셋째자리까지 반올림하여 출력하시오.
- (단, 결측치가 존재하는 행은 제외)
- 범주형 변수 : ['Pclass', 'Sex', 'Embarked', 'SibSp', 'Parch']

 

# 방법1

# Pclass vs Survived
tab1 = pd.crosstab(df['Pclass'], df['Survived'])
chi_value1, pvalue1, _,  _ = chi2_contingency(tab1)

print('카이제곱통계량(Pclass):', chi_value1)
print('유의확률(Pclass):', pvalue1)

# Sex vs Survived
tab2 = pd.crosstab(df['Sex'], df['Survived'])
chi_value2, pvalue2, _,  _ = chi2_contingency(tab2)

print('카이제곱통계량(Sex):', chi_value2)
print('유의확률(Sex):', pvalue2)

# Embarked vs Survived
tab3 = pd.crosstab(df['Embarked'], df['Survived'])
chi_value3, pvalue3, _,  _ = chi2_contingency(tab3)

print('카이제곱통계량(Embarked):', chi_value3)
print('유의확률(Embarked):', pvalue3)

# SibSp vs Survived
tab4 = pd.crosstab(df['SibSp'], df['Survived'])
chi_value4, pvalue4, _,  _ = chi2_contingency(tab4)

print('카이제곱통계량(SibSp):', chi_value4)
print('유의확률(SibSp):', pvalue4)

# Parch vs Survived
tab5 = pd.crosstab(df['Parch'], df['Survived'])
chi_value5, pvalue5, _,  _ = chi2_contingency(tab5)

print('카이제곱통계량(Parch):', chi_value5)
print('유의확률(Parch):', pvalue5)

# 가장 유의한 변수 찾기
max(chi_value1, chi_value2, chi_value3, chi_value4, chi_value5)
min(pvalue1, pvalue2, pvalue3, pvalue4, pvalue5)


# 방법2

df_new = df[['Pclass', 'Sex', 'Embarked', 'SibSp', 'Parch', 'Survived']]
df_new = df_new.dropna()

X = df_new.drop(columns = 'Survived')
y = df_new['Survived']\

 

list_chi = [] ; list_p = []
for col in ['Pclass', 'Sex', 'Embarked', 'SibSp', 'Parch'] :
    ctab = pd.crosstab(df_new[col], y)
    chi_value, pvalue, _, _ = chi2_contingency(ctab)
    list_chi.append(chi_value)
    list_p.append(pvalue)
    
df_result = pd.DataFrame({'chisquare':list_chi, 'pvalue':list_p}, index = X.columns)
cname = df_result['pvalue'].idxmin()
result = df_result.loc[cname, 'chisquare']
print(round(result, 3))

list_chi = [] ; list_p = []
for col in ['Pclass', 'Sex', 'Embarked', 'SibSp', 'Parch'] :
    ctab = pd.crosstab(df_new[col], y)
    chi_value, pvalue, _, _ = chi2_contingency(ctab)
    list_chi.append(chi_value)
    list_p.append(pvalue)

df_result = pd.DataFrame({'chisquare':list_chi, 'pvalue':list_p}, index = X.columns)
cname = df_result['pvalue'].idxmin()
result = df_result.loc[cname, 'chisquare']
print(round(result, 3))

258.427

02 적합도 검정

 - 각 클래스별 도수가 기대도수와 일치하는지를 검정

 - ex) 멘델의 유전자 법칙, 3개 반 학생 수가 같은지에 대한 가설검정 등

 

 - 가설형태

 H0 : 기대분포와 일치한다.

 H1 : 기대분포와 일치하지 않는다.

 

[ 예제 - 타이타닉 데이터를 사용하여 생존 / 사망 비율이 같은지에 대한 가설검정 ]

import pandas as pd
df = pd.read_csv('titanic.csv')
df.info()

# 실제 도수 확인
counts = df['Survived'].value_counts().sort_index() # 사망, 생존 순서대로

# 기대비율 -> 기대도수
n = len(df) # 전체도수
exp = [n*0.5, n*0.5] # 기대도수

# 카이제곱 검정
from scipy.stats import chisquare
chi_value, pvalue = chisquare(counts, exp)

print('카이제곱통계량:', chi_value)
print('유의확률:', pvalue)

print('카이제곱통계량:', chi_value)
카이제곱통계량: 48.09090909090909

print('유의확률:', pvalue)
유의확률: 4.069087128968317e-12


# 결론) 유의확률이 0.05보다 작으므로 영가설을 기각한다. 

  따라서 기대분포(두 도수가 같다)가 다르므로, 생존/사망자의 수가 유의하게 다르다고 할 수 있다.


03 클래스 불균등 처리

- 현상 : 분류 분석 시 각 클래스별 도수의 차이가 심한 경우

- 문제 : 소수 클래스의 학습이 충분하지 않으므로 소수클래스 예측률의 저하가 발생한다.

  (3:7부터 불균등 시작, 2:8 ~ 1:9부터 심각)

- 방법:

 -01 언더샘플링

  다수 클래스 관측치를 소수 클래스 관측치수와 유사하게 젲거하여 균등하게 맞추는 샘플링 기법

 

 1) RandomUnderSampler

 - 다수 클래스 데이터를 랜덤하게 선택하여 제거

 - 다수 : 소수 비율이 정확히 1 : 1이 됨

 - 중요한 데이터의 손실 발생 위험이 존재 (가장 위험한 방법)

 

 2) CondensedNearestNeighbour

 - 뭉쳐져있는 다수 클래스를 중복으로 가정하고 제거 -> 결정결계와 먼 다수클래스를 제거

 - 다수 클래스 데이터에서 랜덤하게 데이터를 선택하며, k개의 가장 가까운 이웃을 확인 후, 그 이웃이 다수클래스 (같은편)이면 제거

 

 3) EditedNearestNeighbours

 - 결정경계 근처에 있는 다수클래스 일부를 제거 -> 좀 더 단순한 결정 경계가 만들어짐

 - 다수 클래스 데이터에서 랜덤하게 데이터 선택, k개의 가장 가까운 이웃을 확인, 그 이웃이 소수클래스 (다른편)이면 제거

 

 4) TomekLinks

- TomekLink : 소수, 다수 클래스 각각 관측치 기준 가장 가까운 이웃이 서로가 되는 한 쌍

- TomekLink 쌍 중에서 다수 클래스를 제거하는 방식

- 결정경계가 복잡하지 않은 경우는 다수 클래스가 제거되지 않을 수 있음

 

 5) OneSidedSelection (OSS)

 - TomekLink + CNN


[ 예제 - 각 언더샘플링 기법 비교 ]
# 1. 데이터 로딩
import pandas as pd
df = pd.read_csv('undersampling_data.csv')
X = df[['x1','x2']]
y = df['y']

# 각 클래스 도수 / 비율 확인
y.value_counts()
y.value_counts(normalize = True)  # 0:다수클래스 (91.6%), 1: 소수클래스(8.4%)

y.value_counts(normalize = True)
Out[138]: 
y
0    0.916045
1    0.083955
Name: proportion, dtype: float64

 

# 2. 언더샘플링
from imblearn.under_sampling import RandomUnderSampler, CondensedNearestNeighbour, EditedNearestNeighbours, TomekLinks, OneSidedSelection
import imblearn.under_sampling
dir(imblearn.under_sampling)

m1 = RandomUnderSampler()
m2 = CondensedNearestNeighbour()
m3 = EditedNearestNeighbours()
m4 = TomekLinks()
m5 = OneSidedSelection()

X1, y1 = m1.fit_resample(X,y)
print(pd.Series(y1).value_counts(normalize = True))

print(pd.Series(y1).value_counts(normalize = True))
y
0    0.5
1    0.5
Name: proportion, dtype: float64


X2, y2 = m2.fit_resample(X,y)
print(pd.Series(y2).value_counts(normalize = True))

print(pd.Series(y2).value_counts(normalize = True))
y
1    0.803571
0    0.196429
Name: proportion, dtype: float64


X3, y3 = m3.fit_resample(X,y)
print(pd.Series(y3).value_counts(normalize = True))

print(pd.Series(y3).value_counts(normalize = True))
y
0    0.913958
1    0.086042
Name: proportion, dtype: float64


X4, y4 = m4.fit_resample(X,y)
print(pd.Series(y4).value_counts(normalize = True))

print(pd.Series(y4).value_counts(normalize = True))
y
0    0.915254
1    0.084746
Name: proportion, dtype: float64


X5, y5 = m5.fit_resample(X,y)
print(pd.Series(y5).value_counts(normalize = True))

print(pd.Series(y5).value_counts(normalize = True))
y
1    0.511364
0    0.488636
Name: proportion, dtype: float64

 

# 3. 시각화
import matplotlib.pyplot as plt
fig, ax = plt.subplots(2,3)

# 기본 산점도
ax[0,0].scatter(X[y==0,0], X[y==0,1], c='crimson', s=40, alpha = 0.8)
ax[0,0].scatter(X[y==1,0], X[y==1,1], c='steelblue', s=40, alpha = 0.7)
ax[0,0].set_title('original')

# RandomUnderSampler
ax[0,1].scatter(X[y==0,0], X[y==0,1], c='salmon', s=40, alpha = 0.8)
ax[0,1].scatter(X1[y1==0,0], X1[y1==0,1], c='crimson', s=40, alpha = 0.8)
ax[0,1].scatter(X1[y1==1,0], X1[y1==1,1], c='steelblue', s=40, alpha = 0.7)
ax[0,1].set_title('RandomUnderSampler')

# CondensedNearestNeighbour
ax[0,2].scatter(X[y==0,0], X[y==0,1], c='salmon', s=40, alpha = 0.8)
ax[0,2].scatter(X2[y2==0,0], X2[y2==0,1], c='crimson', s=40, alpha = 0.8)
ax[0,2].scatter(X2[y2==1,0], X2[y2==1,1], c='steelblue', s=40, alpha = 0.7)
ax[0,2].set_title('CondensedNearestNeighbour')

# EditedNearestNeighbours
ax[1,0].scatter(X[y==0,0], X[y==0,1], c='salmon', s=40, alpha = 0.8)
ax[1,0].scatter(X3[y3==0,0], X3[y3==0,1], c='crimson', s=40, alpha = 0.8)
ax[1,0].scatter(X3[y3==1,0], X3[y3==1,1], c='steelblue', s=40, alpha = 0.7)
ax[1,0].set_title('EditedNearestNeighbours')

# TomekLinks
ax[1,1].scatter(X[y==0,0], X[y==0,1], c='salmon', s=40, alpha = 0.8)
ax[1,1].scatter(X4[y4==0,0], X4[y4==0,1], c='crimson', s=40, alpha = 0.8)
ax[1,1].scatter(X4[y4==1,0], X4[y4==1,1], c='steelblue', s=40, alpha = 0.7)
ax[1,1].set_title('TomekLinks')

# OneSidedSelection
ax[1,2].scatter(X[y==0,0], X[y==0,1], c='salmon', s=40, alpha = 0.8)
ax[1,2].scatter(X5[y5==0,0], X5[y5==0,1], c='crimson', s=40, alpha = 0.8)
ax[1,2].scatter(X5[y5==1,0], X5[y5==1,1], c='steelblue', s=40, alpha = 0.7)
ax[1,2].set_title('OneSidedSelection')


 -02 오버샘플링

 

 소수 클래스 관측치를 다수 클래스 관측치수와 유사하게 임의로 생성하여 균등하게 맞추는 샘플링 기법

 1) RandomOverSampler

 - 소수 클래스 관측치를 동일하게 재생성 -> 데이터의 중복 발생

 - 1:1의 비율로 데이터를 샘플하기 때문에 빈도를 잘 맞추어준다.

 

 2) SMOTE

 - 이웃들 중 하나를 선택하여 이웃과의 사이에 새로운 데이터를 생성

 - 임의의 소수 클래스 데이터 선택, k개의 가장 가까운 이웃 데이터 확인, 그 중 랜덤하게 선택된 다수클래스와의 사이에 소수클래스 생성

 - 결정경계 근처가 아닌 공간에도 소수 클래스 데이터가 생성될 수 있다. = 소수와 다수가 밀집한 공간이 아님에도 불구하고 소수클래스를 배치하면 결정경계가 이동할 수도 있다.

 

 3) BorderlineSMOTE

 - SMOTE와 결정경계가 아닌 지역에도 소수클래스를 생성하는 단점을 보완

 - 소스클래스를 다음의 세 집단으로 구분하여 위험 지역에만 소수 클래스를 생성

  > 위험 : 이웃 중 다수가 다수 클래스

  > 안전 : 이웃 중 소수가 다수 클래스

  > 심플 : 이웃 중 다수 클래스가 아예 없는 경우

 

 - 임의의 소수 클래스 데이터 선택, k개의 가장 가까운 이웃 데이터 확인, 그 중 랜덤하게 선택된 다수 클래스와의 사이에 소수클래스 생성

 - 결정경계 근처에만 소수 클래스 데이터가 생성됨

 

 4) ADASYN

 - 인근 다수클래스 비율 확인 -> 비율을 반영하여 소수클래스 생성


# 1. 데이터 로딩
import pandas as pd
df = pd.read_csv('oversampling_data.csv')
X = df[['x1','x2']].values
y = df['y'].values

# 각 클래스 도수 / 비율 확인
pd.Series(y).value_counts()
pd.Series(y).value_counts(normalize = True) # 0 : 다수클래스 (62%), 1 : 소수클래스 (38%)

# 2. 오버샘플링
from imblearn.over_sampling import RandomOverSampler, SMOTE, BorderlineSMOTE, ADASYN
import imblearn.over_sampling

m1 = RandomOverSampler(random_state=0)
m2 = SMOTE(random_state=0, k_neighbors=5)
m3 = BorderlineSMOTE(random_state=0, k_neighbors=5)
m4 = ADASYN(random_state=0, n_neighbors=5)

X1, y1 = m1.fit_resample(X,y)
print(pd.Series(y1).value_counts())

X2, y2 = m2.fit_resample(X,y)
print(pd.Series(y2).value_counts())

X3, y3 = m3.fit_resample(X,y)
print(pd.Series(y3).value_counts())

X4, y4 = m4.fit_resample(X,y)
print(pd.Series(y4).value_counts())

# 3. 시각화
import matplotlib.pyplot as plt
fig, ax = plt.subplots(2,3)

# 기본 산점도
ax[0,0].scatter(X[y==0,0], X[y==0,1], c='crimson', s=40, alpha = 0.8)
ax[0,0].scatter(X[y==1,0], X[y==1,1], c='steelblue', s=40, alpha = 0.7)
ax[0,0].set_title('original')

# RandomOverSampler
ax[0,1].scatter(X1[y1==0,0], X1[y1==0,1], c='crimson', s=40, alpha = 0.8)
ax[0,1].scatter(X1[y1==1,0], X1[y1==1,1], c='steelblue', s=40, alpha = 0.7)
ax[0,1].set_title('RandomOverSampler')

# SMOTE
ax[0,2].scatter(X2[y2==0,0], X2[y2==0,1], c='crimson', s=40, alpha = 0.8)
ax[0,2].scatter(X2[y2==1,0], X2[y2==1,1], c='steelblue', s=40, alpha = 0.7)
ax[0,2].set_title('SMOTE')

# BorderlineSMOTE
ax[1,0].scatter(X3[y3==0,0], X3[y3==0,1], c='crimson', s=40, alpha = 0.8)
ax[1,0].scatter(X3[y3==1,0], X3[y3==1,1], c='steelblue', s=40, alpha = 0.7)
ax[1,0].set_title('BorderlineSMOTE')

# ADASYN
ax[1,1].scatter(X4[y4==0,0], X4[y4==0,1], c='crimson', s=40, alpha = 0.8)
ax[1,1].scatter(X4[y4==1,0], X4[y4==1,1], c='steelblue', s=40, alpha = 0.7)
ax[1,1].set_title('ADASYN')


 -03 가중치 조절

 소수 클래스 예측 실패에 대해 패널티를 부여하는 기법


 -04 임계값 이동

 이진 클래스) 소수 클래스 확률이 0.5 이상이면 소수클래스로 예측하는데, 이 임계값을 낮춤으로써 소수 클래스가 더 많이 나올 수 있도록 조절하는 방식

 다수 클래스) 각 클래스별 확률을 각 클래스별 도수로 나눈 값으로 클래스 예측을 수행하는 방식