6  데이터 분포 변환

Keywords

python, 전처리, 통계, 가설검정, 기계학습, 회귀, 분류, 군집, 모델 학습, 모델 평가

데이터 분포 변환(Distribution Transformation)은 왜곡된 데이터 분포를 정규분포에 가깝게 만드는 과정이다. 많은 통계 모델과 머신러닝 알고리즘은 데이터가 정규분포를 따른다고 가정하므로, 왜곡된 분포는 모델 성능을 저하시킬 수 있다. 분포 변환을 통해 극단값의 영향을 완화하고 모델의 가정을 충족시킬 수 있다. 이 장에서는 로그 변환, 제곱근 변환, Box-Cox 변환, Yeo-Johnson 변환 등 다양한 분포 변환 기법을 학습한다.

예제: 데이터 로드

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

# 데이터 로드
df = sns.load_dataset("penguins")

# 수치형 컬럼 선택
num_cols = [
    "bill_length_mm",
    "bill_depth_mm",
    "flipper_length_mm",
    "body_mass_g"
]

df_num = df[num_cols]

6.1 분포 변환의 필요성

실제 데이터는 정규분포보다는 왜곡된 분포를 보이는 경우가 많다. 특히 양의 왜도(positive skewness)를 가진 데이터가 일반적이다. 왜도는 분포의 비대칭 정도를 나타내는 지표로, 0에 가까울수록 대칭적이고 절댓값이 클수록 왜곡이 심하다.

예제: 원본 데이터의 왜도 확인

# 각 변수의 왜도 계산
skewness = df_num.skew()

print("변수별 왜도:")
print(skewness)
print("\n해석:")
print("- 왜도 > 0.5: 오른쪽으로 치우침 (양의 왜도)")
print("- -0.5 < 왜도 < 0.5: 대칭에 가까움")
print("- 왜도 < -0.5: 왼쪽으로 치우침 (음의 왜도)")
변수별 왜도:
bill_length_mm       0.053118
bill_depth_mm       -0.143465
flipper_length_mm    0.345682
body_mass_g          0.470329
dtype: float64

해석:
- 왜도 > 0.5: 오른쪽으로 치우침 (양의 왜도)
- -0.5 < 왜도 < 0.5: 대칭에 가까움
- 왜도 < -0.5: 왼쪽으로 치우침 (음의 왜도)

왜곡된 분포는 다음과 같은 문제를 일으킨다.

문제점 설명
모델 가정 위배 선형 회귀, PCA 등은 정규성을 가정함
극단값 영향 평균과 분산이 소수 극단값에 의해 왜곡됨
예측 성능 저하 학습 데이터와 테스트 데이터의 분포 차이 발생
해석 어려움 비선형 관계로 인한 계수 해석의 복잡성 증가

분포 변환의 목표는 데이터를 대칭에 가깝게 만들어 극단값의 영향을 완화하고 모델의 가정을 충족시키는 것이다.

6.2 로그 변환 (Log Transform)

로그 변환은 오른쪽 꼬리가 긴 분포(양의 왜도)를 가진 데이터에 가장 효과적인 방법이다. 지수적으로 증가하는 데이터를 선형에 가깝게 만들어준다.

변환 공식

\[ x' = \log(x) \]

로그 변환은 큰 값의 차이를 압축하고 작은 값의 차이를 확대하여 분포를 대칭에 가깝게 만든다. 다만, 0 이하의 값은 변환할 수 없으므로 보정이 필요하다.

예제: 기본 로그 변환

# 로그 변환 (자연로그 사용)
df_log = df_num.copy()

for col in num_cols:
    df_log[col] = np.log(df_log[col])

print("로그 변환 결과 (상위 5개):")
print(df_log.head())
로그 변환 결과 (상위 5개):
   bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
0        3.666122       2.928524           5.198497     8.229511
1        3.676301       2.856470           5.225747     8.242756
2        3.696351       2.890372           5.273000     8.086410
3             NaN            NaN                NaN          NaN
4        3.602777       2.960105           5.262690     8.146130

6.2.1 0 포함 변수의 안전한 처리

데이터에 0이 포함된 경우 np.log(0)-inf를 반환하므로 문제가 발생한다. 이를 방지하기 위해 np.log1p(x)를 사용한다. 이 함수는 \(\log(1 + x)\)를 계산하므로 0도 안전하게 처리할 수 있다.

예제: log1p를 사용한 안전한 로그 변환

# log1p: log(1 + x) 계산
df_log_safe = np.log1p(df_num)

print("log1p 변환 결과 (상위 5개):")
print(df_log_safe.head())
log1p 변환 결과 (상위 5개):
   bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
0        3.691376       2.980619           5.204007     8.229778
1        3.701302       2.912351           5.231109     8.243019
2        3.720862       2.944439           5.278115     8.086718
3             NaN            NaN                NaN          NaN
4        3.629660       3.010621           5.267858     8.146419

6.2.2 왜도 변화 확인

로그 변환 후 왜도가 어떻게 변했는지 확인한다.

예제: 로그 변환 후 왜도 비교

# 변환 전후 왜도 비교
print("원본 왜도:")
print(df_num.skew())
print("\n로그 변환 후 왜도:")
print(df_log_safe.skew())
원본 왜도:
bill_length_mm       0.053118
bill_depth_mm       -0.143465
flipper_length_mm    0.345682
body_mass_g          0.470329
dtype: float64

로그 변환 후 왜도:
bill_length_mm      -0.143463
bill_depth_mm       -0.315791
flipper_length_mm    0.251965
body_mass_g          0.170721
dtype: float64

일반적으로 로그 변환 후 왜도의 절댓값이 감소하여 분포가 더 대칭적이 된다.

적용 상황

  • 왜도가 매우 큰 경우 (> 1.0)
  • 극단값(이상치)이 많은 경우
  • 지수적 증가 패턴을 보이는 데이터
  • 금융 데이터, 인구 데이터, 소득 데이터 등
  • “비율 변화”가 중요한 문제

6.3 제곱근 변환 (Square Root Transform)

제곱근 변환은 로그 변환보다 완만한 변환 방법으로, 왜도가 그리 심하지 않은 데이터에 적합하다. 포아송 분포를 따르는 카운트 데이터에 특히 효과적이다.

변환 공식

\[ x' = \sqrt{x} \]

예제: 제곱근 변환

# 제곱근 변환
df_sqrt = np.sqrt(df_num)

print("제곱근 변환 결과 (상위 5개):")
print(df_sqrt.head())

# 왜도 확인
print("\n제곱근 변환 후 왜도:")
print(df_sqrt.skew())
제곱근 변환 결과 (상위 5개):
   bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
0        6.252999       4.324350          13.453624    61.237244
1        6.284903       4.171331          13.638182    61.644140
2        6.348228       4.242641          13.964240    57.008771
3             NaN            NaN                NaN          NaN
4        6.058052       4.393177          13.892444    58.736701

제곱근 변환 후 왜도:
bill_length_mm      -0.048524
bill_depth_mm       -0.235482
flipper_length_mm    0.298802
body_mass_g          0.322080
dtype: float64

6.3.1 로그 변환과 제곱근 변환 비교

두 변환 방법의 선택 기준은 다음과 같다.

제곱근 변환이 적합한 경우

  • 왜도가 심하지 않음 (0.5 ~ 1.5)
  • 데이터 범위가 크지 않음
  • 분포를 약간만 조정하고 싶을 때
  • 카운트 데이터 (포아송 분포)
  • 변환 전후 해석이 직관적이어야 할 때

로그 변환이 적합한 경우

  • 왜도가 매우 큼 (> 1.5)
  • 극단값(이상치)이 많음
  • 데이터 범위가 매우 넓음 (여러 자릿수 차이)
  • 선형 회귀, PCA, 거리 기반 모델 사용
  • “비율 변화”가 중요한 문제 (예: 수익률, 성장률)

6.4 Box-Cox 변환

Box-Cox 변환은 로그 변환의 일반화된 형태로, 최적의 변환 파라미터 λ(람다)를 자동으로 추정한다. 다양한 변환을 포괄하는 강력한 방법이지만 양수 데이터에만 적용할 수 있다.

변환 공식

\[ x' = \begin{cases} \frac{x^\lambda - 1}{\lambda}, & \lambda \neq 0 \\ \log(x), & \lambda = 0 \end{cases} \]

여기서 λ는 최대우도추정법(MLE)을 통해 자동으로 결정된다. λ 값에 따라 다음과 같은 변환이 수행된다.

λ 값 변환 방법
λ = 1 변환 없음
λ = 0.5 제곱근 변환
λ = 0 로그 변환
λ = -1 역수 변환

예제: Box-Cox 변환

from scipy.stats import boxcox

# Box-Cox 변환
df_boxcox = df_num.copy()
lambdas = {}

for col in num_cols:
    # 결측치 제거
    data = df_boxcox[col].dropna()
    
    # Box-Cox 변환 및 최적 람다 추정
    transformed, lam = boxcox(data)
    
    # 변환 결과 저장
    df_boxcox.loc[data.index, col] = transformed
    lambdas[col] = lam

print("Box-Cox 변환 결과 (상위 5개):")
print(df_boxcox.head())
Box-Cox 변환 결과 (상위 5개):
   bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
0       14.051923      50.248024           0.484671     2.101002
1       14.151114      45.114142           0.484672     2.101288
2       14.348357      47.461799           0.484673     2.097796
3             NaN            NaN                NaN          NaN
4       13.448474      52.676024           0.484672     2.099160

예제: 추정된 λ 값 확인

# 각 변수의 최적 람다 값
print("변수별 최적 λ 값:")
for col, lam in lambdas.items():
    print(f"{col}: {lam:.4f}")
변수별 최적 λ 값:
bill_length_mm: 0.6202
bill_depth_mm: 1.4748
flipper_length_mm: -2.0632
body_mass_g: -0.4657

λ 값이 0에 가까우면 로그 변환에 가깝고, 0.5에 가까우면 제곱근 변환에 가깝다.

적용 상황

  • 양수 데이터만 있는 경우
  • 최적의 변환을 자동으로 찾고 싶을 때
  • 통계적 가정(정규성)을 엄격히 만족해야 하는 경우

6.5 Yeo-Johnson 변환

Yeo-Johnson 변환은 Box-Cox 변환의 확장 버전으로, 0과 음수 값도 처리할 수 있다. 실무에서 가장 권장되는 방법이다.

변환 공식

Yeo-Johnson 변환은 데이터의 부호에 따라 다른 공식을 적용한다.

  • \(x \geq 0, \lambda \neq 0\): \(\frac{(x+1)^\lambda - 1}{\lambda}\)
  • \(x \geq 0, \lambda = 0\): \(\log(x+1)\)
  • \(x < 0, \lambda \neq 2\): \(-\frac{(-x+1)^{2-\lambda} - 1}{2-\lambda}\)
  • \(x < 0, \lambda = 2\): \(-\log(-x+1)\)

예제: Yeo-Johnson 변환

from sklearn.preprocessing import PowerTransformer

# Yeo-Johnson 변환기 생성
pt = PowerTransformer(method="yeo-johnson")

# 변환 수행
df_yeojohnson = pd.DataFrame(
    pt.fit_transform(df_num),
    columns=num_cols
)

print("Yeo-Johnson 변환 결과 (상위 5개):")
print(df_yeojohnson.head())

# 추정된 람다 값 확인
print("\n변수별 최적 λ 값:")
for i, col in enumerate(num_cols):
    print(f"{col}: {pt.lambdas_[i]:.4f}")
Yeo-Johnson 변환 결과 (상위 5개):
   bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
0       -0.878666       0.778009          -1.580741    -0.481138
1       -0.802301       0.099099          -1.104687    -0.407311
2       -0.650437       0.409486          -0.339985    -1.308479
3             NaN            NaN                NaN          NaN
4       -1.343166       1.099289          -0.500529    -0.956498

변수별 최적 λ 값:
bill_length_mm: 0.6153
bill_depth_mm: 1.5132
flipper_length_mm: -2.0775
body_mass_g: -0.4660

Yeo-Johnson 변환은 다음과 같은 장점이 있다.

  • 0과 음수 값을 자동으로 처리
  • 결측치를 제외하고 자동 변환
  • sklearn 파이프라인과 통합 가능
  • 실무에서 가장 안전하고 범용적

적용 상황

  • 0 또는 음수를 포함하는 데이터
  • 실무에서 기본적으로 사용할 변환 방법
  • 자동화된 전처리 파이프라인 구축

6.6 분위수 변환 (Quantile Transform)

분위수 변환은 데이터의 분위수를 목표 분포의 분위수로 매핑하여 분포 자체를 강제로 변환하는 방법이다. 다른 변환 방법과 달리 순위 정보만 유지하고 분포 형태를 완전히 바꾼다.

예제: 분위수 변환 (정규분포)

from sklearn.preprocessing import QuantileTransformer

# 분위수 변환기 생성 (출력 분포: 정규분포)
qt = QuantileTransformer(
    output_distribution="normal",
    random_state=42
)

# 변환 수행
df_quantile = pd.DataFrame(
    qt.fit_transform(df_num),
    columns=num_cols
)

print("분위수 변환 결과 (상위 5개):")
print(df_quantile.head())

# 왜도 확인
print("\n분위수 변환 후 왜도:")
print(df_quantile.skew())
분위수 변환 결과 (상위 5개):
   bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
0       -0.704794       0.690633          -1.663512    -0.366106
1       -0.649469       0.033085          -1.127632    -0.269985
2       -0.506872       0.288983          -0.176300    -1.347428
3             NaN            NaN                NaN          NaN
4       -1.237926       1.107167          -0.335021    -0.890537

분위수 변환 후 왜도:
bill_length_mm       0.000202
bill_depth_mm       -0.010882
flipper_length_mm   -0.020028
body_mass_g          0.006647
dtype: float64

분위수 변환은 왜도가 거의 0에 가까워지며 완벽한 정규분포에 가까운 형태가 된다.

장단점

장점 단점
완벽한 정규분포 생성 원본 데이터의 실제 거리 정보 손실
이상치 영향 제거 새로운 데이터 처리 시 범위 문제 발생 가능
비정상 분포에도 효과적 해석이 어려움

적용 상황

  • 극심하게 왜곡된 비정상 분포
  • 트리 기반 모델이 아닌 알고리즘 사용
  • 이상치를 완전히 무시하고 싶을 때
  • 순위 정보만 중요한 경우

6.7 변환 전후 왜도 비교

다양한 변환 방법의 효과를 정량적으로 비교한다.

예제: 변환 방법별 왜도 비교

# 왜도 비교 테이블
skew_compare = pd.DataFrame({
    "원본": df_num.skew(),
    "로그": df_log_safe.skew(),
    "제곱근": df_sqrt.skew(),
    "Box-Cox": df_boxcox.skew(),
    "Yeo-Johnson": df_yeojohnson.skew(),
    "분위수": df_quantile.skew()
})

print("변환 방법별 왜도 비교:")
print(skew_compare.round(3))
변환 방법별 왜도 비교:
                      원본     로그    제곱근  Box-Cox  Yeo-Johnson    분위수
bill_length_mm     0.053 -0.143 -0.049   -0.024       -0.024  0.000
bill_depth_mm     -0.143 -0.316 -0.235   -0.055       -0.053 -0.011
flipper_length_mm  0.346  0.252  0.299    0.050        0.050 -0.020
body_mass_g        0.470  0.171  0.322    0.026        0.026  0.007

일반적으로 분위수 변환이 왜도를 가장 효과적으로 0에 가깝게 만들지만, 원본 데이터의 정보 손실이 가장 크다.

6.8 분포 변환과 스케일링의 관계

분포 변환과 스케일링은 서로 다른 목적을 가진 전처리 과정이다.

분포 변환과 스케일링 비교

구분 분포 변환 스케일링
목적 분포의 모양 변경 (대칭화) 변수의 크기 조정
효과 왜도 감소, 정규성 개선 변수 간 스케일 통일
변환 예시 로그, 제곱근, Box-Cox Min-Max, Standard, Robust
적용 순서 먼저 수행 나중에 수행

일반적인 전처리 순서

  1. 결측치 처리: 결측값 제거 또는 대체
  2. 이상치 처리: 이상치 탐지 및 제거/대체
  3. 분포 변환: 왜곡된 분포를 대칭화 (본 장)
  4. 스케일링: 변수 간 크기 통일 (이전 장)
  5. 인코딩: 범주형 변수를 수치형으로 변환

분포 변환을 먼저 수행하는 이유는 왜곡된 분포에서 스케일링을 하면 극단값의 영향을 충분히 제거하지 못하기 때문이다.

6.9 요약

이 장에서는 왜곡된 데이터 분포를 정규분포에 가깝게 만드는 다양한 변환 기법을 학습했다. 주요 내용은 다음과 같다.

분포 변환 방법 비교

방법 공식 장점 단점 적용 상황
로그 \(\log(x)\) 간단, 해석 용이 0 이하 불가 왜도가 큰 양수 데이터, 지수적 증가
제곱근 \(\sqrt{x}\) 완만한 변환, 직관적 효과 제한적 약한 왜도, 카운트 데이터
Box-Cox \(\frac{x^\lambda - 1}{\lambda}\) 최적 λ 자동 추정 양수 전용 양수 데이터, 최적 변환 필요
Yeo-Johnson Box-Cox 확장 0, 음수 처리 가능 해석 복잡 실무 기본 권장 (범용적)
분위수 순위 기반 매핑 완벽한 정규분포 생성 정보 손실 극심한 왜곡, 비정상 분포

분포 변환 선택 가이드

  1. 기본 선택: Yeo-Johnson 변환 (0, 음수 처리 가능, 실무 권장)
  2. 단순한 경우: 로그 변환 (양수만 있고 왜도가 큰 경우)
  3. 약한 왜곡: 제곱근 변환 (카운트 데이터, 포아송 분포)
  4. 최적화 필요: Box-Cox 변환 (양수 데이터, 통계적 엄격성)
  5. 극심한 왜곡: 분위수 변환 (비정상 분포, 순위만 중요)

실무 전처리 순서

분포 변환 → 스케일링 순서로 진행한다.

  • 분포 변환: 데이터 분포의 모양을 대칭에 가깝게 변경
  • 스케일링: 변수 간 크기를 통일

이 순서를 지키면 극단값의 영향을 효과적으로 완화하고 모델의 성능을 향상시킬 수 있다.

분포 변환은 모델의 가정을 충족시키고 예측 성능을 향상시키는 중요한 전처리 과정이다. 데이터의 분포 특성과 모델의 요구사항을 고려하여 적절한 변환 방법을 선택하는 것이 중요하다. 다음 장에서는 범주형 변수를 수치형으로 변환하는 인코딩 기법을 학습할 것이다.