用于分类特征的 LabelEncoder?

Ars*_*nOz 5 python machine-learning correlation scikit-learn feature-engineering

这可能是一个初学者问题,但我看到很多人使用 LabelEncoder() 用序数替换分类变量。很多人通过一次传递多个列来使用此功能,但是我对某些功能中的顺序错误以及它将如何影响我的模型有些怀疑。下面是一个例子:

输入

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

a = pd.DataFrame(['High','Low','Low','Medium'])
le = LabelEncoder()
le.fit_transform(a)
Run Code Online (Sandbox Code Playgroud)

输出

array([0, 1, 1, 2], dtype=int64)
Run Code Online (Sandbox Code Playgroud)

如您所见,序号值没有正确映射,因为我的 LabelEncoder 只关心列/数组中的顺序(它应该是 High=1、Med=2、Low=3,反之亦然)。错误的映射对模型的影响有多大,除了 OrdinalEncoder() 之外,还有其他简单的方法可以正确映射这些值吗?

yat*_*atu 14

TL; DR:使用LabelEncoder以编码顺序 任何一种功能是一个坏主意!


这实际上在文档中明确说明,其中提到,顾名思义,这种编码方法旨在对标签进行编码:

这个转换器应该用于编码目标值,即y,而不是输入 X

正如您在问题中正确指出的那样,将序数特征的有序数映射到错误的尺度将对模型的性能产生非常负面的影响(即与特征的相关性成正比)。这同样适用于分类特征,只是原始特征没有序数。

一种直观的思考方式是决策树设置边界的方式。在训练期间,决策树将学习在每个节点设置的最佳特征,以及一个最佳阈值,根据这些值,看不见的样本将遵循一个或另一个分支。

如果我们使用 simple 对序数特征进行编码LabelEncoder,则可能会导致特征表示1表示温暖2这可能会转化为,以及0表示沸腾。在这种情况下,结果将最终成为具有不必要的大量拆分的树,因此对于应该更简单建模的内容而言,复杂性要高得多。

相反,正确的方法是使用OrdinalEncoder,并为序数特征定义适当的映射方案。或者在具有分类特征的情况下,我们应该查看Category Encoders 中OneHotEncoder可用的各种编码


虽然实际上看到为什么这是一个坏主意将比仅仅用文字更直观。

让我们用一个简单的例子来说明上面的内容,包括两个序数特征,其中包含一个范围,一个学生准备考试的时间和所有以前作业的平均成绩,以及一个指示考试是否通过的目标变量或不。我已将数据框的列定义为pd.Categorical

df = pd.DataFrame(
        {'Hours of dedication': pd.Categorical(
              values =  ['25-30', '20-25', '5-10', '5-10', '40-45', 
                         '0-5', '15-20', '20-25', '30-35', '5-10',
                         '10-15', '45-50', '20-25'],
              categories=['0-5', '5-10', '10-15', '15-20', 
                          '20-25', '25-30','30-35','40-45', '45-50']),

         'Assignments avg grade': pd.Categorical(
             values =  ['B', 'C', 'F', 'C', 'B', 
                        'D', 'C', 'A', 'B', 'B', 
                        'B', 'A', 'D'],
             categories=['F', 'D', 'C', 'B','A']),

         'Result': pd.Categorical(
             values = ['Pass', 'Pass', 'Fail', 'Fail', 'Pass', 
                       'Fail', 'Fail','Pass','Pass', 'Fail', 
                       'Fail', 'Pass', 'Pass'], 
             categories=['Fail', 'Pass'])
        }
    )
Run Code Online (Sandbox Code Playgroud)

将分类列定义为熊猫的分类列的优点是我们可以在其类别之间建立顺序,如前所述。这允许基于既定顺序而不是词法排序进行更快的排序。它也可以作为一种简单的方法来根据不同类别的顺序获取代码。

所以我们将使用的数据框如下所示:

print(df.head())

  Hours_of_dedication   Assignments_avg_grade   Result
0               20-25                       B     Pass
1               20-25                       C     Pass
2                5-10                       F     Fail
3                5-10                       C     Fail
4               40-45                       B     Pass
5                 0-5                       D     Fail
6               15-20                       C     Fail
7               20-25                       A     Pass
8               30-35                       B     Pass
9                5-10                       B     Fail
Run Code Online (Sandbox Code Playgroud)

相应的类别代码可以通过以下方式获得:

X = df.apply(lambda x: x.cat.codes)
X.head()

   Hours_of_dedication   Assignments_avg_grade   Result
0                    4                       3        1
1                    4                       2        1
2                    1                       0        0
3                    1                       2        0
4                    7                       3        1
5                    0                       1        0
6                    3                       2        0
7                    4                       4        1
8                    6                       3        1
9                    1                       3        0
Run Code Online (Sandbox Code Playgroud)

现在让我们拟合 a DecisionTreeClassifier,看看树是如何定义分割的:

from sklearn import tree

dt = tree.DecisionTreeClassifier()
y = X.pop('Result')
dt.fit(X, y)
Run Code Online (Sandbox Code Playgroud)

我们可以使用plot_tree以下方法可视化树结构:

t = tree.plot_tree(dt, 
                   feature_names = X.columns,
                   class_names=["Fail", "Pass"],
                   filled = True,
                   label='all',
                   rounded=True)
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

这就是全部??嗯……是的!我实际上以这样一种方式设置了特征,即奉献时间特征与是否通过考试之间存在这种简单而明显的关系,从而清楚地表明问题应该很容易建模。


现在让我们尝试通过使用我们可以获得的编码方案直接编码所有特征来做同样的事情,例如通过 a 获得LabelEncoder,因此忽略特征的实际序数,并随机分配一个值:

df_wrong = df.copy()
df_wrong['Hours_of_dedication'].cat.set_categories(
             ['0-5','40-45', '25-30', '10-15', '5-10', '45-50','15-20', 
              '20-25','30-35'], inplace=True)
df_wrong['Assignments_avg_grade'].cat.set_categories(
             ['A', 'C', 'F', 'D', 'B'], inplace=True)
Run Code Online (Sandbox Code Playgroud)
rcParams['figure.figsize'] = 14,18
X_wrong = df_wrong.drop(['Result'],1).apply(lambda x: x.cat.codes)
y = df_wrong.Result

dt_wrong = tree.DecisionTreeClassifier()
dt_wrong.fit(X_wrong, y)

t = tree.plot_tree(dt_wrong, 
                   feature_names = X_wrong.columns,
                   class_names=["Fail", "Pass"],
                   filled = True,
                   label='all',
                   rounded=True)
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

正如预期的那样,对于我们试图建模的简单问题,树结构比所需的复杂得多。为了让树正确预测所有训练样本,它已经扩展到深度4,此时单个节点就足够了。

这意味着分类器可能会过度拟合,因为我们正在大幅增加复杂性。通过修剪树和调整必要的参数以防止过度拟合,我们也没有解决问题,因为我们通过错误地编码特征添加了太多的噪音。

所以总而言之,一旦对特征进行编码,保持特征的序数是至关重要的,否则正如这个例子所表明的那样,我们将失去所有可预测的能力,只会向我们的模型添加噪声