paint-brush
Frage im Vorstellungsgespräch zum Thema Data Science: ROC- und Präzisions-Recall-Kurven von Grund auf erstellenvon@varunnakra1
501 Lesungen
501 Lesungen

Frage im Vorstellungsgespräch zum Thema Data Science: ROC- und Präzisions-Recall-Kurven von Grund auf erstellen

von Varun Nakra8m2024/05/30
Read on Terminal Reader

Zu lang; Lesen

Dies ist eine der beliebtesten Fragen in Vorstellungsgesprächen zum Thema Datenwissenschaft, bei der man ROC- und ähnliche Kurven von Grund auf neu erstellen muss. Für die Zwecke dieser Geschichte gehe ich davon aus, dass die Leser sich der Bedeutung und der Berechnungen hinter diesen Metriken bewusst sind und wissen, was sie darstellen und wie sie interpretiert werden. Wir beginnen mit dem Importieren der erforderlichen Bibliotheken (wir importieren auch Mathematik, da dieses Modul in Berechnungen verwendet wird).
featured image - Frage im Vorstellungsgespräch zum Thema Data Science: ROC- und Präzisions-Recall-Kurven von Grund auf erstellen
Varun Nakra HackerNoon profile picture
0-item
1-item

Dies ist eine der beliebtesten Fragen in Vorstellungsgesprächen zum Thema Datenwissenschaft, bei der man den ROC und ähnliche Kurven von Grund auf neu erstellen muss, also ohne Daten zur Hand zu haben. Für die Zwecke dieser Geschichte gehe ich davon aus, dass die Leser sich der Bedeutung und der Berechnungen hinter diesen Kennzahlen bewusst sind und wissen, was sie darstellen und wie sie interpretiert werden. Daher werde ich mich auf den Implementierungsaspekt derselben konzentrieren. Wir beginnen mit dem Importieren der erforderlichen Bibliotheken (wir importieren auch Mathematik, da dieses Modul in Berechnungen verwendet wird).

 import pandas as pd import numpy as np import matplotlib.pyplot as plt import math


Der erste Schritt besteht darin, die „tatsächlichen“ Daten von 1 (schlecht) und 0 (gut) zu generieren, da diese zur Berechnung und zum Vergleich der Modellgenauigkeit über die oben genannten Metriken verwendet werden. Für diesen Artikel erstellen wir den „tatsächlichen Vektor“ aus der Gleichverteilung. Für den nachfolgenden und verwandten Artikel verwenden wir die Binomialverteilung.

 actual = np.random.randint(0, 2, 10000)


Der obige Code generiert 10.000 zufällige Ganzzahlen, die zu [0,1] gehören, was unser Vektor der tatsächlichen binären Klasse ist. Nun benötigen wir natürlich einen weiteren Wahrscheinlichkeitsvektor für diese tatsächlichen Klassen. Normalerweise sind diese Wahrscheinlichkeiten eine Ausgabe eines maschinellen Lernmodells. Hier werden wir sie jedoch zufällig generieren und dabei einige nützliche Annahmen treffen. Nehmen wir an, das zugrunde liegende Modell ist ein „logistisches Regressionsmodell“, daher ist die Linkfunktion logistisch oder logit.


Die folgende Abbildung beschreibt die Standardlogistikfunktion. Für ein logistisches Regressionsmodell wird der Ausdruck -k(x-x_0) durch einen „Score“ ersetzt. Der „Score“ ist eine gewichtete Summe der Modellmerkmale und Modellparameter.

logistische Regression mit einer Variablen x - der Exponent ist der „Score“


Wenn also der „Score“ = 0 ist, muss die logistische Funktion auf der Y-Achse durch 0,5 verlaufen. Dies liegt daran, dass logit(p) = log-odds(p) = log(p/(1-p)) = 0 => p = 1-p => p =0,5. Beachten Sie auch, dass sich die Funktion asymptotisch entweder in Richtung 1 (schlecht) oder 0 (gut) bewegt, wenn der „Score“ hohe positive oder hohe negative Werte erreicht. Je höher also der absolute Wert des „Scores“ ist, desto höher ist auch die vorhergesagte Wahrscheinlichkeit. Aber was bewerten wir? Wir bewerten jeden Dateneingang, der in unserem „tatsächlichen Vektor“ vorhanden ist. Wenn wir dann davon ausgehen möchten, dass unser zugrunde liegendes logistisches Regressionsmodell kompetent, d. h. prädiktiv ist, sollte das Modell schlechten im Vergleich zu guten vergleichsweise höhere Scores zuweisen. Schlechte sollten also mehr positive Scores haben (um sicherzustellen, dass die vorhergesagte Wahrscheinlichkeit nahe bei 1 liegt) und gute sollten mehr negative Scores haben (um sicherzustellen, dass die vorhergesagte Wahrscheinlichkeit nahe bei 0 liegt). Dies wird vom Modell als Rangordnung bezeichnet. Mit anderen Worten sollte es eine Unterscheidung oder Trennung zwischen den Werten und damit den vorhergesagten Wahrscheinlichkeiten für Gutes und Schlechtes geben. Da wir gesehen haben, dass ein Wert von 0 eine Wahrscheinlichkeit für Gutes = Wahrscheinlichkeit für Schlechtes = 0,5 impliziert, würde dies bedeuten, dass das Modell nicht zwischen Gut und Böse unterscheiden kann. Da wir aber wissen, dass der Datenpunkt tatsächlich entweder gut oder schlecht sein wird, ist ein Wert von 0,5 der schlechteste mögliche Wert des Modells. Dies gibt uns eine gewisse Intuition, um zum nächsten Schritt überzugehen.


Die Bewertungen können zufällig mithilfe der Standardnormalverteilung mit einem Mittelwert von 0 und einer Standardabweichung von 1 generiert werden. Wir möchten jedoch unterschiedliche vorhergesagte Bewertungen für Gute und Schlechte. Wir möchten auch, dass die schlechten Bewertungen höher sind als die guten Bewertungen. Daher verwenden wir die Standardnormalverteilung und verschieben ihren Mittelwert, um eine Trennung zwischen Gut und Schlechte zu schaffen.

 # 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) 

Wertungen für Gutes (orange) vs. Schlechtes (blau)

Im oben genannten Code haben wir schlechte und gute Werte aus zwei verschiedenen Standardnormalverteilungen entnommen, diese aber verschoben, um eine Trennung zwischen den beiden zu schaffen. Wir verschieben die schlechten Werte (im Bild durch die blaue Farbe dargestellt) um 1 nach rechts und umgekehrt um 1 nach links. Dadurch wird Folgendes sichergestellt:

  1. Die schlechten Bewertungen sind höher als die guten Bewertungen für eine wesentlich höhere Anzahl (visueller) Fälle
  2. Die schlechten Bewertungen haben proportional mehr positive Bewertungen und die guten Bewertungen haben proportional mehr negative Bewertungen


Wir können diesen Abstand natürlich maximieren, indem wir den Parameter „shift“ erhöhen und ihm Werte über 1 zuweisen. In dieser Geschichte werden wir das jedoch nicht tun. Wir werden das in den nachfolgenden verwandten Geschichten untersuchen. Sehen wir uns nun die Wahrscheinlichkeiten an, die durch diese Werte generiert werden.

 # 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) 

Wahrscheinlichkeiten für Gutes (orange) vs. Schlechtes (blau)

Wie bereits erwähnt, erhalten wir die Wahrscheinlichkeiten, wenn wir die „Scores“ durch die logistische Funktion schieben. Es ist offensichtlich, dass die schlechten Wahrscheinlichkeiten (blaue Farbe) höher sind (und in Richtung 1 tendieren) als die guten Wahrscheinlichkeiten (orange Farbe) (und in Richtung 0 tendieren).


Der nächste Schritt besteht darin, die tatsächlichen und vorhergesagten Vektoren in einem einzigen Datenrahmen für die Analyse zu kombinieren. Wir weisen schlechte Wahrscheinlichkeiten zu, wo die Dateninstanz tatsächlich schlecht ist und umgekehrt

 # 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')


Der nächste Schritt besteht darin, Bins zu erstellen. Dies liegt daran, dass die Kurven, die wir generieren möchten, diskreter Natur sind. Für jeden Bin berechnen wir unsere gewünschten Metriken kumulativ. Mit anderen Worten, wir generieren kumulative Verteilungsfunktionen für die diskreten Zufallsvariablen - Gut und Böse.

  1. Die Anzahl der Bins ist beliebig (wir weisen n_bins = 50 zu).
  2. Beachten Sie die Verwendung der Floor-Funktion. Dies liegt daran, dass die Länge des Datenrahmens möglicherweise nicht gleichmäßig in 50 Bins aufgeteilt werden kann. Daher nehmen wir den Floor davon und ändern unseren Code so, dass der letzte Bin (50. Bin) die zusätzlichen Beobachtungen enthält (die < 50 sein werden).
 n_bins = 50 bin_size = math.floor(len(predicted_df) / n_bins) curve_metrics = []


Wir sollten die Mengen an Waren und Gütern sowie die Behältergrößen als Referenz auflisten

 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)

Anzahl der Fehler: 4915

Anzahl der Waren: 5085

Anzahl der Gesamtdatenpunkte: 10000

Behältergröße: 200,0


Als nächstes folgt der Hauptcodeausschnitt , in dem wir die eigentlichen Berechnungen der zugrunde liegenden Metriken durchführen

 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])


Beachten Sie, dass die For-Schleife von 1 bis n_bins läuft, d. h. am Ende wird eins ausgelassen. Daher haben wir n_bins + 1 als „Stoppwert“.


Für k = 1 bis k = n_bins-1 führen wir kumulative Berechnungen von True Positives, False Positives, False Negatives, True Negatives, kumulativen BDs und kumulativen Gütern unter Verwendung der Bin-Größe durch.


  1. Beachten Sie, dass der Ausschnitt „predicted_df.loc[ : k* bin_size-1, "actual"].sum()“ von index = 0 bis index = kbin_size-1 laufen würde. Somit wird der Block gleich k *bin_size herausgenommen . Daher subtrahieren wir 1 von k *bin_size

  2. In ähnlicher Weise würde der Snippet „predicted_df.loc[(k*bin_size) : , "actual"].sum()“ von Index = k*bin_size bis zum Endindex laufen. Wenn der Bin also von 0 bis 49 (Größe 50) reicht, läuft der Snipper ab Index = 50 (was gleich bin_size ist)


Für k = n_bins erweitern wir es einfach bis zum Endindex des Datensatzes. Dabei summiert der Ausschnitt „predicted_df.loc[ : , "actual"].sum()“ alle Bads, während die Indizierung von Index = 0 bis zum Endindex des Datenrahmens läuft. Wir können es auch durch „TP = bads“ ersetzen. FN und TN sind beide = 0, weil wir beim letzten Cut-off alles als „bad“ zuweisen. Daher gibt es kein False Negative (tatsächlich schlecht) oder True Negative (tatsächlich gut) mehr. Denn die negative Klasse existiert nicht, wenn k = n_bins.


Es ist nützlich zu prüfen, wie die kumulative Matrix aussieht.

 curve_metrics 

Liste zur Generierung von ROC- und Precision-Recall-Kurven

Beachten Sie, dass wir für k = n_bins = 50 alle Güter (5085) und alle Schlechten (4915) akkumuliert haben.


Jetzt sind wir bereit, die eigentlichen Berechnungen durchzuführen, die für die gewünschten Kurven erforderlich sind.

 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"])
  1. Die ROC-Kurve ist eine Kurve zwischen kumulierten Nachteilen (Y-Achse) und kumulierten Vorteilen (X-Achse).
  2. Die ROC-Kurve ist eine Kurve zwischen Sensitivität (die auch kumulative Fehler oder Rückrufe sind: Y-Achse) und 1-Spezifität (X-Achse)
  3. Die Präzisions-Recall-Kurve ist eine Kurve zwischen Präzision (Y-Achse) und Recall (die auch Sensitivität oder kumulative Fehler ist: X-Achse)


Das ist es. Wir haben jetzt alles, was wir zum Zeichnen unserer Kurven brauchen.

 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() 

Alle Kurven bestätigen die Verwendung eines hochqualifizierten Modells (wie wir es zu Beginn formuliert hatten). Damit ist unsere Aufgabe, diese Kurven von Grund auf neu zu erstellen, abgeschlossen.


Denkanstoß: „Was passiert mit diesen Kurven, wenn die Klassen stark unausgeglichen sind?“ Dies wird das Thema der nächsten Geschichte sein.


Wenn Ihnen das gefällt, schauen Sie sich bitte auch meine anderen Geschichten an.