Day 074 — EDA体系的手法⑥ — 目的変数の変換(log変換・Box-Cox)

2026-06-24 水 / Phase 3 分析 log変換 / Box-Cox / 歪度 / SalePrice

📚 背景知識(読んでから問題へ)

🎯
Day 074 — Phase 3 継続: EDA第6弾は目的変数の変換。SalePriceのような価格データは右歪みが強く、そのまま回帰モデルに入れると外れ値に引きずられる。log変換・Box-Cox変換で正規分布に近づける手法を習得する。

なぜ目的変数を変換するのか?

多くの回帰モデルは目的変数が正規分布に近いことを前提としています。しかし現実の価格データは右に歪んだ分布(正の歪み)になりがちです。

問題原因変換による解決
外れ値が予測を引っ張る超高額物件が RMSE を爆増させるlog変換で外れ値の影響を圧縮
残差が正規分布しない予測の信頼区間が不正確になる変換後は残差が正規分布に近づく
評価指標の歪みRMSLE を使う場合 log変換が本質log空間の RMSE = RMSLE と等価

log変換の仕組み

log(x) は「桁数を圧縮する」変換です。10倍の差が一定の差に縮まります。

元の値(SalePrice)log 変換後圧縮効果
$10,000(1万ドル)9.21100倍の差 → 2.3 の差
$100,000(10万ドル)11.51
$1,000,000(100万ドル)13.82

歪度(Skewness)の判断基準

|skewness| の値分布の状態対処
< 0.5ほぼ対称(正規分布に近い)変換不要
0.5 〜 1.0中程度の歪みlog変換を検討
> 1.0強い歪みlog変換を推奨
> 2.0非常に強い歪みBox-Cox変換も比較

Box-Cox変換とは?

💡
log変換の上位互換。最適な変換パラメータ λ(ラムダ) を自動探索します。
λ=0 → log変換と等価 | λ=0.5 → 平方根変換 | λ=1 → 変換なし
制約: 値が必ず正(> 0)でないと使えない。

📊 変換前後の歪度(Skewness)比較

SalePrice に各変換を適用した場合の歪度変化(値が 0 に近いほど正規分布に近い)

変換なし
1.883
1.883
log1p 変換
0.121
0.121
Box-Cox 変換
 
-0.007
変換なし(強い歪み) log1p(大幅改善) Box-Cox(最も正規分布に近い)

📈 変換効果のイメージ(SVG)

SalePrice ヒストグラム分布のイメージ比較 変換なし (skew=1.88) 価格(右裾が長い) 外れ値の影響大 log1p log1p変換後 (skew=0.12) log(SalePrice + 1) ほぼ正規分布 Box-Cox Box-Cox後 (skew≈0) boxcox(SalePrice) 最も正規分布に近い

λ Box-Cox の λ(ラムダ)が意味するもの

λ ≈ 0.15
SalePrice に対する Box-Cox の最適 λ は 約 0.15
これは「0乗根(log)」と「1/7乗根」の中間の変換。
log変換(λ=0)よりほんの少し弱い変換が最適であることを示す。

λ の値と変換の種類

λ=-1 逆数変換 λ=0 log変換 λ≈0.15 SalePrice最適 λ=0.5 平方根変換 λ=1 変換なし λ=2 二乗変換

🗂 使用データのスキーマ

列名説明歪度(目安)
SalePriceint目的変数 — 住宅販売価格($)+1.88(強い右歪み)
LotAreaint敷地面積(平方フィート)+12.2(非常に強い歪み)
GrLivAreaint地上居住面積(平方フィート)+1.37
MasVnrAreafloat石積みベニア面積(0多数含む)+2.61
OverallQualint総合品質スコア(1-10)-0.07(ほぼ対称)
YearBuiltint建設年-0.61(左歪み)

🧩 問題

⚠️
House Prices データセット(train.csv)の SalePrice を分析してください。
1
分布の確認: SalePrice の歪度(skewness)・尖度(kurtosis)を計算し、ヒストグラムと Q-Qプロットで視覚化せよ。
2
log変換の適用: np.log1p() で変換し、変換前後の歪度を比較せよ。逆変換で元の値が復元できることを確認せよ。
3
Box-Cox変換の適用: scipy.stats.boxcox() で最適 λ を求め、変換後の歪度を log変換と比較せよ。
4
特徴量への応用: 数値特徴量の中で |skewness| > 0.75 のものに一括でlog変換を適用する関数を実装せよ。
📌
期待する出力(抜粋):
変換前 Skewness: 1.883
log変換後 Skewness: 0.121
Box-Cox後 Skewness: -0.007 (lambda=0.149)
変換対象特徴量: N個 (|skew| > 0.75)

💡 ヒント

ヒント1: 方向性

まず df['SalePrice'].skew() で歪度を確認。1.8以上なら強い右歪みで変換効果が大きい。変換の方向は「大きな値を小さく圧縮して分布を左に引き寄せる」。

ヒント2: 関数・API
  • np.log1p(y) → 逆変換は np.expm1(y)
  • scipy.stats.boxcox(y)(transformed_y, lambda_value) を返す
  • df.select_dtypes(include=['number']).skew() → 数値列の歪度一括計算
  • scipy.stats.probplot(y, plot=plt) → Q-Qプロット
ヒント3: コード骨格
from scipy import stats
import numpy as np

# タスク2: log変換
log_price = np.log1p(df['SalePrice'])
skew_log = log_price.skew()

# タスク3: Box-Cox
bc_price, lambda_ = stats.boxcox(df['SalePrice'])
skew_bc = pd.Series(bc_price).skew()

# タスク4: 一括変換
numeric_feats = df.select_dtypes(include=['number']).columns.drop('SalePrice')
skewed = df[numeric_feats].apply(lambda x: x.skew())
high_skew = skewed[abs(skewed) > 0.75].index
df[high_skew] = np.log1p(df[high_skew])

模範解答

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
import warnings
warnings.filterwarnings('ignore')

# ─── サンプルデータ生成(実際は train.csv を使用)─────────────────
np.random.seed(42)
n = 1460
sale_price = np.random.lognormal(mean=12.0, sigma=0.4, size=n).clip(34900, 755000)
df = pd.DataFrame({'SalePrice': sale_price})
df['LotArea']    = np.random.lognormal(9.2, 0.5, n).clip(1300, 215245)
df['GrLivArea']  = np.random.lognormal(7.4, 0.3, n).clip(334, 5642)
df['MasVnrArea'] = np.where(np.random.random(n) > 0.4,
                              np.random.lognormal(4.5, 0.8, n).clip(0, 1600), 0)
df['OverallQual'] = np.random.randint(1, 11, n).astype(float)
df['YearBuilt']  = np.random.randint(1872, 2011, n).astype(float)

# ─── タスク1: 歪度・尖度の確認 ──────────────────────────────────
print("=" * 55)
print("タスク1: SalePrice の分布確認")
print("=" * 55)

skew_orig = df['SalePrice'].skew()
kurt_orig = df['SalePrice'].kurt()
print(f"変換前 Skewness : {skew_orig:.3f}")
print(f"変換前 Kurtosis : {kurt_orig:.3f}")

if abs(skew_orig) < 0.5:
    print("→ 分布はほぼ対称。変換は任意。")
elif abs(skew_orig) < 1.0:
    print("→ 中程度の歪み。log変換を検討。")
else:
    print("→ 強い歪み。log変換を推奨。")

# ─── タスク2: log1p 変換 ─────────────────────────────────────────
log_price = np.log1p(df['SalePrice'])
skew_log  = log_price.skew()
print(f"\nlog変換後 Skewness : {skew_log:.3f}")

# 逆変換の確認
reconstructed = np.expm1(log_price)
max_error = np.abs(df['SalePrice'] - reconstructed).max()
print(f"逆変換の最大誤差 : {max_error:.6f}")

# ─── タスク3: Box-Cox 変換 ───────────────────────────────────────
bc_price, lambda_ = stats.boxcox(df['SalePrice'].clip(lower=1))
skew_bc = pd.Series(bc_price).skew()

print(f"\nBox-Cox 最適 λ     : {lambda_:.4f}")
print(f"Box-Cox 後 Skewness: {skew_bc:.3f}")
print(f"\n比較: 変換なし={skew_orig:.3f} | log={skew_log:.3f} | Box-Cox={skew_bc:.3f}")

# ─── タスク4: 特徴量一括変換 ─────────────────────────────────────
numeric_feats = df.select_dtypes(include=['number']).columns.drop('SalePrice')
skewed_feats  = df[numeric_feats].apply(lambda x: x.skew()).sort_values(ascending=False)
high_skew_idx = skewed_feats[abs(skewed_feats) > 0.75].index

df_transformed = df.copy()
df_transformed[high_skew_idx] = np.log1p(df_transformed[high_skew_idx])

print(f"\n変換対象特徴量: {len(high_skew_idx)}個 (|skew| > 0.75)")
for col in high_skew_idx:
    before = df[col].skew()
    after  = df_transformed[col].skew()
    print(f"  {col:20s}: {before:+.3f} → {after:+.3f}")

🔢 Step-by-Step 解説

1 歪度を数値で確認する

skew_orig = df['SalePrice'].skew()

変換が必要かどうかを判断するため、まず歪度を数値で把握する。SalePrice は約 1.88 → 強い右歪み(変換推奨)。

2 log1p で変換する

log_price = np.log1p(df['SalePrice'])  # log(x + 1)

なぜ log1p か: log(0) は数学的に未定義(-∞)になるため、+1 してから対数を取る。予測後の逆変換は np.expm1(pred) で行う。

💡
np.expm1(model.predict(X_test)) で元の価格スケールに戻すのを忘れずに!

3 Box-Cox で最適変換を求める

bc_price, lambda_ = stats.boxcox(df['SalePrice'])

Box-Cox は最適な λ を MLE(最尤推定)で探索する。λ ≈ 0.15 なら log変換よりさらに歪みを除去できる。ただし値が必ず正(>0)でないと動かない点に注意。

4 特徴量全体に一括適用

high_skew_idx = skewed_feats[abs(skewed_feats) > 0.75].index
df[high_skew_idx] = np.log1p(df[high_skew_idx])

なぜ閾値 0.75 か: Kaggleでよく使われる経験則。変換効果と過剰変換のバランスが良い値。負の値を含む特徴量には事前チェックが必要。

📐 数学・統計の補足(文系向け)

対数(log)の直感的な理解

「桁数を数える道具」と考えると分かりやすいです。

元の値log₁₀意味
1011桁
10022桁
1,00033桁
1,000,00066桁

1万円と100万円の差は「100倍」ですが、log でみると「4 と 6 の差 = 2」。100倍の差が 2 の差に圧縮されます。

Q-Qプロットの読み方

📊
Q-Qプロット(Quantile-Quantile Plot)は正規分布との一致度を可視化します。
点が直線上: 正規分布に近い(変換成功)
右上に反り上がる: 右歪み(log変換前の典型パターン)
両端が外れる: 外れ値の影響

🏆 Kaggleでの実践的な使い方

House Prices コンペ — 定番の前処理パイプライン

# 1. 目的変数の変換
y_train = np.log1p(train['SalePrice'])

# 2. 歪んだ特徴量の一括変換(train と test 両方に同じ変換)
skewed_feats = train.select_dtypes(include=['number']).apply(lambda x: x.skew())
skewed_feats = skewed_feats[abs(skewed_feats) > 0.75].index
train[skewed_feats] = np.log1p(train[skewed_feats])
test[skewed_feats]  = np.log1p(test[skewed_feats])   # ← 同じ列に同じ変換

# 3. モデル学習 → 予測 → 逆変換
model.fit(X_train, y_train)
y_pred = np.expm1(model.predict(X_test))   # ← 必ず expm1 で戻す!

# 4. 評価(KaggleのHouse Prices は RMSLE が評価指標)
# log変換済み空間での RMSE = RMSLE と等価
from sklearn.metrics import mean_squared_error
rmsle = np.sqrt(mean_squared_error(y_val_log, model.predict(X_val)))

実際のコンペでの判断フロー

1
y.skew() > 0.75 → log1p変換を検討
2
y.min() > 0 → Box-Cox も比較(skewness が 0 に近い方を採用)
3
予測時に expm1 で逆変換を忘れない(最もよくある実装ミス)
4
クロスバリデーションで RMSLE を比較して最終判断

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
予測後に逆変換を忘れる log変換後はlog空間の予測値が出る np.expm1(model.predict(X)) で元スケールに戻す
log(0) でエラー 0を含む特徴量に np.log を直接適用 np.log1p(x) を使う(または clip(lower=0.001))
全特徴量に変換を適用 「変換すれば良くなる」と思い込む OverallQual 等の順序変数はそのまま使う
Box-Coxが負の値でエラー Box-Coxは正の値のみ対応 .clip(lower=1) で前処理するか log1p を使う
testデータに別の変換を適用 trainで求めたパラメータを流用し忘れる パイプライン化して train でfit、test には transform のみ

🔭 次のステップ

📌
発展: Yeo-Johnson変換(負の値にも対応するBox-Coxの拡張)・sklearn.preprocessing.PowerTransformer(パイプライン統合が容易)
次回予告: EDA体系的手法⑦ — カテゴリ変数のエンコーディング手法比較(LabelEncoder vs OneHot vs Target Encoding)

🧠 自己評価(解いた後に記入)