Day 056 — Confusion Matrix の可視化と詳細分析

2026-06-06 緑 / Phase 2 コーディング heatmap・FP/FN・ビジネス解釈

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

🎯
Day 056 — Day 055 で学んだ ROC曲線・AUC は「すべての閾値での総合評価」でした。Confusion Matrix は特定の閾値での予測結果を4つのマス(TN/FP/FN/TP)に分解して可視化します。エラー分析・閾値チューニングの出発点。

Confusion Matrix の4要素

名称英語実際予測身近な例(Titanic)
TN(真陰性) True Negative 死亡(0) 死亡(0) 死亡者を「死亡」と正しく予測 ✅
FP(偽陽性) False Positive 死亡(0) 生存(1) 死亡者を「生存」と誤予測 ⚠️
FN(偽陰性) False Negative 生存(1) 死亡(0) 生存者を「死亡」と誤予測 ❌
TP(真陽性) True Positive 生存(1) 生存(1) 生存者を「生存」と正しく予測 ✅

評価指標との対応

評価指標計算式重視するとき
Accuracy(TP + TN) / 全件数均衡データ・全体の正確さ
Precision(適合率)TP / (TP + FP)FP を減らしたいとき
Recall(感度)TP / (TP + FN)FN を減らしたいとき
F1 Score2×P×R / (P+R)PとRのバランス
Specificity(特異度)TN / (TN + FP)陰性を正確に判定したいとき

📊 Confusion Matrix のイメージ

Titanic データの典型的な Confusion Matrix(行=実際のラベル / 列=予測ラベル)

予測: 死亡(0) 予測: 生存(1) 実際 死亡(0) 実際 生存(1) TN 真陰性 正しく「死亡」と予測 FP 偽陽性 誤って「生存」と予測 FN 偽陰性 誤って「死亡」と予測 TP 真陽性 正しく「生存」と予測 cm.ravel() → (TN, FP, FN, TP)
💡
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() で4値を一発取得できる(2値分類専用)

⚖️ FP vs FN のビジネスコスト比較

ユースケース別: FP(誤検知)と FN(見逃し)どちらが重大か

FN(見逃し)が深刻なケース

癌検診(見逃しは命取り)
FN コスト 非常に高い
Critical
不正検出(詐欺見逃し)
FN コスト 高い
High
故障予知(設備停止回避)
FN コスト 高い
High

FP(誤検知)が深刻なケース

スパムフィルター(誤削除)
FP コスト 中〜高
Medium+
融資審査(優良客拒否)
FP コスト 高い
High
保険料算定(誤って低く設定)
FP コスト 中
Medium

🗂️ データスキーマ(Titanic)

特徴量説明前処理
pclassint客室クラス(1=上流/2=中流/3=下流)そのまま
agefloat年齢(欠損あり)中央値補完
farefloat運賃(ポンド)StandardScaler
sibspint兄弟姉妹・配偶者の数そのまま
parchint親・子供の数そのまま
sex_numint性別(male=0 / female=1).map({'male':0,'female':1})

Titanic 生存者の分布(survived=1 約38%, survived=0 約62%)

死亡(0) ≈ 62% 生存(1) ≈ 38%

📝 問題

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
    confusion_matrix, ConfusionMatrixDisplay,
    classification_report
)

# Titanic データの読み込み(seaborn から)
titanic = sns.load_dataset('titanic')

問1 — データ前処理

  1. titanic から pclass / age / fare / sibsp / parch / sex_num を特徴量として選択せよ(sexmale=0, female=1 で数値化)
  2. 目標変数: survived
  3. age の欠損値を中央値で補完せよ
  4. train_test_split(80:20, stratify=survived, random_state=42)で分割せよ

問2 — モデル学習と基本評価

  1. StandardScaler + LogisticRegression(max_iter=1000, random_state=42) の Pipeline を学習せよ
  2. テストデータの y_pred を取得せよ
  3. classification_report を出力せよ(target_names=['死亡(0)', '生存(1)']

問3 — Confusion Matrix の可視化

  1. confusion_matrix(y_test, y_pred) で混同行列を計算せよ
  2. seaborn.heatmap で可視化せよ(annot=True, fmt='d', cmap='Blues'
  3. TP・TN・FP・FN の値をそれぞれ取り出して出力せよ

問4 — 正規化 Confusion Matrix

  1. normalize='true' で行正規化した混同行列を計算せよ
  2. 「生存クラス(1)の Recall」を正規化行列から読み取れるか確認せよ
  3. 「死亡クラス(0)の Recall」も同様に確認せよ

問5 — FP/FN のビジネス解釈

以下の2つのシナリオについて、どちらの誤りが重大かを考察せよ:

  • シナリオA: Titanic の生存予測を「救助ボートの優先順位付け」に使う場合
  • シナリオB: Titanic の生存予測を「保険料の算定」に使う場合

💡 ヒント

ヒント1(方向性)
  • confusion_matrix[[TN, FP], [FN, TP]] の2×2配列を返す
  • TP は cm[1, 1]、TN は cm[0, 0]、FP は cm[0, 1]、FN は cm[1, 0]
  • Recall = TP / (TP + FN) = 正規化行列の cm_norm[1, 1]
ヒント2(アプローチ)
# sex の数値化
titanic['sex_num'] = titanic['sex'].map({'male': 0, 'female': 1})

# confusion_matrix から各値を取り出す
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()
ヒント3(コード骨格)
# heatmap の描画
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# 絶対数
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0],
            xticklabels=['死亡(0)', '生存(1)'],
            yticklabels=['死亡(0)', '生存(1)'])
axes[0].set_title('Confusion Matrix(絶対数)')
axes[0].set_ylabel('実際のラベル')
axes[0].set_xlabel('予測ラベル')

# 正規化(行方向)
cm_norm = confusion_matrix(y_test, y_pred, normalize='true')
sns.heatmap(cm_norm, annot=True, fmt='.2f', cmap='Blues', ax=axes[1],
            xticklabels=['死亡(0)', '生存(1)'],
            yticklabels=['死亡(0)', '生存(1)'])
axes[1].set_title('Confusion Matrix(行正規化)')

模範解答

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
    confusion_matrix, classification_report
)

# ── 問1: データ前処理 ──
titanic = sns.load_dataset('titanic')

titanic['sex_num'] = titanic['sex'].map({'male': 0, 'female': 1})

features = ['pclass', 'age', 'fare', 'sibsp', 'parch', 'sex_num']
target = 'survived'

df = titanic[features + [target]].copy()
df['age'] = df['age'].fillna(df['age'].median())

X = df[features].values
y = df[target].values

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

print(f"[問1] 訓練データ: {X_train.shape}, テストデータ: {X_test.shape}")
print(f"  y=0(死亡): {(y_test==0).sum()}件, y=1(生存): {(y_test==1).sum()}件")

# ── 問2: モデル学習 ──
pipeline = Pipeline([
    ('scaler', StandardScaler()),
    ('model', LogisticRegression(max_iter=1000, random_state=42))
])
pipeline.fit(X_train, y_train)

y_pred = pipeline.predict(X_test)

print("\n[問2] classification_report:")
print(classification_report(y_test, y_pred, target_names=['死亡(0)', '生存(1)']))

# ── 問3: Confusion Matrix の可視化 ──
cm = confusion_matrix(y_test, y_pred)

print(f"\n[問3] Confusion Matrix:")
print(cm)

tn, fp, fn, tp = cm.ravel()
print(f"  TN(真陰性)= {tn}  → 死亡を正しく死亡と予測")
print(f"  FP(偽陽性)= {fp}  → 死亡なのに生存と誤予測")
print(f"  FN(偽陰性)= {fn}  → 生存なのに死亡と誤予測")
print(f"  TP(真陽性)= {tp}  → 生存を正しく生存と予測")

# heatmap(絶対数 + 正規化)
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0],
            xticklabels=['死亡(0)', '生存(1)'],
            yticklabels=['死亡(0)', '生存(1)'])
axes[0].set_title('Confusion Matrix(絶対数)')
axes[0].set_ylabel('実際のラベル')
axes[0].set_xlabel('予測ラベル')

# ── 問4: 正規化 Confusion Matrix ──
cm_norm = confusion_matrix(y_test, y_pred, normalize='true')

sns.heatmap(cm_norm, annot=True, fmt='.2f', cmap='Blues', ax=axes[1],
            xticklabels=['死亡(0)', '生存(1)'],
            yticklabels=['死亡(0)', '生存(1)'])
axes[1].set_title('Confusion Matrix(行正規化)')
axes[1].set_ylabel('実際のラベル')
axes[1].set_xlabel('予測ラベル')

plt.tight_layout()
plt.savefig('confusion_matrix.png', dpi=150)
plt.show()

print(f"\n[問4] 正規化 Confusion Matrix:")
print(cm_norm.round(3))
print(f"  生存クラス(1)の Recall(TPR)= {cm_norm[1, 1]:.3f}(正規化行列の右下)")
print(f"  死亡クラス(0)の Recall(TNR)= {cm_norm[0, 0]:.3f}(正規化行列の左上)")

# ── 問5: ビジネス解釈 ──
print("""
[問5] FP/FN のビジネス解釈:

シナリオA: 救助ボートの優先順位付け
  FN が重大(生存できる人を見逃すと救助できない)
  → Recall(感度)を最大化する閾値を選ぶべき
  → 閾値を下げる(0.5 → 0.3 など)

シナリオB: 保険料の算定
  FP が重大(死亡リスクの高い人を「生存」と誤判定 → 保険料を低く設定)
  → Precision(適合率)を重視した閾値を選ぶべき
  → 閾値を上げる(0.5 → 0.7 など)
""")

🎚️ 閾値を変えると FP/FN はどう変わるか

閾値を低くする(0.3)
→ 陽性(生存)と判定する件数が増える
TPR(Recall)上昇(見逃し減)
FPR 上昇(誤検知増)
→ FN が減り、FP が増える
救助優先・癌検診向け
閾値を高くする(0.7)
→ 陽性(生存)と判定する件数が減る
TPR(Recall)低下(見逃し増)
FPR 低下(誤検知減)
→ FP が減り、FN が増える
保険・融資審査向け

閾値ごとの FP・FN トレードオフ(イメージ)

閾値(低 ← 0 〜 1 → 高) 件数 FP(偽陽性) FN(偽陰性) 最適閾値候補

🪜 Step-by-Step 解説

1confusion_matrix の構造を理解する

cm = confusion_matrix(y_test, y_pred)
print(cm)
# [[TN  FP]
#  [FN  TP]]
#
# cm[0, 0] = TN(実際0, 予測0)
# cm[0, 1] = FP(実際0, 予測1)← 偽陽性
# cm[1, 0] = FN(実際1, 予測0)← 偽陰性
# cm[1, 1] = TP(実際1, 予測1)

# ravel() で1発取り出し(2値分類のみ)
tn, fp, fn, tp = cm.ravel()

2ravel() vs インデックス参照

# 方法1: ravel()(2値分類のみ)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel()

# 方法2: インデックス参照(多クラスにも拡張できる)
cm = confusion_matrix(y_test, y_pred)
tn = cm[0, 0]
fp = cm[0, 1]
fn = cm[1, 0]
tp = cm[1, 1]

3正規化行列と評価指標の対応

cm_norm = confusion_matrix(y_test, y_pred, normalize='true')
#          予測0(死亡)    予測1(生存)
# 実際0    TNR(特異度)   FPR(偽陽性率)
# 実際1    FNR(見逃し率)  TPR(感度=Recall)

# つまり:
# Recall(生存クラス)= cm_norm[1, 1]
# Recall(死亡クラス)= cm_norm[0, 0](= Specificity)

4閾値を変えて FP/FN をコントロール

y_prob = pipeline.predict_proba(X_test)[:, 1]

for threshold in [0.3, 0.4, 0.5, 0.6, 0.7]:
    y_pred_t = (y_prob >= threshold).astype(int)
    cm_t = confusion_matrix(y_test, y_pred_t)
    tn_t, fp_t, fn_t, tp_t = cm_t.ravel()
    print(f"閾値{threshold:.1f}: TN={tn_t}, FP={fp_t}, FN={fn_t}, TP={tp_t}")

5ConfusionMatrixDisplay(sklearn 0.23+)

from sklearn.metrics import ConfusionMatrixDisplay

disp = ConfusionMatrixDisplay(
    confusion_matrix=cm,
    display_labels=['死亡(0)', '生存(1)']
)
disp.plot(cmap='Blues')
plt.title('Confusion Matrix')
plt.show()

6FP/FN サンプルを取り出してエラー分析

X_test_df = pd.DataFrame(X_test, columns=features)
X_test_df['y_true'] = y_test
X_test_df['y_pred'] = y_pred

# 偽陰性(FN): 生存なのに死亡と予測したサンプル
false_negatives = X_test_df[(X_test_df['y_true'] == 1) & (X_test_df['y_pred'] == 0)]
print("FN サンプルの特徴:")
print(false_negatives.describe())

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

4つのマスから計算できる全評価指標

指標計算式(TN/FP/FN/TP を使用)正規化 CM での位置
Accuracy(TP + TN) / 全件数対角和 / 全体
PrecisionTP / (TP + FP)列方向正規化 cm_p[1,1]
Recall (TPR)TP / (TP + FN)行方向正規化 cm_t[1,1]
Specificity (TNR)TN / (TN + FP)行方向正規化 cm_t[0,0]
FPRFP / (FP + TN)行方向正規化 cm_t[0,1]
F1 Score2×P×R / (P+R)直接読めない(計算が必要)
⚠️
不均衡データの罠: 死亡62% / 生存38% のデータで「全員死亡と予測」すると Accuracy = 62%。でも Recall(生存)= 0%。必ず CM + Precision/Recall を確認すること。

🏆 Kaggleでの実践的な使い方

用途Confusion Matrix の見方
評価指標の確認FP・FN から Precision・Recall を手計算して確認
閾値チューニング複数閾値で CM を出力し、ビジネス要件に合わせる
エラー分析FP・FN のサンプルを取り出して特徴を分析
アンサンブル後の確認各モデル・アンサンブル後の CM を比較して改善を確認
# Kaggle実践: CV で threshold チューニング
from sklearn.model_selection import StratifiedKFold, cross_val_predict

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
y_prob_cv = cross_val_predict(pipeline, X, y, cv=cv, method='predict_proba')[:, 1]

best_f1, best_thr = 0, 0.5
from sklearn.metrics import f1_score

for thr in np.arange(0.3, 0.7, 0.02):
    y_cv = (y_prob_cv >= thr).astype(int)
    f = f1_score(y, y_cv, average='macro')
    if f > best_f1:
        best_f1, best_thr = f, thr

print(f"最適閾値(CV): {best_thr:.2f}  F1(macro): {best_f1:.4f}")

🚫 よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
cm[0,1] が FN だと思う 行・列の順序を混同 cm[行=実際, 列=予測]。FP は cm[0,1](実際0→予測1)、FN は cm[1,0]
normalize='pred' で Recall を読む 正規化方向を間違える Recall は行方向(normalize='true')で正規化した cm_norm[1,1]
不均衡データで Accuracy だけ見る デフォルトの直感 Accuracy は不均衡では無意味。必ず CM + Recall/Precision を確認
FP と FN のコストが同じと思う デフォルトで Accuracy を最適化 ビジネス要件でコストは非対称。class_weight='balanced' や閾値調整で対応
ravel() を多クラスで使う 2値で動いたので使いまわす ravel() は2値分類専用。多クラスではインデックス参照が必要

🚀 次のステップ

  • 発展: class_weight='balanced' で不均衡データに対応した学習
  • 次回予告(Day 057): PR 曲線(Precision-Recall Curve)と Average Precision — 極端な不均衡データの評価

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

✍️

理解度: [ ] 完全理解   [ ] おおむね理解   [ ] 要復習

自分の回答:

気づき・メモ: