在 python 中,如何在考虑 class 的情况下使用精度作为标准对连续变量进行离散化

In python, how to discretize continuous variable using accuracy as a criterion taking class into consideration

对于一组受试者,我有一个范围为 0-100 的连续变量,表示受试者状态的量化 cont_attribute。对于每个主题,我还有一个序数变量,表示 reader 将主题状态注释为四种状态之一(例如 1、2、3、4)class_labelcont_attribute 的值在 class 之间重叠。我的目标是离散化 cont_attribute 以便优化与 class 的一致性。

当离散化cont_attribute时,任意阈值x1x2x3可以直接应用于连续变量,以产生四个有序类别和一致的bin用reader注解class可以评估:

cohen_kappa_score((pd.cut(df['cont_attribute'],bins=[0, x1, x2, x3, 100], labels=['1','2','3','4']).astype('int'))
, df['class_label'].astype('int'))

我找到了几个连续变量离散化的选项,例如 Jenks natural breakssklearn Kmeans,尽管这些选项不采用考虑到 class.

我试过的:

我尝试优化上面的函数以使用 scipy.optimize.minimize 产生最大值。这里对于两个 classes 之间的每个阈值,我使用较大的 class 的最小值和较小的 classes 的最大值作为找到各自最佳截止值的范围那些 classes 之间的点。用这个方法我运行遇到了问题,提示:

ValueError: bins must increase monotonically.

def objfunc(grid):
    x1, x2, x3 = grid
    return (cohen_kappa_score((pd.cut(df.cont_attribute,bins=[0, x1, x2, x3, 100],labels=['1','2','3','4'], duplicates='drop').astype('int'))
, df['class_label'].astype('int'))) * (-1);

grid = (slice(df[(df['class_label'] == 2)]['cont_attribute'].min(), df[(df['class_label'] == 1)]['cont_attribute'].max(), 0.5), (slice(df[(df['class_label'] == 3)]['cont_attribute'].min(), df[(df['class_label'] == 2)]['cont_attribute'].max(), 0.5), (slice(df[(df['class_label'] == 4)]['cont_attribute'].min(), df[(df['class_label'] == 3)]['cont_attribute'].max(), 0.5))
solution = brute(objfunc, grid, finish=None,full_output = True)
solution

在 python 中,是否有一种直接的方法来优化阈值 x1x2x3 并考虑与 class 的一致性(监督离散化)?或者,如何使用 scipy.optimize.minimize?

重写上述函数以产生最大值

错误信息并不太难。 pandas cut 方法要求切割向量 [0,x1,x2,x3,100] 是严格单调的。通过某种机制来确保没有无效值被传递给 cut 函数,我们是安全的。这就是我在下面实现的。为了表示 invalid 设置,习惯上使用 np.inf 因为所有其他值都较低。因此,每个 minizmier 都会说这样一个无效的解决方案是不可取的。见下文的实施。我还包含了所有导入和一些数据生成,因此使用代码很简单。请在以后的问题中也这样做。

您可能希望在强力搜索中每个维度使用 10 个以上的分箱。

另外 - 代码效率很低。由于它对 x1、x2、x3 的所有组合进行暴力破解,但其中很多组合都是无效的(例如 x2<=x1),因此您可能希望在 (x1,x2-x1, x3-x2) 中参数化问题,并在第二个和第三个分量中搜索非负值。

最后,brute 方法是一个最小化器,所以你应该 return -cohen_kappa 来自 objective

#%%
import numpy as np
from sklearn.metrics import cohen_kappa_score, confusion_matrix
from scipy.stats import truncnorm
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.optimize import brute

#
# Generate Data
#
n = 1000
np.random.seed(0)
y = np.random.choice(4, p=[0.1, 0.3, 0.4, 0.2], size=n)
x = np.zeros(n)
for i in range(5):
    low = 0
    high = 100
    mymean = 20 * i
    myscale = 8
    a, b = (low - mymean) / myscale, (high - mymean) / myscale
    x[y == i] = truncnorm.rvs(a=a, b=b, loc=mymean, scale=myscale, size=np.sum(y == i))
data = pd.DataFrame({"cont_attribute": x, "class_label": y})

# make a loss function that accounts for the bad orderings
def loss(cuts):
    x1, x2, x3 = cuts
    if 0 >= x1 or x1 >= x2 or x2 >= x3 or x3 >= 100:
        return np.inf
    yhat = pd.cut(
        data["cont_attribute"],
        bins=[0, x1, x2, x3, 100],
        labels=[0, 1, 2, 3],
        # duplicates="drop",
    ).astype("int")
    return -cohen_kappa_score(data["class_label"], yhat)


# Compute the result via brute force
ranges = [(0, 100)] * 3
Ns=30
result = brute(func=loss, ranges=ranges, Ns=Ns)
print(result)
print(-loss(result))

# Evaluate the final result in a confusion matrix
x1, x2, x3 = result
data["class_pred"] = pd.cut(
    data["cont_attribute"],
    bins=[0, x1, x2, x3, 100],
    labels=[0, 1, 2, 3],
    duplicates="drop",
).astype("int")
mat = confusion_matrix(y_true=data['class_label'],y_pred=data['class_pred'])
plt.matshow(mat)
# Loop over data dimensions and create text annotations.
for i in range(4):
    for j in range(4):
        text = plt.text(j, i, mat[i, j],
                       ha="center", va="center", color="grey")
plt.xlabel('Predicted class')
plt.ylabel('True class')
plt.show()

# Evaluate result graphically
# inspect the data
fig,ax = plt.subplots(2,1)
sns.histplot(data=data, x="cont_attribute", hue="class_label",ax=ax[0],multiple='stack')
sns.histplot(data=data, x="cont_attribute", hue="class_pred",ax=ax[1],multiple='stack')
plt.show()

关于 scipy.optimize.minimize 的使用,当使用 cohen kappa 作为目标时,这是不可能的。由于不可微,因此优化起来并不容易。考虑改用交叉熵损失函数。但在那种情况下,您需要一个(参数)模型来执行分类任务。

statsmodels 中的序数回归包中提供了标准序数分类器。它将比粗暴方法快得多,但在 cohen kappa 上评估时可能不太准确。如果要获得更多的垃圾箱,我可能会走那条路。