📚 背景知識(読んでから問題へ)
データ分析で避けられないのが「欠損値(missing value)」です。Kaggleのコンペデータにはほぼ必ず欠損が含まれており、適切に処理しないとモデルがエラーになったり、予測精度が大きく落ちたりします。
欠損値の3種類
欠損の発生が他の変数と完全に無関係
例: アンケートで誤ってスキップ
欠損が他の変数と関係する
例: 年配者ほど体重を記入しない
欠損値そのものが意味を持つ
例: 高収入者ほど収入欄を非記入
主要な3つのメソッド
欠損の位置を True/False で確認.sum() で欠損数を集計
欠損を特定の値で埋める
平均・中央値・最頻値など
欠損を含む行(or列)を削除
データが減るリスクあり
🗂️ データスキーマ
今回使用するタイタニック風データ(10行8列)のスキーマです。
| 列名 | 型 | 説明 | 欠損 | 欠損率 |
|---|---|---|---|---|
PassengerId | int64 | 乗客ID | なし | 0% |
Survived | int64 | 生存フラグ (0/1) | なし | 0% |
Pclass | int64 | チケットクラス (1/2/3) | なし | 0% |
Sex | object | 性別 | なし | 0% |
Age | float64 | 年齢 | 2件(行5・6) | 20.0% |
SibSp | int64 | 兄弟・配偶者数 | なし | 0% |
Fare | float64 | 運賃 | 2件(行3・10) | 20.0% |
Embarked | object | 乗船港 (S/C/Q) | 1件(行6) | 10.0% |
📊 欠損状況ビジュアル
各列の Non-Null Count(欠損ゼロ行数)の棒グラフです。全10行中、赤・黄の列に欠損があります。
緑 = 欠損なし | 赤 = 欠損 20% | 黄 = 欠損 10%
🎯 補完戦略の選び方
| 補完方法 | 向いているケース | コード例 | 注意点 |
|---|---|---|---|
| 平均値補完 | 外れ値がない数値列 | 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())) |
最も精度が高い |
📝 問題
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("全欠損の補完が完了しました ✓")
補完前後の比較
| PassengerId | Sex | Pclass | Age(補完前) | Age(補完後) | Fare(補完前) | Fare(補完後) |
|---|---|---|---|---|---|---|
| 1 | male | 3 | 22.0 | 22.0 | 7.25 | 7.25 |
| 2 | female | 1 | 38.0 | 38.0 | 71.28 | 71.28 |
| 3 | female | 3 | 26.0 | 26.0 | NaN | 9.12 |
| 4 | female | 1 | 35.0 | 35.0 | 53.10 | 53.10 |
| 5 | male | 3 | NaN | 22.0 | 8.05 | 8.05 |
| 6 | male | 3 | NaN | 22.0 | 8.46 | 8.46 |
| 7 | male | 1 | 54.0 | 54.0 | 51.86 | 51.86 |
| 8 | male | 3 | 2.0 | 2.0 | 21.07 | 21.07 |
| 9 | female | 3 | 27.0 | 27.0 | 11.13 | 11.13 |
| 10 | female | 2 | 14.0 | 14.0 | NaN | 30.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 が必要か
グループごとに1行に圧縮される。元のDataFrameの行と対応しないため、補完には使えない。
df.groupby("Sex")["Age"].agg("median")
# Sex
# female 30.5
# male 22.0
# → 2行しか返らない!
元のDataFrameと同じ行数を保ったまま変換。各行にそのグループの値が入るため補完に最適。
df.groupby("Sex")["Age"].transform("median")
# 0 30.5 (female)
# 1 30.5 (female)
# 4 22.0 (male)
# → 元の行数(10行)で返る!
🔢 数学・統計の補足(文系向け)
補完はなぜ必要か — 欠損値とモデルの関係
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 == 1、False == 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での定石処理 |
|---|---|---|
Age | 20% | Sex × Pclass のグループ中央値で補完 |
Embarked | 0.2% | 最頻値("S")で補完 |
Fare | 0.1% | Pclassの中央値で補完(test のみ1件) |
Cabin | 77% | 欠損多すぎ → "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 でのヒストグラム・散布図・ボックスプロット)