[머신러닝] 데이터가 너무 한쪽으로 치우쳐있는 문제 해결하기 - bin/로그 변환/이상치 제거/Box-Cox 변환

2025. 2. 28. 21:00AI & DS/머신러닝

bin이란?

  • bin은 히스토그램(histogram)에서 데이터를 나누는 구간의 개수를 의미
    • 히스토그램은 연속적인 데이터를 특정 범위로 나누어 빈도수를 시각적으로 표현하는 그래프
  • Bin 개수(구간 수)가 적으면?
    • 너무 뭉뚱그려져서 데이터의 세부적인 분포를 확인하기 어려움
  • Bin 개수(구간 수)가 많으면?
    • 너무 세분화되어서 노이즈(noise)가 많아 보일 수 있음
📌 bin이 많을 때 노이즈가 증가하는 이유
- 과도한 세분화(over-segmentation)
  - 데이터를 작은 구간으로 너무 많이 나누면, 각 bin에 포함된 샘플 개수가 적어짐.
  - 샘플 개수가 적으면 작은 변동(랜덤한 노이즈)도 강조되어 패턴이 아닌 우연한 변동(random fluctuation)이 더 도드라져 보임.

- 데이터의 변동성이 커 보임
  - bin 개수가 적으면 데이터를 전반적인 경향(trend)으로 볼 수 있음.
  - 하지만 bin 개수가 많으면, 미세한 변화까지 시각적으로 강조되면서 실제 의미 없는 변동이 중요한 것처럼 보일 수 있음.
    - 특히, 데이터가 원래부터 잡음이 많은 경우라면 bin을 많이 설정할수록 노이즈가 심해 보일 가능성이 큼.

- 일반화(generalization) 어려움
  - bin을 많이 설정하면 데이터가 너무 좁은 범위로 나뉘어 오버피팅(overfitting)과 비슷한 효과가 나타남
      - 모델이 학습할 때 불필요한 세부적인 특징까지 학습하여 오버피팅하는 것과 같은 효과
  - 즉, 데이터의 근본적인 패턴을 보기 어려워지고, 작은 차이들만 강조될 수 있음.

 - 같은 데이터라도 bin 개수에 따라 다르게 보일 수 있음.
  - 예시 1)  bin이 적을 때 (5개)
      - 전반적인 분포를 쉽게 볼 수 있음. 세부적인 노이즈는 덜 보이고, 큰 패턴(trend)이 부각됨.
  - 예시 2)  bin이 많을 때 (50개)
      - 너무 세분화되어 특정 구간의 작은 차이들이 강조됨. 원래 없던 패턴이 있는 것처럼 보일 수 있음 (노이즈 증가).

 


sns.histplot(df['MedInc'], bins=10)  # 구간을 10개로 설정
sns.histplot(df['MedInc'], bins=50)  # 구간을 50개로 설정 (더 세밀하게 표현)
  • bins=10 → 데이터가 적게 나뉘어 대략적인 분포만 확인 가능
  • bins=50 → 데이터가 더 세밀하게 나뉘어 패턴을 더 잘 볼 수 있음

 해당 그래프에서 bin 개수 조절 이유

bins_dict={
    "MedInc": 35, "HouseAge": 30, "AveRooms": 40, "AveBedrms": 40,
    "Population": 50, "AveOccup": 50, "Latitude": 30, "Longitude": 30
}
  • MedInc, AveRooms, AveBedrms → 이상치(outlier) 포함 가능성 있음 → bin 수를 늘려서 세부 패턴 확인
  • Population, AveOccup → 일부 지역에서 급격한 변화 가능 → bin을 50개로 설정하여 작은 변화도 관찰 가능
  • Latitude, Longitude → 지역적 패턴 확인 → bin을 30개로 설정

그래프 개선 방법 (로그 변환 & 정규화)

히스토그램을 보면 인구수(Population)와 가구당 평균 거주 인원(AveOccup) 등의 데이터는 한쪽으로 치우친(skewed) 분포
이 경우, 로그 변환(log transformation) 또는 정규화(normalization)을 사용하면 더 나은 해석이 가능

 

로그 변환 (Log Transformation)

  • 한쪽으로 치우친 데이터를 정규 분포(Normal Distribution)에 가깝게 변환 가능
  • 값의 차이가 클 때(예: 1 ~ 1000000) 범위를 줄여서 패턴을 더 명확하게 보이도록 만듦

로그 변환 적용 코드

import numpy as np

df['Population_log'] = np.log1p(df['Population'])  # log1p(1 + x) 변환
df['AveOccup_log'] = np.log1p(df['AveOccup'])

 

sns.histplot(df['Population_log'], bins=30, kde=True)
plt.show()
  • Population이 한쪽으로 몰려 있던 분포가 더 균형 잡힌 형태로 변형됨
  • 작은 값과 큰 값의 차이가 줄어들어 해석이 더 쉬워짐

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# 예제 데이터 생성 (Population 데이터 예시)
np.random.seed(42)
population = np.random.exponential(scale=10000, size=1000)  # 원래 데이터 (비로그)
population_log = np.log1p(population)  # 로그 변환된 데이터

# 히스토그램 그리기 (로그 변환된 데이터)
plt.figure(figsize=(10, 5))
sns.histplot(population_log, bins=50, kde=True)

# X축 눈금(로그 변환된 값) -> 원래 값으로 변환
xticks_log = np.linspace(population_log.min(), population_log.max(), 5)  # 로그 스케일 값 선택
xticks_original = np.expm1(xticks_log)  # 원래 값으로 변환 (exp1 = e^x - 1)

plt.xticks(xticks_log, labels=[f"{int(x):,}" for x in xticks_original])  # 원래 값으로 X축 표시
plt.xlabel("Population (Original Scale)")  # X축 라벨 변경
plt.title("Histogram with Original Scale X-axis")
plt.show()

 

 

  • 그래프의 데이터는 여전히 로그 변환된 상태이지만, X축 눈금만 원래 값으로 표시
    • 데이터는 로그 변환된 상태로 유지됨 (왜곡된 분포를 완화하기 위해)
    • X축 눈금은 원래 값으로 변환되어 가독성이 높아짐
    • 모델 학습을 위해서는 로그 변환된 데이터를 사용하고, 시각화할 때만 원래 값으로 표현 가능

정규화 (Normalization)

  • 데이터를 0~1 범위로 변환하여 값의 크기를 일정하게 조정하는 과정
  • 데이터가 서로 다른 스케일을 가질 경우 모델이 학습하기 쉽게 만들어줌

Min-Max 정규화 적용 코드

from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
df[['MedInc', 'HouseAge', 'Population', 'AveRooms']] = scaler.fit_transform(df[['MedInc', 'HouseAge', 'Population', 'AveRooms']])
  • 모든 데이터가 0~1 사이의 값으로 변환됨
  • Population처럼 값의 크기가 큰 특성이 다른 변수보다 영향을 많이 주는 문제 해결

import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import MinMaxScaler

# 데이터 로그 변환
df['Population_log'] = np.log1p(df['Population'])
df['AveOccup_log'] = np.log1p(df['AveOccup'])

# Min-Max 정규화 적용
scaler = MinMaxScaler()
df[['MedInc', 'HouseAge', 'Population', 'AveRooms']] = scaler.fit_transform(df[['MedInc', 'HouseAge', 'Population', 'AveRooms']])

# 히스토그램 다시 그리기
fig, axes = plt.subplots(4, 2, figsize=(12, 10))
fig.suptitle("Feature Distribution (After Log Transformation & Normalization)", fontsize=14)

selected_features = ['MedInc', 'HouseAge', 'AveRooms', 'AveBedrms', 'Population_log', 'AveOccup_log', 'Latitude', 'Longitude']
bins_dict = {"MedInc": 35, "HouseAge": 30, "AveRooms": 40, "AveBedrms": 40, "Population_log": 30, "AveOccup_log": 30, "Latitude": 30, "Longitude": 30}

for i, feature in enumerate(selected_features):
    ax = axes[i//2, i%2]
    sns.histplot(df[feature], bins=bins_dict[feature], kde=True, ax=ax)

plt.show()
  • Population, AveOccup의 분포가 더 균형 있게 변형됨
  • 데이터가 너무 한쪽으로 몰리지 않고, 해석하기 쉬운 형태로 변환됨
  • 전체적인 히스토그램이 더 직관적으로 바뀜

Original / Log Transform / Box-Cox Transform

(좌측) 원본 데이터 (Original)

  • AveRooms (상단 왼쪽)
    • 한쪽(왼쪽)에 데이터가 몰려 있고, 오른쪽에 긴 꼬리 (Right-Skewed, 우측 편향)
    • 평균 방 개수가 대부분 낮은 값(1~10)에 분포
    • 일부 값(예: 100개 이상의 방 개수)은 극단적인 이상치 (Outliers)로 보임
  • AveBedrms (하단 왼쪽)
    • AveRooms와 유사하게 왼쪽에 몰려 있고, 긴 꼬리를 가짐 (우측 편향)
    • 대부분 침실 개수가 1~5 사이에 존재
    • 이상치가 많아 보이며, 분석 및 모델 학습에 불리할 가능성

(중앙) 로그 변환 적용 (Log Transform)

  • AveRooms_log (상단 중앙)
    • 데이터가 정규 분포에 가까워짐 (대칭적인 종 모양)
    • 긴 꼬리가 줄어들었으며, 극단적인 이상치의 영향이 완화됨
    • 모델이 데이터를 학습하기 더 적합한 형태로 변경됨
  • AveBedrms_log (하단 중앙)
    • AveRooms_log와 유사하게 더 정규 분포에 가까워짐
    • 하지만 여전히 일부 데이터가 한쪽에 몰려 있음
로그 변환(Log Transform)의 효과
 - 한쪽으로 치우친 분포(우측 편향, Skewed Right)를 완화하여 정규 분포에 가깝게 변환
 - 값이 큰 이상치(Outlier)의 영향을 줄이고, 작은 값들 사이의 차이를 강조

(우측) Box-Cox 변환 적용 (Box-Cox Transform)

  • AveRooms_boxcox (상단 우측)
    • 가장 정규 분포(Normal Distribution)에 가까운 형태
    • 대칭적인 종 모양을 가지며, 이상치의 영향을 최소화
    • 로그 변환보다 더 정규 분포에 가깝게 변형됨
  • AveBedrms_boxcox (하단 우측)
    • AveRooms_boxcox와 유사하게 대칭적인 분포를 가짐
    • 분포가 부드러워지면서, 모델 학습 시 데이터가 더 잘 활용될 가능성
Box-Cox 변환(Box-Cox Transform)의 효과
 - 로그 변환보다 더 정규 분포에 가깝게 변형
 - 데이터 분포를 정규성에 맞추는 데 효과적 (특히 선형 회귀 모델에 적합)

 

로그 변환 vs Box-Cox 변환
 - 로그 변환은 간단하면서도 데이터 분포를 균형 잡히게 하는 데 효과적
 - Box-Cox 변환은 로그 변환보다 더 정규 분포에 가깝게 만들지만, 데이터에 0 이하의 값이 있으면 적용 불가