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