paint-brush
Question d'entretien sur la science des données : création de courbes de rappel ROC et de précision à partir de zéropar@varunnakra1
501 lectures
501 lectures

Question d'entretien sur la science des données : création de courbes de rappel ROC et de précision à partir de zéro

par Varun Nakra8m2024/05/30
Read on Terminal Reader

Trop long; Pour lire

Il s'agit de l'une des questions d'entretien populaires en science des données qui nécessite de créer le ROC et des courbes similaires à partir de zéro. Pour les besoins de cette histoire, je supposerai que les lecteurs sont conscients de la signification et des calculs derrière ces mesures, de ce qu'ils représentent et de la manière dont ils sont interprétés. Nous commençons par importer les bibliothèques nécessaires (nous importons également les mathématiques car ce module est utilisé dans les calculs)
featured image - Question d'entretien sur la science des données : création de courbes de rappel ROC et de précision à partir de zéro
Varun Nakra HackerNoon profile picture
0-item
1-item

Il s'agit de l'une des questions d'entretien populaires en science des données qui nécessite de créer le ROC et des courbes similaires à partir de zéro, c'est-à-dire sans aucune donnée disponible. Pour les besoins de cette histoire, je supposerai que les lecteurs sont conscients de la signification et des calculs derrière ces mesures, de ce qu'ils représentent et de la manière dont ils sont interprétés. Par conséquent, je me concentrerai sur l’aspect mise en œuvre de celui-ci. Nous commençons par importer les bibliothèques nécessaires (nous importons également les mathématiques car ce module est utilisé dans les calculs)

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


La première étape consiste à générer les données « réelles » de 1 (mauvais) et de 0 (biens), car elles seront utilisées pour calculer et comparer la précision du modèle via les métriques susmentionnées. Pour cet article, nous allons créer le « vecteur réel » à partir de la distribution uniforme. Pour l'article suivant et connexe, nous utiliserons la distribution binomiale.

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


Le code ci-dessus génère 10 000 entiers aléatoires appartenant à [0,1] qui est notre vecteur de la classe binaire réelle. Bien sûr, nous avons besoin d’un autre vecteur de probabilités pour ces classes réelles. Normalement, ces probabilités sont le résultat d’un modèle d’apprentissage automatique. Cependant, nous allons ici les générer de manière aléatoire en faisant quelques hypothèses utiles. Supposons que le modèle sous-jacent soit un « modèle de régression logistique ». Par conséquent, la fonction de lien est logistique ou logit.


La figure ci-dessous décrit la fonction logistique standard. Pour un modèle de régression logistique, l'expression -k(x-x_0) est remplacée par un « score ». Le « score » est une somme pondérée des caractéristiques et des paramètres du modèle.

régression logistique utilisant une variable x - l'exposant est le « score »


Ainsi, lorsque le « score » = 0, la fonction logistique doit passer par 0,5 sur l'axe Y. En effet, logit(p) = log-odds(p) = log(p/(1-p)) = 0 => p = 1-p => p =0,5. Notez également que lorsque le « score » atteint des valeurs positives ou négatives élevées, la fonction se déplace asymptotiquement vers 1 (mauvais) ou 0 (bon). Ainsi, plus la valeur absolue du « score » est élevée, plus la probabilité prédite est également élevée. Mais qu’est-ce qu’on marque ? Nous notons chaque entrée de données présente dans notre « vecteur réel ». Ensuite, si nous voulons supposer que notre modèle de régression logistique sous-jacent est compétent, c'est-à-dire prédictif ; le modèle devrait attribuer des scores comparativement plus élevés aux mauvais par rapport aux biens. Ainsi, les mauvais devraient avoir des scores plus positifs (pour garantir que la probabilité prédite est proche de 1) et les biens devraient avoir plus de scores négatifs (pour garantir que la probabilité prédite est proche de 0). C'est ce qu'on appelle l'ordre de classement par modèle. En d’autres termes, il devrait y avoir une discrimination ou une séparation entre les scores et donc les probabilités prédites des mauvais par rapport aux bons. Depuis, nous avons vu que le score de 0 implique probabilité de bien = probabilité de mal = 0,5 ; cela signifierait que le modèle est incapable de faire la différence entre le bien et le mal. Mais puisque nous savons que le point de données sera en réalité soit bon, soit mauvais, un score de 0,5 est donc le pire score possible du modèle. Cela nous donne une certaine intuition pour passer à l’étape suivante.


Les scores peuvent être générés de manière aléatoire en utilisant la distribution Standard Normal avec une moyenne de 0 et un écart type de 1. Cependant, nous voulons des scores prédits différents pour les mauvais et les bons. Nous voulons également que les mauvais scores soient supérieurs aux bons scores. Ainsi, nous utilisons la distribution normale standard et décalons sa moyenne pour créer une séparation entre les bons et les mauvais.

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

Scores des biens (orange) et des mauvais (bleu)

Dans le code susmentionné, nous avons échantillonné les mauvais scores et les bons scores de deux distributions normales standard différentes, mais nous les avons décalés pour créer une séparation entre les deux. On décale les mauvais scores (représentés par la couleur bleue dans l'image) de 1 vers la droite et inversement de 1 vers la gauche. Cela garantit ce qui suit :

  1. Les mauvais scores sont plus élevés que les bons scores pour des cas considérablement élevés (selon le visuel).
  2. Les mauvais scores ont un nombre proportionnellement plus élevé de scores positifs et les bons scores ont un nombre proportionnellement plus élevé de scores négatifs.


On peut bien sûr maximiser cette séparation en augmentant le paramètre 'shift' et en lui attribuant des valeurs supérieures à 1. Cependant, dans cette histoire, nous ne ferons pas cela. Nous explorerons cela dans les histoires connexes suivantes. Regardons maintenant les probabilités générées par ces scores.

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

Probabilités de biens (orange) et de mauvais (bleu)

Comme indiqué précédemment, lorsque les « scores » sont transmis à la fonction logistique, nous obtenons les probabilités. Il est évident que les mauvaises probabilités (couleur bleue) sont plus élevées (et biaisées vers 1) que les bonnes probabilités (couleur orange) (et biaisées vers 0).


L'étape suivante consiste à combiner les vecteurs réels et prédits en une seule base de données pour l'analyse. Nous attribuons de mauvaises probabilités là où l'instance de données est réellement mauvaise et vice versa

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


L'étape suivante consiste à créer des bacs. En effet, les courbes que nous souhaitons générer sont de nature discrète. Pour chaque bac, nous calculons cumulativement les métriques souhaitées. En d’autres termes, nous générons des fonctions de distribution cumulative pour les variables aléatoires discrètes – les biens et les mauvais.

  1. Le nombre de bacs est arbitraire (nous attribuons n_bins = 50).
  2. Notez l'utilisation de la fonction étage. En effet, la longueur de la trame de données peut ne pas être divisée également en 50 groupes. Ainsi, nous en prenons la parole et modifions notre code de telle sorte que le dernier bin (50ème bin) contienne les observations supplémentaires (qui seront < 50).
 n_bins = 50 bin_size = math.floor(len(predicted_df) / n_bins) curve_metrics = []


Nous devrions enregistrer les volumes de biens et de biens et les tailles des bacs pour notre référence

 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)

nombre de mauvais : 4915

nombre de marchandises : 5085

nombre total de points de données : 10 000

taille du bac : 200,0


Vient ensuite l' extrait de code principal dans lequel nous effectuons les calculs réels des métriques sous-jacentes.

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


Notez que la boucle for s'étendrait de 1 à n_bins, c'est-à-dire qu'elle en laisserait un à la fin. Par conséquent, nous avons n_bins + 1 comme « valeur d’arrêt ».


Pour k = 1 à k = n_bins-1, nous effectuons des calculs cumulatifs de vrais positifs, de faux positifs, de faux négatifs, de vrais négatifs, de bds cumulés et de biens cumulés en utilisant bin_size.


  1. Notez que l'extrait « predicted_df.loc[ : k* bin_size-1, "actual"].sum() » s'exécuterait de index = 0 à index = kbin_size-1. Ainsi, il supprime le morceau égal à k *bin_size. Par conséquent, nous soustrayons 1 de k *bin_size

  2. De même, l'extrait « predicted_df.loc[(k*bin_size) : , "actual"].sum() » s'exécuterait de index = k*bin_size jusqu'à l'index final. Ainsi, si le bac est compris entre 0 et 49 (taille 50), le snipper s'exécute à partir de l'index = 50 (qui est égal à bin_size).


Pour k = n_bins, nous l'étendons simplement à l'index final de l'ensemble de données. Où, l'extrait « predicted_df.loc[ : , "actual"].sum() » résume tous les défauts lorsque l'indexation s'étend de index = 0 à l'index final de la trame de données. On peut aussi le remplacer par « TP = bads ». FN et TN sont tous deux = 0 car au dernier cut-off, nous attribuons tout comme « mauvais ». Par conséquent, il ne reste plus de faux négatif (réel mauvais) ou de vrai négatif (réel bon). Parce que la classe négative n'existe pas lorsque k = n_bins.


Il est utile de vérifier à quoi ressemble la matrice cumulée.

 curve_metrics 

Liste de génération des courbes ROC et Precision-Recall

Notez que pour k = n_bins = 50, nous avons accumulé tous les biens (5085) et tous les mauvais (4915).


Nous sommes maintenant prêts à effectuer les calculs nécessaires pour les courbes souhaitées.

 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. La courbe ROC est une courbe entre les mauvais cumulés (axe Y) et les biens cumulés (axe X)
  2. La courbe ROC est une courbe entre la sensibilité (qui est aussi un mauvais cumul ou rappel : axe Y) et la spécificité 1 (axe X)
  3. La courbe de rappel de précision est une courbe entre la précision (axe Y) et le rappel (qui est également la sensibilité ou les défauts cumulés : axe X)


C'est ça. Nous avons désormais tout ce qu’il faut pour tracer nos courbes.

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

Toutes les courbes confirment l'utilisation d'un modèle hautement qualifié (tel que nous l'avions formulé au départ). Ceci termine la tâche de créer ces courbes à partir de zéro.


Matière à réflexion : « qu’arrive-t-il à ces courbes lorsque les classes sont fortement déséquilibrées ? » Ce sera le sujet de la prochaine histoire.


Si vous aimez cela, jetez un œil à mes autres histoires .