Task3 二手车数据特征工程

前言

在之前的工作中Task2 数据探索性分析
探索数据和了解数据
发现数据一些需要处理的地方有
1、缺失值,类别特征(bodyType, gearbox, fullType, notRepaired)
2、异常值,可通过箱线图分析或长尾阶段
3、类别倾斜的特征(seller, offtype),删掉
4、类别数据需要编码
5、数值数据归一化和标准化
6、价格不服从正态分布,需要进行log转换
7、power特征分布有异常,需要

特征工程是什么

有这么一句话在业界广泛流传:数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。那特征工程到底是什么呢?顾名思义,其本质是一项工程活动,目的是最大限度地从原始数据中提取特征以供算法和模型使用。
引用下群里大佬的总结
在这里插入图片描述
数据清洗
目的:提高数据质量,降低算法用错误数据建模的风险。

  1. 特征变换:模型无法处理或不适合处理
    a) 定性变量编码:Label Encoder;Onehot Encoder;Distribution coding;
    b) 标准化和归一化:z分数标准化(标准正太分布)、min-max 归一化;
  2. 缺失值处理:增加不确定性,可能会导致不可靠输出
    a) 不处理:少量样本缺失;
    b) 删除:大量样本缺失;
    c) 补全:(同类)均值/中位数/众数补全;高维映射(One-hot);模型预测;最邻近补全;
    矩阵补全(R-SVD);
  3. 异常值处理:减少脏数据
    a) 简单统计:如 describe() 的统计描述;散点图等;
    b) 3∂ 法则(正态分布)/箱型图截断;
    c) 利用模型进行离群点检测:聚类、K近邻、One Class SVM、Isolation Forest;
  4. 其他:删除无效列/更改dtypes/删除列中的字符串/将时间戳从字符串转换为日期时间格式等

特征构造
目的:增强数据表达,添加先验知识。

  1. 统计量特征:
    a) 计数、求和、比例、标准差;
  2. 时间特征:
    a) 绝对时间、相对时间、节假日、双休日;
  3. 地理信息:
    a) 分桶;
  4. 非线性变换:
    a) 取 log/平方/根号;
  5. 数据分桶:
    a) 等频/等距分桶、Best-KS 分桶、卡方分桶;
  6. 特征组合

特征选择
目的:降低噪声,平滑预测能力和计算复杂度,增强模型预测性能。

  1. 过滤式(Filter):先用特征选择方法对初识特征进行过滤然后再训练学习器,特征
    选择过程与后续学习器无关。
    a) Relief/方差选择/相关系数/卡方检验/互信息法
  2. 包裹式(Wrapper):直接把最终将要使用的学习器的性能作为衡量特征子集的评
    价准则,其目的在于为给定学习器选择最有利于其性能的特征子集。
    a) Las Vegas Wrapper(LVM)
  3. 嵌入式(Embedding):结合过滤式和包裹式方法,将特征选择与学习器训练过程
    融为一体,两者在同一优化过程中完成,即学习器训练过程中自动进行了特征选择。
    a) LR+L1或决策树

类别不平衡
缺点:少类别提供信息太少,没有学会如何判别少数类。

  1. 扩充数据集;
  2. 尝试其他评价指标:AUC等;
  3. 调整θ值;
  4. 重采样:过采样/欠采样;
  5. 合成样本:SMOTE;
  6. 选择其他模型:决策树等;
  7. 加权少类别人样本错分代价;
  8. 创新:
    a) 将大类分解成多个小类;
    b) 将小类视为异常点,并用异常检测建模。

特征工程常见方法

常见的特征工程包括:
1、异常处理:
通过箱线图(或 3-Sigma)分析删除异常值;
BOX-COX 转换(处理有偏分布);
长尾截断;
2、特征归一化/标准化:
标准化(转换为标准正态分布);
归一化(抓换到 [0,1] 区间);
针对幂律分布,可以采用公式:
3、数据分桶:
等频分桶;
等距分桶;
Best-KS 分桶(类似利用基尼指数进行二分类);
卡方分桶;
4、缺失值处理:
不处理(针对类似 XGBoost 等树模型);
删除(缺失数据太多);
插值补全,包括均值/中位数/众数/建模预测/多重插补/压缩感知补全/矩阵补全等;
分箱,缺失值一个箱;
5、特征构造:
构造统计量特征,报告计数、求和、比例、标准差等;
时间特征,包括相对时间和绝对时间,节假日,双休日等;
地理信息,包括分箱,分布编码等方法;
非线性变换,包括 log/ 平方/ 根号等;
特征组合,特征交叉;
仁者见仁,智者见智。
6、特征筛选
过滤式(filter):先对数据进行特征选择,然后在训练学习器,常见的方法有 Relief/方差选择发/相关系数法/卡方检验法/互信息法;
包裹式(wrapper):直接把最终将要使用的学习器的性能作为特征子集的评价准则,常见方法有 LVM(Las Vegas Wrapper) ;
嵌入式(embedding):结合过滤式和包裹式,学习器训练过程中自动进行了特征选择,常见的有 lasso 回归;
7、降维
PCA/ LDA/ ICA;
特征选择也是一种降维

主要工作

长尾截断

查看power分布情况(本应在[0,600])

print(Train_data['power'].describe())
print(Train_data['power'][Train_data['power']>600].describe())
plt.hist(Train_data['power'][Train_data['power']>600], histtype = 'bar') 
plt.show()
print(Test_data['power'].describe())
print(Test_data['power'][Test_data['power']>600].describe())
plt.hist(Test_data['power'][Test_data['power']>600], histtype = 'bar') 
plt.show()

可以看到超过600的部分
在这里插入图片描述
在这里插入图片描述
有超过最大范围600的值,这里采用长尾截断,对超过600的值,取值为600

Train_data['power'][Train_data['power']>600]=600
Test_data['power'][Test_data['power']>600]=600

箱线图去异常值

在这里插入图片描述
箱型图,是利用数据中的五个统计量:最小值、第一四分位数、中位数、第三四分位数与最大值来描述数据的一种方法,它也可以粗略地看出数据是否具有有对称性,分布的分散程度等信息,特别可以用于对几个样本的比较。
四分位距IQR=Q3-Q1
图片红色是k=1.5时,计算出的是中度异常的范围。
蓝色是K=3计算出的是极度异常的范围。
1、内限
上限是非异常范围内的最大值。上限=Q3+1.5IQR
下限是非异常范围内的最小值。下限=Q1-1.5IQR
这个界限叫内限
2、外限
外限与内限的计算方法相同,唯一的区别就在与:上面的T形线段所延伸到的极远处,是Q3+3IQR与剔除异常值后的极大值两者取最小,下面的T形线段所延伸到的极远处,是Q1-3IQR与剔除异常值后的极小值两者取最大。

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))#IQR
        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]]#布尔值或操作,生成索引
    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, 'price', scale=3)
Train_data['price'] = np.log1p(Train_data['price'])
Train_data = outliers_proc(Train_data, 'price', scale=3)

在这里插入图片描述
在这里插入图片描述
做了log(1+x)处理后明显改善了
回归模型大多要将非正态分布数据转化为正态分布,这是模型假设决定的

缺失值处理

缺失值处理有几个方案
1、不处理(xgboost等树模型)
2、缺失的过多可以直接删除该列
3、有(均值、中位数、众数)补全
4、分箱处理,有缺失值的分为一个箱

# 删除重复值
data.drop_duplicates()
# data.dropna()可以直接删除缺失样本,但会丢失大量训练样本

# 填充固定值
train_data.fillna(0, inplace=True) # 填充 0
data.fillna({0:1000, 1:100, 2:0, 4:5})   # 可以使用字典的形式为不同列设定不同的填充值

train_data.fillna(train_data.mean(),inplace=True) # 填充均值
train_data.fillna(train_data.median(),inplace=True) # 填充中位数
train_data.fillna(train_data.mode(),inplace=True) # 填充众数

train_data.fillna(method='pad', inplace=True) # 填充前一条数据的值,但前一条也不一定有值
train_data.fillna(method='bfill', inplace=True) # 填充后一条数据的值,但后一条也不一定有值

"""插值法:用插值法拟合出缺失的数据,然后进行填充。"""
for f in features: 
    train_data[f] = train_data[f].interpolate()    
train_data.dropna(inplace=True)

由上一篇文章中的结论缺失值较多的列有,bodyType、gearbox、fuelTypehe和notRepairedDamage,这些特征都是类别特征,所以用均值中位数众数填充或插值法都不合理,暂时没有一个特别好的方法,后面可以用树模型自行处理带缺失值带缺失值的数据
在这里插入图片描述

特征构造

1、使用时间:data[‘creatDate’] - data[‘regDate’]得到汽车使用时间,一般来说价格与使用时间成反比

# 训练集和测试集放在一起,方便构造特征
Train_data['train']=1
Test_data['train']=0
data = pd.concat([Train_data, Test_data], ignore_index=True)#两份数据列方向拼接

# 要注意,数据里有时间出错的格式,所以我们需要 errors='coerce'
#转为datetime形式,可做时间运算
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 之类的决策树,其本身就能处理缺失值,所以可以不用管;
data['used_time'].isnull().sum()

2、从邮编中提取城市信息

data['city'] = data['regionCode'].apply(lambda x : str(x)[:-3])

3、计算某品牌的销售统计量(构造表征品牌的特征)

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')
# 删除不需要的数据
data = data.drop(['creatDate', 'regDate', 'regionCode','SaleID','seller','offerType'], axis=1)

数据分桶(特征离散化)

这时候我们的缺失值也进桶了,
为什么要做数据分桶
离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;
特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化
当然还有很多原因,LightGBM 在改进 XGBoost 时就增加了数据分桶,增强了模型的泛化性

bin = [i*10 for i in range(31)]#定义31个桶
data['power_bin'] = pd.cut(data['power'], bin, labels=False)#以10为间隔分桶
data[['power_bin', 'power']].head()

#目前的数据其实已经可以给树模型使用了,所以我们导出一下
data.to_csv('data_for_tree.csv', index=0)#不保存行索引

归一化

我们可以再构造一份特征给 LR NN 之类的模型用
之所以分开构造是因为,不同模型对数据集的要求不同
我们看下数据分布:

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(Train_data['power'])) / (np.max(Train_data['power']) - np.min(Train_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.min(x))
data['brand_amount'] = max_min(data['brand_amount']

对类别特征one_hot编码

one_hot编码

  1. 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
  2. LR 属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
  3. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
                                     'gearbox', 'notRepairedDamage', 'power_bin'])
 # 这份数据可以给 LR 用
data.to_csv('data_for_lr.csv', index=0)

特征筛选

用斯皮尔曼,不用皮尔逊相关系数,因为要求正态分布

# 相关性分析,各个特征与价格的相关性
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'))

输出
0.572828519605
-0.408256970162
0.0581566100256
0.383490957606
0.259066833881
0.386910423934

也可输出图

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)

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章