可以在此处找到本文随附的代码。
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种最常用的特征工程技术。