본문으로 건너뛰기

041 타겟 변수 변환 (로그 변환)

키워드: 로그 변환, log transform, 비대칭 분포

개요

많은 실제 데이터에서 타겟 변수는 오른쪽으로 치우친(right-skewed) 분포를 가집니다. 주택 가격, 매출, 소득 등이 대표적입니다. 로그 변환을 통해 이러한 비대칭 분포를 정규 분포에 가깝게 만들 수 있습니다.

실습 환경

  • Python 버전: 3.11 권장
  • 필요 패키지: flaml[automl], scikit-learn, numpy
pip install flaml[automl] scikit-learn numpy matplotlib scipy

왜 로그 변환이 필요한가?

비대칭 분포의 문제

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats

# 041 로그 정규 분포 데이터 (실제 가격 데이터와 유사)
np.random.seed(42)
prices = np.random.lognormal(mean=12, sigma=0.5, size=1000)

print("원본 데이터 통계:")
print(f" 최소: {prices.min():,.0f}")
print(f" 최대: {prices.max():,.0f}")
print(f" 평균: {prices.mean():,.0f}")
print(f" 중앙값: {np.median(prices):,.0f}")
print(f" 왜도(Skewness): {stats.skew(prices):.2f}") # 0에 가까울수록 대칭

# 041 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

axes[0].hist(prices, bins=50, edgecolor='black', alpha=0.7)
axes[0].set_xlabel('Price')
axes[0].set_ylabel('Frequency')
axes[0].set_title(f'Original (Skewness: {stats.skew(prices):.2f})')

axes[1].hist(np.log(prices), bins=50, edgecolor='black', alpha=0.7, color='green')
axes[1].set_xlabel('Log(Price)')
axes[1].set_ylabel('Frequency')
axes[1].set_title(f'Log Transformed (Skewness: {stats.skew(np.log(prices)):.2f})')

plt.tight_layout()
plt.show()

MSE에 미치는 영향

# 041 비대칭 분포에서 MSE의 문제
y_true = np.array([100, 150, 200, 250, 10000]) # 이상치 포함
y_pred = np.array([110, 140, 190, 260, 9000])

mse = np.mean((y_true - y_pred) ** 2)
print(f"\n원본 MSE: {mse:,.0f}")
print("→ 이상치(10000)가 MSE를 지배함")

# 041 로그 변환 후
y_true_log = np.log(y_true)
y_pred_log = np.log(y_pred)
mse_log = np.mean((y_true_log - y_pred_log) ** 2)
print(f"로그 MSE: {mse_log:.4f}")
print("→ 상대적 오차에 집중")

로그 변환 방법

기본 로그 변환

# 041 양수 데이터에 대한 로그 변환
y = np.array([100, 500, 1000, 5000, 10000])

y_log = np.log(y)
print("기본 로그 변환:")
print(f" 원본: {y}")
print(f" log: {y_log.round(2)}")

# 041 역변환
y_original = np.exp(y_log)
print(f" 역변환: {y_original.round(0)}")

log1p 변환 (0 포함 데이터)

# 0을 포함하는 데이터
y_with_zero = np.array([0, 1, 10, 100, 1000])

# 041 log(0)은 -inf
# 041 y_log_fail = np.log(y_with_zero) # 에러!

# 041 log1p: log(1 + y) 사용
y_log1p = np.log1p(y_with_zero)
print("\nlog1p 변환 (0 포함):")
print(f" 원본: {y_with_zero}")
print(f" log1p: {y_log1p.round(2)}")

# 041 역변환: expm1
y_original = np.expm1(y_log1p)
print(f" 역변환: {y_original.round(0)}")

Box-Cox 변환

from scipy.stats import boxcox, boxcox_normmax

# 041 Box-Cox는 양수만 가능
y_positive = prices.copy()

# 041 최적 lambda 찾기
y_boxcox, lambda_opt = boxcox(y_positive)

print(f"\nBox-Cox 변환:")
print(f" 최적 lambda: {lambda_opt:.4f}")
print(f" lambda=0이면 log 변환")
print(f" 변환 후 왜도: {stats.skew(y_boxcox):.4f}")

FLAML에서 로그 변환 적용

데이터 준비

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from flaml import AutoML
from sklearn.metrics import mean_squared_error, r2_score

# 041 캘리포니아 주택 데이터
data = fetch_california_housing()
X = data.data
y = data.target

# 041 타겟 분포 확인
print("캘리포니아 주택 가격 분포:")
print(f" 왜도: {stats.skew(y):.4f}")

X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42
)

변환 없이 학습

# 041 원본 타겟
automl_original = AutoML()
automl_original.fit(
X_train, y_train,
task="regression",
time_budget=60,
metric="mse",
verbose=0
)

y_pred_original = automl_original.predict(X_test)
mse_original = mean_squared_error(y_test, y_pred_original)
r2_original = r2_score(y_test, y_pred_original)

print(f"원본 타겟:")
print(f" MSE: {mse_original:.4f}")
print(f" R²: {r2_original:.4f}")

로그 변환 후 학습

# 041 로그 변환
y_train_log = np.log(y_train)
y_test_log = np.log(y_test)

automl_log = AutoML()
automl_log.fit(
X_train, y_train_log, # 로그 변환된 타겟
task="regression",
time_budget=60,
metric="mse",
verbose=0
)

# 041 예측 후 역변환
y_pred_log = automl_log.predict(X_test)
y_pred_exp = np.exp(y_pred_log) # 역변환

mse_log = mean_squared_error(y_test, y_pred_exp)
r2_log = r2_score(y_test, y_pred_exp)

print(f"\n로그 변환 타겟:")
print(f" MSE: {mse_log:.4f}")
print(f" R²: {r2_log:.4f}")

결과 비교

print("\n비교 요약:")
print("-" * 40)
print(f"{'방법':<20} {'MSE':<10} {'R²':<10}")
print("-" * 40)
print(f"{'원본':<20} {mse_original:<10.4f} {r2_original:<10.4f}")
print(f"{'로그 변환':<20} {mse_log:<10.4f} {r2_log:<10.4f}")

로그 변환 래퍼 클래스

class LogTransformedRegressor:
"""로그 변환을 자동 적용하는 회귀 래퍼"""

def __init__(self, base_model=None, use_log1p=True):
self.base_model = base_model
self.use_log1p = use_log1p

def fit(self, X, y, **kwargs):
# 로그 변환
if self.use_log1p:
self.y_transformed = np.log1p(y)
else:
self.y_transformed = np.log(y)

# 모델 학습
if self.base_model is None:
self.base_model = AutoML()

self.base_model.fit(X, self.y_transformed, **kwargs)
return self

def predict(self, X):
# 예측 후 역변환
y_pred_transformed = self.base_model.predict(X)

if self.use_log1p:
return np.expm1(y_pred_transformed)
else:
return np.exp(y_pred_transformed)

def score(self, X, y):
y_pred = self.predict(X)
return r2_score(y, y_pred)

# 041 사용 예
log_regressor = LogTransformedRegressor(use_log1p=False)
log_regressor.fit(X_train, y_train, task="regression", time_budget=30, verbose=0)

print(f"\n래퍼 클래스 R²: {log_regressor.score(X_test, y_test):.4f}")

언제 로그 변환을 사용할까?

def should_use_log_transform(y, threshold=0.5):
"""로그 변환 필요성 판단"""
skewness = stats.skew(y)
all_positive = (y > 0).all()
has_outliers = (y > y.mean() + 3 * y.std()).any()

print("로그 변환 필요성 분석:")
print(f" 왜도: {skewness:.4f} (|왜도| > {threshold}이면 비대칭)")
print(f" 모두 양수: {all_positive}")
print(f" 이상치 존재: {has_outliers}")

if abs(skewness) > threshold and all_positive:
print(" → 로그 변환 권장")
return True
else:
print(" → 로그 변환 불필요")
return False

# 041 테스트
should_use_log_transform(y)

변환 비교 시각화

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 041 원본 vs 예측 (원본)
axes[0, 0].scatter(y_test, y_pred_original, alpha=0.3, s=10)
axes[0, 0].plot([0, 5], [0, 5], 'r--')
axes[0, 0].set_xlabel('Actual')
axes[0, 0].set_ylabel('Predicted')
axes[0, 0].set_title(f'Original (R² = {r2_original:.4f})')

# 041 원본 vs 예측 (로그 변환)
axes[0, 1].scatter(y_test, y_pred_exp, alpha=0.3, s=10)
axes[0, 1].plot([0, 5], [0, 5], 'r--')
axes[0, 1].set_xlabel('Actual')
axes[0, 1].set_ylabel('Predicted')
axes[0, 1].set_title(f'Log Transformed (R² = {r2_log:.4f})')

# 041 잔차 분포 (원본)
residuals_original = y_test - y_pred_original
axes[1, 0].hist(residuals_original, bins=50, edgecolor='black', alpha=0.7)
axes[1, 0].axvline(x=0, color='r', linestyle='--')
axes[1, 0].set_xlabel('Residual')
axes[1, 0].set_title('Residuals (Original)')

# 041 잔차 분포 (로그 변환)
residuals_log = y_test - y_pred_exp
axes[1, 1].hist(residuals_log, bins=50, edgecolor='black', alpha=0.7, color='green')
axes[1, 1].axvline(x=0, color='r', linestyle='--')
axes[1, 1].set_xlabel('Residual')
axes[1, 1].set_title('Residuals (Log Transformed)')

plt.tight_layout()
plt.show()

정리

  • 로그 변환은 오른쪽 치우친 분포를 정규화합니다.
  • np.log(): 기본 로그 변환 (양수만)
  • np.log1p(): 0을 포함하는 데이터에 사용
  • 예측 후 역변환(exp)을 잊지 마세요.
  • 왜도(skewness)가 높고 모두 양수일 때 효과적입니다.
  • 가격, 매출, 인구 등 비음수 연속값에 유용합니다.

다음 글 예고

다음 글에서는 회귀에서의 이상치 처리에 대해 알아보겠습니다. 이상치가 회귀 모델에 미치는 영향과 처리 방법을 다룹니다.


FLAML AutoML 마스터 시리즈 #041