特征工程
经过几天的学习来到了数据挖掘中最重要的一步: 特征工程。数据挖掘的大部分时间就花费在特征工程上。有一句经典的话: “数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已” 。足以看出特征工程的重要性。
特征工程在维基上的定义:
Feature engineering is the process of using domain knowledge of the data to create features that make machine learning algorithms work.
谷歌翻译是:特征工程是使用数据的领域知识来创建使机器学习算法起作用的特征的过程。简单来说特征工程就是通过X,创造新的X’。而新的X’可以使你的问题得到更好的解决。
特征工程的一般步骤:
- 数据理解
- 数据清洗
(1)异常值、缺失值、重复值处理
(2)特征变换 - 特征构造
(1)统计
(2)数据分桶
(3)非线性变换
(4)特征组合
(5)降维 - 特征选择
(1)过滤
(2)包裹
(3)嵌入
引用一位大佬的思维导图:
下面利用train_data和test_data进行学习。
3.1 数据理解
这个任务在EDA阶段基本完成。
3.2 数据清洗
3.2.1 异常值处理
以‘power’变量为例,可以先封装一个函数来处理异常值。以下函数是利用箱型图进行判别,箱型图提供了识别异常值的一个标准,即异常值通常被定义为小于QL-1.5IQR或大于QU+1.5IQR的值。
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)
index = np.arange(data_series.shape[0])[rule[0] | rule[1]] # 获取index,Ture返回。
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())
# 作图
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)
运行结果:
- IQR称为四分位数间距,是上四分位数QU与下四分位数QL之差,其间包含了全部观察值的一半。
- pandas.Series.quantile 给定分位数的返回值。
- 其他异常检测方法还有拉依达准则,可参考:异常值检测方法
3.2.2 缺失值处理
删除(Deletion)
删除缺失值本身,然后用样本剩余数据进行训练,这样不同的变量可能会有不同的样本大小。当数据缺失是随机产生时,可以考虑使用删除的方法。
均值/众数/中位数填充
使用均值/众数/中位数填充缺失值的方法。其目标是使用能够从数据集中的有效值中识别出的关系来评估缺失值。
-
整体填充:利用统一的指标(均值/中位数等)填充缺失值
-
相似填充:对于其他维度相似的样本,采用相同的值进行填充。如:对于不同性别采用不同的统计量进行缺失值填充。
使用预测模型
利用预测模型对缺失值进行估计的方法。将不含缺失值的数据集作为训练集,含有缺失值的样本作为测试集,而含有缺失值的变量则是目标变量。可以使用回归,ANOVA,logistic回归等方法进行预测。
KNN填充
利用和包含缺失值的样本最为相似的样本的属性值填充缺失值。相似度由距离度量。
3.3 特征构造
在开始构造时,我们可以将训练集和测试集放在一起,方便构造特征
train_data['train']=1
test_data['train']=0
data = pd.concat([train_data, test_data], ignore_index=True, sort=False)
先回顾一下所有变量:
train_data.columns
test_data.columns
构造车辆已使用时间(特征组合)
我们可以利用createDate和regDate,即regDate-createDate来构造车辆已使用时间。
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
- errors=‘coerce’ 将无效解析设置为NaT
构造完成后可以查看一下无效的值,若无效的值比较少,可以考虑删除掉。
data['used_time'].isnull().sum()
城市信息(变量变换)
利用regionCode构造城市信息,因为我们知道是德国的数据,所以参考德国的邮编,这里相当于加入了先验知识。
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
品牌信息(统计)
可以按brand来对数据进行分组,再进行相关统计,从而获得品牌信息(销量,平均价格等)
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') # 合并,left相当数据库中的左连接
数据分桶
对power进行分桶
bin = [i*10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)
data[['power_bin', 'power']].head()
之后可以把原本的特征去掉,并保存。这份数据可以给树模型使用。
data = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
print(data.shape)
data.columns
data.to_csv('data_for_tree.csv', index=0)
非线性变换
对power数据进行log变换
data['power'].plot.hist()
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()
- from sklearn import preprocessing
无量纲化(最大最小标准化)
利用数据的最值进行缩放,返回[0, 1]的值。我们对已行驶公里和品牌数据进行缩放。
def max_min(x):
return (x - np.min(x)) / (np.max(x) - np.min(x))
data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /
(np.max(data['kilometer']) - np.min(data['kilometer'])))
data['kilometer'].plot.hist()
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'])))
对类别特征进行 OneHotEncoder
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType','gearbox', 'notRepairedDamage', 'power_bin'])
one-hot的意义:
- 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
- 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
- LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
- 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
- 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
3.4 特征选择
3.4.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)
3.4.2包裹式
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)
- from mlxtend.feature_selection import SequentialFeatureSelector as SFS
- from sklearn.linear_model import LinearRegression
- SequentialFeatureSelector(estimator,k_features = 1,forward = True,float = False,verbose = 0,scoring = None,cv = 5,n_jobs = 1,pre_dispatch =‘2 n_jobs’,clone_estimator = True,fixed_features = None)
分类和回归的顺序特征选择。API DOC
查看边际效益(增加特征的收益)
fig1 = plot_sfs(sfs.get_metric_dict(), kind='std_dev')
plt.grid()
plt.show()
- from mlxtend.plotting import plot_sequential_feature_selection as plot_sfs
- get_metric_dict(confidence_interval = 0.95) 返回指标字典
3.4.3嵌入式
Lasso 回归和决策树可以完成嵌入式特征选择
特征工程入门简单,想做好却是十分困难。只能靠不断练习与积累。通过这次任务,也算是对特征工程有了一定的了解,本文只是简单的总结,学好特征工程还有很长的路要走。
文章参考: