はじめに
分類モデルを作ったとき、性能をどう評価するかで結論が変わってしまうことがあります。正解率(Accuracy)だけを見ていると、クラスの数が偏ったデータでは実態を見誤ることがあります。たとえば「解約する人」が全体の5%しかいないデータで、全員を「解約しない」と予測しても正解率は95%になってしまいます。
AUCは、こうした閾値の置き方やクラスの偏りの影響を受けにくい指標として、分類モデルの評価でよく使われています。
この記事では、まずAUCの元になっているROC曲線とは何かを押さえたうえで、TPR・FPRの計算方法、ROC曲線の描き方、AUCの算出方法を、サンプルコードと一緒に紹介していきます。
AUCとは何か?
多くの分類モデルは、0か1かのラベルをいきなり出すのではなく、「予測確率」というスコアを出力します。以降ではこの予測確率のことを単に「スコア」と呼びます。このスコアに対して閾値(たとえば0.5以上なら陽性、など)を決めることで、はじめて「予測ラベル」が決まります。
実際に、同じスコアでも閾値の位置によって予測ラベルがどう変わるのかを、図で確認してみます。ここで作る会員の解約予測データ(df1)は、この記事の中で以降も使い回します。
コード
!pip install japanize-matplotlib -q
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
from matplotlib.lines import Line2D
df1 = pd.DataFrame({
"id": ["A","B","C","D","E","F","G","H","I","J"],
"true_churn": [1,0,1,0,0,1,0,0,1,0],
"score": [0.82,0.35,0.58,0.62,0.55,0.74,0.10,0.40,0.89,0.68],
})
def plot_threshold(ax, df, threshold):
pred = (df["score"] >= threshold).astype(int)
colors = ["tab:red" if p == 1 else "tab:blue" for p in pred]
ax.scatter(df["score"], df["true_churn"], c=colors, s=120, edgecolor="black", zorder=3)
ax.axvline(threshold, color="gray", linestyle="--")
ax.set_yticks([0, 1])
ax.set_yticklabels(["実際:解約なし", "実際:解約あり"])
ax.set_xlabel("予測確率(スコア)")
ax.set_xlim(0, 1)
ax.set_title(f"閾値 = {threshold}")
fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)
plot_threshold(axes[0], df1, 0.3)
plot_threshold(axes[1], df1, 0.6)
legend_elements = [
Line2D([0], [0], marker="o", color="w", markerfacecolor="tab:red", markersize=10, label="予測:解約する"),
Line2D([0], [0], marker="o", color="w", markerfacecolor="tab:blue", markersize=10, label="予測:解約しない"),
]
fig.legend(handles=legend_elements, loc="upper center", ncol=2)
plt.tight_layout()
plt.show()
出力結果

点線が閾値の位置です。同じデータ・同じスコアでも、閾値を0.3にすると多くの点が赤(予測:解約する)になり、閾値を0.6にすると赤い点の数が減っていることが分かります。点が閾値の右側にあるか左側にあるかだけで予測ラベルが決まるため、閾値をどこに置くかによって、同じ会員でも予測結果が変わってしまいます。
閾値を高くすると、陽性と予測される件数が減るので、本来は陰性なのに誤って陽性と判定してしまうこと(誤検知)は減ります。ただしその分、本来は陽性なのに陰性と判定して見落としてしまうこと(見逃し)が増えます。逆に閾値を低くすると、見逃しは減りますが誤検知が増えます。つまり閾値の位置によって、「見逃しの少なさ」と「誤検知の少なさ」はトレードオフの関係にあります。
このトレードオフを数値で表したものが、TPR(真陽性率:見逃しの少なさを表す指標)とFPR(偽陽性率:誤検知の多さを表す指標)です。正確な定義は次の章で改めて説明しますが、ここでは「TPRは高いほど見逃しが少なく、FPRは低いほど誤検知が少ない」とだけ押さえておいてください。
ROC曲線は、閾値を1から0まで動かしたときに、このTPRとFPRがどう変化するかを1本の線で表したものです。そしてAUCは、そのROC曲線の下側の面積のことです。
AUCは0から1の値をとります。読み方の目安は次の通りです。
- 1.0に近いほど、陽性と陰性をきれいに分離できていることを表します
- 0.5は、ランダムに予測しているのと同程度の性能であることを表します
- 0.5を下回る場合は、ランダムより悪い予測をしていることになります(多くの場合、陽性・陰性の判定が実質的に逆になっているサインです)
実務でよく見るのは0.5〜1.0の範囲で、1.0に近いほど良いモデルと判断します。
ここから、AUCを理解するために必要な次の3つを順番に説明していきます。
- 混同行列とTPR・FPR
- ROC曲線
- AUCの計算方法
混同行列とTPR・FPR
前の章で触れたTPR(True Positive Rate)とFPR(False Positive Rate)を、ここで正式に定義します。
TPRとFPRは、どちらも「混同行列」という表から計算します。混同行列とは、実際のラベル(本当に解約したかどうか)と予測ラベル(モデルが解約すると予測したかどうか)の組み合わせを、4つのマスに分けて件数を集計した表のことです。
| 予測:陽性 | 予測:陰性 | |
|---|---|---|
| 実際:陽性 | TP (正しく陽性と予測できた件数) | FN(見逃してしまった件数) |
| 実際:陰性 | FP(誤って陽性と予測してしまった件数) | TN (正しく陰性と予測できた件数) |
TPRとFPRの定義は次の通りです。
TPRは「実際に陽性だった人のうち、正しく陽性と予測できた割合」で、Recall(再現率)とも呼ばれます。FPRは「実際は陰性だった人のうち、誤って陽性と予測してしまった割合」です。
例:閾値0.5のときの混同行列を作る
前の章で見たように、スコアを予測ラベルに変換するには閾値を決める必要がありました。ここではまず閾値を0.5に固定して、実際に混同行列を作ってみます。先ほど図を作るときに使ったdf1(会員の解約予測データ)を、そのまま使います。
コード
from sklearn.metrics import confusion_matrix
df1["pred_churn"] = (df1["score"] >= 0.5).astype(int)
cm = confusion_matrix(df1["true_churn"], df1["pred_churn"])
tn, fp, fn, tp = cm.ravel()
print(cm)
print(f"TPR: {tp / (tp + fn):.2f}")
print(f"FPR: {fp / (fp + tn):.2f}")
出力結果

このデータでは、閾値0.5のときTPRは1.0、FPRは0.5になります。解約する人は全員見逃さず拾えている一方で、実際は解約しない人の半分を誤って「解約しそう」と判定してしまっています。
このように、閾値を1つ決めただけでは「見逃しの少なさ」と「誤検知の多さ」のどちらか一方しか見えません。閾値を変えながらこの2つがどう動くかを見るのが、次のROC曲線です。
ROC曲線
ROC曲線は、閾値を1(すべて陰性と予測)から0(すべて陽性と予測)まで動かしながら、そのときどきのFPRを横軸、TPRを縦軸にとってプロットした線です。閾値ごとに点を打っていくと、左下(0,0)から右上(1,1)へ向かう曲線ができます。
ランダムに予測した場合、この曲線はだいたい対角線(y=x)に沿います。モデルの性能が良いほど、曲線は左上に膨らんでいきます。
例:ROC曲線を描く
コード
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(df1["true_churn"], df1["score"])
plt.plot(fpr, tpr, marker="o", label="モデル")
plt.plot([0, 1], [0, 1], linestyle="--", color="gray", label="ランダム予測")
plt.xlabel("FPR(偽陽性率)")
plt.ylabel("TPR(真陽性率)")
plt.title("ROC曲線")
plt.legend()
plt.show()
出力結果

曲線が対角線より上にあるほど、モデルがランダムより優れていることを表します。このROC曲線の下側の面積が、次に説明するAUCです。
AUCの計算
定義:面積としてのAUC
ROC曲線をTPR = f(FPR)という関数だと考えると、AUCはその積分、つまり曲線の下側の面積として定義できます。
実務では積分を直接計算することはなく、scikit-learnのようなライブラリが台形近似などで計算してくれます。
例:sklearnで計算する
from sklearn.metrics import roc_auc_score
auc = roc_auc_score(df1["true_churn"], df1["score"])
print(f"AUC: {auc:.3f}")
出力結果

このデータでのAUCはおよそ0.92になります。
もう1つの見方:確率としてのAUC
AUCには、面積とは別にもう1つの解釈があります。それは「陽性のデータと陰性のデータをランダムに1つずつ選んだとき、陽性の方が高いスコアになっている確率」です。
(スコアが同点の場合は0.5としてカウントします)
この解釈は、AUCが「順位をどれだけ正しくつけられているか」を測る指標だということをよく表しています。実際に全ての陽性・陰性のペアを総当たりで比較しても、同じ値になることを確認してみます。
コード
import itertools
pos_scores = df1.loc[df1["true_churn"] == 1, "score"]
neg_scores = df1.loc[df1["true_churn"] == 0, "score"]
pairs = list(itertools.product(pos_scores, neg_scores))
correct = sum(1 for p, n in pairs if p > n)
tie = sum(1 for p, n in pairs if p == n)
manual_auc = (correct + 0.5 * tie) / len(pairs)
print(f"ペア比較によるAUC: {manual_auc:.3f}")
出力結果

sklearnの「roc_auc_score」と、総当たりのペア比較で計算した値が一致することが確認できます。
まとめ
この記事では、分類モデルの評価指標であるAUCについて、TPR・FPR・ROC曲線・AUCの計算方法の3つの観点から紹介しました。
AUCは閾値によらずモデルの性能を1つの数値で表せる指標ですが、万能ではありません。AUCはスコアの「順位」だけを見ているため、確率としての正確さ(キャリブレーション)までは評価できませんし、陽性データが極端に少ない不均衡なデータでは、AUCが高くてもビジネスで実際に使いたい閾値での適合率が低いことがあります。また、AUCが1.0に近すぎる場合は、良いモデルができたと喜ぶ前に、リーク(本来使えない情報が特徴量に混ざっていないか)を疑ってみることも大切です。
大事なのは、「このAUCの値が自分たちのビジネスにとって十分なのか」を自分の言葉で説明できるかどうかです。数字だけでなく、実際にどのデータが誤って分類されているのかも合わせて確認しながら、指標を使いこなしていってください。
参考:scikit-learn公式ドキュメント(roc_auc_score)
https://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_auc_score.html

