📚 背景知識(読んでから問題へ)
🎯
Day 071 — Phase 3 継続: EDA第3弾は欠損値の徹底調査。「とりあえず平均で埋める」だけでは上位に入れない。なぜ欠損しているのかを理解することが正しい補完戦略の出発点。
欠損値EDAの5ステップ
1. 全体俯瞰
isnull().mean()
変数ごとの欠損率(%)を降順でリスト化。何が問題かを最初に把握する。
2. パターン確認
sum(axis=1)
複数変数が同時に欠損する「共欠損」の発見。Garage系・Basement系は同時欠損が多い。
3. 目的変数との関係
欠損フラグ × SalePrice
欠損フラグ(0/1)を作って目的変数との相関を見る。MNAR の証拠になる。
4. 他変数との関係
groupby / boxplot
欠損している行の他変数の分布が非欠損と異なるか確認。補完戦略の根拠になる。
🔍 欠損値の3タイプ(MCAR / MAR / MNAR)
| タイプ | House Prices での例 | 欠損の理由 | 推奨対応 |
|---|---|---|---|
| MNAR | PoolQC (99.5%欠損) |
プールのない物件 → 欠損 | "None"カテゴリとして扱う |
| MNAR | GarageType (5.5%欠損) |
ガレージなし物件 → 欠損 | "None" + 数値列は0 |
| MAR | LotFrontage (17.7%欠損) |
近隣地区で欠損傾向が異なる | Neighborhood別中央値で補完 |
| MCAR | Electrical (0.07%) |
1件のみ → 記録漏れ | 最頻値で補完 |
📊 House Prices 欠損率ランキング(上位10変数)
赤(>50%): 施設「なし」を意味する、オレンジ(10-50%): 真の欠損または条件付き、青(<10%): 軽微な欠損
PoolQC
99.5%
MiscFeature
96.3%
Alley
93.8%
Fence
80.8%
FireplaceQu
47.3%
LotFrontage
17.7%
GarageType
5.5%
GarageYrBlt
5.5%
GarageFinish
5.5%
BsmtQual
2.5%
💡
Garage系は全て同じ欠損率(5.5%) — これは「ガレージのない81物件」が一斉に欠損しているサイン。MNAR の典型例。
🔗 共欠損パターン — Garage系の同時欠損
Garage系6変数が 同時に 欠損している行数の分布。「施設なし」パターンの証拠。
Garage系変数の同時欠損数(0=欠損なし, 6=全変数欠損)
0変数欠損(全て存在)
1,379行
6変数同時欠損(ガレージなし)
81行
1〜5変数欠損(中間)
≈0行
⚠️
「0個欠損 or 全部欠損」に二極化している → MNAR の証拠。部分欠損がほぼ存在しない = ガレージの有無という構造的理由。この場合は「None」で統一補完が最善。
LotFrontage 欠損フラグ × SalePrice の関係
欠損グループの方が価格が低い傾向 → MNAR の可能性あり → 欠損フラグを特徴量に
🗂️ 主要欠損変数のスキーマ
| 変数名 | 型 | 欠損率 | 欠損の理由 | 補完戦略 |
|---|---|---|---|---|
PoolQC |
カテゴリ | 99.5% | プールなし物件 (MNAR) | fillna("None") |
MiscFeature |
カテゴリ | 96.3% | 特殊設備なし (MNAR) | fillna("None") |
Alley |
カテゴリ | 93.8% | 路地なし (MNAR) | fillna("None") |
Fence |
カテゴリ | 80.8% | フェンスなし (MNAR) | fillna("None") |
LotFrontage |
数値 | 17.7% | 真の欠損 (MAR) | Neighborhood別中央値 |
GarageType |
カテゴリ | 5.5% | ガレージなし (MNAR) | fillna("None") |
GarageYrBlt |
数値 | 5.5% | ガレージなし → 年が存在しない | fillna(0) |
BsmtQual |
カテゴリ | 2.5% | 地下室なし (MNAR) | fillna("None") |
Electrical |
カテゴリ | 0.07% | 記録漏れ1件 (MCAR) | 最頻値で補完 |
🧩 問題
📂
House Prices データセット(
train.csv)を使って以下の欠損値EDAを実装せよ。1 欠損率ランキング(降順)の可視化
欠損を含む全変数について欠損率(%)を計算し降順で表示。水平棒グラフで上位20変数を可視化すること。
2 共欠損パターンの分析
Garage系・Basement系に絞り、「どの行が同時に欠損しているか」を isnull().sum(axis=1) などで分析せよ。
3 欠損フラグと目的変数の関係
LotFrontage(欠損率17.7%)について:
- 欠損フラグ列
LotFrontage_missing(0/1)を作成 - 欠損あり・なしグループで
SalePriceの分布を boxplot で比較 - 欠損の有無が価格に影響するかを数値で確認
4 適切な補完戦略の実装
| 変数 | 推奨補完戦略 |
|---|---|
PoolQC (+ MiscFeature, Alley, Fence) | "None"(施設なし) |
LotFrontage | Neighborhood ごとの中央値 |
GarageYrBlt 等 Garage数値列 | 0(ガレージなし) |
💡 ヒント
ヒント1 — 方向性(クリックで展開)
欠損値の分析は「なぜ欠損しているか」を考えることから始まる。
まずは全変数の欠損率をソートして一覧表示してみよう。
df.isnull().sum() で件数、/ len(df) * 100 でパーセントに変換。まずは全変数の欠損率をソートして一覧表示してみよう。
ヒント2 — アプローチ(クリックで展開)
欠損率:
共欠損:
欠損フラグ:
グループ別中央値補完:
df.isnull().mean() * 100 で一発計算共欠損:
df[garage_cols].isnull().sum(axis=1).value_counts()欠損フラグ:
df['LotFrontage_missing'] = df['LotFrontage'].isnull().astype(int)グループ別中央値補完:
df.groupby('Neighborhood')['LotFrontage'].transform('median')
ヒント3 — コード骨格(クリックで展開)
# タスク1: 欠損率計算
missing_rate = df.isnull().mean() * 100
missing_rate = missing_rate[missing_rate > 0].sort_values(ascending=False)
# タスク2: Garage系共欠損
garage_cols = [c for c in df.columns if 'Garage' in c]
df[garage_cols].isnull().sum(axis=1).value_counts()
# タスク3: 欠損フラグ
df['LotFrontage_missing'] = df['LotFrontage'].isnull().astype(int)
# タスク4: 補完
df['PoolQC'] = df['PoolQC'].fillna('None')
df['LotFrontage'] = df.groupby('Neighborhood')['LotFrontage'].transform(
lambda x: x.fillna(x.median())
)
✅ 模範解答
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib
matplotlib.rcParams['font.family'] = 'IPAexGothic' # Colab: !apt install fonts-ipafont
df = pd.read_csv('train.csv')
# ──────────────────────────────────────
# タスク1: 欠損率ランキング(上位20)
# ──────────────────────────────────────
missing_rate = df.isnull().mean() * 100
missing_rate = missing_rate[missing_rate > 0].sort_values(ascending=False)
fig, ax = plt.subplots(figsize=(9, 8))
colors = ['#ef4444' if v > 50 else '#f97316' if v > 10 else '#3b82f6'
for v in missing_rate.head(20).values]
ax.barh(missing_rate.head(20).index[::-1],
missing_rate.head(20).values[::-1],
color=colors[::-1])
ax.set_xlabel('欠損率 (%)')
ax.set_title('欠損率ランキング(上位20変数)')
ax.axvline(50, color='red', linestyle='--', alpha=0.5, label='50%')
ax.axvline(10, color='orange', linestyle='--', alpha=0.5, label='10%')
ax.legend()
plt.tight_layout()
plt.show()
print(f"欠損を含む変数数: {len(missing_rate)} / {df.shape[1]}")
print("\n上位10変数:")
print(missing_rate.head(10).round(1).to_string())
# ──────────────────────────────────────
# タスク2: Garage系の共欠損パターン
# ──────────────────────────────────────
garage_cols = [c for c in df.columns if 'Garage' in c]
basement_cols = [c for c in df.columns if 'Bsmt' in c]
print("\n=== Garage系変数の欠損 ===")
print(df[garage_cols].isnull().sum())
garage_missing_count = df[garage_cols].isnull().sum(axis=1)
print("\nGarage系: 同時欠損変数の数")
print(garage_missing_count.value_counts().sort_index())
# → 「0変数欠損」or「全変数同時欠損」の二極化 = ガレージのMNAR
print("\n=== Basement系変数の欠損 ===")
bsmt_missing_count = df[basement_cols].isnull().sum(axis=1)
print(bsmt_missing_count.value_counts().sort_index())
# ──────────────────────────────────────
# タスク3: LotFrontage の欠損フラグ × SalePrice
# ──────────────────────────────────────
df['LotFrontage_missing'] = df['LotFrontage'].isnull().astype(int)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
sns.boxplot(x='LotFrontage_missing', y='SalePrice', data=df, ax=axes[0],
palette={0: '#3b82f6', 1: '#ef4444'})
axes[0].set_xticklabels([
f'LotFrontage あり\n(n={(df["LotFrontage_missing"]==0).sum()})',
f'LotFrontage 欠損\n(n={(df["LotFrontage_missing"]==1).sum()})'
])
axes[0].set_title('LotFrontage欠損 vs SalePrice')
axes[0].set_ylabel('SalePrice ($)')
stats = df.groupby('LotFrontage_missing')['SalePrice'].agg(['mean', 'median', 'std'])
stats.index = ['あり', '欠損']
axes[1].axis('off')
table_data = [[idx, f"${row['mean']:,.0f}", f"${row['median']:,.0f}", f"${row['std']:,.0f}"]
for idx, row in stats.iterrows()]
table = axes[1].table(
cellText=table_data,
colLabels=['グループ', '平均', '中央値', '標準偏差'],
loc='center', cellLoc='center'
)
table.auto_set_font_size(False)
table.set_fontsize(12)
table.scale(1.2, 2.5)
axes[1].set_title('SalePrice統計(欠損フラグ別)')
plt.tight_layout()
plt.show()
missing_mean = df[df['LotFrontage_missing']==1]['SalePrice'].mean()
exist_mean = df[df['LotFrontage_missing']==0]['SalePrice'].mean()
print(f"LotFrontage欠損グループの平均価格: ${missing_mean:,.0f}")
print(f"LotFrontage存在グループの平均価格: ${exist_mean:,.0f}")
print(f"差: ${exist_mean - missing_mean:,.0f} ({(exist_mean/missing_mean - 1)*100:.1f}%)")
# ──────────────────────────────────────
# タスク4: 補完戦略の実装
# ──────────────────────────────────────
# (a) 施設なし → "None"
for col in ['PoolQC', 'MiscFeature', 'Alley', 'Fence']:
if col in df.columns:
df[col] = df[col].fillna('None')
# (b) LotFrontage: Neighborhood別中央値補完
lot_before = df['LotFrontage'].isnull().sum()
df['LotFrontage'] = df.groupby('Neighborhood')['LotFrontage'].transform(
lambda x: x.fillna(x.median())
)
lot_after = df['LotFrontage'].isnull().sum()
print(f"\nLotFrontage 補完: {lot_before}件 → {lot_after}件 欠損")
# (c) Garage数値列: 0補完
for col in ['GarageYrBlt', 'GarageArea', 'GarageCars']:
if col in df.columns:
df[col] = df[col].fillna(0)
# Garageカテゴリ列: "None"補完
for col in ['GarageType', 'GarageFinish', 'GarageQual', 'GarageCond']:
if col in df.columns:
df[col] = df[col].fillna('None')
print(f"Garage系 欠損残: {df[garage_cols].isnull().sum().sum()}件")
print("\n最終欠損チェック(上位5):")
print((df.isnull().mean()*100).sort_values(ascending=False).head())
🪜 Step-by-Step 解説
1
欠損率の計算:
df.isnull().mean() * 100isnull() は欠損なら True(=1)、存在なら False(=0)。.mean() で列ごとの平均 → 欠損率(0〜1)。*100 でパーセントに変換。[missing_rate > 0] で欠損ゼロの変数を除外して見やすくする。
↓
2
共欠損の確認:
df[garage_cols].isnull().sum(axis=1)axis=1 は「行方向に合計」= 1行あたりの欠損変数数。Garage系なら「0個欠損 or 全部欠損」に二極化するはず → これが MNAR(ガレージ自体がない)の証拠。
↓
3
欠損フラグの作成:
欠損情報を特徴量として保持するテクニック。補完した後も「もともと欠損だった」情報をモデルに伝えられる。Kaggle上位解法でほぼ必須。必ず補完前にフラグを作ること。
df['col_missing'] = df['col'].isnull().astype(int)欠損情報を特徴量として保持するテクニック。補完した後も「もともと欠損だった」情報をモデルに伝えられる。Kaggle上位解法でほぼ必須。必ず補完前にフラグを作ること。
↓
4
グループごとの中央値補完:
各行を「自分が属するグループの統計量」で補完。
groupby + transformdf.groupby('Neighborhood')['LotFrontage'].transform(lambda x: x.fillna(x.median()))各行を「自分が属するグループの統計量」で補完。
transform は元のDataFrameと同じ行数のSeriesを返すので直接代入できる。単純な全体中央値より精度が高い。
📐 数学・統計の補足(文系向け)
なぜ「平均」でなく「中央値」で補完するのか?
外れ値があると平均は引っ張られる。
例: [100, 100, 100, 100, 1000]
価格や面積のような歪んだ分布では中央値補完が安全。
欠損率と情報価値のトレードオフ
💡
欠損率が高い変数(>50%)は削除候補に見えるが、Kaggleでは残す方が良い場合が多い。
PoolQC=None(プールなし)という情報自体が高額物件フィルタになるからだ。削除ではなく "None" カテゴリとして活用せよ。🏆 Kaggleでの実践的な使い方
欠損値EDAのコンペ別チェックリスト
| データ種別 | 主な欠損パターン | 推奨アプローチ |
|---|---|---|
| 表形式(House Prices等) | 施設なし・記録漏れ | "None"カテゴリ + 欠損フラグ |
| 時系列データ | センサー停止・取引なし日 | 前後の値で線形補間 (interpolate()) |
| 医療データ | 検査未実施(意図的欠損) | 欠損フラグ必須 + IterativeImputer |
| ユーザー行動ログ | 未操作 = 暗黙の0 | 0埋め or スパース行列 |
上位解法でよく見るメタ特徴量
# 各行の欠損数をメタ特徴として追加
df['n_missing_total'] = df.isnull().sum(axis=1)
# カテゴリ別欠損数
df['n_missing_garage'] = df[garage_cols].isnull().sum(axis=1)
df['n_missing_bsmt'] = df[basement_cols].isnull().sum(axis=1)
# 欠損フラグをまとめて作成
for col in ['LotFrontage', 'MasVnrArea', 'GarageYrBlt']:
df[f'{col}_missing'] = df[col].isnull().astype(int)
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| 欠損を全て平均で埋める | 「何か値を入れなきゃ」という焦り | 欠損の理由を先に調べる。MNARは"None"カテゴリが最適 |
| 欠損率が高い変数を削除する | 「欠損 = 使えない」という思い込み | 欠損自体が重要な情報(PoolQC=99.5%でも有用) |
| 補完後に欠損フラグを作る | 順番を気にしない | 補完で情報が消える!必ず補完前にフラグを作成 |
| groupby補完でNaNが残る | グループ内が全員欠損のエッジケース | .fillna(df['col'].median()) で二段構えに |
| 訓練+テストを一緒に補完 | 「一気に処理」の効率優先 | テストへのデータリーク!訓練データのみで統計値を計算し、テストに適用 |
🚀 次のステップ
発展1: missingno
可視化ライブラリ
欠損パターンのヒートマップ・デンドログラム可視化。共欠損を直感的に把握できる。
発展2: IterativeImputer
scikit-learn
多変量回帰補完。他の変数を使ってMissing値を予測する高精度な補完手法。
次回予告
Day 072
EDA体系的手法④ — 外れ値の検出と処理(IQR法・Isolation Forest・外れ値の扱い方)
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: