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

#8 클래스 불균등 처리

ecosso 2026. 6. 15. 16:13

01 클래스 불균등 처리

 -01 언더샘플링

 -02 오버샘플링

 -03 가중치 조절

 -04 임계값 이동


01 클래스 불균등 처리

 -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 이상이면 소수클래스로 예측하는데, 이 임계값을 낮춤으로써 소수 클래스가 더 많이 나올 수 있도록 조절하는 방식

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

 

  1) 클래스 2개

 ex ) 소수클래스 (y=1), 다수클래스 (Y=0)

 

*  기본 클래스 결정 방식 : 소수클래스 확률 기준으로 0.5 이상 여부 확인

P(Y=1) >= 0.5 -> Y는 1로 결정

P(Y=0) < 0.5 -> Y는 0으로 결정

 

* 소수 클래스 예측률을 올리려면? 소수클래스 예측 결과를 더 많이 얻을 수 있도록 임계값을 변경

P(Y=1) >= 0.3 -> Y는 1로 결정 (임계값이 0.5일때보다 소수클래스가 더 많아짐)

P(Y=1) < 0.3 -> Y는 0으로 결정

 

 2) 클래스 3개 이상

각 클래스의 확률을 각 클래스 빈도 / 비율로 나누어서

다수클래스 확률을 보다 작게, 소수클래스 확률을 보다 크게 만드는 방식

 

0.7           0.2         0.1 <---비율

 

P(Y=0)  P(Y=1)  P(Y=2)

0.1           0.2         0.7            -> 2

0.5           0.2         0.3             -> 0

 

활성화 함수는 softmax : 각 비율을 기준으로 확률이 가장 높은 집단을 선택하는 방식

 

 

[ 연습문제 - 클래스 불균등 조치 ]

 

**** 추후 추가

 


02 회귀 분석

 Y가 존재하는 지도학습 형태

 Y가 수치형인 경우의 분석

 

** 파이썬에서의 회귀분석 모델링 방식

 -01 sklearn 선형회귀모델 사용

 - 코드 구현이 간편 (상수항 추가 기본)

 - score 메서드로 평가점수 쉽게 확인 가능

  (분류에서는 accuracy, 회귀에서는 R2 (수정계수 아님!))

 - 유의성 검정 결과 확인 불가

 

 -02 statsmodels 선형회귀모델 사용

 - 코드 구현 비교적 불편 (상수항 추가 필수)

 - 유의성 검정 결과 확인 가능 (p-value)

 - (빅분기) 작업 3유형에서 회귀모형에서의 특정 변수 p-value를 확인하라는 경우가 있어서 외우는 것이 좋다.

 

 -03 sklearn 분류 알고리즘을 사용한 회귀 적합 (SVR, DTR, RFR 등)

 - 코드 구현 간편 (상수항 추가 기본)

 - score 멤서드(R2)로 평가점수 쉽게 확인 가능

 - 유의성 검정 결과 확인 불가

 - 여러 모형의 장점을 활용한 회귀 적합 가능

 - (빅분기) 작업 2유형을 위해서 외우는 것이 좋다.

 

# ** 회귀 알고리즘

사전에 등분산성을 검정한다.

두 집단의 분산이 같을 때와 다를 때 검정통계량을 계산하는 수식이 달라진다.

 

정규성 가정 -> F 가정 (이론적인 가정으로 현업에서는 잘 사용하지 않는다.)

레빈 테스트 등을 더 많이 사용한다. (정규성 가정을 하지 않는다.)

 

 1) 전통회귀 (릿지, 라쏘, 엘라스틱넷)

 - 여러 통계적 가정 피룡

 - 변수 선택 중요

 - 다중공선성 확인 필수

 - 유의성 검정 결과

 - 인과 관계 파악 가능

 

 - 릿지, 라쏘 엘라스틱넷은 패널티 모형

 각각의 회귀계수가 너무 커지거나 과적합이 되는 것을 막아준다.

 때로는 회귀계수를 0으로 만들어준다. (해당 변수를 제거하는 효과가 있다.)

 

 2) 분류 알고리즘 회귀 적합 (SVM, RF, KNN 등)

 - 예측력 우수

 - 변수선택이 필수가 아님

 - 이상치에 민감하지 않을 수 있음

 - 유의성 검정 결과 제시 x

 - 인과 관계 파악 불가 


[ 예제 - 보스턴 주택가격 예측 ]

# 1. 데이터 로딩
df = pd.read_csv('boston.csv')

X = df.drop(columns = 'medv')
y = df['medv']

# 2. 데이터 분리
from sklearn.model_selection import train_test_split
train_x, test_x, train_y, test_y = train_test_split(X, y, random_state = 0)

# 3. 모델링
# 1) sklearn 선형회귀
from sklearn.linear_model import LinearRegression
m_lr = LinearRegression()
m_lr.fit(train_x, train_y)
m_lr.score(train_x, train_y) # 0.7698
m_lr.score(test_x, test_y)   # 0.6355

# 예측
pre_tr_lr = m_lr.predict(train_x)
pre_te_lr = m_lr.predict(test_x)


# 2) statsmodels
import statsmodels.api as sm

# 상수항 추가
train_x_const = sm.add_constant(train_x)

# 회귀모형 적합
m_lr2 = sm.OLS(train_y, train_x_const).fit()

# 결과확인
m_lr2.summary()  # 유의성 검정 결과 (R2 = 0.770)
# H0 : 모형이 적합하지 않다.
# H1 : 모형이 적합하다.

# Prob (F-statistic):          9.85e-108
# 해당 모형은 적합하다.

m_lr2.summary()
Out[268]: 
<class 'statsmodels.iolib.summary.Summary'>
"""
                            OLS Regression Results                            
==============================================================================
Dep. Variable:                   medv   R-squared:                       0.770
Model:                            OLS   Adj. R-squared:                  0.762
Method:                 Least Squares   F-statistic:                     93.87
Date:                Mon, 15 Jun 2026   Prob (F-statistic):          9.85e-108
Time:                        16:04:56   Log-Likelihood:                -1102.0
No. Observations:                 379   AIC:                             2232.
Df Residuals:                     365   BIC:                             2287.
Df Model:                          13                                         
Covariance Type:            nonrobust                                         
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
const         36.9333      5.682      6.500      0.000      25.759      48.107
crim          -0.1177      0.037     -3.143      0.002      -0.191      -0.044
zn             0.0440      0.015      2.933      0.004       0.015       0.074
indus         -0.0058      0.067     -0.086      0.931      -0.137       0.125
chas           2.3934      0.962      2.488      0.013       0.502       4.285
nox          -15.5894      4.331     -3.600      0.000     -24.106      -7.073
rm             3.7690      0.472      7.981      0.000       2.840       4.698
age           -0.0070      0.015     -0.472      0.637      -0.036       0.022
dis           -1.4350      0.224     -6.401      0.000      -1.876      -0.994
rad            0.2401      0.073      3.301      0.001       0.097       0.383
tax           -0.0113      0.004     -2.779      0.006      -0.019      -0.003
ptratio       -0.9855      0.145     -6.793      0.000      -1.271      -0.700
black          0.0084      0.003      2.782      0.006       0.002       0.014
lstat         -0.4991      0.058     -8.667      0.000      -0.612      -0.386
==============================================================================
Omnibus:                      141.715   Durbin-Watson:                   2.025
Prob(Omnibus):                  0.000   Jarque-Bera (JB):              665.545
Skew:                           1.549   Prob(JB):                    3.01e-145
Kurtosis:                       8.705   Cond. No.                     1.53e+04
==============================================================================

Notes:
[1] Standard Errors assume that the covariance matrix of the errors is correctly specified.
[2] The condition number is large, 1.53e+04. This might indicate that there are
strong multicollinearity or other numerical problems.
"""

 

결론)

1. 모형의 유의성 검정 결과 확인 (F검정)

 H0 : 모형이 유의하지 않다.

 H1 : 모형이 유의하다.

 

dir(m_lr2)

m_lr2.f_pvalue

m_lr2.f_pvalue
Out[271]: np.float64(9.846025348751037e-108)

 

 < 0.05이므로 영가설 기각 -> 모형이 유의함

 

2. 독립변수 유의성 검정 결과 확인 (T검정)

 H0 : 각 회귀계수가 0이다.

 H1 : 각 회귀계수가 0이 아니다.

 

m_lr2.pvalues 

m_lr2.pvalues
Out[273]: 
const      2.645323e-10
crim       1.809488e-03
zn         3.572238e-03
indus      9.311837e-01
chas       1.327649e-02
nox        3.624962e-04
rm         1.901692e-14
age        6.373763e-01
dis        4.734867e-10
rad        1.058885e-03
tax        5.732066e-03
ptratio    4.479130e-11
black      5.678867e-03
lstat      1.461667e-16
dtype: float64

 

# 유의하지 않은 변수 확인

m_lr2.pvalues[m_lr2.pvalues > 0.05]

m_lr2.pvalues[m_lr2.pvalues > 0.05]
Out[274]: 
indus    0.931184
age      0.637376
dtype: float64

 

# 가장 유의한 변수 이름

m_lr2.pvalues.drop('const').idxmin()

m_lr2.pvalues.drop('const').idxmin()
Out[276]: 'lstat'

 

# 가장 유의한 변수의 회귀 계수 확인

m_lr2.params['lstat']

m_lr2.params['lstat']
Out[279]: np.float64(-0.4991167973261213)

 

# 3. 모형의 설명력
m_lr2.rsquared     # 결정계수
m_lr2.rsquared_adj # 수정결정계수 (독립변수의 수가 다른 모형 비교시 사용)

m_lr2.rsquared     # 결정계수
Out[281]: np.float64(0.7697699488741148)

m_lr2.rsquared_adj # 수정결정계수 (독립변수의 수가 다른 모형 비교시 사용)
Out[282]: np.float64(0.7615699744504532)

 

# 3) 기타 분류모형 회귀 적합
from sklearn.svm import SVR
from sklearn.ensemble import RandomForestClassifier