例えば以下のような正解ラベルが付いたデータのそれぞれに対して、あるモデルが以下のようなスコアを出しているとします。正解ラベルを「スパムメールか否か」、モデルのスコアを「モデルが推定したスパムメールらしさ」などと考えてもいいです。
真の正解ラベル | モデルの出力スコア |
---|---|
0 | 2 |
0 | 1 |
0 | 2 |
1 | 4 |
1 | 2 |
0 | 1 |
1 | 3 |
1 | 5 |
from sklearn import metrics list_label = [0, 0, 0, 1, 1, 0, 1, 1] list_score = [2, 1, 2, 4, 2, 1, 3, 5] auc = metrics.roc_auc_score(list_label, list_score) print(auc) fpr, tpr, thresholds = metrics.roc_curve(list_label, list_score) auc = metrics.auc(fpr, tpr) print(auc)
0.9375 0.9375
AUC は 0.9375 となります。この AUC がどんな曲線の下の面積だったのかを知るには、metrics.roc_curve の出力をプロットしてみるとわかります。
%matplotlib inline import matplotlib.pyplot as plt from pylab import rcParams rcParams['figure.figsize'] = 6, 4 rcParams['font.family'] = 'Ume Gothic O5' rcParams['font.size'] = 16 print("fpr: ", fpr) print("tpr: ", tpr) print("thresholds: ", thresholds) plt.plot(fpr, tpr, marker="o") plt.xlabel("fpr") plt.ylabel("tpr") plt.show()
fpr: [0. 0. 0.5 1. ] tpr: [0.25 0.75 1. 1. ] thresholds: [5 3 2 1]
正解ラベル | スコア▼ | 判定(閾値=5) | 判定(閾値=3) | 判定(閾値=2) | 判定(閾値=1) |
---|---|---|---|---|---|
1:スパム | 5 | スパム(正) | スパム(正) | スパム(正) | スパム(正) |
1:スパム | 4 | 非スパム(誤) | スパム(正) | スパム(正) | スパム(正) |
1:スパム | 3 | 非スパム(誤) | スパム(正) | スパム(正) | スパム(正) |
0:非スパム | 2 | 非スパム(正) | 非スパム(正) | スパム(誤) | スパム(誤) |
0:非スパム | 2 | 非スパム(正) | 非スパム(正) | スパム(誤) | スパム(誤) |
1:スパム | 2 | 非スパム(誤) | 非スパム(誤) | スパム(正) | スパム(正) |
0:非スパム | 1 | 非スパム(正) | 非スパム(正) | 非スパム(正) | スパム(誤) |
0:非スパム | 1 | 非スパム(正) | 非スパム(正) | 非スパム(正) | スパム(誤) |
真のスパムメールのうち スパムと判定されている割合 | 25% | 75% | 100% | 100% | |
真の非スパムメールのうち スパムと判定されている割合 | 0% | 0% | 50% | 100% |
- 例えば完璧なモデルなら、閾値を下げていったときに先に完全にスパムメールをカバーし切って、その後非スパムメールがカバーされ始めます。つまり、fpr=0% のまま(非スパムメールを1件も巻き込まないまま)tpr=100% に到達するので(全てのスパムメールをカバーするので)、このときの fpr-tpr 曲線は (0%、0%)→(0%、100%)→(100%、100%)の形を描きます。この曲線の下の面積は 1 です。
- 逆にスパムメールと非スパムメールを全然識別できないモデルなら、閾値を下げていったときにスパムメールも非スパムメールも同じ割合ずつカバーしていってしまいそうです。このときの fpr-tpr 曲線は (0%、0%)→(100%、100%)を直線に結びます。この曲線の下の面積は 0.5 です。
これをみると結局 sklearn.metrics.roc_curve と sklearn.metrics.auc がよばれていることがわかります。roc_curve は以下です。
この引数をみて1つ解決した疑問があります。上の例では roc_curve の戻り値の thresholds は [5 3 2 1] となっており 4 が含まれていませんでした。しかし 4 というスコアのデータもあるので、「閾値=4」での fpr、tpr も計算するべきではないかという気もします。それは roc_curve の drop_intermediate という引数がデフォルトで True だったために、ROCカーブの形状を変えない座標の fpr、tpr が省かれていたということがここにきてわかります。試しに、drop_intermediate=False としてもう一度実行してみます。
fpr, tpr, thresholds = metrics.roc_curve(list_label, list_score, drop_intermediate=False) print("fpr: ", fpr) print("tpr: ", tpr) print("thresholds: ", thresholds) plt.plot(fpr, tpr, marker="o") plt.xlabel("fpr") plt.ylabel("tpr") plt.show()
fpr: [0. 0. 0. 0.5 1. ] tpr: [0.25 0.5 0.75 1. 1. ] thresholds: [5 4 3 2 1]
import numpy as np from sklearn.utils.extmath import stable_cumsum def my_binary_clf_curve(y_true, y_score, pos_label=None): y_true = np.array(y_true) y_score = np.array(y_score) # y_true および y_score を y_score の降順にソートする desc_score_indices = np.argsort(y_score, kind="mergesort")[::-1] y_score = y_score[desc_score_indices] y_true = y_true[desc_score_indices] print("y_score(ソート済み): ", y_score) print("y_true(ソート済み): ", y_true) # ほしいのはこの y_score のそれぞれを閾値としたときに、検出されるスパムメールと非スパムメールの個数のベクトル # ただ y_score はしばしば同値データを多く含む # 次の要素との差がゼロでないインデックスでのみスパムメールと非スパムメールの個数がほしいのでそのようなインデックスを取得する distinct_value_indices = np.where(np.diff(y_score))[0] print("次の要素との差がゼロでないインデックス: ", distinct_value_indices) threshold_idxs = np.r_[distinct_value_indices, y_true.size - 1] # 最後の fp、tp が取れないので最後のインデックスは補う # y_true を cumsum すればよい tps = stable_cumsum(y_true)[threshold_idxs] # その閾値で検出されるスパムメールの個数 fps = 1 + threshold_idxs - tps # その閾値で検出される非スパムメールの個数(インデックス+1 が検出されるデータ総数なので tps を引けばよい) return fps, tps, y_score[threshold_idxs] print("y_score: ", list_score) print("y_true: ", list_label) fp, tp, thresholds = my_binary_clf_curve(list_label, list_score) print("fp: ", fp) print("tp: ", tp) print("thresholds: ", thresholds)
y_score: [2, 1, 2, 4, 2, 1, 3, 5] y_true: [0, 0, 0, 1, 1, 0, 1, 1] y_score(ソート済み): [5 4 3 2 2 2 1 1] y_true(ソート済み): [1 1 1 1 0 0 0 0] 次の要素との差がゼロでないインデックス: [0 1 2 5] fp: [0. 0. 0. 2. 4.] tp: [1. 2. 3. 4. 4.] thresholds: [5 4 3 2 1]
fp、tp は閾値を thresholds にしたがって変化させていったときに検出される非スパムメールとスパムメールの個数です。呼び出し元の roc_curve ではこの fp と tp をそれぞれ最後のインデックスの値で割って割合(fpr、tpr)に直しています。drop_intermediate=True の場合は先に不要な座標の除去が行われます。この fpr、tpr が sklearn.metrics.auc に渡されます。
そしてこの auc から必要な動作を抜き出すと以下です。というか numpy.trapz に面積を求めさせているだけです。つまり、ROCカーブの下の面積は台形則で求められている(各座標を直線で結んだ折れ線の下の面積が求められている)ことがわかります。
def my_auc(x, y): area = np.trapz(y, x) return area fpr = fp / fp[-1] tpr = tp / tp[-1] auc = my_auc(fpr, tpr) print(auc)
0.9375