×

Python 的 9 大特征工程技术

hqy hqy 发表于2025-05-01 10:04:47 浏览9 评论0百度已收录

抢沙发发表评论

在之前的几篇文章中,我们特别关注机器学习模型的性能。首先,我们讨论了如何量化机器学习模型的性能,以及如何通过正则化来改进它。然后我们介绍了其他优化技术,包括基本的优化技术,如梯度下降高级技术,如Adam。最后,我们能够了解如何执行超参数优化并为您的模型获得最佳"配置"。

但是,到目前为止,我们尚未考虑的是如何通过修改数据本身来提高性能。我们专注于模型。到目前为止,在我们关于 SVM 聚类的文章中,我们将一些技术(如缩放)应用于数据,但我们还没有对此过程进行更深入的分析,以及数据集的操作如何帮助我们提高性能。在本文中,我们正是这样做的,探索最有效的特征工程技术,这些技术通常是获得良好结果所必需的。

不要误会我的意思,特征工程不仅仅是为了优化模型。有时我们需要应用这些技术,以便我们的数据与机器学习算法兼容。机器学习算法有时期望数据以某种方式格式化,这就是特征工程可以帮助我们的地方。除此之外,重要的是要注意,数据科学家和工程师将大部分时间花在数据预处理上。这就是为什么掌握这些技术很重要的原因。在本文中,我们将探讨:

归 责分类编码处理异常值分档缩放日志转换功能选择功能分组功能拆分

数据集和先决条件

出于本教程的目的,请确保您已安装以下 Python 库:

NumPy – 如果您需要安装帮助,请遵循本指南SciKit Learn – 如果您需要安装方面的帮助,请遵循本指南熊猫 - 如果您需要安装帮助,请遵循本指南Matplotlib – 如果您需要安装帮助,请遵循本指南SeaBorn - 如果您需要安装帮助,请遵循本指南

安装后,请确保已导入本教程中使用的所有必要模块。

import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sb from sklearn.preprocessing import StandardScaler, MinMaxScaler, MaxAbsScaler, QuantileTransformer from sklearn.feature_selection import SelectKBest, f_classif

我们在本文中使用的数据来自PalmerPenguins Dataset。该数据集最近被引入,作为著名的Iris数据集的替代方案。它由Kristen Gorman博士和南极洲LTER的Palmer站创建。您可以在此处或通过Kaggle获取此数据集。该数据集主要由两个数据集组成,每个数据集包含344只企鹅的数据。就像在鸢尾花数据集中一样,有3种不同种类的企鹅来自帕尔默群岛的3个岛屿。

此外,这些数据集还包含每个物种的 culmen 维度。Culmen是鸟喙的上脊。在简化的企鹅数据中,culmen的长度和深度被重命名为变量culmen_length_mmculmen_depth_mm。加载此数据集是使用 Pandas 完成的:

data = pd.read_csv(./data/penguins_size.csv) data.head()

1. 插补

我们从客户那里获得的数据可以有各种形式。通常它是稀疏的,这意味着某些样本可能会丢失某些要素的数据。我们需要检测这些实例并删除这些样本或用某些内容替换空值。根据数据集的其余部分,我们可能会应用不同的策略来替换这些缺失值。例如,我们可以用平均特征值或最大特征值填充这些空槽。但是,让我们首先检测丢失的数据。为此,我们可以使用熊猫

print(data.isnull().sum())species 0 island 0 culmen_length_mm 2 culmen_depth_mm 2 flipper_length_mm 2 body_mass_g 2 sex 10

这意味着我们的数据集中存在某些要素中缺少值的实例。有两个实例缺少culmen_length_mm特征值,10 个实例缺少性别特征。即使在前几个样本中,我们也能够看到这一点(NaN表示不是数字,意味着缺失值):

处理缺失值最简单的方法是从数据集中删除缺少值的样本,实际上,某些机器学习平台会自动为您执行此操作。但是,由于数据集的减少,这可能会降低数据集的性能。最简单的方法是再次使用熊猫

data = pd.read_csv(./data/penguins_size.csv) data = data.dropna() data.head()

请注意,第三个缺少值的示例将从数据集中删除。这不是最佳的,但有时这是必要的,因为大多数机器学习算法不适用于稀疏数据。另一种方法是使用插补,这意味着替换缺失值。为此,我们可以选择一些值,或者使用特征的平均值,或者特征的平均值等。不过,我们需要小心。观察索引为 3 的行处的缺失值:

如果只是将其替换为简单值,我们将相同的值应用于分类和数值特征:

data = data.fillna(0)

这并不好。所以,这是正确的方法。我们在数值特征culmen_length_mm、culmen_depth_mmflipper_length_mm和body_mass_g中检测到缺失的数据。对于这些特征的插补值,我们将使用特征的平均值。对于分类特征"性别",我们使用最常见的值。我们是这样做的:

data = pd.read_csv(./data/penguins_size.csv) data[culmen_length_mm].fillna((data[culmen_length_mm].mean()), inplace=True) data[culmen_depth_mm].fillna((data[culmen_depth_mm].mean()), inplace=True) data[flipper_length_mm].fillna((data[flipper_length_mm].mean()), inplace=True) data[body_mass_g].fillna((data[body_mass_g].mean()), inplace=True) data[sex].fillna((data[sex].value_counts().index[0]), inplace=True) data.reset_index() data.head()

观察上面提到的第三个示例现在的样子:

通常,数据不会丢失,但它的值无效。例如,我们知道对于"性别"特征,我们可以有两个值:女性和男性。我们可以检查我们是否有除此以外的值:

data.loc[(data[sex] != FEMALE) & (data[sex] != MALE)]

事实证明,我们有一个记录具有此功能的值".",这是不正确的。我们可以将这些实例观察为缺失数据,并删除或替换它们:

data = data.drop([336]) data.reset_index()

2. 分类编码

改进预测的一种方法是在处理类别变量时应用聪明的方法。顾名思义,这些变量具有离散值,并表示某种类别或类。例如,颜色可以是分类变量("红色"、"蓝色"、"绿色")。

挑战在于将这些变量纳入数据分析,并将其与机器学习算法结合使用。一些机器学习算法支持分类变量,无需进一步操作,但有些则不支持。这就是我们使用分类编码的原因。在本教程中,我们将介绍几种类型的分类编码,但在继续之前,让我们将这些变量从数据集中提取到一个单独的变量中,并将它们标记为分类类型:

data["species"] = data["species"].astype(category) data["island"] = data["island"].astype(category) data["sex"] = data["sex"].astype(category) data.dtypesspecies category island category culmen_length_mm float64 culmen_depth_mm float64 flipper_length_mm float64 body_mass_g float64 sex categorycategorical_data = data.drop([culmen_length_mm, culmen_depth_mm, flipper_length_mm, \ body_mass_g], axis=1) categorical_data.head()

好了,现在我们准备好了。我们从最简单的编码形式标签编码开始。

2.1 标签编码

标签编码将每个分类值转换为某个数字。例如,"物种"特征包含 3 个类别。我们可以将值 0 赋给 Adelie,1 赋给 Gentoo,2 赋给 Chinstrap。要执行此技术,我们可以使用熊猫:

categorical_data["species_cat"] = categorical_data["species"].cat.codes categorical_data["island_cat"] = categorical_data["island"].cat.codes categorical_data["sex_cat"] = categorical_data["sex"].cat.codes categorical_data.head()

如您所见,我们添加了三个新功能,每个功能都包含编码的分类特征。从前五个实例中,我们可以看到物种类别Adelie用值0编码,岛屿类别Torgenesn用值2编码,性别类别FECE雄性分别用值0和1编码。

2.2 一热编码

这是最流行的分类编码技术之一。它将要素中的值分散到多个标志要素,并为其赋值 0 或 1。此二进制值表示非编码要素和编码要素之间的关系

例如,在我们的数据集中,我们在"性别"特征中有两个可能的值:女性男性。此技术将创建两个单独的功能,分别标记为"sex_female"和"sex_male"。如果在"性别"特征中,我们对某些样本的值为"FEMALE",则将为"sex_female"分配值 1,并为"sex_male"分配值 0。同样,如果在"性别"特征中,我们对某些样本的值为"MALE",则将为"sex_male"分配值1,并为"sex_female"分配值0。让我们将此技术应用于我们的分类数据,看看我们得到了什么:

encoded_spicies = pd.get_dummies(categorical_data[species]) encoded_island = pd.get_dummies(categorical_data[island]) encoded_sex = pd.get_dummies(categorical_data[sex]) categorical_data = categorical_data.join(encoded_spicies) categorical_data = categorical_data.join(encoded_island) categorical_data = categorical_data.join(encoded_sex)

正如你一样,我们在那里给出了一些新的专栏。从本质上讲,每个功能中的每个类别都有一个单独的列。通常,只有一个热编码值被用作机器学习算法的输入。

2.3 计数编码

计数编码是将每个分类值转换为其频率,即。它在数据集中出现的次数。例如,如果"物种"特征包含 6 次类 Adelie 的出现,我们将用数字 6 替换每个 Adelie 值。以下是我们在代码中执行此操作的方法:

categorical_data = data.drop([culmen_length_mm, culmen_depth_mm, \ flipper_length_mm, body_mass_g], axis=1) species_count = categorical_data[species].value_counts() island_count = categorical_data[island].value_counts() sex_count = categorical_data[sex].value_counts() categorical_data[species_count_enc] = categorical_data[species].map(species_count) categorical_data[island_count_enc] = categorical_data[island].map(island_count) categorical_data[sex_count_enc] = categorical_data[sex].map(sex_count) categorical_data

请注意每个类别值如何替换为出现次数。

2.4 目标编码

与以前的技术不同,这个技术有点复杂。它将分类值替换为该要素值的输出(即目标)的平均值。从本质上讲,您需要做的就是计算具有特定类别值的所有行的平均输出。现在,当输出值为数字时,这是非常直接的。如果输出是分类的,就像我们的PalmerPenguins数据集一样,我们需要应用一些以前的技术。

通常,此平均值与整个数据集的结果概率混合,以减少很少出现的值的方差。请务必注意,由于类别值是根据输出值计算的,因此这些计算应在训练数据集上完成,然后应用于其他数据集。否则,我们将面临信息泄漏,这意味着我们将在训练集内包含有关测试集输出值的信息。这将使我们的测试无效或给我们虚假的信心。好吧,让我们看看如何在代码中执行此操作:

categorical_data["species"] = categorical_data["species"].cat.codes island_means = categorical_data.groupby(island)[species].mean() sex_means = categorical_data.groupby(sex)[species].mean()

在这里,我们对输出特征使用标签编码,然后计算分类特征""和"性别"的平均值。以下是我们为"岛屿"功能获得的内容:

island_meansisland Biscoe 1.473054 Dream 0.548387 Torgersen 0.000000

这意味着值 BiscoeDreamTorgersen 将分别替换为值 1.473054、0.548387 和 0。对于"性别"功能,我们也有类似的情况:

sex_meanssex FEMALE 0.909091 MALE 0.921348

这意味着值 FEMALEMALE 将分别替换为 0.909091 和 0.921348。以下是数据集中的外观:

categorical_data[island_target_enc] = categorical_data[island].map(island_means) categorical_data[sex_target_enc] = categorical_data[sex].map(sex_means) categorical_data

2.5 省略一个目标编码

我们在本教程中探讨的最后一种编码类型是建立在目标编码之上的。它的工作方式与目标编码相同,但有一个区别。当我们计算样本的平均输出值时,我们排除了该样本。下面是在代码中完成它的方法。首先,我们定义一个执行以下操作的函数:

def leave_one_out_mean(series): series = (series.sum() - series)/(len(series) - 1) return series

然后,我们将其应用于数据集中的分类值:

categorical_data[island_loo_enc] = categorical_data.groupby(island)[species].apply(leave_one_out_mean) categorical_data[sex_loo_enc] = categorical_data.groupby(sex)[species].apply(leave_one_out_mean) categorical_data

3. 处理异常值

异常值是偏离数据整体分布的值。有时这些值是错误的和错误的测量值,应该从数据集中删除,但有时它们是有价值的边缘情况信息。这意味着有时我们希望将这些值保留在数据集中,因为它们可能携带一些重要信息,而其他时候我们希望删除这些样本,因为信息错误。

简而言之,我们可以使用四分位数间范围来检测这些点。四分位数间范围或 IQR 指示 50% 的数据所在的位置。当我们寻找这个值时,我们首先寻找中位数,因为它将数据分成两半。然后,我们定位数据下端的中位数(表示为Q1)和数据较高端的中位数(表示为Q3)。

Q1Q3 之间的数据是 IQR。异常值定义为低于 Q11.5(IQR) 或高于 Q3 + 1.5(IQR) 的样本。我们可以使用箱线图来执行此操作。箱线图的目的是可视化分布。从本质上讲,它包括重要点:最大值,最小值,中位数和两个IQR点(Q1,Q3)。以下是箱线图的一个示例:

让我们将其应用于PalmerPenguins数据集:

fig, axes = plt.subplots(nrows=4,ncols=1) fig.set_size_inches(10, 30) sb.boxplot(data=data,y="culmen_length_mm",x="species",orient="v",ax=axes[0], palette="Oranges") sb.boxplot(data=data,y="culmen_depth_mm",x="species",orient="v",ax=axes[1], palette="Oranges") sb.boxplot(data=data,y="flipper_length_mm",x="species",orient="v",ax=axes[2], palette="Oranges") sb.boxplot(data=data,y="body_mass_g",x="species",orient="v",ax=axes[3], palette="Oranges")

检测和删除异常值的另一种方法是使用标准差。

factor = 2 upper_lim = data[culmen_length_mm].mean () + data[culmen_length_mm].std () * factor lower_lim = data[culmen_length_mm].mean () - data[culmen_length_mm].std () * factor no_outliers = data[(data[culmen_length_mm] < upper_lim) & (data[culmen_length_mm] > lower_lim)] no_outliers

请注意,现在此操作后我们只剩下 100 个样本。在这里,我们需要定义乘以标准偏差的因子。通常,我们为此目的使用介于 2 和 4 之间的值。

最后,我们可以使用一种方法来检测异常值,即使用百分位数。我们可以假设顶部或底部的值的一定百分比为异常值。同样,我们用作异常值边界的百分位数的值取决于数据的分布。以下是我们可以在PalmerPenguins数据集上做的事情:

upper_lim = data[culmen_length_mm].quantile(.95) lower_lim = data[culmen_length_mm].quantile(.05) no_outliers = data[(data[culmen_length_mm] < upper_lim) & (data[culmen_length_mm] > lower_lim)] no_outliers

完成此操作后,数据集中有 305 个样本。使用这种方法,我们需要非常小心,因为它减小了数据集的大小,并且高度依赖于数据分布。

4. 分档

分箱是一种简单的技术,可将不同的值分组分箱中。例如,当我们想要对看起来像这样的数值特征进行分箱时:

0-10 – 低10-50 – 中等50-100 – 高

在这种特殊情况下,我们将数值特征替换为分类特征。

但是,我们也可以对分类值进行分箱。例如,我们可以按其所在的大陆对国家/地区进行分箱:

塞尔维亚 – 欧洲德国 – 欧洲日本 – 亚洲中国 – 亚洲美国 – 北美加拿大 – 北美

分箱的问题在于它可以降低性能,但它可以防止过度拟合并提高机器学习模型的鲁棒性。以下是代码中的外观:

bin_data = data[[culmen_length_mm]] bin_data[culmen_length_bin] = pd.cut(data[culmen_length_mm], bins=[0, 40, 50, 100], \ labels=["Low", "Mid", "High"]) bin_data

5. 缩放

在之前的文章中,我们经常有机会了解扩展如何帮助机器学习模型做出更好的预测。缩放的完成原因很简单,如果特征不在同一范围内,机器学习算法将以不同的方式对待它们。用蹩脚的话来说,如果我们有一个特征的值范围从0-10到另一个0-100,机器学习算法可能会推断出第二个特征比第一个特征更重要,因为它具有更高的值。

我们已经知道,情况并非总是如此。另一方面,期望真实数据处于同一范围内是不现实的。这就是为什么我们使用缩放,将我们的数值特征放入相同的范围。这种数据标准化是许多机器学习算法的共同要求。其中一些甚至要求特征看起来像标准的正态分布数据。有几种方法可以扩展和标准化数据,但在我们通过它们之前,让我们观察一下PalmerPenguins数据集"body_mass_g"的一个特征。

scaled_data = data[[body_mass_g]] print(Mean:, scaled_data[body_mass_g].mean()) print(Standard Deviation:, scaled_data[body_mass_g].std())Mean: 4199.791570763644 Standard Deviation: 799.9508688401579

此外,请观察此功能的分布:

首先,让我们探讨一下保留分发的缩放技术。

5.1 标准缩放

这种类型的缩放会删除均值,并将数据缩放到单位方差。它由以下公式定义:

其中均值是训练样本的均值,std 是训练样本的标准差。理解它的最好方法是在实践中看待它。为此,我们使用SciKit LearnStandardScaler类:

standard_scaler = StandardScaler() scaled_data[body_mass_scaled] = standard_scaler.fit_transform(scaled_data[[body_mass_g]]) print(Mean:, scaled_data[body_mass_scaled].mean()) print(Standard Deviation:, scaled_data[body_mass_scaled].std())Mean: -1.6313481178165566e-16 Standard Deviation: 1.0014609211587777

我们可以看到,数据的原始分布被保留了下来。但是,现在数据在 -3 到 3 的范围内。

5.2 最小-最大缩放(规范化)

最流行的缩放技术是规范化(也称为最小-最大规范化最小-最大缩放)。它将 0 到 1 范围内的所有数据缩放。此技术由以下公式定义:

如果我们使用SciKit learn库中的MinMaxScaler:

minmax_scaler = MinMaxScaler() scaled_data[body_mass_min_max_scaled] = minmax_scaler.fit_transform(scaled_data[[body_mass_g]]) print(Mean:, scaled_data[body_mass_min_max_scaled].mean()) print(Standard Deviation:, scaled_data[body_mass_min_max_scaled].std())Mean: 0.4166087696565679 Standard Deviation: 0.2222085746778217

分布是保留的,但数据现在的范围为 0 到 1。

5.3 分位数变换

正如我们所提到的,有时机器学习算法要求我们的数据分布是均匀正态的。我们可以使用SciKit Learn的QuantileTransformer类来实现这一点。首先,当我们将数据转换为均匀分布时,如下所示:

qtrans = QuantileTransformer() scaled_data[body_mass_q_trans_uniform] = qtrans.fit_transform(scaled_data[[body_mass_g]]) print(Mean:, scaled_data[body_mass_q_trans_uniform].mean()) print(Standard Deviation:, scaled_data[body_mass_q_trans_uniform].std())Mean: 0.5002855778903038 Standard Deviation: 0.2899458384920982

下面是将数据放入正态分布的代码:

qtrans = QuantileTransformer(output_distribution=normal, random_state=0) scaled_data[body_mass_q_trans_normal] = qtrans.fit_transform(scaled_data[[body_mass_g]]) print(Mean:, scaled_data[body_mass_q_trans_normal].mean()) print(Standard Deviation:, scaled_data[body_mass_q_trans_normal].std())Mean: 0.0011584329410665568 Standard Deviation: 1.0603614567765762

从本质上讲,我们在构造函数中使用output_distribution参数来定义分布的类型。最后,我们可以观察所有特征的缩放值,具有不同类型的缩放:

6. 日志转换

最流行的数据数学变换之一是对数变换。从本质上讲,我们只需将 log 函数应用于当前值。重要的是要注意,数据必须是正数,因此,如果您需要缩放或事先规范化数据。这种转变带来了许多好处。其中之一是数据的分布变得更加正常。反过来,这有助于我们处理偏斜的数据,并减少异常值的影响。以下是代码中的外观:

log_data = data[[body_mass_g]] log_data[body_mass_log] = (data[body_mass_g] + 1).transform(np.log) log_data

如果我们检查未转换数据和转换数据的分布,我们可以看到转换后的数据更接近正态分布:

7. 功能选择

来自客户端的数据集通常很大。我们可以有数百甚至数千个功能。特别是如果我们从上面执行一些技术。大量特征可能导致过度拟合。除此之外,优化超参数和训练算法通常需要更长的时间。这就是为什么我们要从一开始就选择最相关的功能。

在功能选择方面有几种技术,但是,在本教程中,我们仅介绍最简单的一种(也是最常用的)技术 - 单变量功能选择。此方法基于单变量统计检验。它使用统计检验(如 χ2)计算输出特征对数据集中每个特征的依赖程度。在这个例子中,我们利用SelectKBest,当涉及到使用的统计测试时,它有几个选项(但是默认值是χ2,我们在这个例子中使用了那个)。以下是我们是如何做到的:

feature_sel_data = data.drop([species], axis=1) feature_sel_data["island"] = feature_sel_data["island"].cat.codes feature_sel_data["sex"] = feature_sel_data["sex"].cat.codes # Use 3 features selector = SelectKBest(f_classif, k=3) selected_data = selector.fit_transform(feature_sel_data, data[species]) selected_dataarray([[ 39.1, 18.7, 181. ], [ 39.5, 17.4, 186. ], [ 40.3, 18. , 195. ], ..., [ 50.4, 15.7, 222. ], [ 45.2, 14.8, 212. ], [ 49.9, 16.1, 213. ]])

使用超参数 k,我们定义了要从数据集中保留 3 个最有影响力的特征。此操作的输出是包含所选功能的 NumPy 数组。要使其成为pandas Dataframe,我们需要执行以下操作:

selected_features = pd.DataFrame(selector.inverse_transform(selected_data), index=data.index, columns=feature_sel_data.columns) selected_columns = selected_features.columns[selected_features.var() != 0] selected_features[selected_columns].head()

8. 功能分组

到目前为止,我们观察到的数据集在所谓的"整洁"方面几乎是一个完美的情况。这意味着每个要素都有自己的列,每个观测值都是一行,每种类型的观测单位都是一个表。

但是,有时我们的观测值分布在几行中。功能分组的目标是将这些行连接成一个行,然后使用这些聚合行。这样做的主要问题是哪种类型的聚合函数将应用于特征。这对于分类特征尤其复杂。

正如我们所提到的,PalmerPenguins数据集非常典型,因此以下示例只是为了展示可用于此操作的代码而具有教育意义:

grouped_data = data.groupby(species) sums_data = grouped_data[culmen_length_mm, culmen_depth_mm].sum().add_suffix(_sum) avgs_data = grouped_data[culmen_length_mm, culmen_depth_mm].mean().add_suffix(_mean) sumed_averaged = pd.concat([sums_data, avgs_data], axis=1) sumed_averaged

在这里,我们按 spicies 值对数据进行分组,并为每个数值创建了两个具有 sum 和平均值的新特征。

9. 功能拆分

有时,数据不是通过行连接,而是通过 colum 连接。例如,假设您在其中一个功能中具有名称列表:

data.names0 Andjela Zivkovic 1 Vanja Zivkovic 2 Petar Zivkovic 3 Veljko Zivkovic 4 Nikola Zivkovic

因此,如果我们只想从此功能中提取名字,我们可以执行以下操作:

data.names0 Andjela 1 Vanja 2 Petar 3 Veljko 4 Nikola

此技术称为特征拆分,通常与字符串数据一起使用。

从这里到哪里去?

通常,机器学习算法是一些更大应用程序的一部分。通常,我们需要为应用程序的其他部分使用功能。这只是将机器学习应用程序投入生产特别具有挑战性的众多原因之一。缓解此问题的一种方法是使用所谓的功能商店。这是数据架构中一个相对较新的概念,但它已经被Uber和Gojek等公司应用。

要素存储既是计算服务又是存储服务。从本质上讲,它们公开了功能,因此可以发现它们并将其用作机器学习管道和联机应用程序的一部分。由于它们需要同时提供存储大量数据和提供低计算延迟这两者,因此功能存储作为双数据库系统实现。一端是低延迟键值存储或实时数据库,另一端是可以存储大量数据的SQL数据库。这是一个有趣的概念,可以进一步探讨。

结论

在本文中,我们有机会探讨了特征工程最常用的9种技术。

原文标题:Top 9 Feature Engineering Techniques with Python

原文链接:

https://rubikscode.net/2021/06/29/top-9-feature-engineering-techniques/

作者:Machine Learning

编译:LCR