二手车价格预测预测-特征工程

特征工程是比赛中最至关重要的的一块,特别的传统的比赛,大家的模型可能都差不多,调参带来的效果增幅是非常有限的,但特征工程的好坏往往会决定了最终的排名和成绩。

特征工程的主要目的还是在于将数据转换为能更好地表示潜在问题的特征,从而提高机器学习的性能。比如,异常值处理是为了去除噪声,填补缺失值可以加入先验知识等。

特征构造也属于特征工程的一部分,其目的是为了增强数据的表达。
有些比赛的特征是匿名特征,这导致我们并不清楚特征相互直接的关联性,这时我们就只有单纯基于特征进行处理,比如装箱,groupby,agg 等这样一些操作进行一些特征统计,此外还可以对特征进行进一步的 log,exp 等变换,或者对多个特征进行四则运算(如上面我们算出的使用时长),多项式组合等然后进行筛选。由于特性的匿名性其实限制了很多对于特征的处理,当然有些时候用 NN 去提取一些特征也会达到意想不到的良好效果。

对于知道特征含义(非匿名)的特征工程,特别是在工业类型比赛中,会基于信号处理,频域提取,丰度,偏度等构建更为有实际意义的特征,这就是结合背景的特征构建,在推荐系统中也是这样的,各种类型点击率统计,各时段统计,加用户属性的统计等等,这样一种特征构建往往要深入分析背后的业务逻辑或者说物理原理,从而才能更好的找到 magic。

当然特征工程其实是和模型结合在一起的,这就是为什么要为 LR NN 做分桶和特征归一化的原因,而对于特征的处理效果和特征重要性等往往要通过模型来验证。

总的来说,特征工程是一个入门简单,但想精通非常难的一件事。

天池二手车价格预测与数据集下载

1.导入数据

导入第三方工具包

import pandas as pd
import numpy as np
from sklearn import preprocessing
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from operator import itemgetter  # 用于获取对象的位置

导入数据

train_data = pd.read_csv(r'...\used_car_train_20200313.csv', sep=' ')
test_data = pd.read_csv(r'...used_car_testA_20200313.csv', sep=' ')

数据的探索工作可以参考二手车价格预测的数据探索EDA工作,此处略,数据的EDA工作看个人的情况,重点观察数据的整体情况和内在联系!

2.查找异常值并删除
这里参考大佬的一个处理异常值的包装好的函数,可以随意选择调用,原则就是确定正常值的边界范围,可以调整scale的值调整,scale越大,边界值的范围变大,异常值的数量相应地减少!

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:
        '''
        # quantile(0.75) - 求数据的上四分位数 - Q3
        # quantile(0.25) - 求数据的下四分位数 - Q1
        # data_ser.quantile(0.75) - data_ser.quantile(0.25) = Q3 - Q1 = ΔQ --> 四分位距
        '''
        boxplot默认的上边缘到上四分位数的间距是1.5ΔQ,即 scale=1.5

        这里设定的为3ΔQ:
        超过了上边缘Q3+3ΔQ和下边缘Q1-3ΔQ的部分视为异常值
        '''
        iqr = box_scale * (data_ser.quantile(0.75) - data_ser.quantile(0.25))  # iqr - 上边缘到上四分位数的间距,即3ΔQ
        val_low = data_ser.quantile(0.25) - iqr  # 下边缘 Q1-3ΔQ
        val_up = data_ser.quantile(0.75) + iqr  # 上边缘 Q3+3ΔQ
        rule_low = (data_ser < val_low)  # 低于下边缘 Q1-3ΔQ的为异常值
        rule_up = (data_ser > val_up)  # 高于上边缘 Q3+3ΔQ的为异常值
        return (rule_low, rule_up), (val_low, val_up)  # 得到异常值 / 上边缘与下边缘之间的值

    data_n = data.copy()  # 拷贝一份数据的副本
    data_series = data_n[col_name]  # 转化成pandas.Series数据格式
    rule, value = box_plot_outliers(data_series, box_scale=scale)
    # data_series.shape[0] - 看data_series这个一维数组有几行,即原数据集的总列数
    '''
    np.arange() - 函数返回一个有终点和起点的固定步长的排列
                    一个参数时:参数值为终点,起点取默认值0,步长取默认值1
                    两个参数时:第一个参数为起点,第二个参数为终点,步长取默认值1
                    三个参数时:第一个参数为起点,第二个参数为终点,第三个参数为步长,其中步长支持小数
    '''
    # np.arange(data_series.shape[0]) - 取N个数,N为数据集字段数,步长为1  --> 生成的是列表
    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)  # 按索引查找并删除

    '''
    reset_index() - 重塑索引 (因为有时候对dataframe做处理后索引可能是乱的,就像上面删除了异常值一样)

    参数详解:
    drop - True:把原来的索引index列去掉,重置index      False:保留原来的索引,添加重置的index
    inplace - True:原数组不变,对数据进行修改之后结果给新的数组     False:直接在原数组上对数据进行修改
    '''
    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]]  # 挑出位于下异常值区间的序号,放进标记为index_low的列表中
    outliers_low = data_series.iloc[index_low]  # 把位于下异常值区间的数据放进outliers中
    print('Description of data less than the lower bound is: ')
    print(pd.Series(outliers_low).describe())  # 对于位于下异常值区间的数据,做一个统计描述

    index_up = np.arange(data_series.shape[0])[rule[1]]  # 挑出位于上异常值区间的序号,放进标记为index_up的列表中
    outliers_up = data_series.iloc[index_up]  # 把位于上异常值区间的数据放进outliers_up中
    print('Description of data larger than the lower bound is: ')
    print(pd.Series(outliers_up).describe())  # 对于位于上异常值区间的数据,再做一个统计描述

    fig, ax = plt.subplots(1, 2, figsize=(10, 7))

    '''
    sns.boxplot - 箱线图

    参数详解:
    x, y, hue - 数据或向量数据的变量名称
    data - 用于绘图的数据集
    palette - 调色板名称
    ax - 绘图时使用的matplotlib轴对象
    '''
    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

这里需要自己判断异常值是否采取上述的方式处理删除还是选择长尾截断,根据自己的模型和想法决定!

3.特征构造

#把两个数据集都加一列'train'标识,合并之后用来区分数据来源
train_data['train'] = 1
test_data['train'] = 0

'''
pd.concat()参数说明:
ignore_index - 如果两个表的index都没有实际含义,可以令 ignore_index = True
                合并的两个表根据列字段对齐,然后重塑新的索引

sort - 默认为False;
        设置为True时表示合并时会根据给定的列值来进行排序后再输出
'''
data = pd.concat([train_data, test_data], ignore_index=True, sort=False)

构造一个车辆使用时间特征 一般来说:使用时间和价格会呈负相关

'''
车辆使用时间 = 汽车上线时间 - 汽车注册时间
            这里的汽车注册时间应该是二手车交易过户后在登记本上的注册时间,而不是新车首次注册的时间,不然不合逻辑

注意:数据的时间有出错的格式,我们需要设置参数 errors = 'coerce'
    errors='coerce' Pandas - 遇到不能转换的数据就会赋值为 NaN
'''

'''
datetime标准形式:   xxxx - xx - xx
dt.days - 求datetime数据的天数
dt.years - 求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

上述是比较普遍的做法,但我观察到时间的类型都是整数类型,可以通过数值运算获得比较准确的数据,自己尝试后发现是可以的,就是过程周折了点

从邮编中提取城市信息

'''
数据来源德国,因此参考德国的邮编 - 先验知识
'''
data['city'] = data['regionCode'].apply(lambda x: str(x)[:-3])  # [:-3] --> [0:-3] - 左闭右开:取第一个到倒数第4个

计算某品牌的销售统计量 - 计算统计量的时候得以训练集的数据进行统计

train_gb = train_data.groupby('brand')

all_info = {}  # 新建一个字典,用来存放统计量

for kind, kind_data in train_gb:  # kind - 品牌;        # kind_data - 所属品牌对应的数据
    info = {}
    kind_data = kind_data[kind_data['price'] > 0]  # 去掉不合理的价格 - 售价不会小于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)  # 这里数量加了1,防止分母为0
    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') 

数据分桶 - 以power为例

'''
数据分桶的原因:
1.分桶就是离散化,离散后,2333333333333333稀疏向量内积乘法运算速度会更快,计算结果也方便储存     # 这是onehot的好处
2.离散后,特征对异常值更具鲁棒性 - 系统或组织有抵御或克服不利条件的能力 - 常被用来描述可以面对复杂适应系统的能力
3.LR(线性回归)属于广义线性模型,表达能力有限。经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合
4.离散后特征可以进行特征交叉(one-hot编码等),由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力
5.特征离散后模型更稳定,如用户年龄区间不会因为用户年龄长了一岁就变化

'''
bin = [i * 10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels=False)  # 按值切分,bin为区间
data[['power', 'power_bin']]

以上特征处理完了,我们可以删掉原始多余的特征数据,减少运行的速度

data = data.drop(['SaleID', 'creatDate', 'regDate', 'regionCode'], axis=1)

以上将数据输出到自己的本地后便可以用简单的决策树模型来训练了!

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值