これは、データ サイエンスの面接でよく聞かれる質問の 1 つで、ROC や類似の曲線をゼロから作成する必要があります。つまり、手元にデータがない状態です。この記事では、読者がこれらのメトリックの意味と計算、それらが表すもの、解釈方法を理解していることを前提としています。そのため、実装の側面に焦点を当てます。まず、必要なライブラリをインポートします (計算にはそのモジュールが使用されるため、math もインポートします)。
import pandas as pd import numpy as np import matplotlib.pyplot as plt import math
最初のステップは、1 (不良品) と 0 (良品) の「実際の」データを生成することです。これは、前述のメトリックを使用してモデルの精度を計算し、比較するために使用されます。この記事では、一様分布から「実際のベクトル」を作成します。後続の記事と関連記事では、二項分布を使用します。
actual = np.random.randint(0, 2, 10000)
上記のコードは、実際のバイナリ クラスのベクトルである [0,1] に属する 10,000 個のランダムな整数を生成します。ここで、もちろん、これらの実際のクラスには別の確率のベクトルが必要です。通常、これらの確率は機械学習モデルの出力です。ただし、ここでは、いくつかの有用な仮定に基づいてランダムに生成します。基礎となるモデルが「ロジスティック回帰モデル」であると仮定すると、リンク関数はロジスティックまたはロジットになります。
下の図は、標準のロジスティック関数を示しています。ロジスティック回帰モデルの場合、式 -k(x-x_0) は「スコア」に置き換えられます。「スコア」は、モデル機能とモデル パラメータの加重合計です。
したがって、「スコア」= 0 の場合、ロジスティック関数は Y 軸の 0.5 を通過する必要があります。これは、logit(p) = log-odds(p) = log(p/(1-p)) = 0 => p = 1-p => p =0.5 であるためです。また、「スコア」が正または負の大きな値に達すると、関数は漸近的に 1 (悪い) または 0 (良い) のいずれかに移動することに注意してください。したがって、「スコア」の絶対値が高いほど、予測される確率も高くなります。しかし、何をスコアリングしているのでしょうか。「実際のベクトル」に存在する各データ入力をスコアリングしています。次に、基礎となるロジスティック回帰モデルが熟練している、つまり予測的であると仮定する場合、モデルは悪いものに対して良いものよりも比較的高いスコアを割り当てる必要があります。したがって、悪いものにはより多くの正のスコア (予測される確率が 1 に近くなるように) が、良いものにはより多くの負のスコア (予測される確率が 0 に近くなるように) がなければなりません。これは、モデルによるランク付けとして知られています。言い換えれば、スコアと、したがって不良と良の予測確率の間には区別または分離が必要です。スコア 0 は、良の確率 = 不良の確率 = 0.5 を意味することがわかりました。これは、モデルが良と不良を区別できないことを意味します。ただし、データ ポイントは実際には良か不良のいずれかであることがわかっているため、スコア 0.5 はモデルから得られる最悪のスコアです。これにより、次のステップに進むための直感が得られます。
スコアは、平均 0、標準偏差 1 の標準正規分布を使用してランダムに生成できます。ただし、不良品と良品には異なる予測スコアが必要です。また、不良スコアは良品スコアよりも高くする必要があります。そのため、標準正規分布を使用し、その平均をシフトして良品と不良品を区別します。
# scores for bads bads = np.random.normal(0, 1, actual.sum()) + 1 # scores for goods goods = np.random.normal(0, 1, len(actual) - actual.sum()) - 1 plt.hist(bads) plt.hist(goods)
前述のコードでは、2 つの異なる標準正規分布から bads スコアと goods スコアをサンプリングしましたが、これらをシフトして 2 つを分離しました。bads スコア (画像では青色で表示) を右に 1 シフトし、逆に bads スコアを左に 1 シフトします。これにより、次のことが保証されます。
もちろん、「shift」パラメータを増やして 1 より大きい値を割り当てることで、この分離を最大化できます。ただし、このストーリーでは、その方法は使いません。これについては、後続の関連ストーリーで説明します。では、これらのスコアによって生成される確率を見てみましょう。
# prob for bads bads_prob = list((map(lambda x: 1/(1 + math.exp(-x)), bads))) # prob for goods goods_prob = list((map(lambda x: 1/(1 + math.exp(-x)), goods))) plt.hist(bads_prob) plt.hist(goods_prob)
前に説明したように、「スコア」をロジスティック関数に通すと、確率が得られます。悪い確率 (青色) は良い確率 (オレンジ色) (0 に偏っている) よりも高く (1 に偏っている) なっていることがわかります。
次のステップは、実際のベクトルと予測ベクトルを1つのデータフレームに統合して分析することです。データインスタンスが実際に悪い場合は悪い確率を割り当て、その逆も同様です。
# create predicted array bads = 0 goods = 0 predicted = np.zeros((10000)) for idx in range(0, len(actual)): if actual[idx] == 1: predicted[idx] = bads_prob[bads] bads += 1 else: predicted[idx] = goods_prob[goods] goods += 1 actual_df = pd.DataFrame(actual, columns=['actual']) predicted_df = pd.DataFrame(predicted, columns=['predicted']) predicted_df = pd.concat([actual_df, predicted_df], axis = 1) predicted_df = predicted_df.sort_values(['predicted'], ascending = False).reset_index() predicted_df = predicted_df.drop(columns = 'predicted')
次のステップはビンを作成することです。これは、生成したい曲線が本質的に離散的であるためです。各ビンについて、必要なメトリックを累積的に計算します。言い換えると、離散ランダム変数 (goods と bads) の累積分布関数を生成します。
n_bins = 50 bin_size = math.floor(len(predicted_df) / n_bins) curve_metrics = []
参考までに、荷物と商品の量と箱のサイズを記載しておく必要があります。
print("number of bads:", bads) print("number of goods:", goods) print("number of total data points:", len(actual)) print("bin size:", len(predicted_df) / n_bins)
悪い数: 4915
商品数: 5085
総データポイント数: 10000
ビンサイズ: 200.0
次に、基礎となるメトリックの実際の計算を行うメインのコードスニペットが続きます。
for k in range(1, n_bins + 1): if k < n_bins: TP = predicted_df.loc[ : k*bin_size-1, "actual"].sum() FP = k*bin_size - TP FN = predicted_df.loc[(k*bin_size) : , "actual"].sum() TN = len(actual) - k*bin_size - FN cum_bads = predicted_df.loc[ : k*bin_size-1, "actual"].sum() cum_goods = k*bin_size - cum_bads else: TP = predicted_df.loc[ : , "actual"].sum() FP = len(actual) - TP FN = 0 TN = 0 cum_bads = bads cum_goods = goods curve_metrics.append([k, TP, FP, TN, FN, cum_bads, cum_goods])
for ループは 1 から n_bins まで実行されることに注意してください。つまり、最後に 1 が省略されます。したがって、n_bins + 1 が「停止値」になります。
k = 1 から k = n_bins-1 まで、bin_size を使用して、真陽性、偽陽性、偽陰性、真陰性、累積 bds、累積商品の累積計算を行います。
スニペット「predicted_df.loc[ : k* bin_size-1, "actual"].sum()」は、index = 0 から index = kbin_size-1 まで実行されることに注意してください。したがって、k *bin_size に等しいチャンクが取り出されます。したがって、 k *bin_size から 1 を減算します。
同様に、スニペット「predicted_df.loc[(k*bin_size) : , "actual"].sum()」は、インデックス = k*bin_size から最終インデックスまで実行されます。したがって、ビンが 0 から 49 (サイズ 50) の場合、スニペットはインデックス = 50 (bin_size に等しい) から先に実行されます。
k = n_bins の場合、データセットの最終インデックスまで拡張します。ここで、スニペット “predicted_df.loc[:, "actual"].sum()” は、インデックスがインデックス = 0 からデータフレームの最終インデックスまで実行されるときに、すべての bad を合計します。これを “TP = bads” に置き換えることもできます。最後のカットオフですべてを 'bad' として割り当てるため、FN と TN は両方とも = 0 です。したがって、False Negative (実際の bad) または True Negative (実際の good) は残りません。k = n_bins の場合、負のクラスは存在しないためです。
累積行列がどのようになっているかを確認すると便利です。
curve_metrics
k = n_bins = 50 の場合、すべての商品 (5085) とすべての不良品 (4915) が累積されていることに注意してください。
これで、希望する曲線に必要な実際の計算を行う準備が整いました。
curve_metrics_df = pd.DataFrame(curve_metrics, columns=["cut_off_index", "TP", "FP", "TN", "FN", "cum_bads", "cum_goods"]) curve_metrics_df["cum%bads"] = curve_metrics_df["cum_bads"] / (actual.sum()) curve_metrics_df["cum%goods"] = curve_metrics_df["cum_goods"] / (len(actual) - actual.sum()) curve_metrics_df["precision"] = curve_metrics_df["TP"] / (curve_metrics_df["TP"] + curve_metrics_df["FP"]) curve_metrics_df["recall"] = curve_metrics_df["TP"] / (curve_metrics_df["TP"] + curve_metrics_df["FN"]) curve_metrics_df["sensitivity"] = curve_metrics_df["TP"] / (curve_metrics_df["TP"] + curve_metrics_df["FN"]) # specificity is the recall on the negative class curve_metrics_df["specificity"] = curve_metrics_df["TN"] / (curve_metrics_df["TN"] + curve_metrics_df["FP"])
これで完了です。これで曲線をプロットするために必要なものがすべて揃いました。
plt.plot(curve_metrics_df["cum%goods"], curve_metrics_df["cum%bads"], label ="roc curve") plt.xlabel("cum%goods") plt.ylabel("cum%bads") plt.title("ROC Curve") plt.legend() plt.show() plt.plot(1 - curve_metrics_df["specificity"], curve_metrics_df["sensitivity"], label ="sensitivity specificity curve") plt.xlabel("1 - Specificity") plt.ylabel("Sensitivity") plt.title("Sensitivity vs 1-Specificity Curve") plt.legend() plt.show() plt.plot(curve_metrics_df["recall"], curve_metrics_df["precision"], label ="precision recall curve") plt.xlabel("Precision") plt.ylabel("Recall") plt.title("Precision Recall Curve") plt.legend() plt.show()
すべての曲線は、高度なスキルを持つモデル (最初に作成したもの) の使用を証明します。これで、これらの曲線をゼロから作成するタスクが完了します。
考えるべきこと - 「クラスのバランスが著しく崩れた場合、これらの曲線はどうなるでしょうか?」これが次のストーリーのトピックになります。
これが気に入ったら、私の他の物語もぜひご覧ください。