序数编码#
序数编码通过为每个类别分配一个唯一的整数,将分类数据转换为数值数据,这是大多数数据科学项目中常见的数据预处理步骤。
序数编码在分类变量中存在固有的顺序或排名时特别有用。例如,具有 small、medium 和 large 值的变量 size 显示出清晰的排名,即 small < medium < large,因此序数编码是一种合适的编码方法。
在实践中,我们应用序数编码,而不考虑变量的内在顺序,因为一些机器学习模型,例如基于决策树的模型,能够从这些任意分配的值中学习。
序数编码的优点之一是它保持了特征空间的紧凑性,与独热编码相比,独热编码可以显著增加数据集的维度。
任意 vs 有序序数编码#
在序数编码中,分类变量可以根据任意规则或某些定义的逻辑被编码为数值。
任意序数编码#
任意序数编码 是执行序数编码的传统方法,其中每个类别被替换为一个唯一的数值,而不考虑其他因素。这种编码方法根据类别在数据集中出现的顺序为其分配数字,每次遇到新类别时增加数值。
任意分配序数提供了一种从分类数据中获取数值变量的简单方法,并且它往往能很好地与基于决策树的机器学习模型配合工作。
有序序数编码#
有序序数编码 是一种更复杂的方式来实现序数编码。它首先根据与每个类别相关联的目标变量的平均值对类别进行排序,然后根据这个顺序分配数值。
例如,对于变量 colour,如果目标的平均值分别为蓝色 0.5、红色 0.8 和灰色 0.1,那么我们首先按其平均值对类别进行排序:灰色 (0.1)、蓝色 (0.5)、红色 (0.8)。然后,我们将灰色替换为 0,蓝色替换为 1,红色替换为 2。
有序编码试图定义编码变量与目标变量之间的单调关系。这种方法有助于机器学习算法,特别是线性模型(如线性回归),更好地捕捉和学习编码特征与目标之间的关系。
请记住,有序序数编码将创建编码变量与目标变量之间的单调关系 仅当 存在 类别与目标变量之间的内在关系时。
未见的类别#
序数编码无法自然处理未见过的类别。
未见类别 是指在测试、验证或实时数据中出现,但在训练数据中未出现的分类值。这些类别是有问题的,因为编码方法只为训练数据中存在的类别生成映射。这意味着我们将缺少任何新出现的、未见类别的编码。未见类别在推理时间(机器学习模型用于对新数据进行预测的阶段)引起错误,因为我们的特征工程管道无法将该值转换为数字。
序数编码本身不处理未见过的类别。然而,我们可以用任意值(如 -1,记住序数编码从 0 开始)替换未见过的类别。这种做法可能对线性模型效果良好,因为 -1 将是分类变量的最小值,并且由于线性模型在变量和目标之间建立线性关系,它将为未见过的类别返回最低(或最高)的响应值。
然而,对于基于树的模型,这种替换未见类别的方法可能无效,因为树创建了非线性分区,使得难以预测树将如何处理值为 -1 的情况,从而导致不可预测的结果。
如果我们期望我们的变量有大量的未见类别,最好选择另一种能够开箱即用地处理未见类别的编码技术,例如目标编码,或者相反,将稀有类别分组在一起。
序数编码的优缺点#
序数编码实现快速且简单,并且它不会像独热编码那样增加数据集的维度。
不利的一面是,它可能会在类别之间强加误导性的关系;它无法处理未见过的类别;并且它不适合大量类别,即具有高基数的特征。
序数编码 vs 标签编码#
序数编码有时也被称为标签编码。它们遵循相同的过程。Scikit-learn 提供了两种不同的转换器:OrdinalEncoder 和 LabelEncoder。两者都用序数数据替换值,即类别。OrdinalEncoder 旨在转换预测变量(训练集中的那些变量),而 LabelEncoder 旨在转换目标变量。两种转换器的最终结果是相同的;原始值被序数数字替换。
在我们看来,这引发了一些关于标签编码和序数编码是否是处理分类数据的不同预处理方式的困惑。有人认为标签编码是用任意分配的数字替换类别,而序数编码是根据变量的固有顺序(如变量大小)分配数字。我们不做这样的区分,并将这两种技术视为可互换的。
OrdinalEncoder#
Feature-engine 的 OrdinalEncoder() 实现了序数编码。也就是说,它通过将每个类别替换为一个从 0 到 k-1 的唯一数字来编码分类特征,其中 ‘k’ 是数据集中类别的不同数量。
OrdinalEncoder() 支持 任意 和 有序 编码方法。所需的方法可以通过 encoding_method 参数指定,该参数接受 “任意” 或 “有序”。如果未定义,encoding_method 默认为 "有序"。
如果 encoding_method 被定义为 “arbitrary”,那么 OrdinalEncoder() 将按照先到先得的原则,即按照类别在数据集中出现的顺序,为分类变量分配数值。
如果 encoding_method 被定义为 “ordered”,那么 OrdinalEncoder() 将根据每个类别目标变量的平均值分配数值。目标平均值最高的类别将被替换为整数值 k-1,而目标平均值最低的类别将被替换为 0。这里的 ‘k’ 是类别的不同数量。
当遇到未见过的类别时,OrdinalEncoder() 可以选择引发错误并失败,忽略稀有类别,在这种情况下它将被编码为 np.nan,或者将其编码为 -1。您可以通过 unseen 参数定义此行为。
Python 实现#
在页面的其余部分,我们将展示如何通过 Feature-engine 的 OrdinalEncoder() 使用序数编码的不同方法。
任意序数编码#
我们将展示如何使用 Titanic Dataset 通过 Feature-engine 的 OrdinalEncoder() 实现序数编码。
让我们加载数据集并将其分割为训练集和测试集:
import pandas as pd
from sklearn.model_selection import train_test_split
from feature_engine.datasets import load_titanic
from feature_engine.encoding import OrdinalEncoder
X, y = load_titanic(
return_X_y_frame=True,
handle_missing=True,
predictors_only=True,
cabin="letter_only",
)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=0,
)
print(X_train.head())
我们看到了下面的泰坦尼克号数据集:
pclass sex age sibsp parch fare cabin embarked
501 2 female 13.000000 0 1 19.5000 M S
588 2 female 4.000000 1 1 23.0000 M S
402 2 female 30.000000 1 0 13.8583 M C
1193 3 male 29.881135 0 0 7.7250 M Q
686 3 female 22.000000 0 0 7.7250 M Q
让我们设置 OrdinalEncoder() 来编码分类变量 cabin、embarked 和 sex,并任意分配整数:
encoder = OrdinalEncoder(
encoding_method='arbitrary',
variables=['cabin', 'embarked', 'sex'])
OrdinalEncoder() 默认会对训练集中的**所有**分类变量进行编码,除非我们像在前一个代码块中那样指定要编码的变量。
让我们拟合编码器,以便它学习每个类别的映射:
encoder.fit(X_train)
编码映射存储在其 encoder_dict_ 参数中。让我们显示它们:
encoder.encoder_dict_
在 encoder_dict_ 中,我们找到将替换每个变量编码类别的整数。通过这个字典,我们可以将变量的原始值映射到新值。
{'cabin': {'M': 0,
'E': 1,
'C': 2,
'D': 3,
'B': 4,
'A': 5,
'F': 6,
'T': 7,
'G': 8},
'embarked': {'S': 0, 'C': 1, 'Q': 2, 'Missing': 3},
'sex': {'female': 0, 'male': 1}}
根据之前的映射,变量cabin中的类别M将被替换为0,类别E将被替换为1,依此类推。
准备好映射后,我们可以继续转换数据。transform() 方法将学习到的映射应用于训练集和测试集中的分类特征,返回序数变量。
train_t = encoder.transform(X_train)
test_t = encoder.transform(X_test)
print(train_t.head())
在以下输出中,我们看到结果数据框,其中cabin、embarked和sex中的原始变量值现在被替换为整数:
pclass sex age sibsp parch fare cabin embarked
501 2 0 13.000000 0 1 19.5000 0 0
588 2 0 4.000000 1 1 23.0000 0 0
402 2 0 30.000000 1 0 13.8583 0 1
1193 3 1 29.881135 0 0 7.7250 0 2
686 3 0 22.000000 0 0 7.7250 0 2
逆变换#
我们可以使用 inverse_transform() 方法将编码后的值还原回原始类别。这在模型解释、调试或当我们需要以原始类别形式向利益相关者展示结果时非常有用。
train_inv = encoder.inverse_transform(train_t)
print(train_inv.head())
上一个命令返回一个包含原始类别值的数据框:
pclass sex age sibsp parch fare cabin embarked
501 2 female 13.000000 0 1 19.5000 M S
588 2 female 4.000000 1 1 23.0000 M S
402 2 female 30.000000 1 0 13.8583 M C
1193 3 male 29.881135 0 0 7.7250 M Q
686 3 female 22.000000 0 0 7.7250 M Q
编码数值变量#
数值变量本质上也可以是分类的。OrdinalEncoder() 默认只会编码数据类型为对象或分类的变量。然而,我们也可以通过设置 ignore_format=True 来编码数值变量。
在泰坦尼克号数据集中,变量 pclass 表示乘客所乘坐的舱位等级(即头等舱、二等舱和三等舱)。这个变量可能已经很好了,不需要进一步的数据预处理,但为了展示如何使用 OrdinalEncoder() 对数值变量进行编码,我们将把它视为分类变量并进行序数编码。
让我们设置 OrdinalEncoder() 来对变量 pclass 进行序数编码,然后将其拟合到训练集,以便它学习映射关系:
encoder = OrdinalEncoder(
encoding_method='arbitrary',
variables=['pclass'],
ignore_format=True)
train_t = encoder.fit_transform(X_train)
fit_transform() 方法将编码器拟合到训练数据,学习每个类别的映射,然后使用这些映射转换训练数据。让我们看看生成的编码。
encoder.encoder_dict_
生成的编码将是:
{'pclass': {2: 0, 3: 1, 1: 2}}
我们看到第二个类别将被替换为0,第三个类别将被替换为1,第一个类别将被替换为2。
如果你想查看结果的数据框,请执行 train_t.head()。
有序序数编码#
有序编码包括基于目标均值分配整数。我们将使用 加利福尼亚住房数据集 来演示有序编码。该数据集包含数值特征,如 MedInc、HouseAge 和 AveRooms 等。目标变量是 MedHouseVal,即加利福尼亚地区的房屋中位数价值,以十万美元($100,000)为单位表示。
首先设置数据集。
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from feature_engine.encoding import OrdinalEncoder
from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing(as_frame=True)
data = housing.frame
print(data.head())
下面,我们看到数据集:
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude \
0 8.3252 41.0 6.984127 1.023810 322.0 2.555556 37.88
1 8.3014 21.0 6.238137 0.971880 2401.0 2.109842 37.86
2 7.2574 52.0 8.288136 1.073446 496.0 2.802260 37.85
3 5.6431 52.0 5.817352 1.073059 558.0 2.547945 37.85
4 3.8462 52.0 6.281853 1.081081 565.0 2.181467 37.85
Longitude MedHouseVal
0 -122.23 4.526
1 -122.22 3.585
2 -122.24 3.521
3 -122.25 3.413
4 -122.25 3.422
为了展示有序编码的威力,我们将把 HouseAge 变量(这是一个连续变量)转换为一个具有四个类别的有序变量:新、较新、旧 和 非常旧。
data['HouseAgeCategorical'] = pd.qcut(data['HouseAge'], q=4, labels=['new', 'newish', 'old', 'very_old'])
print(data[['HouseAge', 'HouseAgeCategorical']].head())
HouseAge HouseAgeCategorical
0 41.0 very_old
1 21.0 newish
2 52.0 very_old
3 52.0 very_old
4 52.0 very_old
HouseAgeCategorical 的类别(new、newish、old、very_old)是离散的,代表房屋年龄的范围。它们很可能与目标变量有顺序关系,因为较老的房子往往更便宜,这使得它们成为有序编码的合适候选。
现在让我们将数据分为训练集和测试集。
X = data.drop('MedHouseVal', axis=1)
y = data['MedHouseVal']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=0)
print(X_train.head())
训练集现在包括了我们为 HouseAge 创建的分类特征。
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude \
1989 1.9750 52.0 2.800000 0.700000 193.0 4.825000 36.73
256 2.2604 43.0 3.671480 1.184116 836.0 3.018051 37.77
7887 6.2990 17.0 6.478022 1.087912 1387.0 3.810440 33.87
4581 1.7199 17.0 2.518000 1.196000 3051.0 3.051000 34.06
1993 2.2206 50.0 4.622754 1.161677 606.0 3.628743 36.73
Longitude HouseAgeCategorical
1989 -119.79 very_old
256 -122.21 very_old
7887 -118.04 new
4581 -118.28 new
1993 -119.81 very_old
让我们定义 OrdinalEncoder() 来使用 有序 编码对分类变量 HouseAgeCategorical 进行编码。
ordered_encoder = OrdinalEncoder(
encoding_method='ordered',
variables=['HouseAgeCategorical']
)
让我们拟合编码器,使其学习映射关系。请注意,对于有序的序数编码,我们需要将目标变量传递给 fit() 方法:
X_train_t = ordered_encoder.fit_transform(X_train, y_train)
X_test_t = ordered_encoder.transform(X_test)
请注意,我们首先在训练数据上拟合编码器,然后使用从训练集学到的映射来转换训练和测试数据。
让我们显示结果数据框:
print(X_train_t.head())
我们可以在下面看到生成的数据框,其中变量 HouseAgeCategorical 现在包含编码后的值。
MedInc HouseAge AveRooms AveBedrms Population AveOccup Latitude \
1989 1.9750 52.0 2.800000 0.700000 193.0 4.825000 36.73
256 2.2604 43.0 3.671480 1.184116 836.0 3.018051 37.77
7887 6.2990 17.0 6.478022 1.087912 1387.0 3.810440 33.87
4581 1.7199 17.0 2.518000 1.196000 3051.0 3.051000 34.06
1993 2.2206 50.0 4.622754 1.161677 606.0 3.628743 36.73
Longitude HouseAgeCategorical
1989 -119.79 3
256 -122.21 3
7887 -118.04 0
4581 -118.28 0
1993 -119.81 3
让我们检查一下从类别到整数的映射结果:
ordered_encoder.encoder_dict_
我们看到了将用于替换以下显示中类别的值:
{'HouseAgeCategorical':
{'new': 0,
'newish': 1,
'old': 2,
'very_old': 3}
}
为了理解结果,让我们查看 HouseAgeCategorical 中每个类别的平均目标值:
test_set = X_test_t.join(y_test)
mean_target_per_encoded_category = test_set[['HouseAgeCategorical', 'MedHouseVal']].groupby('HouseAgeCategorical').mean().reset_index()
print(mean_target_per_encoded_category)
这将产生以下输出:
HouseAgeCategorical MedHouseVal
0 0 1.925929
1 1 2.043071
2 2 2.083013
3 3 2.237240
首先根据目标均值对类别进行排序,然后根据此顺序分配数字。例如,very_old 年龄类别编码为 ‘3’ 的房屋的平均中位数房屋价值约为 $223,724,而 new 年龄类别编码为 ‘0’ 的房屋的平均中位数房屋价值约为 $192,593。原则上,这与我们最初的假设相反:即较旧的房屋会更便宜。但这是数据告诉我们的。
我们现在可以绘制测试集编码后每个类别的目标均值,以展示单调关系。
mean_target_per_encoded_category['HouseAgeCategorical'] = mean_target_per_encoded_category['HouseAgeCategorical'].astype(str)
plt.scatter(mean_target_per_encoded_category['HouseAgeCategorical'], mean_target_per_encoded_category['MedHouseVal'])
plt.title('Mean target value per category')
plt.xlabel('Encoded category')
plt.ylabel('Mean target value')
plt.show()
这将给我们以下输出:
如上图所示,有序序数编码能够捕捉 HouseAgeCategorical 变量与房屋中位价值之间的单调关系,使得机器学习模型能够学习到这一趋势,否则这一趋势可能会被忽略。
序数有序编码器的强大之处在于其内在的寻找单调关系的能力。
其他资源#
在下面的笔记本中,你可以找到更多关于 OrdinalEncoder() 功能的详细信息以及带有编码变量的示例图:
关于此功能及其他特征工程方法的更多详情,请查看以下资源和教程:
机器学习的特征工程#
或者阅读我们的书:
Python 特征工程手册#
我们的书籍和课程都适合初学者和更高级的数据科学家。通过购买它们,您正在支持 Feature-engine 的主要开发者 Sole。
