本文将以天池的一道赛题入手,详细介绍数据挖掘的步骤,实际操作性强。
适合人群:想入门数据挖掘,入门数据挖掘类比赛,熟悉python,pandas,Numpy等库运用性选手
本文是从0开始入门数据挖掘系列文章的第二篇,第一篇介绍的是EDA部分,也就是数据探索性分析。这一篇文章将给大家介绍特征工程。
特征工程基础知识
特征工程(Feature Engineering): 将数据转换成为更好地表达潜在问题的特征,从而提高机器学习性能。
-
一般来说, 特征工程分为如下几个步骤:
1.数据理解
2.数据清洗
3.特征构造
4.特征选择
5.类别不平衡 -
在介绍特征工程常见步骤之前,我们先看看数据理解的几个不同层次
我们一般可以把数据理解层度分为不同的层次:
第一层:数据层——知道数据是什么?
第二层:描述层——在数据层的基础上,进行统计性描述,比如四分位,方差之类的
第三层:解读层——解读数据变化的原因
第四层:结论层——得到结论
如下图:图片来自大佬阿泽
作为特征工程的第一步的数据理解其实就是上篇文章介绍的EDA部分。
特征工程详细步骤
1.数据理解
目的:探索数据,了解数据,主要在EDA阶段完成
EDA部分我们可以从定性数据和定量数据两大块进行分析。
定性数据本质是类别,反应是描述某样东西的性质。
定量数据本质上是数值,应该是衡量某样东西的数量。
基本示例:
以华氏度或者摄氏度表示的气温是定量的
阴天或者晴天是定性的
献血的血量是定量的
定性数据又分为:定类和定序,定类数据只按照名称分类,例如血型动物名称。在这类数据上,我们不能执行任何定量数学操作,例如加减乘除,因为这些操作没有意义,因为没有加减乘除,所以在这类数据上找不到平均值,我们可以做的是计数,找众数,即出现类别最多的是哪一类。可以制作的表图是条形图和饼图。
定序数据,这一类数据相比较定类数据它可以排序,这就意味着这类数据可以找到某些数据比其他数据更大或者更好的数据。可以执行的数据操作为计数和排序。因此不仅可以继续画条形图和饼图,因为能排序和比较,所以能计算中位数和百分位数,对于中位数和百分位数,我们可以画茎叶图和箱线图。
定量数据可以分为定距和定比数据;定距数据不仅可以向定序数据那样排序还可以进行加减。定距数据一个经典的例子是温度。这类数据可以执行的数学操作是:算数平均值和标准差。这类数据我们不能使用条形图或饼图,因为值太多了,定量的数据最常用的是直方图,直方图是条形图的近亲,用不同的桶包含不同的数据,对数据的频率进行可视化。
最后是定比数据,在这一类数据上,我们可以执行数学的乘除法。当处理金融数据时,我们可以说100人民币是50的两倍,这个除法有意义。但你要说今天的云层是昨天的两倍厚就没多大意义了。
这里总结一下:
2.数据清洗
目的:提高数据质量,降低算法用错误数据模型的风险
-
特征变换:模型无法处理或不适合处理
a) 定性变量编码:Label Encoder; Onehot Encoder; Distribution Encoder.
b) 标准化或归一化:Z分数标准化(标准正太分布),min-max归一化 -
缺失值处理:增加不确定性,可能会导致不可靠输出
a) 不处理:少量样本删除
b) 删除:大量样本缺失
c) 补全:(同类)均值/中位数/众数补全;高维映射;模型预测;最近邻补全 -
异常值处理:减少脏数据
a) 简单统计:如describe()的统计描述;散点图等
b) 3西格玛法则(正太分布)/箱型图删除/截断等
c) 利用模型进行离群点检测:聚类、K近邻、One class SVM, Isloation Forest; -
其他:删除无效列/更改dtypes/删除列中的字符串/将时间戳从字符串转换为日期时间
代码实例:
# 2.1删除异常值
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_outliter(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) # 是一个bool值可以用来刷选特征
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_outliter(data_series,box_scale=scale) # 获取异常值的范围
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("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()) # 获取值比较高的异常值信息
print("the lower bound is:{}".format(index_low))
print("the upper bound is:{}".format(index_up))
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
# 关于power的柱状图 plt.hist()
plt.hist(train['power'], orientation = 'vertical',histtype = 'bar', color ='red')
plt.show()
# 关于power的线型图 sns.distplot()
plt.figure(2);
plt.title('power')
sns.distplot(train['power'])
# 我们可以删掉一些异常数据,以 power 为例。
# 这里删不删同学可以自行判断
# 但是要注意 test 的数据不能删 = = 不能掩耳盗铃是不是
train = outliers_proc(train, 'power', scale=3)
# 2.2我们对其取 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()
# km 的比较正常,应该是已经做过分桶了
data['kilometer'].plot.hist()
# 所以我们可以直接做归一化
data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer']))/(np.max(data['kilometer']) - np.min(data['kilometer'])))
data['kilometer'].plot.hist()
# 除此之外 还有我们刚刚构造的统计量特征:
# 'brand_amount', 'brand_price_average', 'brand_price_max',
# 'brand_price_median', 'brand_price_min', 'brand_price_std',
# 'brand_price_sum'
# 这里不再一一举例分析了,直接做变换,
def max_min(x):
return (x-np.min(x))/(np.max(x)-np.mix(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.2 对类别特征进行 OneEncoder
data = pd.get_dummies(data,columns =['model','brand','bodyType','fuelType','gearbox','notRepairedDamage','power_bin'])
# 3 特征构造
# 3.1 根据结束时间和开始时间构造时间变量
# 训练集和测试集放在一起,方便构造特征
train['train']=1
test['train']=0
data = pd.concat([train, test], ignore_index=True, sort=False)
# 使用时间:data['creatDate'] - data['regDate'],反应汽车使用时间,一般来说价格与使用时间成反比
# 不过要注意,数据里有时间出错的格式,所以我们需要 errors='coerce'
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
# 看一下空数据,有 15k 个样本的时间是有问题的,我们可以选择删除,也可以选择放着
# 但是这里不建议删除,因为删除缺失数据占总样本量过大,7.5%
data['used_time'].isnull().sum()/data['used_time'].count()
# 从邮编中提取城市信息,因为是德国的数据,所以参考德国的邮编,相当于加入了先验知识
data['city'] = data['regionCode'].apply(lambda x: str(x)[:-3])
3.特征构造
目的:增强数据表达、添加先验知识
- 统计量特征
计数/求和/比例/标准差 - 时间特征
绝对时间/相对时间/节假日/双休日 - 地理信息
分桶 - 非线性变换
去log/平方/根号 - 数据分桶
等频/等距分桶,Best-KS 分桶,卡方分桶 - 特征组合/特征交叉
代码示例:
# 3.2 构造统计量特征
# 计算某品牌的销售统计量,同学们还可以计算其他特征的统计量
# 这里要以 train 的数据计算统计量
train_gb = train.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
print(all_info)
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"}) # 将列的命名index重命名为brand
data = data.merge(brand_fe, how='left', on='brand')
# 3.3 分桶
# 数据分桶 以 power 为例
# 这时候我们的缺失值也进桶了
# 为什么要做数据分桶呢,原因有很多,= =
# 1. 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
# 2. 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
# 3. LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
# 4. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
# 5. 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
# 当然还有很多原因,LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'],bin,labels = False)
data[['power_bin', 'power']].head()
4.特征选择
目的:平衡预测能力和计算复杂度:降低噪声,增强模型预测能力
- 过滤式(Filter): 先用特征选择方法对初始特征进行过滤然后再训练学习器,特征选择过程与后续学习器无关
Relief/方差选择/相关系数/卡方检验/互信息法 - 包裹式(Wrapper):直接把终将要使用的学习器性能作为衡量特征子集的评价准则,其目的在于给定学习器选择最有利其性能的特征子集。
- 嵌入式(Embedding): 结合过滤式和包裹式方法,将特征选择与学习器训练过程融为一体,两者在同一优化过程中完成,即学习器训练过程中自动进行了特征选择。
LR+L1或决策树
代码示例:
# 4.1特征筛选-过滤式
# 相关性分析
print(data['power'].corr(data['price'],method = 'spearman')) # 斯皮尔曼等级相关系数 和皮尔逊相关系数类似求相关系数的
print(data['kilometer'].corr(data['price'], method='spearman'))
print(data['brand_amount'].corr(data['price'], method='spearman'))
print(data['brand_price_average'].corr(data['price'], method='spearman'))
print(data['brand_price_max'].corr(data['price'], method='spearman'))
print(data['brand_price_median'].corr(data['price'], method='spearman'))
# 当然也可以直接看图
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)
# 4.2特征筛选-包裹式
x['city'].replace('', np.nan, inplace=True)
# 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)
sfs.fit(x, y)
sfs.k_feature_names_
#('kilometer', 'v_0', 'v_3', 'v_7', 'used_time', 'brand_price_std', 'brand_price_average', 'model_167.0', 'brand_24', 'gearbox_1.0')
5.类别不平衡
缺点:少类别提供信息太少,没有学会如何判断少类
- 补充数据集
- 尝试其他评价体系,AUC等
- 调增西塔值(调增分类阈值)
- 重采样:过采样/去采样
- 合成样本:SMOTE
- 选择其他模型:决策树等
- 加权少类别的样本错分代价
- 创新:
a) 将大类分解成多个小类
b) 将小类视为异常点,并用异常检测建模
总结:
这篇文章介绍了特征工程的五个详细步骤
1.数据理解
2.数据清洗
3.特征构造
4.特征选择
5.类别不平衡
数据理解层度的四个层度第一层:数据层,第二层:描述层,第三层:解读层,第四层:结论层
以及数据的四种分类:定类,定序,定距和定比。
参考:https://github.com/datawhalechina/team-learning
参考:https://tianchi.aliyun.com/course/video?liveId=41145 阿泽在天池的直播