Day 072 — EDA体系的手法④ — 外れ値の検出と処理戦略

2026-06-22 水 / Phase 3 理論 IQR法 / Zスコア / 対数変換 / 歪度

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

🎯
Day 072 — Phase 3 継続: EDA第4弾は外れ値(Outliers)の検出と処理。外れ値を「とりあえず削除」するのは危険——外れ値には「ノイズ」と「シグナル」の2種類がある。見分け方と対処法を習得する。

外れ値を無視して学習させると、モデルが異常値に引っ張られて汎化性能が落ちる。一方、無闇に削除するとシグナルを失う。House Prices コンペでは特定の外れ値が「削除推奨」とコンペ公式discussionに明記されている稀なケースがあるほど、外れ値分析は重要。

🔍 外れ値の3タイプ

🔴 ノイズ型
削除 / NaN置換
入力ミス・センサー故障など
例: 年齢 = 999、身長 = -10cm
対処: 削除 or NaNに置換して補完
🟢 正当型
保持(重要シグナル)
本当に珍しい現象の記録
例: 超高級物件の価格、記録的売上
対処: 保持 or 外れ値フラグを特徴量に
🟡 分布型
対数変換 / 分布変換
歪んだ分布の端の値
例: 収入・SalePrice の右裾
対処: log変換でスケール圧縮
⚠️
House Prices の有名な外れ値: GrLivArea > 4000 かつ SalePrice < 300,000 の2件。床面積が大きいのに価格が極めて安い → 競売などの特殊取引と推定 → Kaggle上位解法の多くが削除推奨

🛠 検出手法の比較

手法 仕組み 閾値の目安 強み 弱み
IQR法 Q1 − 1.5×IQR < x < Q3 + 1.5×IQR IQRの1.5倍 外れ値に頑健
正規分布不要
端点は任意
Zスコア法 |z| = |(x − μ) / σ| > 3 |z| > 3 解釈が直感的 外れ値で平均・分散が歪む
散布図目視 目的変数との関係を可視化 文脈判断が可能 主観的・自動化困難
Isolation Forest ランダム分割で孤立しやすい点を検出 contamination=0.05 多変量・高次元に強い 解釈が難しい

📊 IQR法のイメージ図(SVG)

下限 Q1−1.5×IQR Q1 25%ile 中央値 50%ile Q3 75%ile 上限 Q3+1.5×IQR ← IQR = Q3 − Q1 → 外れ値 外れ値 IQR法による外れ値検出 箱ひげ図の1.5倍ルール 通常値 外れ値 IQR範囲(中央50%)

📈 対数変換前後の歪度(SalePrice)

歪度(Skewness)は分布の非対称さを示す指標。0に近いほど正規分布に近い。

SalePrice の歪度:変換前後の比較 変換前 SalePrice 歪度 = 1.882 log(SalePrice+1) 歪度 = 0.121 ✓ 0(理想) 2.0
💡
実務の目安: 歪度の絶対値が 0.5 以下なら正規分布に近い。1.0 を超えたら対数変換 or Box-Cox変換を検討。Kaggle の House Prices では SalePrice の歪度が約 1.88np.log1p() 変換で 0.12 まで改善。

🗂 データスキーマ(使用変数)

変数名説明外れ値の特徴
GrLivArea int 地上の居住面積(平方フィート) 4000超の2件が有名な外れ値(削除推奨)
SalePrice int 物件の売却価格(目的変数) 右歪み(歪度≈1.88)。log変換で正規化
LotArea int 土地面積(平方フィート) 右裾が長い。大型農地等で極端に大きい値あり
GrLivArea_outlier int (0/1) GrLivAreaのIQR外れ値フラグ(生成列) 外れ値 = 1、通常 = 0
SalePrice_log float log(SalePrice + 1)(生成列) 歪度 ≈ 0.12 まで改善

🧩 問題

📋
House Prices データセット(train.csv)を使って、以下の4つの外れ値分析を実装せよ。
1
IQR法で外れ値フラグを付ける
GrLivAreaSalePrice に対してIQR法を適用し、それぞれの外れ値フラグ列を追加せよ。Q1・Q3・IQR・下限・上限・外れ値件数を出力すること。
2
外れ値の散布図(色分け)
GrLivArea vs SalePrice の散布図を描き、外れ値(フラグ=1)を赤色、通常値(フラグ=0)を青色で表示せよ。
3
外れ値と目的変数の関係
GrLivArea_outlier フラグでグループ化して SalePrice の中央値を比較せよ。
4
対数変換の効果確認
SalePricelog(SalePrice+1) のヒストグラムを並べて比較し、歪度を計算して改善量を示せ。

💡 ヒント

ヒント1 — 方向性(クリックで展開)
IQR = Q3 − Q1。pandasの quantile() を使えばQ1・Q3が計算できる。外れ値かどうかを判定してboolean列を作り、.astype(int) で0/1に変換する。
ヒント2 — アプローチ(クリックで展開)
Q1 = df['GrLivArea'].quantile(0.25)
Q3 = df['GrLivArea'].quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
散布図の色分けには mask で2回プロットするのが分かりやすい。歪度は .skew() で計算可能。
ヒント3 — コード骨格(クリックで展開)
for col in ['GrLivArea', 'SalePrice']:
    Q1 = df[col].quantile(0.25)
    Q3 = df[col].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    df[f'{col}_outlier'] = ((df[col] < lower) | (df[col] > upper)).astype(int)

# 散布図
mask = df['GrLivArea_outlier'] == 1
ax.scatter(df.loc[~mask, 'GrLivArea'], df.loc[~mask, 'SalePrice'], alpha=0.4, label='通常')
ax.scatter(df.loc[mask, 'GrLivArea'], df.loc[mask, 'SalePrice'], color='red', label='外れ値')

# 対数変換
df['SalePrice_log'] = np.log1p(df['SalePrice'])
print(df['SalePrice'].skew(), df['SalePrice_log'].skew())

模範解答

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

df = pd.read_csv('train.csv')
print(f"データ形状: {df.shape}")

# ─── タスク1: IQR法で外れ値フラグ ───
def add_iqr_outlier_flag(df, col):
    Q1 = df[col].quantile(0.25)
    Q3 = df[col].quantile(0.75)
    IQR = Q3 - Q1
    lower = Q1 - 1.5 * IQR
    upper = Q3 + 1.5 * IQR
    flag = ((df[col] < lower) | (df[col] > upper)).astype(int)
    print(f"\n{col}:")
    print(f"  Q1={Q1:.1f}, Q3={Q3:.1f}, IQR={IQR:.1f}")
    print(f"  下限={lower:.1f}, 上限={upper:.1f}")
    print(f"  外れ値数: {flag.sum()} / {len(df)} ({flag.mean()*100:.1f}%)")
    return flag

df['GrLivArea_outlier'] = add_iqr_outlier_flag(df, 'GrLivArea')
df['SalePrice_outlier'] = add_iqr_outlier_flag(df, 'SalePrice')

# ─── タスク2: 散布図(外れ値を赤色) ───
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

for ax, col_flag, title in [
    (axes[0], 'GrLivArea_outlier', 'GrLivArea外れ値'),
    (axes[1], 'SalePrice_outlier', 'SalePrice外れ値'),
]:
    mask = df[col_flag] == 1
    ax.scatter(df.loc[~mask, 'GrLivArea'], df.loc[~mask, 'SalePrice'],
               alpha=0.3, color='steelblue', s=12, label='通常値')
    ax.scatter(df.loc[mask, 'GrLivArea'], df.loc[mask, 'SalePrice'],
               alpha=0.8, color='red', s=40, label=f'外れ値 (n={mask.sum()})')
    ax.set_xlabel('GrLivArea')
    ax.set_ylabel('SalePrice')
    ax.set_title(title)
    ax.legend()

plt.tight_layout()
plt.show()

# ─── タスク3: 外れ値 × SalePrice中央値 ───
print("\n─── GrLivArea外れ値 × SalePrice中央値 ───")
median_cmp = df.groupby('GrLivArea_outlier')['SalePrice'].agg(['median', 'count'])
median_cmp.index = ['通常値', '外れ値']
print(median_cmp)

# ─── タスク4: 対数変換の効果 ───
df['SalePrice_log'] = np.log1p(df['SalePrice'])

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
skew_raw = df['SalePrice'].skew()
skew_log = df['SalePrice_log'].skew()

axes[0].hist(df['SalePrice'], bins=50, color='steelblue', alpha=0.7)
axes[0].set_title(f'SalePrice\n歪度 = {skew_raw:.3f}')

axes[1].hist(df['SalePrice_log'], bins=50, color='mediumpurple', alpha=0.7)
axes[1].set_title(f'log(SalePrice+1)\n歪度 = {skew_log:.3f}')

plt.tight_layout()
plt.show()

print(f"\nSalePrice 歪度: {skew_raw:.3f}")
print(f"log(SalePrice+1) 歪度: {skew_log:.3f}")
print(f"→ 対数変換で歪度が {abs(skew_raw - skew_log):.3f} 改善")
期待される出力
GrLivArea:
  Q1=1130.0, Q3=1776.8, IQR=646.8
  下限=158.8, 上限=2747.0
  外れ値数: 61 / 1460 (4.2%)

SalePrice:
  Q1=129975.0, Q3=214000.0, IQR=84025.0
  下限=3812.5, 上限=340162.5
  外れ値数: 88 / 1460 (6.0%)

─── GrLivArea外れ値 × SalePrice中央値 ───
       median count
通常値 161500 1399
外れ値 280000 61

SalePrice 歪度: 1.882
log(SalePrice+1) 歪度: 0.121
→ 対数変換で歪度が 1.761 改善

🪜 Step-by-Step 解説

1
IQR法の実装
quantile(0.25)quantile(0.75) でQ1・Q3を取得。
なぜ1.5倍? 統計学者 John Tukey(箱ひげ図の発明者)が提案した経験則。正規分布では外れ値が約0.7%になる設計。3.0倍 にすると極端な外れ値のみ検出(より厳密)。
2
散布図での視覚確認
マスク(mask = df['GrLivArea_outlier'] == 1)で2回 scatter することで色分け可能。
House Prices の有名な外れ値: GrLivArea > 4000 かつ SalePrice < 200,000 の2点 → 上位解法の多くが削除。
3
groupbyで中央値比較
df.groupby('GrLivArea_outlier')['SalePrice'].median()
外れ値グループのSalePriceが高い → MNAR(床面積が大きいから価格も高い = 外れ値フラグ自体が特徴量になり得る)。
4
対数変換
np.log1p(x) = log(x + 1)+1 の理由: log(0) = -∞ を避けるため。
Kaggle の House Prices は評価指標が RMSLE(対数誤差)なので、SalePrice を対数変換してから学習・予測するのが定石

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

歪度(Skewness)とは?

歪度はデータの分布が「どちらに傾いているか」を数値化したもの:

歪度の値意味分布の形
≈ 0 左右対称 正規分布に近い(理想的)
> 0(正の歪み) 右側の裾が長い SalePrice・収入・年齢など右裾が多い
< 0(負の歪み) 左側の裾が長い 試験の満点近くに集中するようなケース
🔑
なぜ歪んだ分布が問題なのか?
線形回帰・ニューラルネットは「データが対称的(正規分布)」を暗黙に仮定することが多い。
歪みがあると予測誤差が偏る(高い値の予測が甘くなる、など)。
対数変換で正規分布に近づけると精度が上がることが多い。

🏆 Kaggleでの実践的な使い方

House Prices コンペの勝ちパターン

1
有名な外れ値2件を削除
df = df[~((df['GrLivArea'] > 4000) & (df['SalePrice'] < 300000))]
上位解法の多くが採用するおまじない的前処理。削除しないとモデルが引っ張られる。
2
SalePrice を log1p 変換して学習
y_train = np.log1p(df['SalePrice'])
コンペ評価指標が RMSLE なので必須。予測後に np.expm1() で逆変換。
3
右歪みの数値変数を Box-Cox 変換
from scipy import stats
df['LotArea_bc'], _ = stats.boxcox(df['LotArea'] + 1)
4
外れ値フラグを特徴量として追加
df['GrLivArea_is_outlier'] = (df['GrLivArea'] > 4000).astype(int)
フラグ自体がモデルの予測精度を上げることがある(MNAR の活用)。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
外れ値は必ず削除すべき 「異常値 = 邪魔」という思い込み 外れ値がシグナルになるケースが多い(MNAR)
Zスコア法は常に優れている 「標準的な手法 = 万能」という誤解 外れ値が存在すると平均・分散が歪むため IQR法が頑健
log変換は全変数に適用すべき 「変換 = 精度改善」の過度な期待 0以下の値があるとエラー。均一分布・二峰性には逆効果
歪度 > 1 なら必ず変換 ルールを暗記してしまう 変換後の改善度(交差検証スコア)で判断すべき
外れ値を見ずにモデリング 「EDAを省略して早く実装」という急ぎ 外れ値1件が線形回帰の係数を大きく歪める

🚀 次のステップ

  • 発展: Box-Cox変換・Yeo-Johnson変換(負の値も扱える対数変換の亜種)
  • 次回予告: EDA体系的手法⑤ — 相関行列と多重共線性の検出・VIF計算

📝 自己評価