文章目录
前言
我们经常在处理数据时,会面临以下问题:
1.的数据格式不对(如 SQL 数据库、JSON、CSV 等)
2.缺失值和异常值
3.标准化
4.减少数据集中存在的固有噪声(部分存储数据可能已损坏)
5.数据集中的某些功能可能无法收集任何信息以供分析
而减少统计分析期间要使用的特征的数量可能会带来一些好处,例如:
1.提高精度
2.降低过拟合风险
3.加快训练速度
4.改进数据可视化
5.增加我们模型的可解释性
事实上,统计上证明,当执行机器学习任务时,存在针对每个特定任务应该使用的最佳数量的特征。如果添加的特征比必要的特征多,那么我们的模型性能将下降(因为添加了噪声)。真正的挑战是找出哪些特征是最佳的使用特征(这实际上取决于我们提供的数据量和我们正在努力实现的任务的复杂性)。这就是特征选择技术能够帮到我们的地方!
1 异常缺失值删除
1.1 导入库与数据
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from operator import itemgetter
# %matplotlib inline
train = pd.read_csv('used_car_train_20200313.csv', sep=' ')
test = pd.read_csv('used_car_testA_20200313.csv', sep=' ')
print(train.shape)
print(test.shape)
print(train.head())
print(train.columns)
1.2异常值删除
这里可以将箱型图中的超过上下限的那些值作为异常值删除。如下图所示,箱型图中间是一个箱体,也就是粉红色部分,箱体左边,中间,右边分别有一条线,左边是下分位数(Q1),右边是上四分位数(Q3),中间是中位数(Median),上下四分位数之差是四分位距(IQR),用Q1-1.5IQR得到下边缘(最小值),用Q3+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)) # quantile是pd内置的求四分位的函数
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]] # 运算就是说只要rule_low和rule_up中只要有一个值为True,就把这个下标取出来
print("Delete number is: {}".format(len(index)))
data_n = data_n.drop(index) # 删除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])
plt.show()
return data_n
这里reset_index可以还原索引,重新变为默认的整型索引
DataFrame.reset_index(level=None, drop=False, inplace=False, col_level=0, col_fill=”)
train = outliers_proc(train, 'power', scale=3)
从这张删除异常值前后的箱型图对比可以看出,剔除异常值后,数据的分布就很均匀了。
下面我们就批量对所有的特征进行一次异常数据删除:
def Bach_drop_outliers(data,scale=1.5):
dataNew = data.copy()
for fea in data.columns:
try:
IQR = scale * (dataNew[fea].quantile(0.75) - dataNew[fea].quantile(0.25)) # quantile是pd内置的求四分位的函数
except:
continue
val_low = dataNew[fea].quantile(0.25) - IQR # 下边缘
val_up = dataNew[fea].quantile(0.75) + IQR # 上边缘
rule_low = (dataNew[fea] < val_low) # 下边缘的极小异常值的下标列表
rule_up = (dataNew[fea] > val_up) # 上边缘的极大异常值的下标列表
index = np.arange(dataNew[fea].shape[0])[rule_low | rule_up] # | 运算就是说只要rule_low和rule_up中只要有一个值为True,就把这个下标取出来
print("feature %s deleted number is %d" % (fea, len(index)))
dataNew = dataNew.drop(index) # 删除index对应下标的元素
dataNew.reset_index(drop=True, inplace=True)
fig, ax = plt.subplots(5, 6, figsize=(20, 15))
x = 0
y = 0
for fea in dataNew.columns:
try:
sns.boxplot(y=dataNew[fea], data=dataNew, palette="Set2", ax=ax[x][y])
y += 1
if y == 6:
y = 0
x += 1
except:
print(fea)
y += 1
if y == 6:
y = 0
x += 1
continue
plt.show()
return dataNew
train = Bach_drop_outliers(train)
2 树模型的特征构造
训练集和测试集放在一起,方便构造特征
train['train']=1
test['train']=0
data = pd.concat([train, test], ignore_index=True, sort=False)
2.1 时间特征构造
1.使用时间:data[‘creatDate’] - data[‘regDate’],反应汽车使用时间,一般来说价格与使用时间成反比
2.不过要注意,数据里有时间出错的格式,所以我们需要 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%
# 我们可以先放着,因为如果我们 XGBoost 之类的决策树,其本身就能处理缺失值,所以可以不用管;
print(data['used_time'].isnull().sum())
2.2 城市信息特征提取
从邮编中提取城市信息,因为是德国的数据,所以参考德国的邮编,相当于加入了先验知识
data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])
2.3.品牌特征提取
计算某品牌的销售统计量,这里要以 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
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
data = data.merge(brand_fe, how='left', on='brand')
print(data)
3 树模型的数据分桶
数据分箱(也称为离散分箱或分段)是一种数据预处理技术,用于减少次要观察误差的影响,是一种将多个连续值分组为较少数量的“分箱”的方法。例如我们有各个年龄的数据的统计值,可以分成某个段的年龄的值。
1.离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
2.离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
3.LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
4.离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量变成 M*N 个变量,进一步引入非线形,提升了表达能力;
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())
可以看出这个分箱的作用就是将同一个区间段的功率值设为同样的值,比如101~109都设置为10.0。 然后就可以删除掉原数据了:
data = data.drop(['creatDate', 'regDate', 'regionCode'], axis=1)
print(data.shape)
data.columns
至此,可以导出给树模型用的数据
data.to_csv('data_for_tree.csv', index=0)
data['power'].plot.hist()
plt.show()
4 LR与NN模型的特征构造
上面的步骤就是一次比较完备的特征构造,我们还可以为其他模型构造特征,主要是由于不用模型需要的数据输入是不同的。
4.1 与归一化
观察一下数据分布
train['power'].plot.hist()
plt.show()
再看看train数据集的分布:
train['power'].plot.hist()
plt.show()
我们对其取 log,再做归一化
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()
看看行驶里程的情况,应该是原始数据已经分好了桶
data['kilometer'].plot.hist()
归一化
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.columns[-10:]
Index(['used_time', 'city', 'brand_amount', 'brand_price_max',
'brand_price_median','brand_price_min', 'brand_price_sum',
'brand_price_std', 'brand_price_average', 'power_bin'],
dtype='object')
for i in data.columns[-10:]:
if np.min(data[i]) != '': # 存在空值的情况
data[i] = max_min(data[i])
4.2 编码
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType', 'gearbox', 'notRepairedDamage', 'power_bin'])
print(data)
将这份数据输出给LR模型使用
data.to_csv('data_for_lr.csv', index=0)
5 特征选择
5.1 过滤式(filter)
相关性分析
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'))
可以看出power,brand_price_average,brand_price_median与price相关性比较高
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=30)
sns.heatmap(correlation, square = True, cmap = 'PuBuGn', vmax=0.8)
看不出啥
5.2包裹式(wrapper)
!pip install mlxtend
# 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()
5.3 嵌入式(embedding)
Lasso 回归和决策树可以完成嵌入式特征选择,大部分情况下都是用嵌入式做特征筛选。
下一步就是建模了