📚 背景知識(読んでから問題へ)
🎯
Day 072 — Phase 3 継続: EDA第4弾は外れ値(Outliers)の検出と処理。外れ値を「とりあえず削除」するのは危険——外れ値には「ノイズ」と「シグナル」の2種類がある。見分け方と対処法を習得する。
外れ値を無視して学習させると、モデルが異常値に引っ張られて汎化性能が落ちる。一方、無闇に削除するとシグナルを失う。House Prices コンペでは特定の外れ値が「削除推奨」とコンペ公式discussionに明記されている稀なケースがあるほど、外れ値分析は重要。
🔍 外れ値の3タイプ
🔴 ノイズ型
削除 / NaN置換
入力ミス・センサー故障など
例: 年齢 = 999、身長 = -10cm
対処: 削除 or NaNに置換して補完
例: 年齢 = 999、身長 = -10cm
対処: 削除 or NaNに置換して補完
🟢 正当型
保持(重要シグナル)
本当に珍しい現象の記録
例: 超高級物件の価格、記録的売上
対処: 保持 or 外れ値フラグを特徴量に
例: 超高級物件の価格、記録的売上
対処: 保持 or 外れ値フラグを特徴量に
🟡 分布型
対数変換 / 分布変換
歪んだ分布の端の値
例: 収入・SalePrice の右裾
対処: log変換でスケール圧縮
例: 収入・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)
📈 対数変換前後の歪度(SalePrice)
歪度(Skewness)は分布の非対称さを示す指標。0に近いほど正規分布に近い。
💡
実務の目安: 歪度の絶対値が 0.5 以下なら正規分布に近い。1.0 を超えたら対数変換 or Box-Cox変換を検討。Kaggle の House Prices では SalePrice の歪度が約 1.88 →
np.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法で外れ値フラグを付ける
GrLivArea と SalePrice に対してIQR法を適用し、それぞれの外れ値フラグ列を追加せよ。Q1・Q3・IQR・下限・上限・外れ値件数を出力すること。↓
2
外れ値の散布図(色分け)
GrLivArea vs SalePrice の散布図を描き、外れ値(フラグ=1)を赤色、通常値(フラグ=0)を青色で表示せよ。
GrLivArea vs SalePrice の散布図を描き、外れ値(フラグ=1)を赤色、通常値(フラグ=0)を青色で表示せよ。
↓
3
外れ値と目的変数の関係
GrLivArea_outlier フラグでグループ化して SalePrice の中央値を比較せよ。↓
4
対数変換の効果確認
SalePrice と log(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法の実装
なぜ1.5倍? 統計学者 John Tukey(箱ひげ図の発明者)が提案した経験則。正規分布では外れ値が約0.7%になる設計。
quantile(0.25) と quantile(0.75) でQ1・Q3を取得。なぜ1.5倍? 統計学者 John Tukey(箱ひげ図の発明者)が提案した経験則。正規分布では外れ値が約0.7%になる設計。
3.0倍 にすると極端な外れ値のみ検出(より厳密)。
2
散布図での視覚確認
マスク(
House Prices の有名な外れ値:
マスク(
mask = df['GrLivArea_outlier'] == 1)で2回 scatter することで色分け可能。House Prices の有名な外れ値:
GrLivArea > 4000 かつ SalePrice < 200,000 の2点 → 上位解法の多くが削除。
3
groupbyで中央値比較
外れ値グループのSalePriceが高い → MNAR(床面積が大きいから価格も高い = 外れ値フラグ自体が特徴量になり得る)。
df.groupby('GrLivArea_outlier')['SalePrice'].median()外れ値グループのSalePriceが高い → MNAR(床面積が大きいから価格も高い = 外れ値フラグ自体が特徴量になり得る)。
4
対数変換
Kaggle の House Prices は評価指標が RMSLE(対数誤差)なので、SalePrice を対数変換してから学習・予測するのが定石。
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 変換して学習
コンペ評価指標が RMSLE なので必須。予測後に
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
外れ値フラグを特徴量として追加
フラグ自体がモデルの予測精度を上げることがある(MNAR の活用)。
df['GrLivArea_is_outlier'] = (df['GrLivArea'] > 4000).astype(int)フラグ自体がモデルの予測精度を上げることがある(MNAR の活用)。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 外れ値は必ず削除すべき | 「異常値 = 邪魔」という思い込み | 外れ値がシグナルになるケースが多い(MNAR) |
| Zスコア法は常に優れている | 「標準的な手法 = 万能」という誤解 | 外れ値が存在すると平均・分散が歪むため IQR法が頑健 |
| log変換は全変数に適用すべき | 「変換 = 精度改善」の過度な期待 | 0以下の値があるとエラー。均一分布・二峰性には逆効果 |
| 歪度 > 1 なら必ず変換 | ルールを暗記してしまう | 変換後の改善度(交差検証スコア)で判断すべき |
| 外れ値を見ずにモデリング | 「EDAを省略して早く実装」という急ぎ | 外れ値1件が線形回帰の係数を大きく歪める |
🚀 次のステップ
- 発展: Box-Cox変換・Yeo-Johnson変換(負の値も扱える対数変換の亜種)
- 次回予告: EDA体系的手法⑤ — 相関行列と多重共線性の検出・VIF計算