Day 069 — EDA体系的手法① — 数値変数の分布・歪度・尖度

2026-06-19 水 / Phase 3 分析 skewness / kurtosis / np.log1p / SalePrice変換

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

🎯
Phase 3 スタート(Day 069〜168) — EDA・特徴量エンジニアリング・Kaggle入門コンペを体系的に学ぶ。今日はEDAの核心、数値変数の分布分析から。

EDA(探索的データ分析)とは

Kaggle上位勢はモデル構築よりEDAに時間をかける。「データを理解してから動かす」が上位入賞の鍵。 数値変数のEDAは以下の3つの観点で行う。

中心
mean / median
データの典型値。平均と中央値の乖離が歪みの最初のサイン。
ばらつき
std / IQR
標準偏差・四分位範囲でデータの広がりを定量化。
形(非対称性)
skewness
分布の左右傾き。|skewness| > 1.0 で変換を検討。
形(とがり)
kurtosis
分布の山の鋭さ。外れ値の多さと関係。

📐 歪度(Skewness)— 分布の傾き

歪度は分布の左右非対称の度合いを数値化したもの。

右歪み(歪度 > 0) 年収・住宅価格・LotArea 最頻値 中央値 平均 対称(歪度 ≈ 0) 身長・テスト点数 平均=中央値=最頻値 左歪み(歪度 < 0) 高得点試験・品質スコア 平均 中央値 最頻値

Kaggle での判断基準(歪度スコア)

ほぼ対称(変換不要)
|skew| < 0.5
|skew| < 0.5
要確認(分布を見る)
確認後判断
0.5 〜 1.0
変換を強く推奨
np.log1p を検討
|skew| > 1.0

📊 尖度(Kurtosis)— 分布のとがり具合

尖度の値分布の形意味典型的なデータ
> 3 急尖(Leptokurtic) 山が鋭く、裾が厚い → 外れ値が多い 株価変動・取引量
≈ 3 正規(Mesokurtic) 正規分布と同じ形 身長・体重
< 3 緩尖(Platykurtic) 山が平坦 → 外れ値が少ない 一様分布に近いデータ
💡
scipy の kurtosis は "超過尖度" を返す(正規分布 = 0)。pandas の .kurtosis() も同様。正規分布より大きいなら正、小さいなら負。

📈 House Prices — 歪度ランキング(視覚化)

典型的な House Prices データの数値変数歪度。高いほど変換の恩恵が大きい。

PoolArea
skew=12.0
12.0
MasVnrArea
skew=9.0
9.0
3SsnPorch
skew=7.5
7.5
LowQualFinSF
skew=6.6
6.6
LotArea
skew=5.0
5.0
OpenPorchSF
skew=3.4
3.4
TotalBsmtSF
skew=2.2
2.2
SalePrice ⭐
skew=1.7
1.7
GrLivArea
skew=1.2
1.2
GarageArea
skew=0.5
0.5

赤: |skew| > 5(要変換) / 橙: |skew| > 3 / 黄: |skew| > 1(変換推奨) / 緑: |skew| ≈ 1 / 青: |skew| < 1

🔄 log1p 変換の効果(前後比較)

np.log1p 変換後の歪度改善を可視化。

変数名 変換前 skewness 変換後 skewness LotArea 5.0 0.5 MasVnrArea 9.0 0.9 OpenPorchSF 3.4 0.7 TotalBsmtSF 2.2 0.4 GrLivArea 1.2 0.2 SalePrice ⭐ 1.7 0.1 閾値 (1.0)
💡
目的変数 SalePrice の変換: 歪度 1.7 → log1p 後 0.1 に改善。House Prices の評価指標 RMSLE は "対数誤差" なので、log 変換は自然に合致する。
予測後は np.expm1(pred) で元のスケールに逆変換すること!

🗄️ データスキーマ(House Prices 数値変数)

列名データ型説明典型的な歪度変換
SalePrice float64 住宅売却価格(目的変数) 1.7 np.log1p ✓
LotArea int64 敷地面積(平方フィート) 5.0 np.log1p ✓
GrLivArea int64 地上居住面積 1.2 np.log1p ✓
TotalBsmtSF float64 地下室総面積 2.2 np.log1p ✓
GarageArea float64 ガレージ面積 0.5 任意
YearBuilt int64 建築年 −0.6 不要
OverallQual int64 総合品質スコア(1-10) 0.2 不要
MasVnrArea float64 石材外装面積(多くが0) 9.0 np.log1p ✓
PoolArea int64 プール面積(ほぼ0) 12.0 二値化も検討

問題

House Prices データセット(住宅価格予測)の数値変数を体系的に分析してください。

  1. 数値変数の一覧と基本統計量を取得する
  2. 歪度(skewness)を全数値変数で計算し、歪度の大きい順に上位10件を表示する
  3. 歪度が 1.0 を超える変数に対して対数変換(np.log1p)を適用し、変換前後の歪度を比較する
  4. 目的変数 SalePrice の分布を確認し、変換が必要かどうかを判断する
📝
使用データ: Kaggle の House Prices サンプルデータ(またはシミュレーション)。実際のデータがある場合は pd.read_csv("train.csv") で読み込む。

💡 ヒント

ヒント1 — 方向性

DataFrame.skew() メソッドで全数値列の歪度を一括計算できます。まず基本統計量を把握してから歪度の計算に進みましょう。

ヒント2 — アプローチ

  • df.select_dtypes(include='number') で数値列だけ選択
  • .skew().sort_values(ascending=False) で歪度を降順ソート
  • np.log1p(x)x + 1 の対数(0の値があっても安全)

ヒント3 — コード骨格

import numpy as np
import pandas as pd

# 数値列の歪度を計算
num_cols = df.select_dtypes(include='number').columns
skewness = df[num_cols].skew().sort_values(ascending=False)
high_skew_cols = skewness[skewness.abs() > 1.0].index

# 対数変換して比較
for col in high_skew_cols:
    before = df[col].skew()
    after  = np.log1p(df[col]).skew()
    print(f"{col:30s} before={before:.2f} → after={after:.2f}")

模範解答

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

# ── データ準備(House Prices サンプル生成)──────────────────────────
np.random.seed(42)
n = 1460

data = {
    'SalePrice':     np.random.lognormal(mean=12.0, sigma=0.4, size=n),
    'LotArea':       np.random.lognormal(mean=9.2,  sigma=0.5, size=n),
    'GrLivArea':     np.random.lognormal(mean=7.3,  sigma=0.3, size=n),
    'TotalBsmtSF':   np.abs(np.random.lognormal(mean=6.8, sigma=0.6, size=n)),
    'GarageArea':    np.abs(np.random.normal(472, 215, n)),
    'YearBuilt':     np.random.randint(1872, 2011, n).astype(float),
    'OverallQual':   np.random.randint(1, 11, n).astype(float),
    'BedroomAbvGr':  np.random.poisson(2.9, n).astype(float),
    'MasVnrArea':    np.where(np.random.random(n) < 0.6, 0,
                              np.abs(np.random.exponential(200, n))),
    'OpenPorchSF':   np.where(np.random.random(n) < 0.4, 0,
                              np.abs(np.random.exponential(80, n))),
    'PoolArea':      np.where(np.random.random(n) < 0.95, 0,
                              np.abs(np.random.exponential(50, n))),
    'LowQualFinSF':  np.where(np.random.random(n) < 0.97, 0,
                              np.abs(np.random.exponential(30, n))),
    '3SsnPorch':     np.where(np.random.random(n) < 0.98, 0,
                              np.abs(np.random.exponential(20, n))),
    'EnclosedPorch': np.where(np.random.random(n) < 0.85, 0,
                              np.abs(np.random.exponential(60, n))),
    'ScreenPorch':   np.where(np.random.random(n) < 0.90, 0,
                              np.abs(np.random.exponential(40, n))),
}
df = pd.DataFrame(data)

# ── Step 1: 基本統計量 ──────────────────────────────────────────────
print("=" * 60)
print("Step 1: 基本統計量")
print("=" * 60)
print(df.describe().round(2).to_string())

# ── Step 2: 歪度ランキング ──────────────────────────────────────────
print("\n" + "=" * 60)
print("Step 2: 歪度ランキング(上位10件)")
print("=" * 60)
num_cols = df.select_dtypes(include='number').columns
skewness = df[num_cols].skew().sort_values(ascending=False)
print(skewness.head(10).round(3))

# ── Step 3: 対数変換の比較 ─────────────────────────────────────────
print("\n" + "=" * 60)
print("Step 3: 対数変換(|skewness| > 1.0 の変数)")
print("=" * 60)
high_skew_cols = skewness[skewness.abs() > 1.0].index
print(f"{'特徴量':<20} {'変換前':>8} {'変換後':>8} {'改善':>8}")
print("-" * 48)
for col in high_skew_cols:
    before = df[col].skew()
    after  = np.log1p(df[col]).skew()
    improvement = abs(before) - abs(after)
    print(f"{col:<20} {before:>8.2f} {after:>8.2f} {improvement:>8.2f}")

# ── Step 4: SalePrice の分析 ───────────────────────────────────────
print("\n" + "=" * 60)
print("Step 4: SalePrice の分布分析")
print("=" * 60)
sp = df['SalePrice']
print(f"  平均:   {sp.mean():>12,.0f}")
print(f"  中央値: {sp.median():>12,.0f}")
print(f"  歪度:   {sp.skew():>12.3f}")
print(f"  尖度:   {sp.kurtosis():>12.3f}")

log_sp = np.log1p(sp)
print(f"\n  log1p変換後:")
print(f"  平均:   {log_sp.mean():>12.3f}")
print(f"  中央値: {log_sp.median():>12.3f}")
print(f"  歪度:   {log_sp.skew():>12.3f}")
print(f"  尖度:   {log_sp.kurtosis():>12.3f}")

# 正規性検定(Shapiro-Wilk、サンプル数を絞る)
_, p_before = stats.shapiro(sp.sample(50, random_state=42))
_, p_after  = stats.shapiro(log_sp.sample(50, random_state=42))
print(f"\n  Shapiro-Wilk p値(変換前): {p_before:.4f}")
print(f"  Shapiro-Wilk p値(変換後): {p_after:.4f}")
print(f"  → log変換により正規性{'改善' if p_after > p_before else '未改善'}")

🪜 Step-by-Step 解説

1
基本統計量で全体把握
df.describe() が返す8行の意味を理解する。特に mean vs 50%(中央値)の差が重要 — 乖離が大きいほど歪みがある。
2
数値列に絞って歪度を一括計算
df.select_dtypes(include='number').skew().sort_values(ascending=False)
PoolAreaMasVnrArea のような「ほぼ0、たまに大きい値」の変数は歪度が非常に高い(ゼロ過剰変数)。
3
なぜ np.log1p か?
家の面積 100㎡→200㎡(2倍)と 1000㎡→2000㎡(2倍)は "比率" は同じだが差は10倍異なる。 対数変換で「比率」を「差」に変換 → モデルが学習しやすくなる。
np.log(x) は x=0 でエラー → np.log1p(x) = log(x+1) を使う。
4
目的変数 SalePrice の変換判断
1) skewness > 1.0 → 変換を検討
2) 変換後の歪度が 0 に近い → 変換有効
3) Shapiro-Wilk p値が向上 → 正規性が改善
House Prices は評価指標が RMSLE(対数誤差)なので log 変換が自然に合致する。
⚠️
逆変換を忘れずに! 目的変数を np.log1p で変換した場合、予測値は np.expm1(pred) で元のスケールに戻すこと。これを忘れると提出スコアが壊滅的になる。

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

歪度の直感的理解

種類歪度値身近な例平均 vs 中央値
対称分布 ≈ 0 日本人の身長 ほぼ等しい
右歪み(正) > 0 年収・住宅価格・LotArea 平均 > 中央値
左歪み(負) < 0 高得点試験・品質スコア 平均 < 中央値

対数変換の直感

「1万円 → 2万円」と「100万円 → 200万円」は倍率は同じ2倍。 でも絶対値の差は1万と100万で100倍違う。
対数を取ると log(2万) - log(1万) = log(2)log(200万) - log(100万) = log(2) も同じになる。 これがモデルに「比率感覚」を教える効果。

🏆 Kaggleでの実践的な使い方

EDA の標準ワークフロー(Phase 3 の核心)

1
df.shape, df.dtypes, df.isnull().sum() — 全体把握
2
df.describe() — 基本統計量(mean vs 50% の乖離に注目)
3
df.skew().sort_values(ascending=False) — 歪度ランキング
4
df[high_skew].hist(bins=30) — 視覚的確認
5
対数変換の適用 → 変換後の歪度確認 → 改善なければ元に戻す
6
目的変数との相関確認(次のステップ)

実際のコンペでの活用例

コンペ歪みの激しい変数対処法効果
House Prices LotArea, MasVnrArea, SalePrice np.log1p で一括変換 RMSE 約10%改善
Ames Housing LotArea, GrLivArea, TotalBsmtSF 歪度1以上を log1p 必須前処理
American Express 金融取引量・残高 log1p + clip で外れ値処理 モデル安定化

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
カテゴリ変数の歪度を計算してしまう OHE後の0/1列に .skew() が適用される select_dtypes(include='number') で事前にフィルタ
np.log で0エラー 0を含む列にlog適用 np.log1p を使う(0→0に変換)
目的変数を変換したまま提出 逆変換を忘れる 予測後に np.expm1 で逆変換 ⚠️
全変数を一律log変換 「変換すれば正義」と思い込む 変換後の歪度を確認し、改善しなければ元に戻す
尖度と歪度を混同 似た名前 歪度=傾き(asymmetry)、尖度=とがり(peakedness)

🚀 次のステップ

  • 発展: EDA体系的手法② — カテゴリ変数の分布・目的変数との関係(Day 070)
  • 次回予告: value_counts() の活用、groupby による目的変数との関係分析、バイオリンプロット・ボックスプロットの読み方
  • 実践課題: Kaggle の House Prices データをダウンロードして今日のコードを実際に動かしてみる

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

理解度チェック:

自分の回答・気づき・メモ: