Python的9个特征工程技术

2020-11-20 10:52:56 浏览数 (1)

可以在此处找到本文随附的代码。

https://github.com/NMZivkovic/top_9_feature_engineering_techniques

在本文中,探索了获得良好结果通常需要的最有效的要素工程技术。

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

  • 归因
  • 分类编码
  • 处理异常值
  • 装箱
  • 缩放比例
  • 日志转换
  • 功能选择
  • 功能分组
  • 功能拆分

数据集和先决条件

就本教程而言,请确保已安装以下Python 库:

  • NumPy
  • SciKit Learn
  • Pandas
  • Matplotlib
  • SeaBorn

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

代码语言:javascript复制
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数据集。最近引入了此数据集,以替代著名的Iris数据集。它是由克里斯汀·高曼(Kristen Gorman)博士和南极洲帕尔默车站(Palmer Station)创建的。可以在此处或通过Kaggle获取此数据集。该数据集实质上由两个数据集组成,每个数据集包含344个企鹅的数据。就像在鸢尾花数据集中一样,帕尔默群岛的3个岛屿中有3种不同的企鹅。

https://github.com/allisonhorst/palmerpenguins

同样,这些数据集包含每个物种的标本维度。高门是鸟嘴的上脊。在简化的企鹅数据中,顶点长度和深度被重命名为culmen_length_mm和culmen_depth_mm变量。使用Pandas加载此数据集:

代码语言:javascript复制
data = pd.read_csv('./data/penguins_size.csv')
data.head()

1.归因

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

代码语言:javascript复制
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表示不是数字,表示缺少值):

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

代码语言:javascript复制
data = pd.read_csv('./data/penguins_size.csv')
data = data.dropna()
data.head()

请注意,缺少值的第三个样本已从数据集中删除。这不是最佳选择,但有时是必要的,因为大多数机器学习算法不适用于稀疏数据。另一种方法是使用插补,即替换缺失值。要做到这一点,可以挑选一些值,或使用平均的特征值,或平均的特征值等。还有必须要小心。在索引3的行中观察缺失值:

如果仅将其替换为简单值,则对于分类和数值特征,将应用相同的值:

代码语言:javascript复制
data = data.fillna(0)

在数字特征culmen_length_mm,culmen_depth_mm,flipper_length_mm和body_mass_g中检测到丢失的数据。对于这些特征的估算值,将使用特征的平均值。对于“性”这一分类特征,使用最频繁的值。这是方法:

代码语言:javascript复制
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()

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

通常数据不会丢失,但是其值无效。例如知道对于“性别”功能,可以有两个值:FEMALE和MALE。可以检查是否还有其他值:

代码语言:javascript复制
data.loc[(data['sex'] != 'FEMALE') & (data['sex'] != 'MALE')]

事实证明,有一条记录的值为“.”。对于此功能,这是不正确的。可以将这些实例视为丢失的数据,并丢弃或替换它们:

代码语言:javascript复制
data = data.drop([336])
data.reset_index()

2.分类编码

一种改进预测的方法是在处理分类变量时采用巧妙的方法。顾名思义这些变量具有离散值,代表某种类别或类别。例如,颜色可以是分类变量(“红色”,“蓝色”,“绿色”)。挑战在于将这些变量包括在数据分析中,并将其与机器学习算法一起使用。有些机器学习算法无需进一步操作即可支持分类变量,但有些则不能。这就是为什么我们使用分类编码。在本教程中,介绍了几种类型的分类编码,但是在继续之前,提取一下将数据集中的这些变量转换为单独的变量,并将其标记为分类类型:

代码语言:javascript复制
data["species"] = data["species"].astype('category')
data["island"] = data["island"].astype('category')
data["sex"] = data["sex"].astype('category')
data.dtypes

species category

island category

culmen_length_mm float64

culmen_depth_mm float64

flipper_length_mm float64

body_mass_g float64

sex category

代码语言:javascript复制
categorical_data = data.drop(['culmen_length_mm', 'culmen_depth_mm', 'flipper_length_mm', 
      'body_mass_g'], axis=1)
categorical_data.head()

好的,现在可以开始了。从最简单的编码标签编码开始。

2.1标签编码

标签编码将每个分类值转换为一些数字。例如“species”功能包含3个类别。可以将值0分配给Adelie,将1分配给Gentoo,将2分配给Chinstrap。要执行此技术,我们可以使用Pandas:

代码语言:javascript复制
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()

如您所见,添加了三个新功能,每个功能都包含编码的分类功能。从前五个实例中,可以看到species类别 Adelie的编码值为 0, island类别 Torgensesn 的编码值为 2 , sex类别 FEMALE 和 MALE 的编码分别为0和1。

2.2一键编码

这是最流行的分类编码技术之一。它将一个要素中的值传播到多个标志要素,并为其分配值0或1。该二进制值表示未编码和编码特征之间的关系。

例如在数据集中,“sex”功能中有两个可能的值:FEMALE和MALE。该技术将创建两个单独的功能,分别标记为“ sex_female ”和“ sex_male ”。如果在“sex”的特征,有值“女为一些样本”中,“ sex_female ”将被分配值1和“ sex_male ”将被分配值0。同样,如果在“sex”的特征,有对于某些样本,值“ MALE ”,将为“ sex_male ”分配值1和“ sex_female”'将被赋值为0。将这种技术应用于分类数据,看看会得到什么:

代码语言:javascript复制
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计数编码

计数编码是将每个分类值转换为其频率,即它出现在数据集中的次数。例如,如果“species”功能包含6次出现的Adelie类,将用数字6替换每个Adelie值。这是在代码中的操作方法:

代码语言:javascript复制
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数据集中,则需要对其应用某些先前的技术。

通常,将这个平均值与整个数据集中的结果概率混合在一起,以减少出现次数很少的值的方差。重要的是要注意,由于类别值是基于输出值计算的,因此这些计算应在训练数据集上进行,然后应用于其他数据集。否则将面临信息泄漏,这意味着将在训练集中包含有关测试集输出值的信息。这会使测试无效或给虚假的信心。好的看看如何在代码中做到这一点:

代码语言:javascript复制
categorical_data["species"] = categorical_data["species"].cat.codes
 
island_means = categorical_data.groupby('island')['species'].mean()
sex_means = categorical_data.groupby('sex')['species'].mean()

在这里,将标签编码用于输出特征,然后为分类特征“岛”和“性别”计算平均值。这就是对“孤岛”功能的了解:

代码语言:javascript复制
island_means

island

Biscoe 1.473054

Dream 0.548387

Torgersen 0.000000

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

代码语言:javascript复制
sex_means

sex

FEMALE 0.909091

MALE 0.921348

这意味着值FEMALE和MALE将分别替换为0.909091和0.921348。这是数据集中的样子:

代码语言:javascript复制
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保留目标编码

在本教程中探讨的最终编码类型是基于目标编码的。它的工作方式与目标编码相同,只是有所不同。当计算样本的平均输出值时,排除该样本。这是在代码中完成的方式。首先定义一个执行此操作的函数:

代码语言:javascript复制
def leave_one_out_mean(series):
series = (series.sum() - series)/(len(series) - 1)
return series

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

代码语言:javascript复制
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)。Q1和Q3之间的数据是IQR。离群值定义为低于Q1 – 1.5(IQR)或高于Q3 1.5(IQR)的样本。可以使用箱线图。箱线图的目的是可视化分布。本质上,它包括重要点:最大值,最小值,中位数和两个IQR点(Q1,Q3)。以下是箱线图的一个示例:

将其应用于PalmerPenguins数据集:

代码语言:javascript复制
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")

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

代码语言:javascript复制
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数据集执行的操作:

代码语言:javascript复制
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.分箱

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

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

在这种情况下,将数字特征替换为分类特征。

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

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

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

代码语言:javascript复制
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,则机器学习算法可能会推断出第二个特征比第一个特征更重要,因为它具有一个更高的价值。我们已经知道并非总是如此。另一方面,期望真实数据在相同范围内是不现实的。这就是为什么我们使用scale来将数值特征置于相同范围内的原因。这种标准化的数据是很多机器学习算法的共同要求。其中一些甚至要求功能看起来像标准的正态分布数据。我们可以通过多种方式缩放和标准化数据,但是在研究它们之前,让观察一下PalmerPenguins数据集“ body_mass_g ”的一项功能。

代码语言:javascript复制
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 Learn和StandardScaler类:

代码语言:javascript复制
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学习库中的MinMaxScaler:

代码语言:javascript复制
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类来实现这一点。首先,这是将数据转换为均匀分布时的样子:

代码语言:javascript复制
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

这是将数据置于正态分布的代码:

代码语言:javascript复制
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函数应用于当前值。重要的是要注意,数据必须为正数,因此,如果需要预先缩放或标准化数据。这种转变带来许多好处。其中之一是数据的分布变得更加正常。反过来,这有助于处理偏斜的数据并减少异常值的影响。这是代码中的样子:

代码语言:javascript复制
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,在本示例中使用该选项)。这是可以做到的:

代码语言:javascript复制
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_data
array([[ 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中,需要执行以下操作:

代码语言:javascript复制
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数据集非常典型,因此下面的示例仅用于说明可用于此操作的代码:

代码语言:javascript复制
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

在这里,将数据按spices值分组,并为每个数值创建了两个具有和和平均值的新特征。

9.功能拆分

有时数据不是跨行连接,而是跨列连接。例如假设在功能之一中具有名称列表:

代码语言:javascript复制
data.names

0 Andjela Zivkovic

1 Vanja Zivkovic

2 Petar Zivkovic

3 Veljko Zivkovic

4 Nikola Zivkovic

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

代码语言:javascript复制
data.names

0 Andjela

1 Vanja

2 Petar

3 Veljko

4 Nikola

这种技术称为特征分割,通常用于字符串数据。

结论

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

0 人点赞