paint-brush
Вопрос на собеседовании по науке о данных: создание кривых ROC и точного отзыва с нуляк@varunnakra1
501 чтения
501 чтения

Вопрос на собеседовании по науке о данных: создание кривых ROC и точного отзыва с нуля

к Varun Nakra8m2024/05/30
Read on Terminal Reader

Слишком долго; Читать

Это один из популярных вопросов на собеседованиях по науке о данных, который требует создания ROC и подобных кривых с нуля. Для целей этой статьи я предполагаю, что читатели осведомлены о значении и расчетах этих показателей, а также о том, что они представляют и как они интерпретируются. Начинаем с импорта необходимых библиотек (импортируем и математику, потому что этот модуль используется в расчетах)
featured image - Вопрос на собеседовании по науке о данных: создание кривых ROC и точного отзыва с нуля
Varun Nakra HackerNoon profile picture
0-item
1-item

Это один из популярных вопросов на собеседованиях по науке о данных, который требует создания ROC и подобных кривых с нуля, то есть без данных под рукой. Для целей этой статьи я предполагаю, что читатели осведомлены о значении и расчетах этих показателей, а также о том, что они представляют собой и как они интерпретируются. Поэтому я сосредоточусь на аспекте реализации. Начинаем с импорта необходимых библиотек (импортируем и математику, потому что этот модуль используется в расчетах)

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


Первым шагом является создание «фактических» данных 1 (плохие) и 0 (хорошие), поскольку они будут использоваться для расчета и сравнения точности модели с помощью вышеупомянутых показателей. В этой статье мы создадим «фактический вектор» из равномерного распределения. В следующей статье мы будем использовать биномиальное распределение.

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


Приведенный выше код генерирует 10 000 случайных целых чисел, принадлежащих [0,1], который является нашим вектором фактического двоичного класса. Теперь, конечно, нам нужен другой вектор вероятностей для этих реальных классов. Обычно эти вероятности являются результатом модели машинного обучения. Однако здесь мы будем генерировать их случайным образом, делая некоторые полезные предположения. Предположим, что базовая модель представляет собой «модель логистической регрессии», поэтому функция связи является логистической или логит.


На рисунке ниже описана стандартная логистическая функция. Для модели логистической регрессии выражение -k(x-x_0) заменяется «оценкой». «Оценка» представляет собой взвешенную сумму характеристик и параметров модели.

логистическая регрессия с использованием одной переменной x — показатель степени — это «оценка»


Таким образом, когда «оценка» = 0, логистическая функция должна проходить через значение 0,5 по оси Y. Это потому, что 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) 

Оценка товаров (оранжевый) и плохих (синий)

В вышеупомянутом коде мы выбрали плохие и хорошие оценки из двух разных стандартных нормальных распределений, но сместили их, чтобы создать разделение между ними. Мы сдвигаем плохие оценки (представленные синим цветом на изображении) на 1 вправо и наоборот на 1 влево. Это обеспечивает следующее:

  1. Оценки «плохо» выше, чем оценки «хорошо» для существенно высоких (по визуальному представлению) случаев.
  2. Плохие оценки имеют пропорционально большее количество положительных оценок, а хорошие оценки имеют пропорционально большее количество отрицательных оценок.


Мы, конечно, можем максимизировать это разделение, увеличив параметр «сдвиг» и присвоив ему значения выше 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) 

Вероятности товаров (оранжевый) и плохих (синий)

Как обсуждалось ранее, когда «оценки» пропускаются через логистическую функцию, мы получаем вероятности. Очевидно, что плохие вероятности (синий цвет) выше (и смещены к 1), чем хорошие вероятности (оранжевый цвет) (и смещены к 0).


Следующим шагом является объединение фактических и прогнозируемых векторов в один кадр данных для анализа. Мы назначаем плохие вероятности там, где экземпляр данных на самом деле плохой, и наоборот.

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


Следующий шаг — создание контейнеров. Это связано с тем, что кривые, которые мы хотим создать, носят дискретный характер. Для каждой ячейки мы рассчитываем желаемые показатели в совокупности. Другими словами, мы генерируем кумулятивные функции распределения для дискретных случайных величин — добра и зла.

  1. Количество бинов произвольное (назначаем n_bins = 50).
  2. Обратите внимание на использование функции пола. Это связано с тем, что длина кадра данных не может делиться поровну на 50 ячеек. Таким образом, мы берем это на себя и модифицируем наш код так, чтобы последний интервал (50-й интервал) содержал дополнительные наблюдения (которых будет <50).
 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, т. е. в конце он пропустит один. Следовательно, у нас есть n_bins + 1 в качестве «стоп-значения».


Для значений от k = 1 до k = n_bins-1 мы выполняем совокупные расчеты истинно положительных результатов, ложных срабатываний, ложных отрицательных результатов, истинно отрицательных результатов, совокупных bds и совокупных товаров, используя bin_size.


  1. Обратите внимание, что фрагмент «predicted_df.loc[ : k* bin_size-1, «actual»].sum()» будет выполняться от index = 0 до index = kbin_size-1. Таким образом, он извлекает чанк, равный k *bin_size. Поэтому мы вычитаем 1 из k *bin_size

  2. Аналогично, фрагмент «predicted_df.loc[(k*bin_size) : , «actual»].sum()» будет выполняться от index = k*bin_size до конечного индекса. Таким образом, если размер ячейки от 0 до 49 (размер 50), сниппер запускается с индекса = 50 (который равен bin_size) и далее.


Для k = n_bins мы просто расширяем его до конечного индекса набора данных. Где фрагмент «predicted_df.loc[ : , «actual»].sum()» суммирует все плохие значения по мере того, как индексация выполняется от индекса = 0 до конечного индекса кадра данных. Мы также можем заменить его на «TP = bads». FN и TN оба = 0, потому что при последнем отсечении мы все определяем как «плохое». Следовательно, не остается ни ложноотрицательного (действительно плохого), ни истинно отрицательного (действительно хорошего). Потому что отрицательный класс не существует, когда k = n_bins.


Полезно проверить, как выглядит накопительная матрица.

 curve_metrics 

Список для создания кривых ROC и Precision-Recall

Обратите внимание, что для 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"])
  1. Кривая ROC — это кривая между совокупным количеством плохих товаров (ось Y) и совокупным количеством хороших товаров (ось X).
  2. Кривая ROC представляет собой кривую между чувствительностью (которая также представляет собой совокупное количество негативных последствий или отзыва: ось Y) и специфичностью 1 (ось X).
  3. Кривая точного отзыва — это кривая между точностью (ось Y) и отзывом (который также является чувствительностью или совокупными ошибками: ось X).


Вот и все. Теперь у нас есть все необходимое для построения кривых.

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

Все кривые подтверждают использование высококвалифицированной модели (как мы сформулировали вначале). Это завершает задачу создания этих кривых с нуля.


Пища для размышлений: «что происходит с этими кривыми, когда классы сильно разбалансированы?» Это будет темой следующего рассказа.


Если вам это нравится, то, пожалуйста, посмотрите другие мои истории .