📚 背景知識(読んでから問題へ)
🎯
Phase 3 スタート(Day 069〜168) — EDA・特徴量エンジニアリング・Kaggle入門コンペを体系的に学ぶ。今日はEDAの核心、数値変数の分布分析から。
EDA(探索的データ分析)とは
Kaggle上位勢はモデル構築よりEDAに時間をかける。「データを理解してから動かす」が上位入賞の鍵。 数値変数のEDAは以下の3つの観点で行う。
中心
mean / median
データの典型値。平均と中央値の乖離が歪みの最初のサイン。
ばらつき
std / IQR
標準偏差・四分位範囲でデータの広がりを定量化。
形(非対称性)
skewness
分布の左右傾き。|skewness| > 1.0 で変換を検討。
形(とがり)
kurtosis
分布の山の鋭さ。外れ値の多さと関係。
📐 歪度(Skewness)— 分布の傾き
歪度は分布の左右非対称の度合いを数値化したもの。
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 データの数値変数歪度。高いほど変換の恩恵が大きい。
赤: |skew| > 5(要変換) / 橙: |skew| > 3 / 黄: |skew| > 1(変換推奨) / 緑: |skew| ≈ 1 / 青: |skew| < 1
🔄 log1p 変換の効果(前後比較)
np.log1p 変換後の歪度改善を可視化。
💡
目的変数 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 データセット(住宅価格予測)の数値変数を体系的に分析してください。
- 数値変数の一覧と基本統計量を取得する
- 歪度(skewness)を全数値変数で計算し、歪度の大きい順に上位10件を表示する
- 歪度が
1.0を超える変数に対して対数変換(np.log1p)を適用し、変換前後の歪度を比較する - 目的変数
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)PoolArea や MasVnrArea のような「ほぼ0、たまに大きい値」の変数は歪度が非常に高い(ゼロ過剰変数)。
↓
3
なぜ np.log1p か?
家の面積 100㎡→200㎡(2倍)と 1000㎡→2000㎡(2倍)は "比率" は同じだが差は10倍異なる。 対数変換で「比率」を「差」に変換 → モデルが学習しやすくなる。
家の面積 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 変換が自然に合致する。
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 データをダウンロードして今日のコードを実際に動かしてみる
📝 自己評価(解いた後に記入)
理解度チェック:
自分の回答・気づき・メモ: