Task3-特征工程
1. 特征工程概述
1.1 相关定义及理解
(1)特征工程:最大限度从原始数据中提取特征以供算法和模型使用。呈现给算法的数据需拥有基本数据的相关结构/属性,特征工程即:将数据属性转化为数据特征,经过特征工程的预处理,能够减少噪声对算法模型的干扰。数据和特征决定机器学习的上限,而模型和算法只是逼近这个上限。
(2)特征工程是能够将数据像艺术一样展现的技术,好的特征工程很好的混合了专业领域知识、直觉和基本的数学能力。
(3)对于特征工程中引用的新特征,需要验证它的确能提高预测的准确度,而不是加入了一个无用特征,不然只会增加算法运算的复杂度。
(4)举例来讲:现在出一非常简答的二分类问题题,请你使用逻辑回归,设计一个身材分类器。输入数据X:身高和体重 ,标签为Y:身材等级(胖,不胖)。显然,不能单纯的根据体重来判断一个人胖不胖,姚明很重,他胖吗?显然不是。针对这个问题,一个非常经典的特征工程是,BMI指数,BMI=体重/(身高^2)。这样,通过BMI指数,就能非常显然地帮助我们,刻画一个人身材如何。甚至,你可以抛弃原始的体重和身高数据。
(5)被认为是一种强大的自动化特征工程工具的deep learning,能够自动学习各种低级高级的特征,我们就不需要特征工程了吗?未必如此。参考链接【3】中作者以声纹识别为例说明,前端依旧需要包含专家知识的特征工程。参考链接【4】中作者认为神经网络并非万能,依旧依赖一个基础的特征库,好的特征能够有效减少模型复杂度。
(6)总结:筛选特征(降噪)→训练模型,对提高模型精度是有意义的。
1.2 内容
摘录参考链接【1】,并结合Datewhale本期教程内容,整理如下:
2. 代码示例
2.1 导入数据
path = './datalab/231784/'
Train_data = pd.read_csv(path+'used_car_train_20200313.csv', sep=' ')
Test_data = pd.read_csv(path+'used_car_testA_20200313.csv', sep=' ')
print(Train_data.shape)
print(Test_data.shape)
2.2 删除异常值(本文利用箱线图剔除异常值)
2.2.1 箱线图
- 粉红色箱体,中间黄线是中位数;左侧线是下四分位数(Q1);右侧线是上四分位数(Q3)
- 四分位距 I Q R = Q 3 − Q 1 IQR=Q3-Q1 IQR=Q3−Q1
- 用 Q 1 − 1.5 ∗ I Q R Q1-1.5*IQR Q1−1.5∗IQR得到最小值(下边缘);用 Q 3 + 1.5 ∗ I Q R Q3+1.5*IQR Q3+1.5∗IQR得到最大值(上边缘)
- 在上下边缘之外的值即为异常值
2.2.2 删除
# 2.2 删除异常值
def outliers_proc(data, col_name, scale=3):
"""
用于清洗异常值,默认用 box_plot(scale=3) 清洗
:param data: 接收 pandas 数据格式
: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)
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) # (rule_low, rule_up), (val_low, val_up)
index = np.arange(data_series.shape[0])[rule[0] | rule[1]] # 返回上下边界外数据的索引号
print("Delete number is: {}".format(len(index))) # 输出删除条目数
data_n = data_n.drop(index) # 删除数据
data_n.reset_index(drop=True, inplace=True)
print("Now column number is: {}".format(data_n.shape[0])) # 输出删除后的条目数
# 输出超出下边界异常值的数据特征
index_low = np.arange(data_series.shape[0])[rule[0]] # 超出下边界的异常值索引
outliers = data_series.iloc[index_low]
print(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])
return data_n
Train_data = outliers_proc(Train_data, 'power', scale=3)
2.3 用于决策树的特征构造
2.3.1 数据集拼接
# 训练集和测试集放在一起,方便构造特征
Train_data['train']=1
Test_data['train']=0
data = pd.concat([Train_data, Test_data], ignore_index=True) # 忽略索引,先拼接,再重构索引
2.3.2 构造特征
# (1)使用时间=data['creatDate'] - data['regDate'],反应汽车使用时间,一般与价格成反比
# 注:数据中有时间出错的格式,所以需要errors='coerce'。根据data['used_time'].isnull().sum()显示,共有15k样本为空,我们 XGBoost 之类的决策树,其本身就能处理缺失值,所以先可以不用管
data['used_time'] = (pd.to_datetime(data['creatDate'], format='%Y%m%d', errors='coerce') -\
pd.to_datetime(data['regDate'], format='%Y%m%d', errors='coerce')).dt.days
# (2)从邮编中提取城市信息
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
data = data
# (3)计算train中的某品牌的销售统计量
Train_gb = Train_data.groupby("brand")
all_info = {}
for kind, kind_data in Train_gb:
info = {}
kind_data = kind_data[kind_data['price'] > 0]
info['brand_amount'] = len(kind_data)
info['brand_price_max'] = kind_data.price.max()
info['brand_price_median'] = kind_data.price.median()
info['brand_price_min'] = kind_data.price.min()
info['brand_price_sum'] = kind_data.price.sum()
info['brand_price_std'] = kind_data.price.std()
info['brand_price_average'] = round(kind_data.price.sum() / (len(kind_data) + 1), 2)
all_info[kind] = info
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
data = data.merge(brand_fe, how='left', on='brand')
2.3.3 数据分桶
为什么要数据分桶:
- 离散后稀疏向量内积乘法运算速度更快,计算结果易于储存,容易扩展
- 对特征值更具有鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰
- LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合
- 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力
- 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
- LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性
pd.cut()
pd.cut(x, bins, right=True, labels=None, retbins=False, precision=3, include_lowest=False, duplicates=‘raise’)
参数 | 含义 |
---|---|
x | 被切分的类数据 |
bins | (1)int,表示被平均分成几份;(2)标量序列,定义被分割后的每个区间边缘;(3)pandas.IntervalIndex,定义要使用的精确区间 |
right | True,包含区间右部;反之不包含 |
labels | 给分割后的bins打标签 |
retbins | bool型的参数,表示是否将分割后的bins返回,当bins为一个int型的标量时比较有用,这样可以得到划分后的区间,默认为False |
precision | 保留区间小数点的位数,默认为3 |
include_lowest | bool型的参数,表示区间的左边是开还是闭的,默认为false,也就是不包含区间左部 |
duplicates | 是否允许重复区间。raise,不允许;drop,允许 |
2.3.4 删除无用数据
data = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
data.to_csv('data_for_tree.csv', index=0)
2.4 (扩展)用于LR的特征构造
不同模型对数据集的要求不同,因此需要单独构造一份特征
2.4.1 power中的异常值处理
在2.3一节中我们利用箱线图对power异常值处理完毕,但是数据可视化后能够发现,由于未对test异常值处理,使得数据分布依旧异常。在这里我们不用于2.3的处理,保留全部异常值,之后用长尾分布截断解决该问题。
data['power'].plot.hist()
Train_data['power'].plot.hist()
在本节中,我们对其取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()
2.4.2 归一化处理
对km和刚刚构造的统计量特征做归一化
data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /
(np.max(data['kilometer']) - np.min(data['kilometer'])))
data['kilometer'].plot.hist()
def max_min(x):
return (x - np.min(x)) / (np.max(x) - np.min(x))
data['brand_amount'] = ((data['brand_amount'] - np.min(data['brand_amount'])) /
(np.max(data['brand_amount']) - np.min(data['brand_amount'])))
data['brand_price_average'] = ((data['brand_price_average'] - np.min(data['brand_price_average'])) /
(np.max(data['brand_price_average']) - np.min(data['brand_price_average'])))
data['brand_price_max'] = ((data['brand_price_max'] - np.min(data['brand_price_max'])) /
(np.max(data['brand_price_max']) - np.min(data['brand_price_max'])))
data['brand_price_median'] = ((data['brand_price_median'] - np.min(data['brand_price_median'])) /
(np.max(data['brand_price_median']) - np.min(data['brand_price_median'])))
data['brand_price_min'] = ((data['brand_price_min'] - np.min(data['brand_price_min'])) /
(np.max(data['brand_price_min']) - np.min(data['brand_price_min'])))
data['brand_price_std'] = ((data['brand_price_std'] - np.min(data['brand_price_std'])) /
(np.max(data['brand_price_std']) - np.min(data['brand_price_std'])))
data['brand_price_sum'] = ((data['brand_price_sum'] - np.min(data['brand_price_sum'])) /
(np.max(data['brand_price_sum']) - np.min(data['brand_price_sum'])))
2.4.3 对类别特征进行OneEncoder
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',\
'gearbox', 'notRepairedDamage', 'power_bin'])
data.to_csv('data_for_lr.csv', index=0)
2.5 特征筛选
2.5.1 过滤式
data_numeric = data[['power', 'kilometer', 'brand_amount', 'brand_price_average',
'brand_price_max', 'brand_price_median']]
correlation = data_numeric.corr()
f , ax = plt.subplots(figsize = (7, 7))
plt.title('Correlation of Numeric Features with Price',y=1,size=16)
sns.heatmap(correlation,square = True, vmax=0.8)
2.5.2 包裹式
# k_feature 太大会很难跑,没服务器,所以提前 interrupt 了
from mlxtend.feature_selection import SequentialFeatureSelector as SFS
from sklearn.linear_model import LinearRegression
sfs = SFS(LinearRegression(),
k_features=10,
forward=True,
floating=False,
scoring = 'r2',
cv = 0)
x = data.drop(['price'], axis=1)
x = x.fillna(0)
y = data['price']
sfs.fit(x, y)
sfs.k_feature_names_
from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs
import matplotlib.pyplot as plt
fig1 = plot_sfs(sfs.get_metric_dict(), kind='std_dev')
plt.grid()
plt.show()
2.5.3 嵌入式
(下一章介绍)
3. 收获及未来工作
- 不能因为用深度学习的方法,就完全不考虑特征工程的工作。反思自己现在做的课题并非是图像、语言、文本类难以进行特征工程的内容,所以还是要基于现实、基于业务的把握,多加思考,组织数据。
- 还是不能不加判断的堆砌特征,需要根据效果判断,更需要好好组织输入的形式,这方面我了解的太少了,多看多实践。
【参考文献/链接】
【1】使用sklearn做单机特征工程
【2】七种常用的特征工程
【3】特征工程到底是什么?-ID:包大人
【4】特征工程到底是什么?-ID:管他叫大靖
【5】统计基础:剔除箱线图所提示的异常值