paint-brush
Pergunta da entrevista de ciência de dados: Criando curvas ROC e de recuperação de precisão do zeropor@varunnakra1
497 leituras
497 leituras

Pergunta da entrevista de ciência de dados: Criando curvas ROC e de recuperação de precisão do zero

por Varun Nakra8m2024/05/30
Read on Terminal Reader

Muito longo; Para ler

Esta é uma das perguntas populares das entrevistas de ciência de dados que exige a criação do ROC e curvas semelhantes do zero. Para os propósitos desta história, assumirei que os leitores estão cientes do significado e dos cálculos por trás dessas métricas, do que elas representam e como são interpretadas. Começamos importando as bibliotecas necessárias (também importamos matemática porque esse módulo é usado em cálculos)
featured image - Pergunta da entrevista de ciência de dados: Criando curvas ROC e de recuperação de precisão do zero
Varun Nakra HackerNoon profile picture
0-item
1-item

Esta é uma das perguntas populares das entrevistas de ciência de dados que exige a criação do ROC e curvas semelhantes do zero, ou seja, nenhum dado disponível. Para os propósitos desta história, assumirei que os leitores estão cientes do significado e dos cálculos por trás dessas métricas, do que elas representam e como são interpretadas. Portanto, vou me concentrar no aspecto de implementação do mesmo. Começamos importando as bibliotecas necessárias (também importamos matemática porque esse módulo é usado em cálculos)

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


O primeiro passo é gerar os dados 'reais' de 1s (ruins) e 0s (bens), pois estes serão usados para calcular e comparar a precisão do modelo por meio das métricas mencionadas. Para este artigo, criaremos o “vetor real” da distribuição uniforme. Para o artigo subsequente e relacionado, usaremos a distribuição binomial.

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


O código acima gera 10.000 inteiros aleatórios pertencentes a [0,1] que é o nosso vetor da classe binária real. Agora, é claro que precisamos de outro vetor de probabilidades para estas classes reais. Normalmente, essas probabilidades são resultados de um modelo de aprendizado de máquina. No entanto, aqui iremos gerá-los aleatoriamente fazendo algumas suposições úteis. Vamos supor que o modelo subjacente seja um 'modelo de regressão logística', portanto, a função de ligação é logística ou logit.


A figura abaixo descreve a função logística padrão. Para um modelo de regressão logística, a expressão -k(x-x_0) é substituída por uma 'pontuação'. A 'pontuação' é uma soma ponderada dos recursos e parâmetros do modelo.

regressão logística usando uma variável x - o expoente é a 'pontuação'


Assim, quando a 'pontuação' = 0, a função logística deve passar por 0,5 no eixo Y. Isso ocorre porque logit(p) = log-odds(p) = log(p/(1-p)) = 0 => p = 1-p => p =0,5. Observe também que quando a 'pontuação' atinge valores positivos altos ou negativos altos, a função se move assintoticamente em direção a 1 (ruim) ou 0 (bom). Assim, quanto maior for o valor absoluto da 'pontuação', maior também será a probabilidade prevista. Mas o que estamos marcando? Estamos pontuando cada entrada de dados presente em nosso ‘vetor real’. Então, se quisermos assumir que o nosso modelo de regressão logística subjacente é qualificado, ou seja, preditivo; o modelo deve atribuir pontuações comparativamente mais altas para bens ruins versus bens. Assim, os produtos ruins deveriam ter pontuações mais positivas (para garantir que a probabilidade prevista esteja próxima de 1) e os bens deveriam ter pontuações mais negativas (para garantir que a probabilidade prevista estivesse próxima de 0). Isso é conhecido como ordenação de classificação pelo modelo. Por outras palavras, deve haver discriminação ou separação entre as pontuações e, portanto, as probabilidades previstas de bens maus versus bens. Desde então, vimos que a pontuação 0 implica probabilidade de bom = probabilidade de ruim = 0,5; isso significaria que o modelo é incapaz de diferenciar entre o bem e o mal. Mas como sabemos que o ponto de dados será realmente bom ou ruim, uma pontuação de 0,5 é a pior pontuação possível do modelo. Isso nos dá alguma intuição para passar para a próxima etapa.


As pontuações podem ser geradas aleatoriamente usando a distribuição Normal Padrão com média 0 e desvio padrão de 1. No entanto, queremos pontuações previstas diferentes para produtos ruins e bons. Também queremos que as pontuações ruins sejam maiores que as pontuações boas. Assim, utilizamos a distribuição normal padrão e deslocamos a sua média para criar uma separação entre os bons e os maus.

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

Pontuações boas (laranja) vs ruins (azul)

No código mencionado acima, amostramos pontuações ruins e pontuações boas de duas distribuições normais padrão diferentes, mas as mudamos para criar uma separação entre as duas. Deslocamos as pontuações ruins (representadas pela cor azul na imagem) em 1 para a direita e vice-versa em 1 para a esquerda. Isso garante o seguinte:

  1. As pontuações ruins são mais altas do que as pontuações boas para casos substancialmente altos (de acordo com o visual).
  2. As pontuações ruins têm um número proporcionalmente maior de pontuações positivas e as pontuações boas têm um número proporcionalmente maior de pontuações negativas.


É claro que podemos maximizar esta separação aumentando o parâmetro 'shift' e atribuindo-lhe valores superiores a 1. Contudo, nesta história, não faremos isso. Exploraremos isso nas histórias relacionadas subsequentes. Agora, vejamos as probabilidades geradas por essas pontuações.

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

Probabilidades de bens (laranja) versus ruins (azul)

Conforme discutido anteriormente, quando as “pontuações” são empurradas através da função logística, obtemos as probabilidades. É evidente que as probabilidades ruins (cor azul) são maiores (e distorcidas para 1) do que as probabilidades boas (cor laranja) (e distorcidas para 0).


A próxima etapa é combinar os vetores reais e previstos em um único quadro de dados para análise. Atribuímos probabilidades ruins onde a instância de dados é realmente ruim e 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')


A próxima etapa é criar caixas. Isso ocorre porque as curvas que queremos gerar são de natureza discreta. Para cada compartimento, calculamos nossas métricas desejadas cumulativamente. Em outras palavras, geramos funções de distribuição cumulativa para as variáveis aleatórias discretas – bens e males.

  1. O número de bins é arbitrário (atribuímos n_bins = 50).
  2. Observe o uso da função de piso. Isso ocorre porque o comprimento do quadro de dados pode não ser dividido igualmente em 50 compartimentos. Assim, pegamos o chão e modificamos nosso código de forma que o último compartimento (50º compartimento) contenha as observações extras (que serão <50).
 n_bins = 50 bin_size = math.floor(len(predicted_df) / n_bins) curve_metrics = []


Devemos listar os volumes de produtos ruins e de mercadorias e os tamanhos dos compartimentos para nossa referência

 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)

número de coisas ruins: 4915

número de mercadorias: 5085

número total de pontos de dados: 10.000

tamanho do compartimento: 200,0


A seguir vem o trecho de código principal onde fazemos os cálculos reais das métricas subjacentes

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


Observe que o loop for iria de 1 a n_bins, ou seja, ele deixaria um no final. Portanto, temos n_bins + 1 como o ‘valor de parada’.


Para k = 1 a k = n_bins-1, fazemos cálculos cumulativos de verdadeiros positivos, falsos positivos, falsos negativos, verdadeiros negativos, bds cumulativos e bens cumulativos usando bin_size.


  1. Observe que o snippet “predicted_df.loc[: k* bin_size-1, "actual"].sum()” seria executado de index = 0 a index = kbin_size-1. Assim, retira o pedaço igual a k *bin_size. Portanto, subtraímos 1 de k *bin_size

  2. Da mesma forma, o snippet “predicted_df.loc[(k*bin_size) : , "actual"].sum()” seria executado de index = k*bin_size até o índice final. Assim, se o bin for de 0 a 49 (tamanho 50), o snipper roda a partir de index = 50 (que é igual a bin_size) em diante


Para k = n_bins, apenas o estendemos ao índice final do conjunto de dados. Onde, o snippet “predicted_df.loc[ : , "actual"].sum()” resume todos os problemas à medida que a indexação vai do índice = 0 até o índice final do dataframe. Também podemos substituí-lo por “TP = bads”. FN e TN são ambos = 0 porque no último ponto de corte atribuímos tudo como “ruim”. Portanto, não resta nenhum falso negativo (realmente ruim) ou verdadeiro negativo (realmente bom). Porque a classe negativa não existe quando k = n_bins.


É útil verificar como é a matriz cumulativa.

 curve_metrics 

Lista para geração de curvas ROC e Precision-Recall

Observe que para k = n_bins = 50, acumulamos todos os bens (5.085) e todos os bens ruins (4.915).


Agora estamos prontos para fazer os cálculos reais necessários para as curvas desejadas

 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. A curva ROC é uma curva entre males cumulativos (eixo Y) e bens cumulativos (eixo X)
  2. A curva ROC é uma curva entre sensibilidade (que também é um dano cumulativo ou recall: eixo Y) e especificidade 1 (eixo X)
  3. A curva Precision Recall é uma curva entre Precisão (eixo Y) e Recall (que também é sensibilidade ou problemas cumulativos: eixo X)


É isso. Temos tudo o que é necessário para traçar nossas curvas agora.

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

Todas as curvas confirmam a utilização de um modelo altamente qualificado (como havíamos formulado no início). Isso completa a tarefa de criar essas curvas do zero.


Alimento para reflexão - “o que acontece com essas curvas quando as classes estão gravemente desequilibradas?” Este será o tema da próxima história.


Se você gostou disso, dê uma olhada nas outras histórias minhas.