Day 054 — 評価指標の深掘り(多クラス版)

2026-06-04 緑 / Phase 2 コーディング Macro / Micro / Weighted F1

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

🎯
Day 054 — ロジスティック回帰(Day 052–053)で使った Accuracy の限界を知り、Macro / Micro / Weighted F1 の違いを Digits データセットで体験します。

なぜ Accuracy だけでは不十分か?

⚠️
クラス不均衡の罠:正常:故障 = 9:1 のデータで「全部正常と予測」しただけで Accuracy 90% になります。しかし故障を1件も検出できていません。

Precision・Recall・F1 の直感的な理解

指標意味身近な例え計算式
Precision 陽性と予測した中で本当に陽性の割合 「魚と言ったものが本当に魚だった確率」 TP / (TP + FP)
Recall 本当に陽性のものを何割検出できたか 「川の魚のうち何割を釣れたか」 TP / (TP + FN)
F1 スコア Precision と Recall の調和平均 両方のバランスが取れたスコア 2×P×R / (P+R)

多クラスの F1 — 3種類の平均化方法

集計方法計算方法特徴使いどころ
Macro 各クラスの F1 を単純平均 少数クラスも均等に重みを置く クラス不均衡を検出したいとき
Micro TP・FP・FN をクラス横断で合算してから F1 計算 サンプル数が多いクラスほど重みが大きい バランスデータの全体精度を見るとき
Weighted 各クラスの F1 をサンプル数で重み付け平均 多数クラスの影響が大きく出る 不均衡データで全体精度を見るとき

3クラス例: クラスA(F1=0.90, 100件) / クラスB(F1=0.60, 10件) / クラスC(F1=0.80, 90件)

Macro F1
(0.90 + 0.60 + 0.80) / 3
= 0.767
← 少数クラスBが引き下げる
Weighted F1
(0.90×100 + 0.60×10 + 0.80×90) / 200
= 0.855
← 多数クラスA・Cが支配

📊 Digits での典型的な指標比較

LogisticRegression(C=1.0) / Digits データセット(均等バランス)での典型値

accuracy
≈ 0.972
≈ 0.972
f1_micro
≈ 0.972
≈ 0.972
f1_weighted
≈ 0.971
≈ 0.971
f1_macro
≈ 0.971
≈ 0.971
precision_macro
≈ 0.973
≈ 0.973
recall_macro
≈ 0.970
≈ 0.970
💡
バランスデータでは accuracy ≈ f1_micro ≈ f1_weighted ≈ f1_macro。不均衡データで差が大きく現れます。

🗂️ データスキーマ(Digits Dataset)

項目説明
X.shape(1797, 64)1797サンプル × 64特徴量(8×8画像をフラット化)
y0〜9の整数手書き数字のラベル(10クラス)
各クラスのサンプル数約178〜180件ほぼ均等(不均衡を人工的に作る問4で差が出る)
特徴量の値域0〜16ピクセルの輝度値(0=黒, 16=白)

8×8画像のイメージ(数字「0」)

8×8 ピクセル画像 輝度値: 0(黒)〜 16(白) 64次元ベクトルにフラット化

📝 問題

import pandas as pd
import numpy as np
from sklearn.datasets import load_digits
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import (
    accuracy_score, classification_report,
    confusion_matrix, f1_score,
    precision_score, recall_score
)

問1 — データの確認

  • load_digits() でデータを読み込む
  • データの形状(サンプル数・特徴量数・クラス数)を確認する
  • 各クラス(0〜9)のサンプル数を確認する(均等か?)
  • X[0].reshape(8, 8) で最初のサンプルが8×8の画像であることを確認せよ

問2 — モデル学習

  • train_test_split(80:20, stratify=y, random_state=42)で分割する
  • StandardScaler + LogisticRegression(C=1.0, max_iter=5000, random_state=42) の Pipeline を作成・学習する
  • テストデータで accuracy_score を計算する

問3 — 評価指標の比較(核心)

以下の6種類の評価指標をすべて計算し、それぞれの値を比較せよ:

metrics = {
    "accuracy":          accuracy_score(y_test, y_pred),
    "f1_macro":          f1_score(y_test, y_pred, average="macro"),
    "f1_micro":          f1_score(y_test, y_pred, average="micro"),
    "f1_weighted":       f1_score(y_test, y_pred, average="weighted"),
    "precision_macro":   precision_score(y_test, y_pred, average="macro"),
    "recall_macro":      recall_score(y_test, y_pred, average="macro"),
}
  • 値が最も低い指標はどれか?
  • なぜその指標が低くなるのか説明せよ

問4 — 不均衡データでの差異を確認

不均衡データを人工的に作り、Macro vs Weighted F1 の差を確認する:

# クラス0を10サンプルだけに削減(不均衡を作る)
mask_0 = (y == 0)
mask_other = (y != 0)

X_0 = X[mask_0][:10]
y_0 = y[mask_0][:10]
X_other = X[mask_other]
y_other = y[mask_other]

X_imb = np.vstack([X_0, X_other])
y_imb = np.hstack([y_0, y_other])
  • 上記の不均衡データで同じパイプラインを学習・評価する
  • f1_macrof1_weighted の差を計算する
  • 差が大きい場合、どちらを使うべきか?理由も述べよ

問5 — classification_report の読み方

classification_report(y_test, y_pred) の出力を見て以下に答えよ:

  • 最も F1 スコアが低いクラス(数字)はどれか?
  • そのクラスの Precision と Recall はそれぞれいくつか?
  • Precision と Recall のどちらが低いか?それは何を意味するか?

🔍 ヒント(段階的開示)

ヒント1 — 方向性
  • 問1: digits = load_digits(); print(digits.data.shape, len(digits.target_names))
  • 問3: f1_score(y_test, y_pred, average="macro/micro/weighted") でパラメータを切り替える
  • 問4: Macro は少数クラスの低い F1 に引きずられる、Weighted は多数クラスが支配する
  • 問5: report = classification_report(y_test, y_pred, output_dict=True) で辞書として取得できる
ヒント2 — アプローチ
# 6種類の指標を一括計算してデータフレームで表示
results = {
    "accuracy":        accuracy_score(y_test, y_pred),
    "f1_macro":        f1_score(y_test, y_pred, average="macro"),
    "f1_micro":        f1_score(y_test, y_pred, average="micro"),
    "f1_weighted":     f1_score(y_test, y_pred, average="weighted"),
    "precision_macro": precision_score(y_test, y_pred, average="macro"),
    "recall_macro":    recall_score(y_test, y_pred, average="macro"),
}
df_metrics = pd.DataFrame.from_dict(results, orient="index", columns=["value"])
df_metrics["value"] = df_metrics["value"].round(4)
print(df_metrics.sort_values("value"))
ヒント3 — コード骨格(ほぼ答え)
# classification_report を辞書として取得し最悪クラスを特定
report_dict = classification_report(y_test, y_pred, output_dict=True)
df_report = pd.DataFrame(report_dict).T  # クラスが行に来るように転置
class_only = df_report.iloc[:-3]  # accuracy, macro avg, weighted avg を除く
worst_class = class_only["f1-score"].idxmin()
print(f"最悪クラス: {worst_class}")
print(df_report.loc[worst_class, ["precision", "recall", "f1-score"]])

模範解答

import pandas as pd
import numpy as np
from sklearn.datasets import load_digits
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 (
    accuracy_score, classification_report,
    confusion_matrix, f1_score,
    precision_score, recall_score
)

# ── 問1: データの確認 ──
digits = load_digits()
X, y = digits.data, digits.target

print(f"データ形状: X={X.shape}, y={y.shape}")  # (1797, 64)
print(f"クラス数: {len(digits.target_names)}")   # 10
print(f"\nクラスごとのサンプル数:")
print(pd.Series(y).value_counts().sort_index())
# 各クラス約178〜180サンプル(ほぼ均等)

print(f"\n最初のサンプル(8×8画像):")
print(X[0].reshape(8, 8).astype(int))

# ── 問2: モデル学習 ──
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

pipeline = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(C=1.0, max_iter=5000, random_state=42))
])
pipeline.fit(X_train, y_train)
y_pred = pipeline.predict(X_test)

acc = accuracy_score(y_test, y_pred)
print(f"\n[問2] Accuracy: {acc:.4f}")

# ── 問3: 評価指標の比較 ──
metrics = {
    "accuracy":          accuracy_score(y_test, y_pred),
    "f1_macro":          f1_score(y_test, y_pred, average="macro"),
    "f1_micro":          f1_score(y_test, y_pred, average="micro"),
    "f1_weighted":       f1_score(y_test, y_pred, average="weighted"),
    "precision_macro":   precision_score(y_test, y_pred, average="macro"),
    "recall_macro":      recall_score(y_test, y_pred, average="macro"),
}

df_metrics = pd.DataFrame.from_dict(metrics, orient="index", columns=["value"])
df_metrics["value"] = df_metrics["value"].round(4)
print("\n[問3] 評価指標の比較:")
print(df_metrics.sort_values("value"))
# → バランスデータなので accuracy ≈ f1_micro ≈ f1_weighted ≈ f1_macro

# ── 問4: 不均衡データでの差異 ──
mask_0 = (y == 0)
mask_other = (y != 0)

X_0 = X[mask_0][:10]
y_0 = y[mask_0][:10]
X_other = X[mask_other]
y_other = y[mask_other]

X_imb = np.vstack([X_0, X_other])
y_imb = np.hstack([y_0, y_other])

X_train_i, X_test_i, y_train_i, y_test_i = train_test_split(
    X_imb, y_imb, test_size=0.2, stratify=y_imb, random_state=42
)

pipe_imb = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(C=1.0, max_iter=5000, random_state=42))
])
pipe_imb.fit(X_train_i, y_train_i)
y_pred_i = pipe_imb.predict(X_test_i)

f1_mac_i = f1_score(y_test_i, y_pred_i, average="macro", zero_division=0)
f1_wgt_i = f1_score(y_test_i, y_pred_i, average="weighted", zero_division=0)

print(f"\n[問4] 不均衡データ:")
print(f"  f1_macro    = {f1_mac_i:.4f}")
print(f"  f1_weighted = {f1_wgt_i:.4f}")
print(f"  差          = {abs(f1_mac_i - f1_wgt_i):.4f}")
# → Macro < Weighted(クラス0の低F1がMacroを引き下げる)
# → 少数クラスも公平に評価したいなら Macro を使うべき

# ── 問5: classification_report の読み方 ──
print("\n[問5] Classification Report (均等データ):")
print(classification_report(y_test, y_pred))

report_dict = classification_report(y_test, y_pred, output_dict=True)
df_report = pd.DataFrame(report_dict).T
class_only = df_report.drop(["accuracy", "macro avg", "weighted avg"])
worst_class = class_only["f1-score"].idxmin()
row = class_only.loc[worst_class]
print(f"最悪クラス: {worst_class}")
print(f"  Precision = {row['precision']:.4f}")
print(f"  Recall    = {row['recall']:.4f}")
print(f"  F1-score  = {row['f1-score']:.4f}")
# Precision < Recall → 予測しすぎ(False Positive が多い)
# Recall < Precision → 見逃しが多い(False Negative が多い)

📈 クラス別 F1 スコア(Digits 典型値)

LogisticRegression(C=1.0) での典型的なクラス別 F1 スコア

クラス0
0.99
≈ 0.99
クラス1
0.96
≈ 0.96
クラス2
0.97
≈ 0.97
クラス3
0.96
≈ 0.96
クラス4
0.98
≈ 0.98
クラス5
0.98
≈ 0.98
クラス6
0.98
≈ 0.98
クラス7
0.98
≈ 0.98
クラス8
0.93
≈ 0.93
クラス9
0.96
≈ 0.96
クラス8(数字の「8」)は他の数字と形が似ているため誤分類されやすい傾向があります。
1.00 0.97 0.94 0.91 0 1 2 3 4 5 6 7 8 クラス(数字) 最低 F1 Score

⚖️ 不均衡データでの Macro vs Weighted の差

Macro F1(不均衡データ)
クラス0(10件)も均等に評価
クラス0のF1が低い → 平均を引き下げる
≈ 0.88〜0.92
少数クラスの問題が見えやすい
Weighted F1(不均衡データ)
クラス0(10件)は重みが小さい
多数クラス(各178件)が支配
≈ 0.96〜0.97
全体精度は高く見える(少数クラスを隠す)
🚨
Kaggle の不均衡データコンペでは Macro F1 が評価指標として使われることが多い。少数クラスを正直に評価するため。Weighted F1 は少数クラスの問題を隠してしまう可能性がある。

🪜 Step-by-Step 解説

1 Accuracy と F1_micro の関係

# バランスデータ(各クラス均等)の場合
# → f1_micro ≈ accuracy(ほぼ一致する)

# 不均衡データの場合
# → accuracy は多数クラスに引きずられ高く見える
# → f1_macro は少数クラスの低 F1 に引きずられ低くなる

2 Macro・Micro・Weighted の計算イメージ

# 3クラス (A, B, C) の例
#   クラスA: Precision=0.90, Recall=0.85 → F1=0.874
#   クラスB: Precision=0.60, Recall=0.40 → F1=0.480
#   クラスC: Precision=0.80, Recall=0.90 → F1=0.847

# Macro F1 = (0.874 + 0.480 + 0.847) / 3 = 0.734
# → 少数クラスBがスコアを引き下げる

# Weighted F1 (A=100件, B=10件, C=90件)
# = (0.874×100 + 0.480×10 + 0.847×90) / 200 = 0.854
# → 多数クラスAとCが支配

3 Precision と Recall のトレードオフ

# 閾値を変えると Precision と Recall はトレードオフになる
# (二値分類でよく使われる概念)

# Precision を優先する場面:
#   → スパムフィルター(正常メールをスパムにしたくない)

# Recall を優先する場面:
#   → 癌検診(見逃しが命取り)

# どちらも重要な場面:
#   → F1 を使う(Kaggleコンペはたいていこちら)

4 scikit-learn での指定方法まとめ

from sklearn.metrics import f1_score

f1_score(y_test, y_pred, average="macro")     # 均等平均
f1_score(y_test, y_pred, average="micro")     # TP/FP/FN を合算して計算
f1_score(y_test, y_pred, average="weighted")  # サンプル数で重み付け平均
f1_score(y_test, y_pred, average=None)        # クラスごとの F1 を配列で返す

5 CV で f1_macro を最大化する

from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
    pipeline, X, y,
    cv=cv,
    scoring="f1_macro"  # ← ここを変えるだけ
)
print(f"CV Macro F1: {scores.mean():.4f} ± {scores.std():.4f}")

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

F1 スコアはなぜ「調和平均」を使うのか?

算術平均(問題あり)
(Precision + Recall) / 2
例: P=1.0, R=0.0 → 0.5
← 片方が0でも高く出てしまう!
調和平均(F1)
2 × P × R / (P + R)
例: P=1.0, R=0.0 → 0.0
← 片方が0なら全体も0になる

Macro vs Weighted の選び方

クラスのサンプル数が大きく偏っている?
YES → 「少数クラスの評価を重視したい?」
YES → Macro F1(少数クラスも平等に評価)
NO → Weighted F1(全体精度を重視)
NO(ほぼ均等)→ どちらもほぼ同じ

🏆 Kaggleでの実践的な使い方

コンペタイプよく使われる指標理由
医療(癌診断)Recall(Macro)見逃しのコストが致命的
スパム検出Precision誤検知のコストが高い
多クラス分類(均等)Accuracy / Macro F1各クラスを平等に評価
多クラス分類(不均衡)Macro F1少数クラスも正直に評価
確率予測Log Loss / AUC-ROCラベルではなく確率の精度を評価
# Kaggle実践: cross_val_score の scoring に f1_macro を指定
from sklearn.model_selection import cross_val_score, StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(
    pipeline, X, y,
    cv=cv,
    scoring="f1_macro"  # "f1_weighted", "f1_micro" にも変更可能
)
print(f"CV Macro F1: {scores.mean():.4f} ± {scores.std():.4f}")

# Kaggle実践: 不均衡データには class_weight="balanced" も有効
pipeline_balanced = Pipeline([
    ("scaler", StandardScaler()),
    ("model", LogisticRegression(
        C=1.0,
        class_weight="balanced",  # 少数クラスに高い重みを自動付与
        max_iter=5000
    ))
])

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
Accuracy だけ見て「精度が高い」と満足する 最もわかりやすい指標だから 不均衡データでは Accuracy が高くても少数クラスを無視している可能性がある
Micro F1 と Accuracy が別物と思う 別の指標名だから バランスデータでは Micro F1 ≈ Accuracy(ほぼ一致)
Macro と Weighted の違いを知らない デフォルト値だけ使う 不均衡データでは Macro の方が少数クラスを正直に評価する
average=None の結果を一つの数値と混同する 戻り値が配列になる average=None は各クラスの F1 の配列を返す(スカラーではない)
Precision と Recall のトレードオフを知らない F1 だけ見ている 閾値を変えると Precision↑Recall↓(またはその逆)の関係になる

🚀 次のステップ

  • 発展: ROC曲線と AUC-ROC スコアの理解(二値分類 → 多クラスへの拡張)
  • 次回予告 (Day 055): ROC曲線と AUC — 閾値・感度・特異度の関係と Kaggle での使い方

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

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