Day 044 — 欠損値の確認と対処

2026-05-24 白 / Phase 1 分析 欠損値処理

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

データ分析で避けられないのが「欠損値(missing value)」です。Kaggleのコンペデータにはほぼ必ず欠損が含まれており、適切に処理しないとモデルがエラーになったり、予測精度が大きく落ちたりします。

欠損値の3種類

MCAR(完全ランダム)

欠損の発生が他の変数と完全に無関係
例: アンケートで誤ってスキップ

MAR(ランダム)

欠損が他の変数と関係する
例: 年配者ほど体重を記入しない

MNAR(非ランダム)

欠損値そのものが意味を持つ
例: 高収入者ほど収入欄を非記入

主要な3つのメソッド

df.isna()

欠損の位置を True/False で確認
.sum() で欠損数を集計

df.fillna(値)

欠損を特定の値で埋める
平均・中央値・最頻値など

df.dropna()

欠損を含む行(or列)を削除
データが減るリスクあり

💡
isna() と isnull() は同じ: pandasではどちらも使えます。コードの一貫性のために片方を統一して使いましょう。

🗂️ データスキーマ

今回使用するタイタニック風データ(10行8列)のスキーマです。

列名説明欠損欠損率
PassengerIdint64乗客IDなし0%
Survivedint64生存フラグ (0/1)なし0%
Pclassint64チケットクラス (1/2/3)なし0%
Sexobject性別なし0%
Agefloat64年齢2件(行5・6)20.0%
SibSpint64兄弟・配偶者数なし0%
Farefloat64運賃2件(行3・10)20.0%
Embarkedobject乗船港 (S/C/Q)1件(行6)10.0%

📊 欠損状況ビジュアル

各列の Non-Null Count(欠損ゼロ行数)の棒グラフです。全10行中、赤・黄の列に欠損があります。

PassengerId
10 / 10 (0%)
Survived
10 / 10 (0%)
Pclass
10 / 10 (0%)
Sex
10 / 10 (0%)
Age ← 欠損!
8 / 10 (20%)
SibSp
10 / 10 (0%)
Fare ← 欠損!
8 / 10 (20%)
Embarked ← 欠損
9 / 10 (10%)

緑 = 欠損なし | 赤 = 欠損 20% | 黄 = 欠損 10%

isna().sum() と isna().mean() の違い df.isna() True False False False True False .sum() .sum() 欠損数 ×100 .mean() * 100 欠損率 (%) True=1, False=0 Age: 2, Fare: 2, ... Age: 20.0%, Fare: 20.0%

🎯 補完戦略の選び方

補完方法向いているケースコード例注意点
平均値補完 外れ値がない数値列 fillna(df["Age"].mean()) 外れ値に引きずられる
中央値補完 外れ値がある数値列 fillna(df["Age"].median()) Kaggleで最もよく使われる
最頻値補完 カテゴリ変数 fillna(df["Embarked"].mode()[0]) mode()はSeriesを返す点に注意
グループ別補完 他の変数と関係がある場合 groupby("Sex")["Age"].transform(lambda x: x.fillna(x.median())) 最も精度が高い
dropna vs fillna — どちらを使うか? 欠損列の欠損率は? 50%以上? いいえ fillna() 値で補完する はい dropna() 列ごと削除を検討 Titanic の Cabin 列は欠損77% → has_cabin 特徴量に変換するのが定石

📝 問題

📌
シナリオ: 以下のデータの欠損値を分析し、適切な方法で補完してください。
import pandas as pd
import numpy as np

data = {
    "PassengerId": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    "Survived":   [0, 1, 1, 1, 0, 0, 0, 0, 1, 1],
    "Pclass":     [3, 1, 3, 1, 3, 3, 1, 3, 3, 2],
    "Sex":        ["male","female","female","female","male","male","male","male","female","female"],
    "Age":        [22.0, 38.0, 26.0, 35.0, np.nan, np.nan, 54.0, 2.0, 27.0, 14.0],
    "SibSp":      [1, 1, 0, 1, 0, 0, 0, 3, 0, 1],
    "Fare":       [7.25, 71.28, np.nan, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, np.nan],
    "Embarked":   ["S", "C", "S", "S", "S", np.nan, "S", "S", "S", "C"],
}
df = pd.DataFrame(data)

タスク1 — 欠損状況の全体把握

isna().sum()isna().mean() を使って、以下の情報を出力してください。

  • 各列の欠損数(整数)
  • 各列の欠損率(パーセント、小数点1桁)
💡
欠損がある列だけ表示するために missing_count[missing_count > 0] で絞り込みましょう。

タスク2 — 数値列の欠損補完

  • Age 列: 性別(Sex)ごとの中央値 で補完してください
  • Fare 列: Pclass ごとの平均値 で補完してください
⚠️
単純な fillna(df["Age"].mean()) ではなく、groupby + transform を使ったグループ別補完を実装してください。

タスク3 — カテゴリ変数の欠損補完

Embarked 列の欠損(1件)を最頻値で補完してください。

💡
mode() は Series を返します。mode()[0] で最頻値のスカラー値を取り出してください。

タスク4 — 補完後の確認

補完後に isna().sum().sum() が 0 になることを確認してください。

💡
assert df.isna().sum().sum() == 0 で自動確認できます。

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • タスク1: isna().sum() は各列の欠損数、isna().mean() * 100 で欠損率(%)
  • タスク2: groupby("Sex")["Age"].transform("median") でグループ別中央値を行に対応させる
  • タスク3: mode() は最頻値を返すが Series なので [0] でスカラーを取り出す
  • タスク4: isna().sum().sum() は全列の欠損数合計
ヒント2 — アプローチ
# タスク1
missing_count = df.isna().sum()
missing_rate  = df.isna().mean() * 100

# タスク2 — グループ別中央値で補完
df["Age"] = df.groupby("Sex")["Age"].transform(
    lambda x: x.fillna(x.median())
)
# Fare — Pclassごとの平均
df["Fare"] = df.groupby("Pclass")["Fare"].transform(
    lambda x: x.fillna(x.mean())
)

# タスク3 — 最頻値補完
df["Embarked"] = df["Embarked"].fillna(df["Embarked"].mode()[0])

# タスク4
assert df.isna().sum().sum() == 0
ヒント3 — コード骨格(ほぼ答え)
# タスク1
missing_count = df.isna().__()
missing_rate  = df.isna().___(___) * 100

# タスク2
df["Age"] = df.groupby(___)[___].transform(
    lambda x: x.fillna(x.___())
)
df["Fare"] = df.groupby(___)[___].transform(
    lambda x: x.fillna(x.___())
)

# タスク3
df["Embarked"] = df["Embarked"].fillna(df["Embarked"].___()[___])

# タスク4
assert df.isna().___().___() == 0

模範解答

import pandas as pd
import numpy as np

data = {
    "PassengerId": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    "Survived":   [0, 1, 1, 1, 0, 0, 0, 0, 1, 1],
    "Pclass":     [3, 1, 3, 1, 3, 3, 1, 3, 3, 2],
    "Sex":        ["male","female","female","female","male","male","male","male","female","female"],
    "Age":        [22.0, 38.0, 26.0, 35.0, np.nan, np.nan, 54.0, 2.0, 27.0, 14.0],
    "SibSp":      [1, 1, 0, 1, 0, 0, 0, 3, 0, 1],
    "Fare":       [7.25, 71.28, np.nan, 53.10, 8.05, 8.46, 51.86, 21.07, 11.13, np.nan],
    "Embarked":   ["S", "C", "S", "S", "S", np.nan, "S", "S", "S", "C"],
}
df = pd.DataFrame(data)

# ── タスク1: 欠損状況の全体把握 ──
missing_count = df.isna().sum()
missing_rate  = df.isna().mean() * 100

print("=== タスク1: 欠損状況 ===")
for col in df.columns:
    cnt  = int(missing_count[col])
    rate = missing_rate[col]
    if cnt > 0:
        print(f"  {col:<12}: {cnt}件 ({rate:.1f}%)")
    else:
        print(f"  {col:<12}: 欠損なし")
# Age      : 2件 (20.0%)
# Fare     : 2件 (20.0%)
# Embarked : 1件 (10.0%)

# ── タスク2: 数値列の欠損補完 ──
# Age — 性別ごとの中央値で補完
df["Age"] = df.groupby("Sex")["Age"].transform(
    lambda x: x.fillna(x.median())
)

# Fare — Pclassごとの平均で補完
df["Fare"] = df.groupby("Pclass")["Fare"].transform(
    lambda x: x.fillna(x.mean())
)

print("\n=== タスク2: 補完後 Age・Fare ===")
print(df[["PassengerId","Sex","Pclass","Age","Fare"]])

# ── タスク3: カテゴリ変数の欠損補完 ──
embarked_mode = df["Embarked"].mode()[0]  # "S"
df["Embarked"] = df["Embarked"].fillna(embarked_mode)

print(f"\n=== タスク3: Embarked 最頻値='{embarked_mode}' で補完済み ===")

# ── タスク4: 欠損ゼロ確認 ──
total_missing = df.isna().sum().sum()
print(f"\n=== タスク4: 残り欠損数 = {total_missing} ===")
assert total_missing == 0, "まだ欠損が残っています!"
print("全欠損の補完が完了しました ✓")

補完前後の比較

PassengerIdSexPclassAge(補完前)Age(補完後)Fare(補完前)Fare(補完後)
1male322.022.07.257.25
2female138.038.071.2871.28
3female326.026.0NaN9.12
4female135.035.053.1053.10
5male3NaN22.08.058.05
6male3NaN22.08.468.46
7male154.054.051.8651.86
8male32.02.021.0721.07
9female327.027.011.1311.13
10female214.014.0NaN30.07

青文字 = 補完された値 | 赤文字 = 補完前の NaN

Age male 中央値: {22.0, 54.0, 2.0} → 22.0 | Fare Pclass=3 平均: (7.25+8.05+8.46+21.07+11.13)/5 = 11.19... ※行3はfemaleでPclass=3 → 同クラス平均で補完 | Fare Pclass=2 平均: 30.07 (行10のみ)

🪜 Step-by-Step 解説

1 isna() → sum() で欠損を数える

isna() は DataFrame 全体を True/False の DataFrame に変換します。True = 欠損sum() は True を 1、False を 0 として合計するため、各列の欠損数が得られます。mean() は True の割合(0〜1)を返すので、×100 でパーセントになります。

2 groupby + transform でグループ別補完

df["Age"] = df.groupby("Sex")["Age"].transform(
    lambda x: x.fillna(x.median())
)

groupby("Sex") で男性グループと女性グループに分割 → 各グループ内で x.fillna(x.median()) を実行 → transform で元の行数に戻してから代入します。単純な fillna(全体中央値) より精度が上がります。

3 mode()[0] で最頻値を取り出す

embarked_mode = df["Embarked"].mode()[0]  # "S"
df["Embarked"] = df["Embarked"].fillna(embarked_mode)

mode() は最頻値が複数ある場合も考慮してリスト(Series)を返します。最頻値が1つの場合でも [0] でスカラーを取り出す必要があります。直接 .fillna(df["Embarked"].mode()) と書くと型が合わず警告が出ることがあります。

4 isna().sum().sum() で全欠損をゼロ確認

assert df.isna().sum().sum() == 0

isna().sum() は各列の欠損数(Series) → 最後の .sum() でそれを合計し、全欠損数を1つの数値で得ます。assert は条件が偽のとき AssertionError を発生させるため、補完漏れを自動検出できます。

⚙️ transform vs agg — なぜ transform が必要か

agg(集計)— NG for 補完

グループごとに1行に圧縮される。元のDataFrameの行と対応しないため、補完には使えない。

df.groupby("Sex")["Age"].agg("median")
# Sex
# female    30.5
# male      22.0
# → 2行しか返らない!
transform(変換)— OK for 補完

元のDataFrameと同じ行数を保ったまま変換。各行にそのグループの値が入るため補完に最適。

df.groupby("Sex")["Age"].transform("median")
# 0    30.5  (female)
# 1    30.5  (female)
# 4    22.0  (male)
# → 元の行数(10行)で返る!
transform の動き — グループ値を元の行に「広げる」 元のAge列 22 (male) 38 (female) NaN (male) NaN (male) transform female 中央値 30.5 male 中央値 22.0 fillna 補完後のAge列 22 (male) 38 (female) 22.0 (male) 22.0 (male)

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

補完はなぜ必要か — 欠損値とモデルの関係

scikit-learnの機械学習モデルは「全ての値が埋まっているDataFrame」を期待しています。NaNが1つでも残っていると、モデル学習時に ValueError: Input contains NaN が発生します。これがKaggleで欠損値処理が必須の理由です。

平均と中央値のどちらを使うか

Age列の値を考えてみましょう。若い人(2歳)と高齢者(54歳)が混在しており、運賃も7.25〜71.28と大きく差があります。このような「外れ値がある分布」では中央値のほうが外れ値の影響を受けないため、補完に適しています。

例: [2, 22, 27, 54] の平均 = 26.25 / 中央値 = (22+27)/2 = 24.5
54という大きな値が平均を引き上げているが、中央値は影響を受けない

isna().sum() の仕組み — Trueを「1」として足す

Pythonでは True == 1False == 0 です。これを使うと sum() が「True(欠損)の個数」を返すことになります。mean() は合計/件数なので、欠損率が返ります。

🏆 Kaggleでの実践的な使い方

Titanic コンペで実際に使われる欠損値対処テンプレートです。

import pandas as pd

train = pd.read_csv("train.csv")
test  = pd.read_csv("test.csv")

# ① 欠損状況を一気に確認(欠損のある列だけ表示)
missing_summary = pd.DataFrame({
    "count": train.isnull().sum(),
    "rate(%)": (train.isnull().mean() * 100).round(1)
}).query("count > 0")
print(missing_summary)

# ② Age: 性別×クラスのグループ中央値で補完(精度が上がる)
for df in [train, test]:
    df["Age"] = df.groupby(["Sex", "Pclass"])["Age"].transform(
        lambda x: x.fillna(x.median())
    )

# ③ Embarked: 最頻値補完(欠損は2件のみ)
train["Embarked"] = train["Embarked"].fillna(train["Embarked"].mode()[0])

# ④ Fare: Pclassごとの中央値で補完(test に1件欠損)
test["Fare"] = test.groupby("Pclass")["Fare"].transform(
    lambda x: x.fillna(x.median())
)

# ⑤ Cabin: 欠損が77%と多い → 新特徴量 "has_cabin" に変換
for df in [train, test]:
    df["has_cabin"] = df["Cabin"].notna().astype(int)
    df.drop("Cabin", axis=1, inplace=True)

# ⑥ 最終確認
print("Train missing:", train.isnull().sum().sum())
print("Test missing: ", test.isnull().sum().sum())
欠損率Titanicでの定石処理
Age20%Sex × Pclass のグループ中央値で補完
Embarked0.2%最頻値("S")で補完
Fare0.1%Pclassの中央値で補完(test のみ1件)
Cabin77%欠損多すぎ → "has_cabin"(0/1)特徴量に変換

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
グループ考慮なし補完(全体平均) 手軽だから 性別・クラスで分布が異なる場合は情報損失大。groupby + transform でグループ別補完を使う
dropna() でとりあえず削除 楽だから データが減りモデルの精度が落ちる。欠損10%以上は補完を優先する
fillna(df["Embarked"].mode()) と直接渡す mode() の返り値を誤解 mode() は Series を返す。mode()[0] でスカラーを取り出してから fillna に渡す
補完後に欠損が残っていないか確認しない 見落とし 必ず isna().sum().sum() == 0 で確認する。モデル学習でエラーになる前に気づく
test を train の統計で補完しない 1つのDataFrameで処理しがち test は train の統計量(平均・中央値)で補完する。test 単体で計算するとデータリークになる

🚀 次のステップ

  • 発展: df.isnull().sum() のヒートマップ可視化(seaborn.heatmap(df.isnull()))で欠損パターンを視覚的に把握する
  • 次回予告: データの可視化(matplotlib / seaborn でのヒストグラム・散布図・ボックスプロット)

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

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