閾値を適用してAUCを扱いたい場合のメモ。
閾値は0.5ではない
sklearnがAUCを算出するときのカットオフ(閾値)は、「デフォルトで0.5」だと思っていたら違った。
例えば、以下のような簡単な例(二値分類)を計算する。
import numpy as np
from sklearn import metrics
y_true = [0, 0, 1, 1]
y_pred = [0.4, 0.3, 0.75, 0.8]
fpr, tpr, thresholds = metrics.roc_curve(y_true, y_pred, pos_label=1)
print(metrics.auc(fpr, tpr))# 1.0
上の例では、当然、ラベル1である確率を出しているので、感度1.0, 特異度1.0でAUCは1.0。
仮に、カットオフを0.5として考えても、当然、AUCは1.0。
y_true = [0, 0, 1, 1]
y_pred = [0.4, 0.3, 0.75, 0.8]
y_pred = [1 if p >= 0.5 else 0 for p in y_pred] # [0,0,1,1] cutoff=0.5
fpr, tpr, thresholds = metrics.roc_curve(y_true, y_pred, pos_label=1)
print(metrics.auc(fpr, tpr))# 1.0
では、次のように、y_pred = [0.5, 0.6, 0.75, 0.8]とするとどうか。
この例では、カットオフを0.5にした場合、感度1.0、特異度0.0であるため、AUCは0.5になる。
y_true = [0, 0, 1, 1]
y_pred = [0.5, 0.6, 0.75, 0.8]
y_pred = [1 if p >= 0.5 else 0 for p in y_pred] # [1,1,1,1]
fpr, tpr, thresholds = metrics.roc_curve(y_true, y_pred, pos_label=1)
print(metrics.auc(fpr, tpr))# 0.5
ところが、閾値で予測結果をラベル化せずに、予測確率をそのまま渡すとどうか。
すると、AUC1.0。
y_true = [0, 0, 1, 1]
y_pred = [0.5, 0.6, 0.75, 0.8]
fpr, tpr, thresholds = metrics.roc_curve(y_true, y_pred, pos_label=1)
print(metrics.auc(fpr, tpr))# 1.0
sklearnは、自動的に最適な閾値でAUCを算出してくれているのか?と頭の中が一瞬混乱するが、しかしこれは当然のことで、scikit-learnのauc関数は、細かく刻んだ閾値ごとにfprとtprを算出して、曲線化面積を計算しているのだから、閾値云々の話ではないわけだ。
ここで、ひとつ疑問が残る。
sklearnが算出してくれたAUCを実現する閾値をどのように求めればよいかがわからない。
作ったモデルを実装するときに、最適な閾値を設定できない問題が発生する。
また、その閾値のときの混同行列がわからないため、感度と特異度も求められない。
ここで、カットオフ0.5で決め打ちしてしまうと、先の例のように、
感度1.0、特異度0.0、なのに、AUC1.0!?
ということで、任意の閾値を適用して評価指標を得る際は、「metrics.auc(fpr, tpr)」関数で得られるAUCを達成するための最適な閾値をROCから求めなければならない。
そして、カットオフを決めた後、予測確率をラベル化してから、諸々の評価指標を算出する。
Youden's index
カットオフを先に計算するためには、Youden's indexが(ユーデンインデックス)よく用いられている。
このように求める。
y_true = [0, 0, 1, 1]
y_pred = [0.5, 0.6, 0.75, 0.8]
fpr, tpr, thresholds = metrics.roc_curve(y_true, y_pred, pos_label=1)
youden_indices = tpr-fpr
index = np.argmax(tpr - fpr)
youden_cutoff = thresholds[index]
# youden indexで予測確率をクラスラベル化
y_pred = [1 if p >= youden_cutoff else 0 for p in y_pred] # [0,0,1,1]
tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()# 評価指標は混同行列からも計算できる
print('youden index',youden_cutoff)# 0.75
print(classification_report(y_true, y_pred))
閾値でクラスラベル化してROCを計算すると、ROCカーブが角ばるが、仕方ない。