評価指標の全体像 — Accuracy/Precision/Recall/F1/AUC

分類モデルを構築したあと、「このモデルはどれだけ良いのか」をどう判断すればよいでしょうか。最も直感的な指標は正解率(Accuracy)ですが、これだけでは不十分な場合が多々あります。

たとえば、1000人の患者のうち10人が病気であるデータを考えてください。「全員健康」と予測するだけで正解率99%が達成できますが、このモデルには何の価値もありません。病気の患者を見逃すことは、健康な人を誤って陽性と判定するよりもはるかに深刻だからです。

このように、何を重視するかによって適切な評価指標は異なります。評価指標の選択はモデルの設計と同じくらい重要な判断であり、ビジネス上の要件を直接反映するものです。

評価指標を理解すると、以下のような場面で適切な判断ができるようになります。

  • 医療診断: 偽陰性(見逃し)を最小化したい → Recall重視
  • スパム検出: 偽陽性(正常メールのスパム判定)を最小化したい → Precision重視
  • クラス不均衡: Accuracyが意味をなさない場面での適切な指標選択
  • 閾値の調整: ROC曲線とPR曲線による最適な分類閾値の決定

本記事の内容

  • 混同行列の定義と各要素の意味
  • Accuracy, Precision, Recall, F1スコアの定義と関係
  • ROC曲線とAUC
  • PR曲線と不均衡データ
  • 多クラス分類での評価指標
  • Pythonでの実装と可視化

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

混同行列(Confusion Matrix)

定義

二値分類の結果は、4つのカテゴリに分類されます。

予測: Positive 予測: Negative
真: Positive TP(真陽性) FN(偽陰性, Type II error)
真: Negative FP(偽陽性, Type I error) TN(真陰性)

この表が混同行列(confusion matrix)です。全ての分類評価指標は、この4つの数値の組み合わせから計算されます。

  • TP(True Positive): 正しく陽性と判定した数
  • FP(False Positive): 誤って陽性と判定した数(第1種の過誤)
  • FN(False Negative): 誤って陰性と判定した数(第2種の過誤)
  • TN(True Negative): 正しく陰性と判定した数

各指標と混同行列の関係

混同行列から導かれる主要な評価指標を見ていきましょう。

Accuracy(正解率)

$$ \begin{equation} \text{Accuracy} = \frac{TP + TN}{TP + FP + FN + TN} \end{equation} $$

全予測のうち正解した割合です。クラスが均等な場合には直感的で有用ですが、クラス不均衡がある場合には多数派クラスに引きずられて高い値を示すため、誤解を招きます。

Precision(適合率/精度)

$$ \begin{equation} \text{Precision} = \frac{TP}{TP + FP} \end{equation} $$

「陽性と予測したもののうち、実際に陽性であった割合」です。偽陽性のコストが高い場面(スパム検出: 重要なメールをスパムに分類するリスク、推薦システム: 不適切な推薦のリスク)で重視されます。

Precisionが高いモデルは「陽性と言ったら本当に陽性」——慎重で保守的な予測をするモデルです。

Recall(再現率/感度)

$$ \begin{equation} \text{Recall} = \frac{TP}{TP + FN} \end{equation} $$

「実際に陽性であるもののうち、陽性と予測できた割合」です。偽陰性のコストが高い場面(医療診断: 病気の見逃し、異常検知: 不正取引の見逃し)で重視されます。

Recallが高いモデルは「陽性を漏れなく検出する」——積極的に陽性を拾うモデルです。

Precision-Recallのトレードオフ

PrecisionとRecallにはトレードオフの関係があります。分類閾値を下げて「少しでも可能性があれば陽性」とすればRecallは上がりますが、FPも増えてPrecisionは下がります。逆に閾値を上げれば Precisionは上がりますがRecallは下がります。

このトレードオフを一つの数値にまとめるのがF1スコアです。

F1スコア

$$ \begin{equation} F_1 = \frac{2 \cdot \text{Precision} \cdot \text{Recall}}{\text{Precision} + \text{Recall}} = \frac{2TP}{2TP + FP + FN} \end{equation} $$

F1スコアはPrecisionとRecallの調和平均です。調和平均は算術平均と異なり、一方の値が極端に低いと全体が低くなる性質があります。例えば、Precision=1.0, Recall=0.01のとき、算術平均は0.505ですがF1は0.0198です。

$F_\beta$ スコア

PrecisionとRecallの相対的な重要度が異なる場合、$F_\beta$ スコアを使います。

$$ F_\beta = (1 + \beta^2) \cdot \frac{\text{Precision} \cdot \text{Recall}}{\beta^2 \cdot \text{Precision} + \text{Recall}} $$

  • $\beta = 1$: PrecisionとRecallを同等に重視(= F1スコア)
  • $\beta = 2$: Recallをより重視(医療診断向き)
  • $\beta = 0.5$: Precisionをより重視(スパム検出向き)
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, precision_score, recall_score, f1_score
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

np.random.seed(42)

# 不均衡データの生成
X, y = make_classification(n_samples=1000, n_features=20, n_informative=10,
                            weights=[0.9, 0.1], random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
y_prob = model.predict_proba(X_test)[:, 1]

fig, axes = plt.subplots(1, 3, figsize=(16, 5))

# (a) 混同行列
ax = axes[0]
y_pred = model.predict(X_test)
cm = confusion_matrix(y_test, y_pred)
im = ax.imshow(cm, cmap="Blues", interpolation="nearest")
ax.set_xticks([0, 1])
ax.set_yticks([0, 1])
ax.set_xticklabels(["Pred Neg", "Pred Pos"], fontsize=11)
ax.set_yticklabels(["True Neg", "True Pos"], fontsize=11)
ax.set_title("Confusion Matrix", fontsize=13)

# 数値を表示
labels_cm = [["TN", "FP"], ["FN", "TP"]]
for i in range(2):
    for j in range(2):
        ax.text(j, i, f"{labels_cm[i][j]}\n{cm[i,j]}",
                ha="center", va="center", fontsize=14,
                color="white" if cm[i,j] > cm.max()/2 else "black")

plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)

# (b) 閾値とPrecision/Recallの関係
ax = axes[1]
thresholds = np.linspace(0, 1, 200)
precisions = []
recalls = []
f1s = []
accuracies = []

for t in thresholds:
    y_pred_t = (y_prob >= t).astype(int)
    if y_pred_t.sum() == 0:
        precisions.append(1.0)
    else:
        precisions.append(precision_score(y_test, y_pred_t, zero_division=1))
    recalls.append(recall_score(y_test, y_pred_t, zero_division=0))
    f1s.append(f1_score(y_test, y_pred_t, zero_division=0))
    accuracies.append(np.mean(y_test == y_pred_t))

ax.plot(thresholds, precisions, "b-", linewidth=2, label="Precision")
ax.plot(thresholds, recalls, "r-", linewidth=2, label="Recall")
ax.plot(thresholds, f1s, "g-", linewidth=2, label="F1")
ax.plot(thresholds, accuracies, "k--", linewidth=1.5, alpha=0.5, label="Accuracy")

best_f1_idx = np.argmax(f1s)
ax.axvline(thresholds[best_f1_idx], color="green", linestyle=":", linewidth=2,
           alpha=0.7, label=f"Best F1 threshold={thresholds[best_f1_idx]:.2f}")

ax.set_xlabel("Classification Threshold", fontsize=12)
ax.set_ylabel("Score", fontsize=12)
ax.set_title("Metrics vs Threshold", fontsize=13)
ax.legend(fontsize=9)
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1.05)

# (c) Precision-Recall曲線
ax = axes[2]
from sklearn.metrics import precision_recall_curve, average_precision_score

precision_curve, recall_curve, _ = precision_recall_curve(y_test, y_prob)
ap = average_precision_score(y_test, y_prob)

ax.plot(recall_curve, precision_curve, "b-", linewidth=2,
        label=f"PR curve (AP={ap:.3f})")
ax.axhline(y_test.mean(), color="red", linestyle="--", linewidth=1.5,
           label=f"Baseline (prevalence={y_test.mean():.2f})")

ax.set_xlabel("Recall", fontsize=12)
ax.set_ylabel("Precision", fontsize=12)
ax.set_title("Precision-Recall Curve", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1.05)

plt.tight_layout()
plt.savefig("classification_metrics.png", dpi=150, bbox_inches="tight")
plt.show()

# 指標の表示
print(f"Accuracy:  {np.mean(y_test == y_pred):.4f}")
print(f"Precision: {precision_score(y_test, y_pred):.4f}")
print(f"Recall:    {recall_score(y_test, y_pred):.4f}")
print(f"F1:        {f1_score(y_test, y_pred):.4f}")

このグラフから、各評価指標の性質が読み取れます。

  1. 左図(混同行列): TNが最も多く、TPとFPとFNの関係が可視化されています。不均衡データでは多数派クラス(Negative)の正解が支配的であり、Accuracyがこれに引きずられて高くなることがわかります

  2. 中央図(閾値と指標の関係): 閾値を下げるとRecall(赤)は上がりますがPrecision(青)は下がるトレードオフが明確に見えます。F1(緑)はこのトレードオフの最適バランスを提供し、緑の点線が最適閾値を示しています。Accuracy(黒破線)は閾値によらずほぼ一定の高い値を示しており、不均衡データでは情報量が少ないことがわかります

  3. 右図(PR曲線): PR曲線はRecallを横軸、Precisionを縦軸にプロットしたもので、曲線が右上に近いほど良いモデルです。赤い破線はランダム分類器のベースライン(有病率)を示しています。Average Precision(AP)は曲線の下の面積に相当し、モデルの総合的な性能を1つの数値で表します

ROC曲線とAUC

ROC曲線の定義

ROC曲線(Receiver Operating Characteristic curve)は、偽陽性率(False Positive Rate, FPR)を横軸、真陽性率(True Positive Rate, TPR = Recall)を縦軸にプロットしたものです。

$$ \text{FPR} = \frac{FP}{FP + TN}, \qquad \text{TPR} = \frac{TP}{TP + FN} $$

分類閾値を0から1まで動かしたときのFPRとTPRの軌跡がROC曲線です。

AUC(Area Under the Curve)

AUCはROC曲線の下の面積で、0から1の値をとります。

  • AUC = 1.0: 完璧な分類器
  • AUC = 0.5: ランダムな分類器(対角線)
  • AUC < 0.5: ランダムより悪い(予測を反転すれば改善される)

AUCの確率的解釈は次の通りです。ランダムに選んだ陽性サンプルと陰性サンプルについて、陽性サンプルのスコアが陰性サンプルのスコアより高い確率がAUCに等しいのです。

$$ \text{AUC} = P(\hat{p}(\bm{x}^+) > \hat{p}(\bm{x}^-)) $$

ROC曲線 vs PR曲線

ROC曲線 PR曲線
クラス均衡時 適切 適切
クラス不均衡時 楽観的になりがち より適切
ベースライン 対角線(AUC=0.5) 有病率
主な用途 モデル比較 不均衡データの評価

クラスが極端に不均衡(例: 陽性0.1%)の場合、FPが多少増えてもFPR $= FP/(FP+TN)$ はTNが非常に大きいためほとんど変化しません。結果としてROC曲線は良い性能を示しますが、実際のPrecisionは低い可能性があります。このような場合にはPR曲線が実態をより正確に反映します。

import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, auc, precision_recall_curve, average_precision_score
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

np.random.seed(42)

# 不均衡データ
X, y = make_classification(n_samples=2000, n_features=20, n_informative=10,
                            weights=[0.95, 0.05], random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

# 複数のモデル
models = [
    ("Logistic Regression", LogisticRegression(max_iter=1000)),
    ("Random Forest", RandomForestClassifier(n_estimators=100, random_state=42)),
]

fig, axes = plt.subplots(1, 2, figsize=(14, 6))

# (a) ROC曲線
ax = axes[0]
ax.plot([0, 1], [0, 1], "k--", linewidth=1.5, alpha=0.5, label="Random (AUC=0.5)")

for name, model in models:
    model.fit(X_train, y_train)
    y_prob = model.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, y_prob)
    roc_auc = auc(fpr, tpr)
    ax.plot(fpr, tpr, linewidth=2, label=f"{name} (AUC={roc_auc:.3f})")

ax.set_xlabel("False Positive Rate (FPR)", fontsize=12)
ax.set_ylabel("True Positive Rate (TPR)", fontsize=12)
ax.set_title("ROC Curves", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1.05)
ax.set_aspect("equal")

# (b) PR曲線
ax = axes[1]
prevalence = y_test.mean()
ax.axhline(prevalence, color="gray", linestyle="--", linewidth=1.5, alpha=0.5,
           label=f"Baseline (prevalence={prevalence:.3f})")

for name, model in models:
    y_prob = model.predict_proba(X_test)[:, 1]
    precision_c, recall_c, _ = precision_recall_curve(y_test, y_prob)
    ap = average_precision_score(y_test, y_prob)
    ax.plot(recall_c, precision_c, linewidth=2, label=f"{name} (AP={ap:.3f})")

ax.set_xlabel("Recall", fontsize=12)
ax.set_ylabel("Precision", fontsize=12)
ax.set_title("Precision-Recall Curves", fontsize=13)
ax.legend(fontsize=10)
ax.grid(True, alpha=0.3)
ax.set_xlim(0, 1)
ax.set_ylim(0, 1.05)

plt.tight_layout()
plt.savefig("roc_pr_curves.png", dpi=150, bbox_inches="tight")
plt.show()

このグラフから、ROC曲線とPR曲線の違いが読み取れます。

  1. 左図(ROC曲線): 両モデルとも高いAUC(0.9以上)を示しており、良好な分類性能に見えます。ROC曲線は左上の角に近いほど良い性能を表し、ランダム分類器の対角線から大きく離れています

  2. 右図(PR曲線): 同じモデルでもPR曲線では性能差がより明確に見えます。陽性クラスが5%しかないため、PR曲線のベースライン(灰色破線)が0.05付近と非常に低く、モデルの改善幅が大きく見えます。Random ForestがLogistic Regressionより高いAPを達成していることが、PR曲線ではより鮮明です

不均衡データではPR曲線がモデルの実用的な性能をより正確に反映するため、ROC曲線と併せて確認することが推奨されます。

多クラス分類の評価指標

マクロ平均とマイクロ平均

多クラス分類では、各クラスのPrecision/Recallを計算した後、それらの集約方法が問題になります。

マクロ平均(macro average): 各クラスの指標の単純平均

$$ \text{Precision}_{\text{macro}} = \frac{1}{K}\sum_{k=1}^K \text{Precision}_k $$

全てのクラスを均等に扱うため、少数派クラスの性能が低い場合にそれが反映されます。

マイクロ平均(micro average): 全クラスのTP, FP, FNを合算してから指標を計算

$$ \text{Precision}_{\text{micro}} = \frac{\sum_k TP_k}{\sum_k (TP_k + FP_k)} $$

データ数の多いクラスの影響が大きくなります。マイクロ平均のPrecision = Recall = F1 = Accuracyが成り立ちます。

加重平均(weighted average): 各クラスのサンプル数で重み付けした平均

$$ \text{Precision}_{\text{weighted}} = \sum_{k=1}^K \frac{n_k}{n} \text{Precision}_k $$

import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

np.random.seed(42)

# 3クラスの不均衡データ
X, y = make_classification(n_samples=1000, n_features=20, n_informative=15,
                            n_classes=3, weights=[0.7, 0.2, 0.1],
                            n_clusters_per_class=1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

model = LogisticRegression(max_iter=1000).fit(X_train, y_train)
y_pred = model.predict(X_test)

# 混同行列
cm = confusion_matrix(y_test, y_pred)

fig, axes = plt.subplots(1, 2, figsize=(14, 5.5))

# (a) 多クラス混同行列
ax = axes[0]
im = ax.imshow(cm, cmap="Blues", interpolation="nearest")
for i in range(3):
    for j in range(3):
        ax.text(j, i, f"{cm[i,j]}", ha="center", va="center", fontsize=14,
                color="white" if cm[i,j] > cm.max()/2 else "black")
ax.set_xticks([0, 1, 2])
ax.set_yticks([0, 1, 2])
ax.set_xticklabels(["Pred 0", "Pred 1", "Pred 2"], fontsize=11)
ax.set_yticklabels(["True 0", "True 1", "True 2"], fontsize=11)
ax.set_title("Multi-class Confusion Matrix", fontsize=13)
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)

# (b) クラスごとの指標
ax = axes[1]
report = classification_report(y_test, y_pred, output_dict=True)

classes = ["0", "1", "2"]
precisions = [report[c]["precision"] for c in classes]
recalls = [report[c]["recall"] for c in classes]
f1s = [report[c]["f1-score"] for c in classes]
supports = [report[c]["support"] for c in classes]

x_pos = np.arange(3)
width = 0.25

ax.bar(x_pos - width, precisions, width, color="steelblue", alpha=0.8, label="Precision")
ax.bar(x_pos, recalls, width, color="coral", alpha=0.8, label="Recall")
ax.bar(x_pos + width, f1s, width, color="lightgreen", alpha=0.8, label="F1")

for i, s in enumerate(supports):
    ax.text(i, max(precisions[i], recalls[i], f1s[i]) + 0.02,
            f"n={s}", ha="center", fontsize=9)

# マクロ/マイクロ平均を追加
macro_f1 = report["macro avg"]["f1-score"]
micro_f1 = report["accuracy"]
ax.axhline(macro_f1, color="red", linestyle="--", linewidth=1.5,
           label=f"Macro F1 = {macro_f1:.3f}")
ax.axhline(micro_f1, color="purple", linestyle=":", linewidth=1.5,
           label=f"Micro F1 (=Acc) = {micro_f1:.3f}")

ax.set_xlabel("Class", fontsize=12)
ax.set_ylabel("Score", fontsize=12)
ax.set_title("Per-class Metrics", fontsize=13)
ax.set_xticks(x_pos)
ax.set_xticklabels([f"Class {c} (n={s})" for c, s in zip(classes, supports)])
ax.legend(fontsize=9, ncol=2)
ax.grid(True, alpha=0.3, axis="y")
ax.set_ylim(0, 1.15)

plt.tight_layout()
plt.savefig("multiclass_metrics.png", dpi=150, bbox_inches="tight")
plt.show()

print(classification_report(y_test, y_pred))

このグラフから、多クラス分類の評価指標の特性が読み取れます。

  1. 左図(混同行列): 3クラスの混同行列で、対角線上の値が正解数を表しています。クラス0(多数派)は正解率が高いですが、クラス2(少数派)はサンプル数が少なく、誤分類されやすいことが読み取れます

  2. 右図(クラスごとの指標): クラスによってPrecision、Recall、F1が異なることが明確です。少数派のクラス2はF1が低く、モデルがこのクラスの分類に苦労していることがわかります。マクロF1(赤破線)は各クラスを均等に扱うため少数派クラスの低い性能を反映しますが、マイクロF1(紫点線、= Accuracy)は多数派クラスに引きずられて高い値を示しています

評価指標の選択ガイド

場面 推奨指標 理由
クラス均衡な二値分類 Accuracy, F1, AUC どれも適切
クラス不均衡な二値分類 F1, PR-AUC, Recall Accuracyは不適切
偽陰性のコストが高い Recall, $F_2$ 見逃しの最小化
偽陽性のコストが高い Precision, $F_{0.5}$ 誤検出の最小化
モデル比較 AUC, AP 閾値に依存しない
多クラス(均等重要) Macro F1 少数派も均等に評価
多クラス(全体重要) Weighted F1, Accuracy データ分布に合わせた評価
ランキング NDCG, MRR 順序の質を評価

まとめ

本記事では、分類問題の主要な評価指標について体系的に解説しました。

  • 混同行列(TP, FP, FN, TN)は全ての分類評価指標の出発点
  • Accuracyはクラス均衡時に有用だが、不均衡データでは誤解を招く
  • Precisionは偽陽性のコストが高い場面、Recallは偽陰性のコストが高い場面で重視する
  • F1スコアはPrecisionとRecallの調和平均で、$F_\beta$ で重要度のバランスを調整できる
  • ROC曲線とAUCは閾値に依存しないモデル比較に適するが、不均衡データでは楽観的になりがち
  • PR曲線とAPは不均衡データの評価に適しており、ROCと併せて使うべき
  • 多クラスではマクロ平均(クラス均等)とマイクロ平均(サンプル数比例)の違いを理解する

次のステップとして、以下の記事も参考にしてください。