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

本文介绍了在天池二手车价格预测任务中进行特征工程的步骤,包括异常值处理(如使用3σ原则和箱型图识别并删除异常值)、数据归一化、数据分桶以及构造新特征。通过新建如使用时间、城市信息、统计信息等特征,增强模型的预测能力。同时,讨论了为何树模型和线性模型在数据预处理上的差异,如线性模型需要数据符合正态分布并进行归一化、标准化和独热编码处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 学习内容
  1. 异常处理

  2. 特征归一化/标准化

  3. 数据分桶

  4. 缺失值处理

  5. 特征构造

  6. 特征筛选

2. 导入相关模块和数据
import pandas as pd
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
 
train_df = pd.read_csv(r'./data/train.csv', sep = ' ')
test_df = pd.read_csv(r'./data/testA.csv', sep = ' ')
 
print(train_df.shape)
print(test_df.shape)
3. 判别异常值

3.1 什么是异常值
异常值指的是在数据集中存在的不合理的值,也叫离群点。结合实际意义或许能更好地理解,比如人年龄为负数,羽毛的重量为1吨等,这些都属于异常值。

3.2 常见的异常值判别方法
通常我们会结合统计情况或者实际情况对某个特征的取值设置一个合理的范围,超出这个范围的值都可以认为是异常值。而不同的异常值判别方法最主要的区别就是范围的选取方式。常见的异常值判别方法有以下几种[1]:

3.2.1 简单统计分析
对特征进行一个描述性的统计,并查看哪些值是不合理的。比如对年龄这个属性进行规约:年龄的区间在[0:200],如果样本中的年龄值不在该区间内,则表示该样本的年龄属性属于异常值。

3.2.2 3σ原则
这个方法适用于服从正太分布的数据。根据正太分布,,这代表是一个小概率事件。如果有数据满足,则说明该数据是一个异常值。
在这里插入图片描述
3.2.3 箱型图
当数据并不服从正态分布时,可以使用此方法来判别异常值。要理解这个方法的原理,首先需要知道箱型图是如何画出来的。

箱线图的绘制方法是:先找出一组数据的最大值、最小值、中位数和两个四分位数;然后,连接两个四分位数画出箱子;再将最大值和最小值与箱子相连接,中位数在箱子中间[2]。不过,这里需要注意的是,为了判别异常值,我们需要人为设置上限和下限并将本该画在最值处的盒须画在上下限处。这样,盒须之外的数据就是异常值了。(实际上,这是两种箱型图。另外,我还发现了一个关于它们的有趣的介绍[3]。)

我们按照从小到大的次序,记第一个、第二个和第三个四分位数分别为Q1、Q2和Q3。记四分位距IQR=Q3-Q1。这样,下限的值就是Q1-k·IQR,上限的值就是Q3+k·IQR。其中k的取值可根据实际情况进行调整,通常默认取1.5。

3.3 异常值处理方法
常见的方法有以下几种:

  1. 删除含有异常值的数据或特征;

  2. 用平均值或中位数来修正;

  3. 将异常值视为缺失值,交给缺失值处理方法来处理;

  4. 不处理。

3.4 异常值处理实现(箱型图+删除异常值)


# 这里是一个异常值处理的函数
def outliers_proc(data, col_name, scale = 3):
    """
    清晰pandas表格中某一列中的异常值,默认用 box_plot(scale=3)进行清洗
    data: 接收pandas表格
    col_name: pandas表格的列名
    scale: 尺度,用来划定数据的上限与下限的
    返回值是处理好的数据
    """
    
    def box_plot_outliers(data_ser, box_scale):
        """
        利用箱线图去除异常值
        data_ser: pandas序列
        param box_scale: 箱型图划定上下限的尺度
        返回值分两部分,第一部分是异常值的位置(用序列表示),第二部分是上下限的具体值
        """
        
        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]]
    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]))
    
    # 输出小于下限和大于上限的异常值的统计数据
    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

对训练数据调用这个函数并设置上下限为3倍的IQR。

train_df = outliers_proc(train_df, 'power', scale = 3)

在这里插入图片描述
左图和右图分别是处理前后的箱型图。可以发现原来许多大于上限的异常值都被去掉了。另外,需要注意的是图中的盒须仍然是画在k=1.5的位置,而非我们自己设置的k=3的位置。

4. 构造新特征并保存数据到文件

4.1 供树模型使用
4.1.1 合并训练集和测试集

train_df['train'] = 1
test_df['train'] = 0
data = pd.concat([train_df, test_df], ignore_index = True, sort = False)
print(data.shape

4.1.2 新建“使用时间”特征

data['creatDate'] = pd.to_datetime(data['creatDate'], \
                                   format = '%Y%m%d', errors = 'coerce')
data['regDate'] = pd.to_datetime(data['regDate'], \
                                 format = '%Y%m%d', errors = 'coerce')
data['used_time'] = (data['creatDate'] - data['regDate']).dt.days

4.1.3 新建“城市信息”特征
这里的城市信息实际上是一种先验知识,也就是处理数据的人已知的正确的知识。加入先验知识有助于提升模型的效果。

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

4.1.4 新建“统计信息”特征(以品牌为例)
代码中使用了pandas的groupby()方法,具体的用法参考[5]。

train_gb_df = train_df.groupby('brand')
 
all_info = {}
# groupby处理后的表格,遍历时有两个参数
# 第一个表示组别编号,第二个表示组别的具体表格
for kind, kind_data in train_gb_df:
    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()
    # round()是以四舍五入的方式计算浮点数的值的函数
    info['brand_price_average'] = round(kind_data.price.sum() / \
                                        (len(kind_data) + 1), 2)
    all_info[kind] = info
brand_fe_df = pd.DataFrame(all_info).T.reset_index().rename(columns = \
                                                            {"index": "brand"})
data = data.merge(brand_fe_df, how = 'left', on = 'brand')

4.1.5 数据分桶(以马力为例)
数据分桶的好处:

  1. 离散后稀疏向量内积乘法运算速度更快,计算结果也方便存储,容易扩展;

  2. 离散后的特征对异常值更具鲁棒性,如 age>30 为 1 否则为 0,对于年龄为 200 的也不会对模型造成很大的干扰;

  3. LR属于广义线性模型,表达能力有限,经过离散化后,每个变量有单独的权重,这相当于引入了非线性,能够提升模型的表达能力,加大拟合;

  4. 离散后特征可以进行特征交叉,提升表达能力,由 M+N 个变量编程 M*N 个变量,进一步引入非线形,提升了表达能力;

  5. 特征离散后模型更稳定,如用户年龄区间,不会因为用户年龄长了一岁就变化。

具体代码如下:

bin = [i * 10 for i in range(31)]
data['power_bin'] = pd.cut(data['power'], bin, labels = False)
data[['power_bin', 'power']].head()

另外,除了使用pandas提供的cut()方法,还可以使用sklearn.preprocessing中提供的KBinsDiscretizer来进行分桶处理[6]。

4.1.6 删除原始特征
data = data.drop([‘creatDate’, ‘regDate’, ‘regionCode’], axis=1)
4.1.7 导出数据到文件
data.to_csv(r’./data/data_for_tree.csv’, index = False)
4.2 供线性模型使用
供线性模型使用的数据相对于供树模型使用的数据要更加复杂。因为,首先,一些树模型可以自动处理空值。因此上文的数据并没有进行空值处理。另外,树模型使用的数据不必做归一化和标准化,也不必进行独热编码。

至于为什么树模型使用的数据不必做归一化和标准化,具体原因可以参考两位大佬的答案:

“树模型也不需要梯度下降之类的来寻找最优解,关心的是变量的概率分布而不是变量的值,所以不需要归一化。”

“树模型的话不处理数据也不太影响, 因为树模型分裂选择特征的时候是基于样本的混乱程度进行分裂的,也就是它是针对每个特征计算合适的分裂点的, 这样特征与特征之间的这种量纲对树分裂影响不大,不过归一化之后应该会收敛的快一点,毕竟数小了嘛。 一般那种需要参数估计的模型才需要归一化或者标准化,理解的话就是因为这种模型一般是wx求和的形式,这样的话如果不统一量纲,那么那个取值范围大的这种肯定就会占优势了啊,变一下子对目标产生的影响就很大。”

因此,供线性模型使用的数据,除了要进行4.1.1到4.1.7的步骤外。还要额外进行空值处理、归一化及标准化和独热编码。空值如何处理可以参考[7],这里不再赘述。如何进行归一化及标准化还有独热编码可以参考[6]。

4.2.1 观察数据分布
线性模型要求数据是服从正太分布的。因此,我们需要观察各特征的分布情况并对不符合正太分布的数据进行转换。

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

在这里插入图片描述

train_df['power'].plot.hist()

在这里插入图片描述
刚刚已经对训练集进行异常值处理了,但是现在还有这么奇怪的分布是因为测试集中的power存在异常值。所以我们其实刚刚测试集中的power异常值不删为好,可以用长尾分布截断来代替。

4.2.2 进行数据归一化
from sklearn.preprocessing import MinMaxScaler
 
min_max_scaler = MinMaxScaler()
# 我们对其取对数,再做归一化
data['power'] = np.log(data['power'] + 1) 
data['power'] = min_max_scaler.fit_transform(data['power'].values.reshape(-1, 1))
data['power'].plot.hist()

在这里插入图片描述
4.2.3 对数据做独热编码

data = pd.get_dummies(data, columns = ['model', 'brand', 'bodyType', 'fuelType',
                                    'gearbox', 'notRepairedDamage', 'power_bin'])
print(data.shape)
data.columns
(199037, 370)
Index(['SaleID', 'name', 'power', 'kilometer', 'seller', 'offerType', 'price',
       'v_0', 'v_1', 'v_2',
       ...
       'power_bin_20.0', 'power_bin_21.0', 'power_bin_22.0', 'power_bin_23.0',
       'power_bin_24.0', 'power_bin_25.0', 'power_bin_26.0', 'power_bin_27.0',
       'power_bin_28.0', 'power_bin_29.0'],
      dtype='object', length=370)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值