分類問題の評価指標で出てくる ROC曲線や PR 曲線、初めて見た時からなかなか理解しづらく、これってなんなの?と思ったまま避けていました。
sklearn で用意されているメソッドを用いるとかんたんに描けたりはするのですが、あえて使わずにこれらカーブを描いてみて、理解してみようと思います。
・データの用意
・ロジスティック回帰で予測
・sklearn のメソッドで曲線を書く
・使わずに曲線を書く
・PR 曲線も同様
データの用意
今回は、Kaggle のクレジットカードのデータセットを使わせてもらいます。
こちらから creditcard.csv をダウンロードして使わせてもらいます。
https://www.kaggle.com/mlg-ulb/creditcardfraud
欠損値がなく、カテゴリカル変数もないきれいなデータで扱いやすいです。
前処理で標準化を施しつつ、テストデータと訓練データに分けるところまで。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
import pandas as pd import numpy as np import matplotlib import matplotlib.pyplot as plt %matplotlib inline from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler plt.style.use('ggplot') font = {'family' : 'meiryo'} matplotlib.rc('font', **font) # データの読み込み data = pd.read_csv("creditcard.csv") X = data.copy().drop(["Class"], axis=1) t = data["Class"].copy() featuresToScale = X.columns scaler = StandardScaler(copy=True) X.loc[:, featuresToScale] = scaler.fit_transform(X[featuresToScale]) x_train, x_test, t_train, t_test = train_test_split(X, t, test_size=0.3, random_state=2020, stratify=t) |
ロジスティック回帰で予測
ロジスティック回帰で学習して推論します。予測確率を使用するため。model.predict ではなく、 model.predict_proba を用います。
1 2 3 4 5 6 |
from sklearn.linear_model import LogisticRegression model = LogisticRegression(random_state=2020) model.fit(x_train, t_train) preds = model.predict_proba(x_test) |
sklearn のメソッドで曲線を書く
まずは ROC曲線から見ていきます。
ROC曲線は、予測の閾値を動かしていった時の FPR (False Positive Rate) と TPR(True Positive Rate)の関係をプロットしたものです。
FPR は 異常でないもの全体の中でを、間違って異常だと言ってしまった割合(コスト)
TPR は 異常なもの全体の中で、異常を見つけることができた割合(ベネフィット)
です。両方合わせて分母ですべてのクラスをカバーしています。
予測の閾値とは、モデルが出力してくれた予測確率がたとえば 0.35 であった場合に、それをラベル 0 とするのか ラベル 1 とするのか決める際の基準です。仮に閾値が 0.5 であれば、この例ではラベル 0 と最終的に決めることになります。一方で、閾値を下げて、0.2 とすればこのサンプルはラベル 1 に属します。
roc_curve メソッドで、閾値ごとの FPR と TPR を取得することができます。
1 2 3 4 5 6 7 8 9 |
from sklearn.metrics import precision_recall_curve, roc_curve fpr, tpr, threshold = roc_curve(t_test, preds[:, 1]) plt.plot(fpr, tpr, marker=".") plt.xlabel("False Positive Rate") plt.ylabel("True Positive Rate") plt.legend() plt.show() |
使わずに曲線を書く
では、先程のメソッドを用いずに曲線を書いてみます!
まずは TPR と FPR の定義を確認です。
TPR = TP / (TP + FN)
FPR = FP / (FP + TN)
※ TP, FP, TN, FN は以下のように考えると覚えやすい
・TP は Positive(1) と予測して正解(True)だった数
・FP は Positive(1)と予測して間違い(False)だった数
・TN は Negative(0)と予測して正解(True)だった数
・FN は Negative(0)と予測して間違い(False)だった数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
preds_df = pd.DataFrame(preds, columns=["proba_0", "proba_1"]) preds_df["true"] = t_test.values preds_df = preds_df.drop("proba_0", axis=1) preds_df.head() ``` proba_1 true 0 0.000162 0 1 0.000045 0 2 0.000153 0 3 0.000193 0 4 0.000129 0 ``` |
まずは確率と正解ラベルを横に並べます。閾値を 0 ~ 1 まで 0.1 刻みで動かした TPR, FPR をもとめてリストに入れます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
from tqdm import tqdm TPRs, FPRs = [], [] thresholds = [ i/10 for i in range(0, 10, 1)] for threshold in tqdm(thresholds): column = "thres_{}".format(threshold) preds_df[column] = (preds_df["proba_1"] > threshold).astype(int) # TPR の計算 ( TP / (TP + FN)) TP = len(preds_df[(preds_df["true"] == 1) & (preds_df[column] == 1)]) FN = len(preds_df[(preds_df["true"] == 1) & (preds_df[column] == 0)]) TPR = TP / (TP + FN) TPRs.append(TPR) # FPR の計算 ( FP / (FP + TN)) FP = len(preds_df[(preds_df["true"] == 0) & (preds_df[column] == 1)]) TN = len(preds_df[(preds_df["true"] == 0) & (preds_df[column] == 0)]) FPR = FP / (FP + TN) FPRs.append(FPR) preds_df = preds_df.drop(column, axis=1) |
プロットをしてみると、
1 2 3 4 5 |
plt.plot(FPRs, TPRs, marker=".") plt.xlabel("False Positive Rate") plt.ylabel("True Positive Rate") plt.legend() plt.show() |
あれ、なんか違う・・。もっと閾値を細かく刻まないと行けないようですね。今度は 50000 回刻むことにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
from tqdm import tqdm TPRs, FPRs = [], [] thresholds = [ i/50000 for i in range(0, 50000, 1)] for threshold in tqdm(thresholds): column = "thres_{}".format(threshold) preds_df[column] = (preds_df["proba_1"] > threshold).astype(int) # TPR の計算 ( TP / (TP + FN)) TP = len(preds_df[(preds_df["true"] == 1) & (preds_df[column] == 1)]) FN = len(preds_df[(preds_df["true"] == 1) & (preds_df[column] == 0)]) TPR = TP / (TP + FN) TPRs.append(TPR) # FPR の計算 ( FP / (FP + TN)) FP = len(preds_df[(preds_df["true"] == 0) & (preds_df[column] == 1)]) TN = len(preds_df[(preds_df["true"] == 0) & (preds_df[column] == 0)]) FPR = FP / (FP + TN) FPRs.append(FPR) preds_df = preds_df.drop(column, axis=1) plt.plot(FPRs, TPRs, marker=".") plt.xlabel("False Positive Rate") plt.ylabel("True Positive Rate") plt.legend() plt.show() |
いい感じになりました!次は PR曲線です。
PR 曲線も同様
PR 曲線は、Precison Recall Curve という名前の通り、Precison と Recall を縦軸横軸にとって、閾値を動かしたときの両者の関係性をプロットしたものです。
こちらも定義を見ておくと、以下のようになっています。
Precision = TP / (TP + FP)
Recall = TP / (TP + FN)
Precision は 異常だと言った中で実際に異常だった割合(”間違わずに”言い当てられたね、という割合。)
Recall は、TPR と定義が一緒で、異常なもの全体の中で、異常を見つけることができた割合(ちゃんと”見逃さずに”検出できたね、という割合。)です。
ROC 曲線と同じように実装できます。
sklearn を使う場合
1 2 3 4 5 6 7 |
precision, recall, threshold = precision_recall_curve(t_test, preds[:, 1]) plt.plot(recall, precision, marker=".") plt.xlabel("Recall") plt.ylabel("Precision") plt.legend() plt.show() |
sklearn を使わない場合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
precisions, recalls = [], [] thresholds = [ i/50000 for i in range(0, 50000, 1)] for threshold in tqdm(thresholds): column = "thres_{}".format(threshold) preds_df[column] = (preds_df["proba_1"] > threshold).astype(int) TP = len(preds_df[(preds_df["true"] == 1) & (preds_df[column] == 1)]) TN = len(preds_df[(preds_df["true"] == 0) & (preds_df[column] == 0)]) FN = len(preds_df[(preds_df["true"] == 1) & (preds_df[column] == 0)]) FP = len(preds_df[(preds_df["true"] == 0) & (preds_df[column] == 1)]) # Precision の計算 ( TP / (TP + FP) ) precision = TP / (TP + FP) # Recall の計算 ( TP / (TP + FN) ) recall = TP / (TP + FN) precisions.append(precision) recalls.append(recall) preds_df = preds_df.drop(column, axis=1) plt.plot(recalls, precisions, marker=".") plt.xlabel("Recall") plt.ylabel("Precision") plt.legend() plt.show() |
・・ちょっとだけわかったような気がしました。最後まで読んでいただきありがとうございました。