一组有助于分析二元分类问题的工具
项目描述
binclass-tools:触手可及的 Python 二进制分类工具
一组 Python 包装器和交互式绘图,有助于分析二元分类问题。
binclass -tools包为您提供以下功能:
-
强大的交互式图表可简化二元分类器性能的分析,包括与单个观察相关的任何数量和成本。
-
一组函数返回对测量二元分类器性能有用的度量值,对于每个阈值(如果依赖于它)。
-
一组函数,用于查找根据与正在分析的二元分类器相关的最流行指标以及与混淆矩阵中的 4 个类别中的每一个相关的任何成本计算的最佳阈值。
-
一组通用包装器,可帮助分析师处理二进制分类的日常操作。
在Towards Data Science上,您会发现以下文章描述了包的所有功能背后的理论以及导致我创建用于分析二进制分类的包的路径,其中还包括计算特定指标的最佳阈值:
快速开始
要求和安装
该项目基于:
- Python 3.6+
- 一组用于处理数据的最流行的包
- Plotly 用于交互式绘图
如果您没有 Python,请先安装它。然后,在您最喜欢的 conda 或虚拟环境中,只需执行以下操作:
pip install binclass-tools
或者,如果您想直接从 github 安装开发版本:
pip install git+https://github.com/lucazav/binclass-tools
示例用法
让我们导入处理数据所需的常用库和 binclass-tools 之一:
import numpy as np
import pandas as pd
import bctools as bc
此外,由于我们将通过 RandomForest 对随机生成的数据训练分类器,因此我们还为此目的导入一些有用的函数:
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
然后让我们训练我们的模型,我们将使用binclass-tools的功能作为分类器进行分析:
# Generate a binary imbalanced classification problem, with 80% zeros and 20% ones.
X, y = make_classification(n_samples=1000, n_features=20,
n_informative=14, n_redundant=0,
random_state=12, shuffle=False, weights = [0.8, 0.2])
# Train - test split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, stratify = y, random_state=0)
# Train a RF classifier
cls = RandomForestClassifier(max_depth=6, oob_score=True)
cls.fit(X_train, y_train)
训练完模型后,让我们计算从训练和测试数据集中获得的预测的估计概率:
# Get prediction probabilities for the train set
train_predicted_proba = cls.predict_proba(X_train)[:,1]
# Get prediction probabilities for the test set
test_predicted_proba = cls.predict_proba(X_test)[:,1]
让我们使用 binclass-tools 包中的函数生成一些已知图表,以检查模型在测试集上的整体行为。请注意,可以通过该库的每个图形函数中的参数'title'和'show_display_modebar'来自定义主标题并选择是否显示plotly bar模式。
我们可以从可视化接收器操作特征 (ROC) 曲线开始,使用以下函数,该函数还返回曲线下面积的值:
area_under_ROC = bc.curve_ROC_plot(true_y= y_test,
predicted_proba = test_predicted_proba)
生成情节:
并返回 AUC 值:
>>> area_under_ROC
0.9748427672955975
接下来,您可以使用 iso-Fbeta 曲线可视化Precision-Recall (PR) 曲线图。首先,我们回顾一下F-beta score的定义:它是precision和recall的加权调和平均值,在1处达到最优值,在0处达到最差值。beta参数决定了recall在组合得分中的权重。beta < 1 赋予精确度更多的权重,而 beta > 1 有利于召回。因此,根据定义,iso-Fbeta 曲线包含精确召回空间中 F-beta 分数相等的所有点。函数curve_PR_plot允许我们显示与 F-beta 得分值 0.2、0.4、0.6 和 0.8 相关的 ISO 曲线。该函数将 beta 参数作为输入(默认值设置为 1):
area_under_PR = bc.curve_PR_plot(true_y= y_test,
predicted_proba = test_predicted_proba,
beta = 1)
这里的输出:
与 ROC 曲线情况一样,此函数还返回曲线下面积的值:
>>> area_under_PR
0.9295134692043583
为了更深入地分析模型的预测概率,我们可以通过小提琴图可视化按相对真实类别分组的概率分布,并且对于每个阈值,查看每个数据点的预测概率是否生成正确的预测或不是。以下 binclass-tools 函数执行刚刚提到的任务,将一个阈值与另一个阈值分开的步长大小作为输入(始终考虑极端值 0 和 1):
threshold_step = 0.05
bc.predicted_proba_violin_plot(true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_step = threshold_step)
这里生成的交互式绘图:
可视化概率密度的另一个有用工具是 predict_proba_density_curve_plot 函数,它为每个真实类绘制核密度估计曲线(默认)或正态分布曲线,具体取决于 curve_type 参数。对于可以通过滑块选择的每个阈值,我们可以看到正确或错误分类的区域:
threshold_step = 0.05
curve_type = 'kde' #'kde' is the default value, can also be set to 'normal'
bc.predicted_proba_density_curve_plot(true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_step = threshold_step,
curve_type = curve_type)
这里是交互式情节:
之后,我们可以对模型的性能进行更详细的阈值相关分析。让我们设置一组变量作为参数传递给我们将使用的后续 binclass-tools 函数。考虑到我们将首先分析模型如何在训练数据集上执行以获得最佳阈值,这些是我们将计算的变量:
-
将一个阈值与另一个分隔的步长大小(始终考虑极值 0 和 1 包括在内)。
-
与测试数据集中每个 observables 相关联的单个数量的列表(由于数据集是由随机值生成的,因此第 13 列的绝对值被视为数量列)。
-
计算最佳阈值的指标(在我们的例子中是所有指标)。
-
使用哪个货币符号。
-
与混淆矩阵的 4 个类别中的每一个相关的成本字典。可以将单个数值关联为该类别中每个观察的平均成本,或与每个观察相关联的值列表。显然,字典中列表的长度必须全部相同,等于正在分析的数据集(在我们的例子中是测试数据集)中的观察数。
具体来说,你有这个:
# set params for the train dataset
threshold_step = 0.05
amounts = np.abs(X_train[:, 13])
optimize_threshold = 'all'
currency = '$'
# The function get_cost_dict can be used to define the dictionary of costs.
# It takes as input, for each class, a float or a list of floats.
# Lists must have coherent lenghts
train_cost_dict = bc.get_cost_dict(TN = 0, FP = 10, FN = np.abs(X_train[:, 12]), TP = 0)
此时,我们可以在训练数据集上可视化交互式混淆矩阵,包括所有可用指标的最佳阈值:
var_metrics_df, invar_metrics_df, opt_thresh_df = bc.confusion_matrix_plot(
true_y = y_train,
predicted_proba = train_predicted_proba,
threshold_step = threshold_step,
amounts = amounts,
cost_dict = train_cost_dict,
optimize_threshold = optimize_threshold,
#N_subsets = 70, subsets_size = 0.2, # default
#with_replacement = False, # default
currency = currency,
random_state = 123,
title = 'Interactive Confusion Matrix for the Training Set');
这里的输出:
如您所见,交互式混淆矩阵图还返回可在需要时在您的代码中使用的度量数据帧。一种是依赖于阈值的指标数据框:
| 临界点 | 准确性 | 平衡准确度 | cohens_kappa | f1_score | matthews_corr_coef | 精确 | 记起 | |
|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0.2025 | 0.5 | 0 | 0.3368 | 0 | 0.2025 | 1 |
| 1 | 0.05 | 0.3988 | 0.623 | 0.1168 | 0.4025 | 0.249 | 0.2519 | 1 |
| 2 | 0.1 | 0.7475 | 0.8417 | 0.4664 | 0.616 | 0.5515 | 0.4451 | 1 |
| 3 | 0.15 | 0.8988 | 0.9365 | 0.7358 | 0.8 | 0.7629 | 0.6667 | 1 |
| 4 | 0.2 | 0.9462 | 0.964 | 0.8479 | 0.8822 | 0.857 | 0.7931 | 0.9938 |
| 5 | 0.25 | 0.9812 | 0.9813 | 0.9431 | 0.955 | 0.9437 | 0.9298 | 0.9815 |
| 6 | 0.3 | 0.9875 | 0.983 | 0.9615 | 0.9693 | 0.9615 | 0.9634 | 0.9753 |
| 7 | 0.35 | 0.99 | 0.9822 | 0.9689 | 0.9752 | 0.9689 | 0.9812 | 0.9691 |
| 8 | 0.4 | 0.9825 | 0.9591 | 0.9443 | 0.9551 | 0.9454 | 0.9933 | 0.9198 |
| 9 | 0.45 | 0.9712 | 0.9313 | 0.9065 | 0.9241 | 0.9098 | 0.9929 | 0.8642 |
| 10 | 0.5 | 0.9612 | 0.9043 | 0.8708 | 0.8942 | 0.8782 | 1 | 0.8086 |
| 11 | 0.55 | 0.9388 | 0.8488 | 0.7862 | 0.8218 | 0.8048 | 1 | 0.6975 |
| 12 | 0.6 | 0.91 | 0.7778 | 0.666 | 0.7143 | 0.7066 | 1 | 0.5556 |
| 13 | 0.65 | 0.8838 | 0.713 | 0.542 | 0.5974 | 0.6097 | 1 | 0.4259 |
| 14 | 0.7 | 0.8675 | 0.6728 | 0.4573 | 0.5138 | 0.5445 | 1 | 0.3457 |
| 15 | 0.75 | 0.8438 | 0.6142 | 0.3207 | 0.3719 | 0.437 | 1 | 0.2284 |
| 16 | 0.8 | 0.8238 | 0.5648 | 0.192 | 0.2295 | 0.3258 | 1 | 0.1296 |
| 17 | 0.85 | 0.805 | 0.5185 | 0.0578 | 0.0714 | 0.1725 | 1 | 0.037 |
| 18 | 0.9 | 0.8012 | 0.5093 | 0.0292 | 0.0364 | 0.1218 | 1 | 0.0185 |
| 19 | 0.95 | 0.7975 | 0.5 | 0 | 0 | 0 | 1 | 0 |
| 20 | 1 | 0.7975 | 0.5 | 0 | 0 | 0 | 1 | 0 |
第二个是阈值不变指标数据框:
| 不变的度量 | 价值 | |
|---|---|---|
| 0 | roc_auc | 0.9992 |
| 1 | pr_auc | 0.9972 |
| 2 | brier_score | 0.0427 |
第三个也是最后一个是包含每个已实施指标的最佳阈值的数据框:
| 优化度量 | 最佳阈值 | |
|---|---|---|
| 0 | 卡帕 | 0.3 |
| 1 | 微控制器 | 0.3 |
| 2 | 鹏 | 0.25 |
| 3 | f1_score | 0.3 |
| 4 | f2_score | 0.25 |
| 5 | f05_score | 0.35 |
| 6 | 成本 | 0.35 |
我们直接从GHOST 存储库中借用了用于计算最佳阈值的代码,引入了更多指标并使用并行性优化了计算。
一旦通过训练数据确定了感兴趣的阈值,就可以为测试数据集绘制交互式混淆矩阵。在这里,我们还避免计算最佳阈值,因为在测试数据集上这样做没有意义:
# You can also analyze the test dataset.
# In this case there is no need to optimize the threshold value for any measure.
threshold_step = 0.05
amounts = np.abs(X_test[:, 13])
optimize_threshold = None
currency = '$'
test_cost_dict = bc.get_cost_dict(TN = 0, FP = 10, FN = np.abs(X_test[:, 12]), TP = 0)
var_metrics_df, invar_metrics_df, __ = bc.confusion_matrix_plot(
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_step = threshold_step,
amounts = amounts,
cost_dict = test_cost_dict,
optimize_threshold = optimize_threshold,
#N_subsets = 70, subsets_size = 0.2, # default
#with_replacement = False, # default
currency = currency,
random_state = 123);
显然,交互式混淆矩阵图不会显示各种指标的最佳阈值表:
从代码中可以看出,这次返回的数据帧只是前两个。
如果您只需要上述数据框而不生成交互式混淆矩阵图,则可以使用专门用于此的功能。您可以获得阈值不变指标数据框,如下所示:
invar_metrics_df = bc.utilities.get_invariant_metrics_df(true_y = y_test,
predicted_proba = test_predicted_proba)
您还可以获得特定阈值的阈值相关指标数据框和混淆矩阵值,如下所示:
conf_matrix, metrics_fixed_thresh_df = bc.utilities.get_confusion_matrix_and_metrics_df(
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold = 0.3 # default = 0.5
)
请记住,混淆矩阵值是在数组中返回的,而不是在数据框中。
最后,也可以直接用下面的代码得到优化后的阈值的dataframe:
threshold_values = np.arange(0.05, 1, 0.05)
opt_thresh_df = bc.thresholds.get_optimized_thresholds_df(
optimize_threshold = ['Kappa', 'Fscore', 'Cost'],
threshold_values = threshold_values,
true_y = y_train,
predicted_proba = train_predicted_proba,
cost_dict = train_cost_dict,
# GHOST parameters (these values are also the default ones)
N_subsets = 70,
subsets_size = 0.2,
with_replacement = False,
random_state = 120)
、N_subset和参数特定于用于查找最佳阈值的 GHOST 算法subset_size。with_replacement更多细节可以直接参考介绍GHOST方法的论文。
另一方面,如果您有兴趣专门优化非基于成本的阈值(特别是其中之一:'ROC'、'MCC'、'Kappa'、'F1'),您可以使用以下函数:
opt_roc_threshold_value = bc.thresholds.get_optimal_threshold(
y_train,
train_predicted_proba,
threshold_values,
ThOpt_metrics = 'ROC', # default = 'Kappa'
# GHOST parameters (these values are also the default ones)
N_subsets = 70,
subsets_size = 0.2,
with_replacement = False,
random_seed = 120)
请记住,如果您选择“Fscore”作为要优化的指标,您将分别返回指标 F1、F2 和 F0.5 的 3 个最佳阈值。
专门针对成本优化(最小化),可以使用以下函数:
opt_cost_threshold_value = bc.thresholds.get_cost_optimal_threshold(
y_train,
train_predicted_proba,
threshold_values,
cost_dict = train_cost_dict,
# GHOST parameters (these values are also the default ones)
N_subsets = 70,
subsets_size = 0.2,
with_replacement = False,
random_seed = 120)
您还可能对可视化与每个类别的混淆矩阵相关的可能金额或成本的趋势感兴趣,因为阈值发生变化。为此,有以下函数可生成交互式混淆折线图:
amount_cost_df, total_amount = bc.confusion_linechart_plot(
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_step = threshold_step,
amounts = amounts,
cost_dict = test_cost_dict,
currency = currency);
这里的输出:
您可以看到还有黑色的“菱形”表示第一个阈值,其中存在金额和成本曲线的交换。曲线交换点也可以不止一个。
此函数除了生成绘图外,还返回两个输出值:所有类别的总和给出的总金额以及每个类别的金额和成本随着阈值的变化的数据框:
print(f'total amount: {currency}{total_amount}')
amount_cost_df
除了总金额(374.24 美元)的结果外,这里还有金额和成本数据框:
| 临界点 | 金额_TN | 金额_FP | 金额_FN | 金额_TP | 成本_TN | 成本_FP | 成本_FN | 成本_TP | 总消耗 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 301.374 | 0 | 72.8675 | 0 | 1590 | 0 | 0 | 1590 |
| 1 | 0.05 | 48.9919 | 252.382 | 0 | 72.8675 | 0 | 1300 | 0 | 0 | 1300 |
| 2 | 0.1 | 139.883 | 161.491 | 0 | 72.8675 | 0 | 830 | 0 | 0 | 830 |
| 3 | 0.15 | 201.993 | 99.3817 | 0 | 72.8675 | 0 | 460 | 0 | 0 | 460 |
| 4 | 0.2 | 251.804 | 49.5706 | 0 | 72.8675 | 0 | 260 | 0 | 0 | 260 |
| 5 | 0.25 | 267.401 | 33.9731 | 5.73307 | 67.1344 | 0 | 160 | 3.47131 | 0 | 163.471 |
| 6 | 0.3 | 287.28 | 14.0945 | 7.87073 | 64.9967 | 0 | 70 | 10.5798 | 0 | 80.5798 |
| 7 | 0.35 | 295.033 | 6.34141 | 12.96 | 59.9075 | 0 | 20 | 15.8962 | 0 | 35.8962 |
| 8 | 0.4 | 301.374 | 0 | 15.0905 | 57.777 | 0 | 0 | 18.9167 | 0 | 18.9167 |
| 9 | 0.45 | 301.374 | 0 | 17.1228 | 55.7447 | 0 | 0 | 19.9586 | 0 | 19.9586 |
| 10 | 0.5 | 301.374 | 0 | 34.1608 | 38.7067 | 0 | 0 | 41.8435 | 0 | 41.8435 |
| 11 | 0.55 | 301.374 | 0 | 41.0564 | 31.811 | 0 | 0 | 49.1584 | 0 | 49.1584 |
| 12 | 0.6 | 301.374 | 0 | 47.5616 | 25.3058 | 0 | 0 | 54.6559 | 0 | 54.6559 |
| 13 | 0.65 | 301.374 | 0 | 58.7947 | 14.0727 | 0 | 0 | 64.8295 | 0 | 64.8295 |
| 14 | 0.7 | 301.374 | 0 | 58.7947 | 14.0727 | 0 | 0 | 64.8295 | 0 | 64.8295 |
| 15 | 0.75 | 301.374 | 0 | 66.5553 | 6.31212 | 0 | 0 | 69.3375 | 0 | 69.3375 |
| 16 | 0.8 | 301.374 | 0 | 71.3319 | 1.53555 | 0 | 0 | 75.9399 | 0 | 75.9399 |
| 17 | 0.85 | 301.374 | 0 | 71.3319 | 1.53555 | 0 | 0 | 75.9399 | 0 | 75.9399 |
| 18 | 0.9 | 301.374 | 0 | 72.8675 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
| 19 | 0.95 | 301.374 | 0 | 72.8675 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
| 20 | 1 | 301.374 | 0 | 72.8675 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
正如我们在其他图中已经看到的那样,可以直接通过特定函数获得数量和成本数据框。特别是,您还可以选择不报告金额,例如,如果您只想分析成本:
# this function requires a list of thresholds, instead of the step, for example:
threshold_values = np.arange(0, 1, 0.05)
# example without amounts
costs_df = bc.utilities.get_amount_cost_df(
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_values = threshold_values,
#amounts = amounts,
cost_dict = test_cost_dict)
有时可能需要比较被认为是收益的表现(例如,因为它逃脱了欺诈的 TP 数量)与被认为是损失的表现(从模型中逃脱的欺诈 FN 的数量 + 代表检查的每个 FP 的固定成本)对被归类为欺诈但不是欺诈的交易进行)。这可以通过交互式金额成本折线图来完成:
amount_classes = ['TP', 'FP']
cost_classes = 'all'
total_cost_amount_df = bc.total_amount_cost_plot(
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_step = threshold_step,
amounts = amounts,
cost_dict = test_cost_dict,
amount_classes = amount_classes,
cost_classes = cost_classes,
currency = currency);
这里产生的情节:
与其他情况一样,此函数返回一个数据框,其中包含与每个阈值相关联的混淆矩阵中的每个类别以及它们的选定聚合的数量和成本值:
| 临界点 | 金额_TP | 金额_FP | 金额总和 | 成本_TN | 成本_FP | 成本_FN | 成本_TP | 成本总和 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 72.8675 | 301.374 | 374.242 | 0 | 1590 | 0 | 0 | 1590 |
| 1 | 0.05 | 72.8675 | 266.572 | 339.44 | 0 | 1380 | 0 | 0 | 1380 |
| 2 | 0.1 | 72.8675 | 152.006 | 224.874 | 0 | 770 | 0 | 0 | 770 |
| 3 | 0.15 | 72.8675 | 88.4092 | 161.277 | 0 | 430 | 0 | 0 | 430 |
| 4 | 0.2 | 72.5494 | 61.6009 | 134.15 | 0 | 290 | 0.221014 | 0 | 290.221 |
| 5 | 0.25 | 66.5301 | 31.6006 | 98.1307 | 0 | 160 | 4.472 | 0 | 164.472 |
| 6 | 0.3 | 65.3813 | 20.9625 | 86.3437 | 0 | 100 | 9.90665 | 0 | 109.907 |
| 7 | 0.35 | 60.9562 | 12.0418 | 72.998 | 0 | 30 | 18.0882 | 0 | 48.0882 |
| 8 | 0.4 | 57.8163 | 4.85876 | 62.6751 | 0 | 10 | 18.0989 | 0 | 28.0989 |
| 9 | 0.45 | 46.3113 | 0 | 46.3113 | 0 | 0 | 34.7334 | 0 | 34.7334 |
| 10 | 0.5 | 37.5392 | 0 | 37.5392 | 0 | 0 | 42.6685 | 0 | 42.6685 |
| 11 | 0.55 | 31.2279 | 0 | 31.2279 | 0 | 0 | 49.2799 | 0 | 49.2799 |
| 12 | 0.6 | 28.4496 | 0 | 28.4496 | 0 | 0 | 51.4823 | 0 | 51.4823 |
| 13 | 0.65 | 19.7851 | 0 | 19.7851 | 0 | 0 | 58.1733 | 0 | 58.1733 |
| 14 | 0.7 | 8.36888 | 0 | 8.36888 | 0 | 0 | 68.444 | 0 | 68.444 |
| 15 | 0.75 | 1.53555 | 0 | 1.53555 | 0 | 0 | 75.9399 | 0 | 75.9399 |
| 16 | 0.8 | 1.53555 | 0 | 1.53555 | 0 | 0 | 75.9399 | 0 | 75.9399 |
| 17 | 0.85 | 0 | 0 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
| 18 | 0.9 | 0 | 0 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
| 19 | 0.95 | 0 | 0 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
| 20 | 1 | 0 | 0 | 0 | 0 | 0 | 75.9666 | 0 | 75.9666 |
您还可以使用已使用的get_amount_cost_df()功能直接访问以前的数据,例如不包括关注成本的金额:
# this function requires a list of thresholds, instead of the step, for example:
threshold_values = np.arange(0, 1, 0.05)
# example without amounts
costs_df = bc.utilities.get_amount_cost_df(
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold_values = threshold_values,
#amounts = amounts,
cost_dict = test_cost_dict)
最后,在第一个版本中还有一个函数可以简化从评分数据帧中提取属于特定类别的混淆矩阵的观察结果。例如,如果要提取属于 TP 类别的所有观测值,则需要以下代码:
# for example, if we want the True Positive data points with a 0.7 threshold:
confusion_category = 'TP'
bc.get_confusion_category_observations_df(
confusion_category = confusion_category,
X_data = X_test,
true_y = y_test,
predicted_proba = test_predicted_proba,
threshold = 0.7 # default = 0.5
)