📚 背景知識(読んでから問題へ)
🎯
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 ≈ f1_micro ≈ f1_weighted ≈ f1_macro。不均衡データで差が大きく現れます。
🗂️ データスキーマ(Digits Dataset)
| 項目 | 値 | 説明 |
|---|---|---|
X.shape | (1797, 64) | 1797サンプル × 64特徴量(8×8画像をフラット化) |
y | 0〜9の整数 | 手書き数字のラベル(10クラス) |
| 各クラスのサンプル数 | 約178〜180件 | ほぼ均等(不均衡を人工的に作る問4で差が出る) |
| 特徴量の値域 | 0〜16 | ピクセルの輝度値(0=黒, 16=白) |
8×8画像のイメージ(数字「0」)
📝 問題
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_macroとf1_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
クラス1
≈ 0.96
クラス2
≈ 0.97
クラス3
≈ 0.96
クラス4
≈ 0.98
クラス5
≈ 0.98
クラス6
≈ 0.98
クラス7
≈ 0.98
クラス8
≈ 0.93
クラス9
≈ 0.96
クラス8(数字の「8」)は他の数字と形が似ているため誤分類されやすい傾向があります。
⚖️ 不均衡データでの 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 での使い方
📋 自己評価(解いた後に記入)
理解度:
自分の回答:
気づき・メモ: