拖延症又犯了,最后一天才写博客,。,不想给自己找啥理由了000。
特征工程介绍
我理解的特征工程是一种更深层的数据分析,为特定的数据做处理,深挖其中的信息,以便后续模型得到更好的效果。
本文是参考天池二手车预测比赛的教程——特征工程做的笔记,task3 中提到了很多特征处理的方法,主要介绍了其中的五种方法:
- 异常数据处理
- 特征构造
- 数据分桶
- 特征归一化
- 特征筛选
首先,导入需要的包,并载入数据:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from operator import itemgetterpath = './data/'
Train_data = pd.read_csv(path+'used_car_train_20200313.csv', sep=' ')
Test_data = pd.read_csv(path+'used_car_testA_20200313.csv', sep=' ')
接下来就是各种特征处理的方法了。
异常数据处理
这部分教程中使用的是箱线图来处理,箱线图的理解可以参考 箱线图的理解
总的来讲,就是根据四分位线,设置一个上边界线和下边界线。上下边界线分别是由上四分位数和下四分位数加减“箱子”的高度得到的。这里的箱子高度是指特定倍数的四分位数差,教程里设置的是三倍四分位数差。
数值超出上下边界线的数据被划分为异常数据。对于异常数据,我们可以选择截断或是直接删除的方式来处理,教程中采用的是后者。
对 power 特征进行数据处理,由于其中得异常值共九百多个,数量较多,因此,截断的方式更好。我对代码做了点更改,改成了截断的方式。代码如下(默认已经导入需要的包):
def outliers_proc(data, col_name, scale=3):"""用于清洗异常值,默认用 box_plot(scale=3)进行清洗:param data: 接收 pandas.DataFrame 数据格式:param col_name: pandas 列名(单列):param scale: 尺度:return: 返回清洗后的数据"""def box_plot_outliers(data_ser, box_scale):"""利用箱线图找出异常值:param data_ser: 接收 pandas.Series 数据格式:param box_scale: 箱线图尺度,:return:"""iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25)) # 计算箱子的高度val_low = data_ser.quantile(0.25) - iqr # 计算下边界val_up = data_ser.quantile(0.75) + iqr # 计算上边界rule_low = data_ser < val_low # type = <class 'pandas.core.series.Series'>rule_up = data_ser > val_up # 是一个布尔类型的一维数组return (rule_low, rule_up), (val_low, val_up)data_n = data.copy()data_series = data_n[col_name] # 取出该特征的所有数据rule, value = box_plot_outliers(data_series, box_scale=scale) # 找出异常值index = np.arange(data_series.shape[0])[rule[0] | rule[1]] # 取出所有为 True 的元素下标,即异常值的元素下标(第一次遇到这种操作,神奇)print(f"abnormal data number is: {len(index)}")# data_n = data_n.drop(index) # 这里不做删除操作,做截断操作# data_n.reset_index(drop=True, inplace=True) # 重置索引,删除异常值时需要将索引重置,drop=True 表示不保留以前的索引for i in range(2):row = np.arange(data_series.shape[0])[rule[i]] # 取出异常值的下标data_n.loc[row, [col_name]] = value[i]# print(data_n.loc[row, [col_name]], value[i])print(f"change {'low' if i == 0 else 'high'} data number is : {len(row)}")##########---------- 到这里数据就已经截断完了,下面是数据的展示 ----------##########print(f"Now column number is: {data_n.shape[0]}")index_low = np.arange(data_series.shape[0])[rule[0]]outliers = data_series.iloc[index_low]print("Description of data less than the lower bound is:")print(pd.Series(outliers).describe())index_up = np.arange(data_series.shape[0])[rule[1]]outliers = data_series.iloc[index_up]print("Description of data larger than the upper bound is:")print(pd.Series(outliers).describe())fig, ax = plt.subplots(1, 2, figsize=(10, 7))sns.boxplot(y=data[col_name], data=data, palette="Set1", ax=ax[0])sns.boxplot(y=data_n[col_name], data=data_n, palette="Set1", ax=ax[1])plt.show()return data_nTrain_data = outliers_proc(Train_data, 'power', scale=3)
运行结果如下:
abnormal data number is: 963
change low data number is : 0
change high data number is : 963
Now column number is: 150000
Description of data less than the lower bound is:
count 0.0
mean NaN
std NaN
min NaN
25% NaN
50% NaN
75% NaN
max NaN
Name: power, dtype: float64
Description of data larger than the upper bound is:
count 963.000000
mean 846.836968
std 1929.418081
min 376.000000
25% 400.000000
50% 436.000000
75% 514.000000
max 19312.000000
Name: power, dtype: float64
可以看到,一共 963 个异常数据,而且异常类型全都是超出上边界线。
特征构造
教程里在特征构造方面,主要是新构造了一些特征,构造一些认为对模型有帮助的特征。
第一个新特征是使用时间:data[‘creatDate’] - data[‘regDate’],反应汽车使用时间,一般来说价格与使用时间成反比。这里的 creatDate
是广告发布时间;regDate
是汽车注册时间。二者相减,就是汽车的使用时间。由于时间数据有的格式解析有问题,在前天晚上的直播中,作者也解释了这个问题,因此,会有部分非法数据。一共有 15101 个非法数据,占总样本量过大,7.5%,教程里不建议删除。
第二个新特征是城市信息,作者根据邮编信息,提取出邮编内的城市信息。(直播中,作者解释了为什么知道这是德国的邮编格式)
最后就是计算某品牌的销售统计量,教程作者根据训练集中的数据信息,进行了一系列的统计计算。
特征如果使用完了,认为没有什么更多的信息了,就可以将其删除,降低一下维度。
数据分桶
数据分桶可以简单地理解成将数据离散化,其优点可以总结成如下三点:
- 稀疏向量,计算速度更快。
- 离散后的特征更稳定,鲁棒性更强,增强了对异常数据的抗干扰性。
- 离散化可以引入非线性,类似于神经网的激活函数。
教程中对 power
特征进行了分桶操作:
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
print(data[['power_bin', 'power']].head())
运行结果:
power_bin power
0 5.0 60
1 NaN 0
2 16.0 163
3 19.0 193
4 6.0 68
特征归一化
特征归一化还是对刚才的 power
特征做的操作,可以先不对其进行异常值截断,先查看其特征分布:
Train_data['power'].plot.hist()
plt.show()
运行结果:
可以看到,特征很符合长尾分布的特点。长尾分布就是尾巴很长的分布,这种分布的尾巴处数据很少,如果数据出现不准的情况,会造成较大影响,毕竟尾巴处可参考的数据很少。
可以先对特征做 log
操作,然后再做归一化操作。
from sklearn import preprocessing
min_max_scaler = preprocessing.MinMaxScaler()
data['power'] = np.log(data['power'] + 1)
data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))
data['power'].plot.hist()
plt.show()
注意这里的 log
中 +1
的操作,目的是使计算后的数据值都大于 0。运行结果如下:
特征筛选
特征筛选有三种方法:(1)过滤式(2)包裹式(3)嵌入式
详细介绍请参考:特征选择/筛选方法总结
过滤式(Filter)
过滤式的思想就是给每个特征“打分”,相当于给特征赋予权值,权值越大,特征越重要。主要的“打分”方法有三种:
- 信息增益
- 卡方检验
- 相关系数
教程中使用的是相关系数进行打分,计算每个特征与 price
的相关系数。由于负相关也是一种强相关,因此打分时,需要将相关系数取绝对值。
教程中画了相关系数的热力图,可以很直观地感受到各特征之间的相关性强弱。
包裹式(Wrapper)
包裹式的主要思想是选择不同的特征子集,对不同的特征子集组合进行打分。将特征选择的问题转换成优化问题,寻找分值较高的特征子集。使用要使用的分类器作为一个评价函数,对特征子集进行打分。
教程里使用的 mlxtend.feature_selection
下的 SequentialFeatureSelector
方法进行特征选择,其中评价函数使用 LinearRegression
。
嵌入式(Embedded)
在模型既定的情况下学习出对提高模型准确性最好的特征。也就是在确定模型的过程中,挑选出那些对模型的训练有重要意义的特征。
这个教程中没有给出案例,以后会有,留待下次记录。
最后
整个教程让人对特征有了一个更深的认识,通过教程,直接和间接地了解到了很多知识,很赞。有点遗憾的是,最近没有花太多时间在这上面,仅仅只是达到了一个入门的状态。不过每天都在进步的感觉还是很有成就感的。