TL;DR:使用LabelEncoder 编码ordinal 任何类型的特征都是一个坏主意!
这实际上在文档中明确说明,其中提到顾名思义这种编码方法旨在编码标签:
这个转换器应该用于编码目标值,即y,而不是输入 X。
正如您在问题中正确指出的那样,将ordinal feature 的固有序数映射到错误的比例将对模型的性能产生非常负面的影响(即与特征的相关性成正比)。同样适用于categorical feature,只是原始特征没有序数。
一种直观的思考方式是decision tree 设置其边界的方式。在训练期间,决策树将学习在每个节点上设置的最佳特征,以及一个最佳阈值,根据这些值,看不见的样本将跟随一个或另一个分支。
如果我们使用简单的LabelEncoder 对序数特征进行编码,则可能导致特征具有说1 表示温暖,2 可能会转换为热,以及一个代表沸腾的0。在这种情况下,结果最终将是一棵具有不必要的大量分裂的树,因此对于应该更简单的建模而言,复杂性要高得多。
相反,正确的方法是使用OrdinalEncoder,并为序数特征定义适当的映射方案。或者在具有分类特征的情况下,我们应该查看OneHotEncoder 或Category Encoders 中可用的各种编码器。
虽然实际上了解为什么这是一个坏主意会比文字更直观。
让我们用一个简单的例子来说明上述情况,它由两个序数特征组成,其中包含一个范围,即学生准备考试所花费的小时数和所有以前作业的平均成绩,以及一个目标变量,指示是否考试过去与否。我已将数据框的列定义为 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'])
}
)
将分类列定义为 pandas 的分类的好处是,我们可以在其类别之间建立顺序,如前所述。这允许基于已建立的顺序而不是词法排序进行更快的排序。它也可以作为一种简单的方法来根据它们的顺序获取不同类别的代码。
所以我们将使用的数据框如下所示:
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
对应的分类代码可以通过:
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
现在让我们拟合DecisionTreeClassifier,看看树是如何定义分裂的:
from sklearn import tree
dt = tree.DecisionTreeClassifier()
y = X.pop('Result')
dt.fit(X, y)
我们可以使用plot_tree可视化树结构:
t = tree.plot_tree(dt,
feature_names = X.columns,
class_names=["Fail", "Pass"],
filled = True,
label='all',
rounded=True)
就这些了吗??嗯……是的!我实际上已经以这样一种方式设置功能,即在奉献时间功能与考试是否通过之间存在这种简单而明显的关系,清楚地表明这个问题应该很容易建模。
现在让我们尝试通过使用我们可以通过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)
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)
正如预期的那样,对于我们试图建模的简单问题而言,树结构要复杂得多。为了让树正确预测所有训练样本,它已经扩展至深度为4,此时单个节点就足够了。
这意味着分类器可能会过度拟合,因为我们正在大幅增加复杂性。通过修剪树和调整必要的参数来防止过度拟合,我们也没有解决问题,因为我们通过错误地编码特征添加了太多的噪音。
总而言之,在编码后保持特征的序数至关重要,否则正如本示例所表明的那样,我们将失去所有可预测的能力,只需将 noise 添加到我们的模型中。