📚 背景知識(読んでから問題へ)
🎯
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.21 | 100倍の差 → 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)でないと使えない。
λ=0 → log変換と等価 | λ=0.5 → 平方根変換 | λ=1 → 変換なし
制約: 値が必ず正(> 0)でないと使えない。
📊 変換前後の歪度(Skewness)比較
SalePrice に各変換を適用した場合の歪度変化(値が 0 に近いほど正規分布に近い)
変換なし
1.883
log1p 変換
0.121
Box-Cox 変換
-0.007
変換なし(強い歪み)
log1p(大幅改善)
Box-Cox(最も正規分布に近い)
📈 変換効果のイメージ(SVG)
λ Box-Cox の λ(ラムダ)が意味するもの
λ ≈ 0.15
SalePrice に対する Box-Cox の最適 λ は 約 0.15。
これは「0乗根(log)」と「1/7乗根」の中間の変換。
log変換(λ=0)よりほんの少し弱い変換が最適であることを示す。
これは「0乗根(log)」と「1/7乗根」の中間の変換。
log変換(λ=0)よりほんの少し弱い変換が最適であることを示す。
λ の値と変換の種類
🗂 使用データのスキーマ
| 列名 | 型 | 説明 | 歪度(目安) |
|---|---|---|---|
SalePrice | int | 目的変数 — 住宅販売価格($) | +1.88(強い右歪み) |
LotArea | int | 敷地面積(平方フィート) | +12.2(非常に強い歪み) |
GrLivArea | int | 地上居住面積(平方フィート) | +1.37 |
MasVnrArea | float | 石積みベニア面積(0多数含む) | +2.61 |
OverallQual | int | 総合品質スコア(1-10) | -0.07(ほぼ対称) |
YearBuilt | int | 建設年 | -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.883log変換後 Skewness: 0.121Box-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₁₀ | 意味 |
|---|---|---|
| 10 | 1 | 1桁 |
| 100 | 2 | 2桁 |
| 1,000 | 3 | 3桁 |
| 1,000,000 | 6 | 6桁 |
1万円と100万円の差は「100倍」ですが、log でみると「4 と 6 の差 = 2」。100倍の差が 2 の差に圧縮されます。
Q-Qプロットの読み方
📊
Q-Qプロット(Quantile-Quantile Plot)は正規分布との一致度を可視化します。
点が直線上: 正規分布に近い(変換成功)
右上に反り上がる: 右歪み(log変換前の典型パターン)
両端が外れる: 外れ値の影響
点が直線上: 正規分布に近い(変換成功)
右上に反り上がる: 右歪み(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の拡張)・
次回予告: EDA体系的手法⑦ — カテゴリ変数のエンコーディング手法比較(LabelEncoder vs OneHot vs Target Encoding)
sklearn.preprocessing.PowerTransformer(パイプライン統合が容易)次回予告: EDA体系的手法⑦ — カテゴリ変数のエンコーディング手法比較(LabelEncoder vs OneHot vs Target Encoding)