二手车交易价格预测:Task3 特征工程

task2中我们已经学了如何导入数据,如何查看其基本的信息(包括数据集的形状,对预测无影响的特征等),在task3中我们将进一步探寻数据的特征,并构造新的特征,去除无用特征,以及分别构造适合树模型和LR模型的数据特征。

一、导入数据

导入数据在task2中已经详细介绍过了,因此不再赘述,直接用以下代码即可导入。

import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno
import scipy.stats as st

path = 'E:/学习资料/car/'
Train_data = pd.read_csv(path + 'used_car_train.csv', sep=' ')    
# 文件以空字符分隔数据,若以','分隔数据,则sep=','或省略(源文件都将数据存到了一列里面)
Test_data = pd.read_csv(path + 'used_car_testA.csv', sep=' ')

二、删除异常值(仅适用于树模型的构造)

在task2中,我们仅仅是凭借观察删除掉了两个特征和一部分特征的异常值,在本次任务中,我们给出了一个比较通用的方法来处理异常值:利用箱线图来去除异常值。我将所有的代码都做了详细注释,希望能帮助到大家理解:

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为四分位函数,其中quantile(0.75)是从小到大排四分之三处的数据大小
        val_low = data_ser.quantile(0.25) - iqr                                 # 设置需要丢弃数据的上下阈值
        val_up = data_ser.quantile(0.75) + iqr
        rule_low = (data_ser < val_low)                                         # 比val_low更小的值要被丢弃(False和True组成的数组)
        rule_up = (data_ser > val_up)                                           # 比val_up更大的值要被丢弃(False和True组成的数组)
        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
    print("Delete number is: {}".format(len(index)))
    data_n = data_n.drop(index)                                                 # 丢弃过小和过大的值
    data_n.reset_index(drop=True, inplace=True)                                 # 数据清洗过后,对数据重新设置连续行索引。
    print("Now row number is: {}".format(data_n.shape[0]))                      # 清洗后该特征剩余的取值个数(原代码的column应该是笔误了)
    index_low = np.arange(data_series.shape[0])[rule[0]]                        # 因为取值过小被清洗掉的数据的index
    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]]                         # 因为取值过大被清洗掉的数据的index
    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

其中第二个print()语句中原文写的是Now column number is,我认为应该是笔者的笔误,此处应该是行数(即该特征的有效数据个数)。
其输出如下所示:

Delete number is: 963
Now row number is: 149037
Description of data less than the lower bound is:
count    0.0
mean     NaN
std      NaN
min      NaN
25%      NaN
50%      NaN
75%      NaN
max      NaN
Name: power, dtype: float64
Description of data larger than the upper bound is:
count      963.000000
mean       846.836968
std       1929.418081
min        376.000000
25%        400.000000
50%        436.000000
75%        514.000000
max      19312.000000
Name: power, dtype: float64

有结果我们可知共删除了过大和过小的值共963个,剩余的有效取值值个数为149037
可视化的结果如下图所示:
在这里插入图片描述
其中左图显示的是删除异常值之前数据的箱线图,右图显示的是删除异常值之后数据的箱线图。经过对比可见,删除前的数据中位数,第一四等分点和第三四等分点都处于图像非常靠下的位置,因此存在非常多异常大的取值,对数据的整体分布产生了较大的影响;删除后的数据中位数,第一四等分点和第三四等分点相对全部的取值取值区间已有较大的区分度,整体分布得到了优化。

三、特征构造(所有模型通用)

本部分我们将应用我们的先验知识构造有助于预测price的新特征。

1.训练集与测试集的合并

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

注意:不熟悉pandas的同学可能会疑问这个ignore_index的作用,下面我们通过控制变量看一下他的作用,当该值取False时,训练集与测试集合并后的序号如下图所示:
在这里插入图片描述
当该值取True时,合并后的序号如下图所示:
在这里插入图片描述
经过对比可见当该值取False时,合并后的表格将序号依然使用的原表格中的序号,而当该值取True时,序号更新为连续的序号。

2.构造特征之使用时间

因为我们知道其售卖时间和出厂时间,所以两者相减可以计算出其使用时间。之所以要计算汽车使用时间,是因为一般来说价格与使用时间成反比。

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

3.构造特征之所属城市

从邮编中可以提取城市特征(4位邮编的第一位,不足4位的邮编认为是异常的)。

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

4.构造特征之品牌

Train_gb = Train_data.groupby("brand")                                # 按照brand来聚合,brand=0的聚合在一起,brand=1的聚合在一起(总共聚合了40类)
all_info = {}
for kind, kind_data in Train_gb:                                      # kind分别从0取到39,kind_data为0-39这40类的具体数据
    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                                              # all_info存储了这40个聚合类的统计学特征
brand_fe = pd.DataFrame(all_info).T.reset_index().rename(columns={"index": "brand"})
data = data.merge(brand_fe, how='left', on='brand')

如上面代码所示,我们根据brand的值聚合为多种类(本题为40类),也就是一共40个品牌,我们分别计算每个品牌的各种数学统计量,并保存在字典all_info中,为了将新构造的特征合并到原来的data中,我们将其转化为和data相同格式的类型,然后将其合并。如下图所示:新的特征就被加入到了原来的data中了。
在这里插入图片描述
5.构造特征之数据分桶

本构造以power特征为例,数据分桶实际上就是数据离散化,其优点如下:
①.离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;
②. 离散后的特征对异常值更具鲁棒性。如 age>30 为 1 否则为 0,所以异常年龄100也不会对模型造成很大的干扰;
③. LR (逻辑斯蒂回归)属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;
④. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;
⑤. 特征离散后模型更稳定。
其具体实现如下所示:

bin = [i*10 for i in range(31)]                                        # bin 是一个整数数列,标明了我们的cut标准
data['power_bin'] = pd.cut(data['power'], bin, labels=False)           # 按照bin所给的数组切割,左开右闭,因此power等于0或者大于300的值都被认为是nan

以上是将power在(0,300]区间内的值都根据bin的值做一定切分处理(除以10)。其结果如下所示:

        power_bin  power
0             5.0     60
1             NaN      0
2            16.0    163
3            19.0    193
4             6.0     68
...           ...    ...
199995       11.0    116
199996        7.0     75
199997       22.0    224
199998        NaN    334
199999        6.0     68

[200000 rows x 2 columns]

细心的同学可能发现了,power=60处理后是变成了5.0,而不是6.0。这是因为cut()函数默认的切割是左开右闭的。之所以这样切割是有好处的,因为power=0也是异常值,这样可以将0处理为Nan。

6.去除无用的特征,导出到文件

我们前面利用已有特征人为构造了一部分新的特征,所以原来特征已经变成了无用特征,我们需要将其删除,然后导出到文件作为树模型的数据使用。

data = data.drop(['creatDate', 'regDate', 'regionCode','seller','offerType'], axis=1)
data.to_csv(path+'data_for_tree.csv',index = 0)

其中sellerofferType为task2中我们寻找到的出现严重倾斜的特征,我们一并删除。

四、特征的异常值处理之二+归一化处理(供LR和NN等模型使用)

1.异常值处理

因为不同模型使用的数据特征是不同的,所以对于树模型和LR等模型的数据需要分开构造。我们看下之前用箱线图去除过异常值的特征power的数据分布。

data['power'].plot.hist()
plt.show()

其分布如下图所示:
在这里插入图片描述
可以看到我们虽然用箱型图去除掉了部分过大过小的异常值,但是仍然有许多过大的异常值,因此我们如果用归回模型的话,不应该使用箱线图去除异常值,而应该使用如下所示的长尾分布截断。

rule = Train_data['power'] > 375
index = np.arange(Train_data.shape[0])[rule]
Train_data_n = Train_data.drop(index)
Train_data_n['power'].plot.hist()

其可视化结果如下所示:
在这里插入图片描述
从上图可见,power特征的数值已经十分接近正态分布了。

2.归一化处理

因为data中的power特征有过多的异常大值,所以我们之间归一化势必会出现问题,因此我们将其取log缩小其差距,然后在归一化。

min_max_scaler = preprocessing.MinMaxScaler()
data['power'] = np.log(data['power'] +1)                                # +1是为了结果都是正数
data['power'] = ((data['power'] - np.min(data['power'])) / (np.max(data['power']) - np.min(data['power'])))
data['power'].plot.hist()
plt.show()

其中取对数的时候+1是为了结果均为正值,否则我们将无法在可视化图形中表示出来。
其他差异不大的特征不用取对数,直接归一化即可,不再赘述。

data['kilometer'] = ((data['kilometer'] - np.min(data['kilometer'])) /
                        (np.max(data['kilometer']) - np.min(data['kilometer'])))
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'])))

最后看一下特征的形状,确认无误后导出到文件中供LR模型使用。

print(data.shape)
data = pd.get_dummies(data, columns=['model', 'brand', 'bodyType', 'fuelType',
                                     'gearbox', 'notRepairedDamage', 'power_bin'])
print(data.shape)
(199037, 37)
(199037, 368)

get_dummies()函数是将编码方式转换为One-Hot方式,因此特征数量增加了许多是正常的结果。所以我们将其保存起来备用。

写在最后:我觉得LR的训练模型应该使用的是长尾分布截断,而不是箱线图(学习资料是用的箱线图),我猜想可能是作者为了避免之后再次赘述合并训练集测试集等重复操作才这样写的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值