这是常见的数据科学面试问题之一,要求从头开始创建 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) 被“分数”取代。“分数”是模型特征和模型参数的加权和。
因此,当“分数”= 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)
在上述代码中,我们从两个不同的标准正态分布中抽样了坏分数和好分数,但我们对它们进行了移动,以在两者之间建立分离。我们将坏分数(由图中的蓝色表示)向右移动 1,反之亦然向左移动 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)。
下一步是将实际值和预测值向量组合成一个数据框进行分析。我们在数据实例实际上是坏的时分配坏概率,反之亦然
# 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')
下一步是创建箱体。这是因为我们想要生成的曲线本质上是离散的。对于每个箱体,我们累积计算所需的指标。换句话说,我们为离散随机变量(好货和坏货)生成累积分布函数。
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,我们使用 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()”将从 index = k*bin_size 运行到最终索引。因此,如果 bin 的范围从 0 到 49(大小为 50),则代码片段将从 index = 50(等于 bin_size)开始运行
对于 k = n_bins,我们只需将其扩展到数据集的最终索引。其中,代码片段“predicted_df.loc[ : , "actual"].sum()”将所有坏项加起来,因为索引从 index = 0 运行到数据框的最终索引。我们也可以用“TP = bads”替换它。FN 和 TN 都为 0,因为在最后的截止点我们将所有内容指定为“坏”。因此,没有假阴性(实际坏)或真阴性(实际好)。因为,当 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()
所有曲线都证实了我们采用了高技能模型(正如我们在开始时所制定的)。这完成了从头开始创建这些曲线的任务。
值得深思的是——“当类别严重不平衡时,这些曲线会发生什么变化?”这将是下一个故事的主题。
如果您喜欢这个,那么请看看我的其他故事。