📚 背景知識(読んでから問題へ)
🎯
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 Score | 2×P×R / (P+R) | PとRのバランス |
| Specificity(特異度) | TN / (TN + FP) | 陰性を正確に判定したいとき |
📊 Confusion Matrix のイメージ
Titanic データの典型的な Confusion Matrix(行=実際のラベル / 列=予測ラベル)
💡
tn, fp, fn, tp = confusion_matrix(y_test, y_pred).ravel() で4値を一発取得できる(2値分類専用)⚖️ FP vs FN のビジネスコスト比較
ユースケース別: FP(誤検知)と FN(見逃し)どちらが重大か
FN(見逃し)が深刻なケース
FP(誤検知)が深刻なケース
🗂️ データスキーマ(Titanic)
| 特徴量 | 型 | 説明 | 前処理 |
|---|---|---|---|
pclass | int | 客室クラス(1=上流/2=中流/3=下流) | そのまま |
age | float | 年齢(欠損あり) | 中央値補完 |
fare | float | 運賃(ポンド) | StandardScaler |
sibsp | int | 兄弟姉妹・配偶者の数 | そのまま |
parch | int | 親・子供の数 | そのまま |
sex_num | int | 性別(male=0 / female=1) | .map({'male':0,'female':1}) |
Titanic 生存者の分布(survived=1 約38%, survived=0 約62%)
📝 問題
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 — データ前処理
titanicからpclass / age / fare / sibsp / parch / sex_numを特徴量として選択せよ(sexをmale=0, female=1で数値化)- 目標変数:
survived ageの欠損値を中央値で補完せよtrain_test_split(80:20,stratify=survived,random_state=42)で分割せよ
問2 — モデル学習と基本評価
StandardScaler+LogisticRegression(max_iter=1000, random_state=42)の Pipeline を学習せよ- テストデータの
y_predを取得せよ classification_reportを出力せよ(target_names=['死亡(0)', '生存(1)'])
問3 — Confusion Matrix の可視化
confusion_matrix(y_test, y_pred)で混同行列を計算せよseaborn.heatmapで可視化せよ(annot=True, fmt='d', cmap='Blues')- TP・TN・FP・FN の値をそれぞれ取り出して出力せよ
問4 — 正規化 Confusion Matrix
normalize='true'で行正規化した混同行列を計算せよ- 「生存クラス(1)の Recall」を正規化行列から読み取れるか確認せよ
- 「死亡クラス(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 トレードオフ(イメージ)
🪜 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) / 全件数 | 対角和 / 全体 |
| Precision | TP / (TP + FP) | 列方向正規化 cm_p[1,1] |
| Recall (TPR) | TP / (TP + FN) | 行方向正規化 cm_t[1,1] |
| Specificity (TNR) | TN / (TN + FP) | 行方向正規化 cm_t[0,0] |
| FPR | FP / (FP + TN) | 行方向正規化 cm_t[0,1] |
| F1 Score | 2×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 — 極端な不均衡データの評価
📋 自己評価(解いた後に記入)
✍️
理解度: [ ] 完全理解 [ ] おおむね理解 [ ] 要復習
自分の回答:
気づき・メモ: