“二手车交易价格预测项目”最终报告

小组成员:

宋昊霖(3120191040)

刘聪聪(3220190840)

李泽宁(3220190835)

曹健(3120190978)

张博(3220190916)

1. 问题背景及分析

1.1 问题背景

随着人们生活水平的提高,越来越多的人会选择购买轿车。购买二手车的人也增长迅速,各种各样的二手车交易平台(瓜子二手车,人人车等)也应运而生。由于购买轿车对于大多数普通人来说是一笔很大的支出,所以二手车的价格是一个很重要的考量,怎样根据车辆状况买到价格合适的轿车也是很多人关心的问题。

1.2 项目分析及数据集说明

本项目以利用来自某交易平台的二手车交易记录来预测二手车的交易价格为任务。所采用数据集总数据量超过40w,包含31列变量信息,其中15列为匿名变量。从中抽取出15万条作为训练集,5万条作为测试集A,5万条作为测试集B,同时对name、model、brand和regionCode等敏感信息进行脱敏。

1.3 数据集属性说明

Field Description
SaleID 交易ID,唯一编码
name 汽车交易名称,已脱敏
regDate 汽车注册日期
model 车型编码,已脱敏
brand 汽车品牌,已脱敏
bodyType 车身类型
fuelType 燃油类型
gearbox 变速箱
power 发动机功率
kilometer 汽车已行驶公里
notRepairedDamage 汽车有尚未修复的损坏
regionCode 地区编码,已脱敏
seller 销售方
offerType 报价类型
creatDate 汽车上线时间
price 二手车交易价格(预测目标)
v系列特征 匿名特征,包含v0-14在内15个匿名特征

2. 问题描述

2.1 数据准备

对数据集进行缺失值处理等预处理,并对数据集进行分析(数据总览,分布情况等)及可视化。

2.2 准备采用的方法或模型

此问题为回归问题,我们拟采用两种不同的算法(如线性回归模型,决策树等)对其进行预测,并对预测结果进行对比。

2.3 预期的挖掘结果

利用数据集中的不同属性建立模型并利用模型实现对测试集数据中二手车价格的预测。

2.4 项目评估指标

本项目最后采用MAE(Mean Absolute Error)作为评价标准:

$$MAE = \frac{\sum_{i=1}^{n}\left | y_{i} - \hat{y}_{i}\right |}{n}$$

MAE越小,说明模型预测越准确。

2.5 项目分工

  • 李泽宁:数据分析及预处理,文档编写
  • 张博:算法1实现及分析,文档编写
  • 刘聪聪:算法2实现及分析,文档编写
  • 宋昊霖: 数据可视化,文档编写
  • 曹健: 文档整合与编写

3. 数据分析与可视化

3.1 数据预处理

  • ‘price’为长尾分布,对该特征进行了处理,更加符合高斯分布
  • SaleID为交易ID,肯定没用,但是我们可以用来统计别的特征的group数量
  • name为汽车交易名称,已经脱敏一般没什么好挖掘的,不过同名的好像不少,统计了一下同名数据个数,为name_count,然后删除了属性信息‘name’
  • 'seller'、'offerType'特征严重倾斜,训练数据中'seller'有一个特殊值,删除了该样本,而且把‘seller’和‘offerType’属性删除了
  • 在题目中规定了power范围为[0, 600],对数据集中power属性进行了修正
  • 'notRepairedDamage'属性中'-'应该也是空值,用nan替换
  • 对于缺失值,用众数对缺失值进行了填充
  • 对于时间属性信息,‘regDates’和‘creatDates’,增添了6列属性,分别为‘regDate_year’、‘regDate_month’、‘regDate_day’、‘creatDate_year’、‘creatDate_month’、‘creatDate_day’

3.2 数据分析及可视化过程

  • 通过describe()来熟悉训练数据、测试数据数值属性的相关统计量,包括均值,方差,最小值,第1四分位数,中位数,第3四分位数,最大值。
  • 通过info()来熟悉数据类型
  • 通过可视化方法分析了训练数据、测试数据中的数值属性趋势
  • 分析训练数据、测试数据类别特征nunique分布
  • 分析训练数据、测试数据缺失值情况
  • 对预测属性‘price’ 进行了相关性分析

3.3 数据分析代码及结果

​ 仓库地址:https://github.com/Zening-Li/BIT_DataMining_project

数据集

  • 训练数据:data/used_car_train_20200313.csv
  • 测试数据:data/used_car_testB_20200421.csv
  • 提交数据:data/used_car_sample_submit.csv

代码及结果展示

In [2]:
%matplotlib inline
import pandas as pd
import numpy as np
from datetime import datetime
import matplotlib.pyplot as plt
import seaborn as sns

import warnings

warnings.filterwarnings('ignore')
pd.set_option('display.max_columns', None)
sns.set(style='whitegrid')
plt.style.use('seaborn-darkgrid')
plt.rcParams['font.sans-serif']=['simhei']   # 用黑体显示中文
plt.rcParams['axes.unicode_minus']=False     # 正常显示负号

3.3.1 数据读取

In [3]:
train = pd.read_csv('./data/used_car_train_20200313.csv', sep=' ')
test = pd.read_csv('./data/used_car_testB_20200421.csv', sep=' ')
  • SaleID - 交易ID,唯一编码

  • name - 汽车交易名称

  • regDate - 汽车注册时间

  • model - 车型编码

  • brand - 品牌

  • bodyType - 车身类型

  • fuelType - 燃油类型

  • gearbox - 变速箱

  • power - 汽车功率

  • kilometer - 汽车行驶公里

  • notRepairedDamage - 汽车有尚未修复的损坏

  • regionCode - 地区编码

  • seller - 销售方

  • offerType - 报价类型

  • creatDate - 广告发布时间

  • price - 汽车价格

  • v_0', 'v_1', ..., 'v_14' - 匿名特征,包含v0-14在内15个匿名特征

In [3]:
# 简略查看训练数据
train.head()
Out[3]:
SaleID name regDate model brand bodyType fuelType gearbox power kilometer notRepairedDamage regionCode seller offerType creatDate price v_0 v_1 v_2 v_3 v_4 v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
0 0 736 20040402 30.0 6 1.0 0.0 0.0 60 12.5 0.0 1046 0 0 20160404 1850 43.357796 3.966344 0.050257 2.159744 1.143786 0.235676 0.101988 0.129549 0.022816 0.097462 -2.881803 2.804097 -2.420821 0.795292 0.914762
1 1 2262 20030301 40.0 1 2.0 0.0 0.0 0 15.0 - 4366 0 0 20160309 3600 45.305273 5.236112 0.137925 1.380657 -1.422165 0.264777 0.121004 0.135731 0.026597 0.020582 -4.900482 2.096338 -1.030483 -1.722674 0.245522
2 2 14874 20040403 115.0 15 1.0 0.0 0.0 163 12.5 0.0 2806 0 0 20160402 6222 45.978359 4.823792 1.319524 -0.998467 -0.996911 0.251410 0.114912 0.165147 0.062173 0.027075 -4.846749 1.803559 1.565330 -0.832687 -0.229963
3 3 71865 19960908 109.0 10 0.0 0.0 1.0 193 15.0 0.0 434 0 0 20160312 2400 45.687478 4.492574 -0.050616 0.883600 -2.228079 0.274293 0.110300 0.121964 0.033395 0.000000 -4.509599 1.285940 -0.501868 -2.438353 -0.478699
4 4 111080 20120103 110.0 5 1.0 0.0 0.0 68 5.0 0.0 6977 0 0 20160313 5200 44.383511 2.031433 0.572169 -1.571239 2.246088 0.228036 0.073205 0.091880 0.078819 0.121534 -1.896240 0.910783 0.931110 2.834518 1.923482

数值属性有发动机功率power,汽车已经行驶公里kilometer,二手车交易价格price以及15个匿名特征,而标称属性有汽车交易名称、车型编码、品牌、车身类型、燃油类型、变速箱、汽车有尚未修复损坏、地区编码、销售方、报价类型

In [4]:
train.shape
Out[4]:
(150000, 31)
In [5]:
# 简略查看测试数据
test.head()
Out[5]:
SaleID name regDate model brand bodyType fuelType gearbox power kilometer notRepairedDamage regionCode seller offerType creatDate v_0 v_1 v_2 v_3 v_4 v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
0 200000 133777 20000501 67.0 0 1.0 0.0 0.0 101 15.0 0.0 5019 0 0 20160308 42.142061 -3.094739 -0.721300 1.466344 1.009846 0.236520 0.000241 0.105319 0.046233 0.094522 3.619512 -0.280607 -2.019761 0.978828 0.803322
1 200001 61206 19950211 19.0 6 2.0 0.0 0.0 73 6.0 0.0 1505 0 0 20160310 43.907034 -3.244605 -0.766430 1.276718 -1.065338 0.261518 0.000000 0.120323 0.046784 0.035385 2.997376 -1.406705 -1.020884 -1.349990 -0.200542
2 200002 67829 20090606 5.0 5 4.0 0.0 0.0 120 5.0 - 1776 0 0 20160309 45.389665 3.372384 -0.965565 -2.447316 0.624268 0.261691 0.090836 0.000000 0.079655 0.073586 -3.951084 -0.433467 0.918964 1.634604 1.027173
3 200003 8892 20020601 22.0 9 1.0 0.0 0.0 58 15.0 0.0 26 0 0 20160314 42.788775 4.035052 -0.217403 1.708806 1.119165 0.236050 0.101777 0.098950 0.026830 0.096614 -2.846788 2.800267 -2.524610 1.076819 0.461610
4 200004 76998 20030301 46.0 6 0.0 NaN 0.0 116 15.0 0.0 738 0 0 20160306 43.670763 -3.135382 -1.134107 0.470315 0.134032 0.257000 0.000000 0.066732 0.057771 0.068852 2.839010 -1.659801 -0.924142 0.199423 0.451014
In [6]:
test.shape
Out[6]:
(50000, 30)

3.3.2 数据分析

(1)数值属性分析

In [7]:
# 通过describe()来熟悉训练数据数值属性的相关统计量
train_numeric_features = [
    'power', 'kilometer', 'price', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6',
    'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14'
]
train[train_numeric_features].describe()
Out[7]:
power kilometer price v_0 v_1 v_2 v_3 v_4 v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
count 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000 150000.000000
mean 119.316547 12.597160 5923.327333 44.406268 -0.044809 0.080765 0.078833 0.017875 0.248204 0.044923 0.124692 0.058144 0.061996 -0.001000 0.009035 0.004813 0.000313 -0.000688
std 177.168419 3.919576 7501.998477 2.457548 3.641893 2.929618 2.026514 1.193661 0.045804 0.051743 0.201410 0.029186 0.035692 3.772386 3.286071 2.517478 1.288988 1.038685
min 0.000000 0.500000 11.000000 30.451976 -4.295589 -4.470671 -7.275037 -4.364565 0.000000 0.000000 0.000000 0.000000 0.000000 -9.168192 -5.558207 -9.639552 -4.153899 -6.546556
25% 75.000000 12.500000 1300.000000 43.135799 -3.192349 -0.970671 -1.462580 -0.921191 0.243615 0.000038 0.062474 0.035334 0.033930 -3.722303 -1.951543 -1.871846 -1.057789 -0.437034
50% 110.000000 15.000000 3250.000000 44.610266 -3.052671 -0.382947 0.099722 -0.075910 0.257798 0.000812 0.095866 0.057014 0.058484 1.624076 -0.358053 -0.130753 -0.036245 0.141246
75% 150.000000 15.000000 7700.000000 46.004721 4.000670 0.241335 1.565838 0.868758 0.265297 0.102009 0.125243 0.079382 0.087491 2.844357 1.255022 1.776933 0.942813 0.680378
max 19312.000000 15.000000 99999.000000 52.304178 7.320308 19.035496 9.854702 6.829352 0.291838 0.151420 1.404936 0.160791 0.222787 12.357011 18.819042 13.847792 11.147669 8.658418
In [8]:
# 通过describe()来熟悉测试数据数值属性的相关统计量
test_numeric_features = [
    'power', 'kilometer', 'v_0', 'v_1', 'v_2', 'v_3', 'v_4', 'v_5', 'v_6',
    'v_7', 'v_8', 'v_9', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14'
]
test[test_numeric_features].describe()
Out[8]:
power kilometer v_0 v_1 v_2 v_3 v_4 v_5 v_6 v_7 v_8 v_9 v_10 v_11 v_12 v_13 v_14
count 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000 50000.000000
mean 119.766960 12.598260 44.400023 -0.065525 0.079706 0.078381 0.022361 0.248147 0.044624 0.124693 0.058198 0.062113 0.019633 0.002759 0.004342 0.004570 -0.007209
std 206.313348 3.912519 2.459920 3.636631 2.930829 2.019136 1.194215 0.045836 0.051664 0.201440 0.029171 0.035723 3.764095 3.289523 2.515912 1.287194 1.044718
min 0.000000 0.500000 31.122325 -4.231855 -4.032142 -5.801254 -4.233626 0.000000 0.000000 0.000000 0.000000 0.000000 -9.119719 -5.662163 -8.291868 -4.157649 -6.098192
25% 75.000000 12.500000 43.120935 -3.193169 -0.967832 -1.456793 -0.922153 0.243436 0.000035 0.062519 0.035413 0.033880 -3.675196 -1.963928 -1.865406 -1.048722 -0.440706
50% 110.000000 15.000000 44.601493 -3.053506 -0.384910 0.118448 -0.068187 0.257818 0.000801 0.095880 0.056804 0.058749 1.632134 -0.375537 -0.138943 -0.036352 0.136849
75% 150.000000 15.000000 45.987018 3.978703 0.239689 1.563490 0.871565 0.265263 0.101654 0.125470 0.079387 0.087624 2.846205 1.263451 1.775632 0.945239 0.685555
max 19211.000000 15.000000 51.676686 7.190759 18.865988 9.386558 4.959106 0.291176 0.153403 1.411559 0.157458 0.211304 12.177864 18.789496 13.384828 5.635374 2.649768
In [9]:
# 通过info()来熟悉训练数据类型
train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 150000 entries, 0 to 149999
Data columns (total 31 columns):
SaleID               150000 non-null int64
name                 150000 non-null int64
regDate              150000 non-null int64
model                149999 non-null float64
brand                150000 non-null int64
bodyType             145494 non-null float64
fuelType             141320 non-null float64
gearbox              144019 non-null float64
power                150000 non-null int64
kilometer            150000 non-null float64
notRepairedDamage    150000 non-null object
regionCode           150000 non-null int64
seller               150000 non-null int64
offerType            150000 non-null int64
creatDate            150000 non-null int64
price                150000 non-null int64
v_0                  150000 non-null float64
v_1                  150000 non-null float64
v_2                  150000 non-null float64
v_3                  150000 non-null float64
v_4                  150000 non-null float64
v_5                  150000 non-null float64
v_6                  150000 non-null float64
v_7                  150000 non-null float64
v_8                  150000 non-null float64
v_9                  150000 non-null float64
v_10                 150000 non-null float64
v_11                 150000 non-null float64
v_12                 150000 non-null float64
v_13                 150000 non-null float64
v_14                 150000 non-null float64
dtypes: float64(20), int64(10), object(1)
memory usage: 35.5+ MB
In [10]:
# 可视化训练数据中的数值属性
f_train = pd.melt(train, value_vars=train_numeric_features)
g_train = sns.FacetGrid(f_train, col="variable",  col_wrap=3, sharex=False, sharey=False)
g_train = g_train.map(sns.distplot, "value")
plt.show()

'price'为长尾分布,需要对其做数据转换

'power'应该存在异常值,需要处理

In [11]:
# 可视化测试数据中的数值属性
f_test = pd.melt(test, value_vars=test_numeric_features)
g_test = sns.FacetGrid(f_test, col="variable",  col_wrap=3, sharex=False, sharey=False)
g_test = g_test.map(sns.distplot, "value")
plt.show()

(2) 类别属性分析

In [12]:
# 分析训练数据类别特征nunique分布
category_features = [
    'name', 'model', 'brand', 'bodyType', 'fuelType', 'gearbox',
    'notRepairedDamage', 'regionCode', 'seller', 'offerType', 'regionCode'
]
for feature in category_features:
    print(feature + "的特征分布如下:")
    print("{}特征有个{}不同的值".format(feature, train[feature].nunique()))
    print(train[feature].value_counts())
    print(72 * '-')
name的特征分布如下:
name特征有个99662不同的值
708       282
387       282
55        280
1541      263
203       233
53        221
713       217
290       197
1186      184
911       182
2044      176
1513      160
1180      158
631       157
893       153
2765      147
473       141
1139      137
1108      132
444       129
306       127
2866      123
2402      116
533       114
1479      113
422       113
4635      110
725       110
964       109
1373      104
         ... 
89083       1
95230       1
164864      1
173060      1
179207      1
181256      1
185354      1
25564       1
19417       1
189324      1
162719      1
191373      1
193422      1
136082      1
140180      1
144278      1
146327      1
148376      1
158621      1
1404        1
15319       1
46022       1
64463       1
976         1
3025        1
5074        1
7123        1
11221       1
13270       1
174485      1
Name: name, Length: 99662, dtype: int64
------------------------------------------------------------------------
model的特征分布如下:
model特征有个248不同的值
0.0      11762
19.0      9573
4.0       8445
1.0       6038
29.0      5186
48.0      5052
40.0      4502
26.0      4496
8.0       4391
31.0      3827
13.0      3762
17.0      3121
65.0      2730
49.0      2608
46.0      2454
30.0      2342
44.0      2195
5.0       2063
10.0      2004
21.0      1872
73.0      1789
11.0      1775
23.0      1696
22.0      1524
69.0      1522
63.0      1469
7.0       1460
16.0      1349
88.0      1309
66.0      1250
         ...  
141.0       37
133.0       35
216.0       30
202.0       28
151.0       26
226.0       26
231.0       23
234.0       23
233.0       20
198.0       18
224.0       18
227.0       17
237.0       17
220.0       16
230.0       16
239.0       14
223.0       13
236.0       11
241.0       10
232.0       10
229.0       10
235.0        7
246.0        7
243.0        4
244.0        3
245.0        2
209.0        2
240.0        2
242.0        2
247.0        1
Name: model, Length: 248, dtype: int64
------------------------------------------------------------------------
brand的特征分布如下:
brand特征有个40不同的值
0     31480
4     16737
14    16089
10    14249
1     13794
6     10217
9      7306
5      4665
13     3817
11     2945
3      2461
7      2361
16     2223
8      2077
25     2064
27     2053
21     1547
15     1458
19     1388
20     1236
12     1109
22     1085
26      966
30      940
17      913
24      772
28      649
32      592
29      406
37      333
2       321
31      318
18      316
36      228
34      227
33      218
23      186
35      180
38       65
39        9
Name: brand, dtype: int64
------------------------------------------------------------------------
bodyType的特征分布如下:
bodyType特征有个8不同的值
0.0    41420
1.0    35272
2.0    30324
3.0    13491
4.0     9609
5.0     7607
6.0     6482
7.0     1289
Name: bodyType, dtype: int64
------------------------------------------------------------------------
fuelType的特征分布如下:
fuelType特征有个7不同的值
0.0    91656
1.0    46991
2.0     2212
3.0      262
4.0      118
5.0       45
6.0       36
Name: fuelType, dtype: int64
------------------------------------------------------------------------
gearbox的特征分布如下:
gearbox特征有个2不同的值
0.0    111623
1.0     32396
Name: gearbox, dtype: int64
------------------------------------------------------------------------
notRepairedDamage的特征分布如下:
notRepairedDamage特征有个3不同的值
0.0    111361
-       24324
1.0     14315
Name: notRepairedDamage, dtype: int64
------------------------------------------------------------------------
regionCode的特征分布如下:
regionCode特征有个7905不同的值
419     369
764     258
125     137
176     136
462     134
428     132
24      130
1184    130
122     129
828     126
70      125
827     120
207     118
1222    117
2418    117
85      116
2615    115
2222    113
759     112
188     111
1757    110
1157    109
2401    107
1069    107
3545    107
424     107
272     107
451     106
450     105
129     105
       ... 
6324      1
7372      1
7500      1
8107      1
2453      1
7942      1
5135      1
6760      1
8070      1
7220      1
8041      1
8012      1
5965      1
823       1
7401      1
8106      1
5224      1
8117      1
7507      1
7989      1
6505      1
6377      1
8042      1
7763      1
7786      1
6414      1
7063      1
4239      1
5931      1
7267      1
Name: regionCode, Length: 7905, dtype: int64
------------------------------------------------------------------------
seller的特征分布如下:
seller特征有个2不同的值
0    149999
1         1
Name: seller, dtype: int64
------------------------------------------------------------------------
offerType的特征分布如下:
offerType特征有个1不同的值
0    150000
Name: offerType, dtype: int64
------------------------------------------------------------------------
regionCode的特征分布如下:
regionCode特征有个7905不同的值
419     369
764     258
125     137
176     136
462     134
428     132
24      130
1184    130
122     129
828     126
70      125
827     120
207     118
1222    117
2418    117
85      116
2615    115
2222    113
759     112
188     111
1757    110
1157    109
2401    107
1069    107
3545    107
424     107
272     107
451     106
450     105
129     105
       ... 
6324      1
7372      1
7500      1
8107      1
2453      1
7942      1
5135      1
6760      1
8070      1
7220      1
8041      1
8012      1
5965      1
823       1
7401      1
8106      1
5224      1
8117      1
7507      1
7989      1
6505      1
6377      1
8042      1
7763      1
7786      1
6414      1
7063      1
4239      1
5931      1
7267      1
Name: regionCode, Length: 7905, dtype: int64
------------------------------------------------------------------------
In [13]:
# 对训练数据类别特征取值较少的,画出直方图
plt.figure(figsize=(15, 12))
i = 1
for feature in category_features:
    if train[feature].nunique() < 50:
        plt.subplot(4, 2, i)
        i += 1
        v = train[feature].value_counts()
        fig = sns.barplot(x=v.index, y=v.values)
        plt.title(feature)
plt.tight_layout()
plt.show()
In [14]:
# 分析测试数据类别特征nunique分布
for feature in category_features:
    print(feature + "的特征分布如下:")
    print("{}特征有个{}不同的值".format(feature, test[feature].nunique()))
    print(test[feature].value_counts())
    print(72 * '-')
name的特征分布如下:
name特征有个37536不同的值
387       94
55        93
1541      86
708       85
203       78
713       75
911       72
1180      71
53        68
290       68
631       67
1186      60
473       54
306       53
2866      52
2044      50
422       49
893       47
1513      46
2765      45
533       44
964       44
1139      41
1479      41
2825      38
444       37
4635      37
984       37
282       35
691       33
          ..
9747       1
7857       1
75120      1
144754     1
15731      1
66932      1
76360      1
66082      1
89231      1
93561      1
161146     1
21886      1
42368      1
101765     1
89653      1
38278      1
89645      1
60809      1
62858      1
195979     1
185951     1
81299      1
168479     1
28057      1
30106      1
97691      1
155039     1
44449      1
112034     1
105129     1
Name: name, Length: 37536, dtype: int64
------------------------------------------------------------------------
model的特征分布如下:
model特征有个245不同的值
0.0      3772
19.0     3226
4.0      2790
1.0      1981
29.0     1778
48.0     1711
40.0     1524
26.0     1512
8.0      1464
31.0     1281
13.0     1214
17.0     1033
65.0      918
49.0      880
46.0      871
30.0      793
44.0      731
5.0       677
21.0      628
10.0      625
23.0      583
11.0      562
73.0      561
69.0      531
63.0      515
16.0      506
22.0      482
7.0       442
88.0      416
66.0      395
         ... 
157.0      12
151.0      12
141.0      12
193.0      12
89.0       12
68.0       11
233.0      11
226.0      11
133.0      11
227.0       8
198.0       8
18.0        8
224.0       7
237.0       7
239.0       6
231.0       6
235.0       6
220.0       6
246.0       4
234.0       4
230.0       4
223.0       3
236.0       3
232.0       3
245.0       3
229.0       2
209.0       2
242.0       1
241.0       1
244.0       1
Name: model, Length: 245, dtype: int64
------------------------------------------------------------------------
brand的特征分布如下:
brand特征有个40不同的值
0     10473
4      5532
14     5345
10     4713
1      4627
6      3500
9      2360
5      1485
13     1386
11      942
3       820
16      770
25      728
7       727
8       708
27      623
21      543
15      476
19      473
20      411
12      399
22      358
26      328
30      321
17      312
24      248
28      216
32      183
29      139
37      117
2       115
31      113
18      107
33       84
35       75
34       75
36       72
23       60
38       31
39        5
Name: brand, dtype: int64
------------------------------------------------------------------------
bodyType的特征分布如下:
bodyType特征有个8不同的值
0.0    13765
1.0    11960
2.0     9886
3.0     4491
4.0     3258
5.0     2494
6.0     2212
7.0      430
Name: bodyType, dtype: int64
------------------------------------------------------------------------
fuelType的特征分布如下:
fuelType特征有个7不同的值
0.0    30489
1.0    15708
2.0      736
3.0       78
4.0       31
5.0       18
6.0       16
Name: fuelType, dtype: int64
------------------------------------------------------------------------
gearbox的特征分布如下:
gearbox特征有个2不同的值
0.0    37131
1.0    10901
Name: gearbox, dtype: int64
------------------------------------------------------------------------
notRepairedDamage的特征分布如下:
notRepairedDamage特征有个3不同的值
0.0    37224
-       8069
1.0     4707
Name: notRepairedDamage, dtype: int64
------------------------------------------------------------------------
regionCode的特征分布如下:
regionCode特征有个6998不同的值
419     120
764      98
176      48
3304     45
85       45
2222     45
3545     44
462      42
1000     42
2154     42
24       41
2775     41
70       41
309      40
1688     40
188      40
792      40
955      39
172      39
3573     39
122      39
759      38
60       38
2418     38
256      38
1483     38
2690     37
125      37
827      37
450      37
       ... 
1521      1
7602      1
5523      1
7538      1
5459      1
7410      1
6630      1
6374      1
6342      1
1010      1
6897      1
5104      1
7089      1
4069      1
6993      1
2052      1
4944      1
2867      1
4912      1
2771      1
6310      1
6865      1
6833      1
4656      1
6609      1
2451      1
4231      1
6513      1
6481      1
6061      1
Name: regionCode, Length: 6998, dtype: int64
------------------------------------------------------------------------
seller的特征分布如下:
seller特征有个1不同的值
0    50000
Name: seller, dtype: int64
------------------------------------------------------------------------
offerType的特征分布如下:
offerType特征有个1不同的值
0    50000
Name: offerType, dtype: int64
------------------------------------------------------------------------
regionCode的特征分布如下:
regionCode特征有个6998不同的值
419     120
764      98
176      48
3304     45
85       45
2222     45
3545     44
462      42
1000     42
2154     42
24       41
2775     41
70       41
309      40
1688     40
188      40
792      40
955      39
172      39
3573     39
122      39
759      38
60       38
2418     38
256      38
1483     38
2690     37
125      37
827      37
450      37
       ... 
1521      1
7602      1
5523      1
7538      1
5459      1
7410      1
6630      1
6374      1
6342      1
1010      1
6897      1
5104      1
7089      1
4069      1
6993      1
2052      1
4944      1
2867      1
4912      1
2771      1
6310      1
6865      1
6833      1
4656      1
6609      1
2451      1
4231      1
6513      1
6481      1
6061      1
Name: regionCode, Length: 6998, dtype: int64
------------------------------------------------------------------------
In [15]:
# 对测试数据类别特征取值较少的,画出直方图
plt.figure(figsize=(15, 12))
i = 1
for feature in category_features:
    if test[feature].nunique() < 50:
        plt.subplot(4, 2, i)
        i += 1
        v = test[feature].value_counts()
        fig = sns.barplot(x=v.index, y=v.values)
        plt.title(feature)
plt.tight_layout()
plt.show()

可以发现两个类别特征严重倾斜,分别是销售方seller和报价类型offerType,上述两个特征对分析预测没有任何帮助。

另外,汽车有尚未修复的损坏notRepairedDamage有3个不同的属性值:0.0,1.0,'-',其中'-'应该也为空值。

3.3.3 数据缺失值

In [4]:
# 'notRepairedDamage'属性中'-'应该也是空值,用nan替换
train['notRepairedDamage'].replace('-', np.nan, inplace=True)
test['notRepairedDamage'].replace('-', np.nan, inplace=True)
In [5]:
# 分析训练数据缺失值
train.isnull().sum()[train.isnull().sum() > 0]
Out[5]:
model                    1
bodyType              4506
fuelType              8680
gearbox               5981
notRepairedDamage    24324
dtype: int64
In [6]:
# 训练数据nan可视化
train_missing = train.isnull().sum()
train_missing = train_missing[train_missing > 0]
train_missing.sort_values(inplace=True)
train_missing.plot.bar()
plt.title("训练数据nan可视化")
plt.show()
In [7]:
# 分析测试数据缺失值
test.isnull().sum()[test.isnull().sum() > 0]
Out[7]:
bodyType             1504
fuelType             2924
gearbox              1968
notRepairedDamage    8069
dtype: int64
In [8]:
# 测试数据nan可视化
test_missing = test.isnull().sum()
test_missing = test_missing[test_missing > 0]
test_missing.sort_values(inplace=True)
test_missing.plot.bar()
plt.title("测试数据nan可视化")
plt.show()

3.3.4 相关性分析

In [20]:
# 对'price'属性进行相关性分析
price_numeric = train[train_numeric_features]
correlation = price_numeric.corr()
print(correlation['price'].sort_values(ascending = False),'\n')
price        1.000000
v_12         0.692823
v_8          0.685798
v_0          0.628397
power        0.219834
v_5          0.164317
v_2          0.085322
v_6          0.068970
v_1          0.060914
v_14         0.035911
v_13        -0.013993
v_7         -0.053024
v_4         -0.147085
v_9         -0.206205
v_10        -0.246175
v_11        -0.275320
kilometer   -0.440519
v_3         -0.730946
Name: price, dtype: float64 

In [21]:
f, ax = plt.subplots(figsize=(8, 8))
plt.title('Correlation of Numeric Features with Price', y=1, size=16)
sns.heatmap(correlation, square=True, vmax=0.8, linewidths=0.1, cmap=sns.cm.rocket_r)
plt.show()

匿名特征v_0, v_3, v_8, v_12与'price'相关性很高

3.4 数据预处理

  • 预处理后数据: process_data/train_data_v1.csv,process_data/test_data_v1.csv

3.4.1 处理目标值长尾分布

In [22]:
# price 为长尾分布,对该特征进行处理
train['price'] = np.log1p(train['price'])

# 可视化处理后'price'分布
plt.figure(figsize=(10, 8))
sns.distplot(train['price'])
plt.show()

3.4.2 处理无用值

In [23]:
# 合并训练数据和测试数据,方便后续数据预处理
df = pd.concat([train, test], axis=0, ignore_index=True)
df.head()
Out[23]:
SaleID bodyType brand creatDate fuelType gearbox kilometer model name notRepairedDamage offerType power price regDate regionCode seller v_0 v_1 v_10 v_11 v_12 v_13 v_14 v_2 v_3 v_4 v_5 v_6 v_7 v_8 v_9
0 0 1.0 6 20160404 0.0 0.0 12.5 30.0 736 0.0 0 60 7.523481 20040402 1046 0 43.357796 3.966344 -2.881803 2.804097 -2.420821 0.795292 0.914762 0.050257 2.159744 1.143786 0.235676 0.101988 0.129549 0.022816 0.097462
1 1 2.0 1 20160309 0.0 0.0 15.0 40.0 2262 - 0 0 8.188967 20030301 4366 0 45.305273 5.236112 -4.900482 2.096338 -1.030483 -1.722674 0.245522 0.137925 1.380657 -1.422165 0.264777 0.121004 0.135731 0.026597 0.020582
2 2 1.0 15 20160402 0.0 0.0 12.5 115.0 14874 0.0 0 163 8.736007 20040403 2806 0 45.978359 4.823792 -4.846749 1.803559 1.565330 -0.832687 -0.229963 1.319524 -0.998467 -0.996911 0.251410 0.114912 0.165147 0.062173 0.027075
3 3 0.0 10 20160312 0.0 1.0 15.0 109.0 71865 0.0 0 193 7.783641 19960908 434 0 45.687478 4.492574 -4.509599 1.285940 -0.501868 -2.438353 -0.478699 -0.050616 0.883600 -2.228079 0.274293 0.110300 0.121964 0.033395 0.000000
4 4 1.0 5 20160313 0.0 0.0 5.0 110.0 111080 0.0 0 68 8.556606 20120103 6977 0 44.383511 2.031433 -1.896240 0.910783 0.931110 2.834518 1.923482 0.572169 -1.571239 2.246088 0.228036 0.073205 0.091880 0.078819 0.121534
In [24]:
# SaleID为交易ID,肯定没用,但是我们可以用来统计别的特征的group数量
# name为汽车交易名称,已经脱敏一般没什么好挖掘的,不过同名的好像不少,可以挖掘一下
df['name_count'] = df.groupby(['name'])['SaleID'].transform('count')
df.drop(['name'], axis=1, inplace=True)

3.4.3 处理特征严重倾斜数据

In [25]:
# 'seller'、'offerType'特征严重倾斜,训练数据中'seller'有一个特殊值,删除该样本
df.drop(df[df['seller'] == 1].index, inplace=True)

df.drop(['seller'], inplace=True, axis=1)
df.drop(['offerType'], inplace=True, axis=1)

3.4.4 处理异常值

In [26]:
# 在题目中规定了power范围为[0, 600]
df['power'] = df['power'].map(lambda x: 600 if x > 600 else x)
In [30]:
# 可视化处理后'power'分布
plt.figure(figsize=(10, 8))
sns.distplot(df['power'])
plt.show()

3.4.5 处理缺失值

In [27]:
# 查看缺失值
df.isnull().sum()[df.isnull().sum() > 0]
Out[27]:
bodyType              6010
fuelType             11604
gearbox               7949
model                    1
notRepairedDamage    32392
price                50000
dtype: int64
In [28]:
# 用众数填充缺失值
df.fuelType.fillna(df.fuelType.mode()[0], inplace=True)
df.gearbox.fillna(df.gearbox.mode()[0], inplace=True)
df.bodyType.fillna(df.bodyType.mode()[0], inplace=True)
df.model.fillna(df.model.mode()[0], inplace=True)
df.notRepairedDamage.fillna(df.notRepairedDamage.mode()[0], inplace=True)
In [29]:
df.isnull().sum()[df.isnull().sum() > 0]
Out[29]:
price    50000
dtype: int64

df是由训练数据和测试数据合并而来,测试数据有50000个样本,预测特征是price,因此df中存在50000个缺失price特征的样本

3.4.6 处理时间属性信息

In [31]:
def date_process(x):
    year = int(str(x)[:4])
    month = int(str(x)[4:6])
    day = int(str(x)[6:8])

    if month < 1:
        month = 1

    date = datetime(year, month, day)
    return date
In [32]:
df['regDates'] = df['regDate'].apply(date_process)
df['creatDates'] = df['creatDate'].apply(date_process)
df['regDate_year'] = df['regDates'].dt.year
df['regDate_month'] = df['regDates'].dt.month
df['regDate_day'] = df['regDates'].dt.day
df['creatDate_year'] = df['creatDates'].dt.year
df['creatDate_month'] = df['creatDates'].dt.month
df['creatDate_day'] = df['creatDates'].dt.day
In [33]:
# 切割数据,导出数据
output_path = './process_data/'
print(df.shape)
train_num = df.shape[0] - 50000
df[:int(train_num)].to_csv(output_path + 'train_data_v1.csv', index=False, sep=' ')
df[train_num:train_num + 50000].to_csv(output_path + 'test_data_v1.csv', index=False, sep=' ')
(199999, 37)

4. 算法一的实验结果及可视化

4.1 算法说明

数据集

代码实现

algorithm1_final.ipynb

说明

  • 算法1使用的训练模型为线性回归模型,评测标准为MAE(Mean Absolute Error)
  • 将数据集data/train_data_v1.csv中的数据拆分为训练集与测试集两部分,进行训练与评测
  • 使用数据集data/test_data_v1.csv进行比赛结果预测
  • 比赛预测结果按照指定格式生成output/submit.csv文件以便提交

4.2 可视化说明

代码实现

​ 见下方所示

可视化过程

  • 构建训练和测试数据集,切分数据集(Train, Val)使用线性回归模型进行模型训练,使用MAE标准进行评价,并进行模型预测
  • 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察,可以看到线性回归模型进行预测时对于同属性值的高price和低price预测较差,更倾向于预测为中间大小price
  • 绘制模型学习率曲线及验证曲线
  • 将预测值生成指定格式的csv文件

4.3 实验代码及结果可视化

In [20]:
import numpy as np
import pandas as pd
import seaborn as sns
import lightgbm as lgb
import matplotlib.pyplot as plt
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, mean_absolute_error
from sklearn.linear_model import LinearRegression, Ridge, LassoCV
from sklearn.model_selection import train_test_split, GridSearchCV

%matplotlib inline

读取预处理好的数据:

In [4]:
# train_data = pd.read_csv('data/train_data_v1.csv', sep=' ')
# test_data = pd.read_csv('data/test_data_v1.csv', sep=' ')
train_data = pd.read_csv(r"C:\Users\shl\Desktop\train_data_v1.csv", sep=' ')
test_data = pd.read_csv(r"C:\Users\shl\Desktop\test_data_v1.csv", sep=' ')
print(train_data.shape)
print(test_data.shape)
(149999, 37)
(50000, 37)

查看训练数据:

In [3]:
train_data.head()
Out[3]:
SaleID bodyType brand creatDate fuelType gearbox kilometer model notRepairedDamage power ... v_9 name_count regDates creatDates regDate_year regDate_month regDate_day creatDate_year creatDate_month creatDate_day
0 0 1.0 6 20160404 0.0 0.0 12.5 30.0 0.0 60 ... 0.097462 108 2004-04-02 2016-04-04 2004 4 2 2016 4 4
1 1 2.0 1 20160309 0.0 0.0 15.0 40.0 0.0 0 ... 0.020582 29 2003-03-01 2016-03-09 2003 3 1 2016 3 9
2 2 1.0 15 20160402 0.0 0.0 12.5 115.0 0.0 163 ... 0.027075 3 2004-04-03 2016-04-02 2004 4 3 2016 4 2
3 3 0.0 10 20160312 0.0 1.0 15.0 109.0 0.0 193 ... 0.000000 2 1996-09-08 2016-03-12 1996 9 8 2016 3 12
4 4 1.0 5 20160313 0.0 0.0 5.0 110.0 0.0 68 ... 0.121534 1 2012-01-03 2016-03-13 2012 1 3 2016 3 13

5 rows × 37 columns

查看数据的统计信息:

In [4]:
train_data.describe()
Out[4]:
SaleID bodyType brand creatDate fuelType gearbox kilometer model notRepairedDamage power ... v_7 v_8 v_9 name_count regDate_year regDate_month regDate_day creatDate_year creatDate_month creatDate_day
count 149999.000000 149999.000000 149999.000000 1.499990e+05 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 ... 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000 149999.000000
mean 74999.493837 1.738525 8.052727 2.016033e+07 0.354096 0.215975 12.597144 47.128581 0.095434 116.861752 ... 0.124693 0.058144 0.061995 16.621251 2003.357196 5.998393 6.502863 2015.999880 3.161581 15.833826
std 43301.558800 1.760784 7.864982 1.067332e+02 0.539748 0.411498 3.919584 49.536165 0.293814 70.074840 ... 0.201410 0.029185 0.035692 48.697958 5.362246 3.521590 3.450316 0.010954 0.380710 9.132285
min 0.000000 0.000000 0.000000 2.015062e+07 0.000000 0.000000 0.500000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 1.000000 1991.000000 1.000000 1.000000 2015.000000 1.000000 1.000000
25% 37499.500000 0.000000 1.000000 2.016031e+07 0.000000 0.000000 12.500000 10.000000 0.000000 75.000000 ... 0.062474 0.035334 0.033930 1.000000 1999.000000 3.000000 4.000000 2016.000000 3.000000 8.000000
50% 74999.000000 1.000000 6.000000 2.016032e+07 0.000000 0.000000 15.000000 30.000000 0.000000 110.000000 ... 0.095867 0.057014 0.058483 1.000000 2003.000000 6.000000 7.000000 2016.000000 3.000000 16.000000
75% 112499.500000 3.000000 13.000000 2.016033e+07 1.000000 0.000000 15.000000 66.000000 0.000000 150.000000 ... 0.125243 0.079382 0.087489 7.000000 2007.000000 9.000000 9.000000 2016.000000 3.000000 24.000000
max 149999.000000 7.000000 39.000000 2.016041e+07 6.000000 1.000000 15.000000 247.000000 1.000000 600.000000 ... 1.404936 0.160791 0.222787 376.000000 2015.000000 12.000000 12.000000 2016.000000 12.000000 31.000000

8 rows × 35 columns

提取所需特征列:

In [5]:
numerical_cols = train_data.select_dtypes(exclude = 'object').columns
print(numerical_cols)
Index(['SaleID', 'bodyType', 'brand', 'creatDate', 'fuelType', 'gearbox',
       'kilometer', 'model', 'notRepairedDamage', 'power', 'price', 'regDate',
       'regionCode', 'v_0', 'v_1', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14',
       'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 'name_count',
       'regDate_year', 'regDate_month', 'regDate_day', 'creatDate_year',
       'creatDate_month', 'creatDate_day'],
      dtype='object')
In [6]:
feature_cols = [x for x in train_data.columns if x not in ['SaleID','name','regDates','creatDates','price','model','brand','regionCode','creatDate']]

构建数据集并进行划分(Train, Val):

In [7]:
train_X = train_data[feature_cols]
test_X = test_data[feature_cols]
train_Y = train_data['price']
In [8]:
print('X train shape:',train_X.shape)
print('X test shape:',test_X.shape)
print('Y train shape:',train_Y.shape)
X train shape: (149999, 29)
X test shape: (50000, 29)
Y train shape: (149999,)
In [8]:
x_train, x_val, y_train, y_val = train_test_split(train_X, train_Y, test_size=0.3)

通过五种模型进行训练,并用MAE评价标准进行比较:

In [13]:
# 线性回归
model_1 = LinearRegression()
model_1.fit(x_train, y_train)
pred_1 = model_1.predict(x_val)
mae_1 = mean_absolute_error(y_val, pred_1)
print('MAE = ', mae_1)
MAE =  0.1941119709880854
In [15]:
# 可视化预测结果
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_val[i], y_val, color='red')
    plt.scatter(x_val[i], pred_1, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()
In [10]:
# 岭回归
model_2 = Ridge(alpha=0.8)
model_2.fit(x_train, y_train)
pred_2 = model_2.predict(x_val)
mae_2 = mean_absolute_error(y_val, pred_2)
print('MAE = ', mae_2)
MAE =  0.19768918894059345
In [16]:
# 可视化预测结果
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_val[i], y_val, color='red')
    plt.scatter(x_val[i], pred_2, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()
In [11]:
# Lasso回归
model_3 = LassoCV()
model_3.fit(x_train, y_train)
pred_3 = model_3.predict(x_val)
mae_3 = mean_absolute_error(y_val, pred_3)
print('MAE = ', mae_3)
MAE =  0.603703436321199
In [17]:
# 可视化预测结果
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_val[i], y_val, color='red')
    plt.scatter(x_val[i], pred_3, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()

从可视化中可以看出Lasso回归的预测price值全是中间值,高price和低price很少。可能是由于该方法是一种压缩估计,压缩了一些回归系数,得到的是一个较为精炼的模型。从MAE值也可以看出,Lasso回归是最高的。

In [12]:
# GDBT
gdbt = GradientBoostingRegressor()
gdbt.fit(x_train, y_train)
pred_4 = gdbt.predict(x_val)
mae_4 = mean_absolute_error(y_val, pred_4)
print('MAE = ', mae_4)
MAE =  0.17660262801012233
In [18]:
# 可视化预测结果
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_val[i], y_val, color='red')
    plt.scatter(x_val[i], pred_4, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()
In [21]:
# LightGBM
estimator = lgb.LGBMRegressor(num_leaves=63, n_estimators=100)
param_grid = {
    'learning_rate': [0.01, 0.05, 0.1],
    }
gbm = GridSearchCV(estimator, param_grid)
gbm.fit(x_train, y_train)
pred_5 = gbm.predict(x_val)
mae_5 = mean_absolute_error(y_val, pred_5)
print('MAE = ', mae_5)
MAE =  0.1371255612762281
In [22]:
# 可视化预测结果
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_val[i], y_val, color='red')
    plt.scatter(x_val[i], pred_5, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()

LightGBM算法MAE值最小,即效果最好。从可视化也可以看出其预测结果很好。对于v_12属性,其他算法效果都不是很好,LightGBM算法则很好。

通过对比可知,LightGBM训练得到的模型效果更好,故我们采用LightGBM训练模型并进行预测。

采用LightGBM模型在原始数据集上进行预测:

In [15]:
estimator = lgb.LGBMRegressor(num_leaves=63, n_estimators=100)
param_grid = {
    'learning_rate': [0.01, 0.05, 0.1],
    }
pred_model = GridSearchCV(estimator, param_grid)
pred_model.fit(train_X, train_Y)
price = pred_model.predict(test_X)

将预测值生成指定格式的csv文件:

In [16]:
submit = pd.DataFrame()
submit['SaleID'] = test_data.SaleID
submit['price'] = price
submit.to_csv('output/submit.csv',index=False)
In [17]:
submit.head(10)
Out[17]:
SaleID price
0 200000 7.146314
1 200001 7.548770
2 200002 8.888396
3 200003 7.084090
4 200004 7.582857
5 200005 7.127663
6 200006 5.990862
7 200007 8.110016
8 200008 9.461447
9 200009 6.423404

5. 算法二的实验结果及可视化

5.1 算法说明

数据集

代码实现

algorithm2.ipynb

说明

  • 使用回归决策树模型

  • 训练集中,根据creatDates和creatDates计算得到used_time

  • 作为特征的属性包括:

    ["bodyType","brand","fuelType","gearbox","kilometer",'model', 'notRepairedDamage', 'power', 'regDate',

    'v_0'- 'v_9', 'name_count','used_time']

  • 训练集80%的数据用于训练,20%用于评价模型,使用AE和决定系数R^2评价模型

  • 最后使用模型预测测试集,结果保存在output/algo2_predict.csv文件中

5.2 可视化说明

代码实现

如下方所示

可视化过程

  • 特征构造,划分训练数据并选择作为特征的属性,同时计算模型使用时间
  • 训练模型
  • 评价模型,对预测结果计算决定系数$R^2$
  • 可视化预测误差
  • 对模型进行MAE标准评价
  • 使用回归模型预测测试集中的数据,并对预测结果进行可视化
  • 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察,可以看到与线性回归模型相比,预测效果是较好的

5.3 实验代码及结果可视化

载入数据

In [1]:
import matplotlib
import numpy as np
import pandas as pd
%matplotlib inline
from matplotlib import pyplot as plt
from sklearn import tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error
from sklearn.preprocessing import StandardScaler
from xgboost.sklearn import XGBRegressor
from sklearn.ensemble import RandomForestRegressor
import joblib

读入经过预处理的数据

In [3]:
train_path = "data/train_data_v1.csv"
train_data = pd.read_csv(train_path,sep=" ")
test_path = "data/test_data_v1.csv"
test_data = pd.read_csv(test_path,sep=" ")
data = train_data.copy(deep=True)

特征选择

选择作为特征的属性如choosed_attris所列出,根据检验推测二手车的使用时间与其价格应该成反比,所以计算regdate和createdate的差得到车辆的使用时间,也作为其中一个特征。

In [4]:
data['used_time'] = (pd.to_datetime(data['creatDates'], format='%Y-%m-%d', errors='coerce') - 
                            pd.to_datetime(data['regDates'], format='%Y-%m-%d', errors='coerce')).dt.days
choosed_attris = ["bodyType","brand","kilometer",
              'model', 'notRepairedDamage', 'power',
              'v_0', 'v_1', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14',
               'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 
              'regDate_year',
              'name_count','used_time']
train = data[choosed_attris]
label = data["price"]
In [5]:
train
Out[5]:
bodyType brand kilometer model notRepairedDamage power v_0 v_1 v_10 v_11 ... v_3 v_4 v_5 v_6 v_7 v_8 v_9 regDate_year name_count used_time
0 1.0 6 12.5 30.0 0.0 60 43.357796 3.966344 -2.881803 2.804097 ... 2.159744 1.143786 0.235676 0.101988 0.129549 0.022816 0.097462 2004 108 4385
1 2.0 1 15.0 40.0 0.0 0 45.305273 5.236112 -4.900482 2.096338 ... 1.380657 -1.422165 0.264777 0.121004 0.135731 0.026597 0.020582 2003 29 4757
2 1.0 15 12.5 115.0 0.0 163 45.978359 4.823792 -4.846749 1.803559 ... -0.998467 -0.996911 0.251410 0.114912 0.165147 0.062173 0.027075 2004 3 4382
3 0.0 10 15.0 109.0 0.0 193 45.687478 4.492574 -4.509599 1.285940 ... 0.883600 -2.228079 0.274293 0.110300 0.121964 0.033395 0.000000 1996 2 7125
4 1.0 5 5.0 110.0 0.0 68 44.383511 2.031433 -1.896240 0.910783 ... -1.571239 2.246088 0.228036 0.073205 0.091880 0.078819 0.121534 2012 1 1531
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
149994 4.0 10 15.0 121.0 0.0 163 45.316543 -3.139095 1.988114 -2.983973 ... -0.736609 -1.505820 0.280264 0.000310 0.048441 0.071158 0.019174 2000 1 5772
149995 0.0 11 10.0 116.0 0.0 125 45.972058 -3.143764 1.839166 -2.774615 ... -2.366699 0.698012 0.253217 0.000777 0.084079 0.099681 0.079371 2009 1 2322
149996 1.0 11 6.0 60.0 0.0 90 44.733481 -3.105721 2.439812 -1.630677 ... -2.279091 1.423661 0.233353 0.000705 0.118872 0.100118 0.097914 2010 1 2003
149997 3.0 10 15.0 34.0 0.0 156 45.658634 -3.204785 2.075380 -2.633719 ... -1.179812 0.620680 0.256369 0.000252 0.081479 0.083558 0.081498 2006 1 3673
149998 6.0 28 12.5 19.0 0.0 193 45.536383 -3.200326 1.978453 -3.179913 ... -0.067144 -1.396166 0.284475 0.000000 0.040072 0.062543 0.025819 1999 1 6239

149999 rows × 24 columns

In [6]:
label
Out[6]:
0         7.523481
1         8.188967
2         8.736007
3         7.783641
4         8.556606
            ...   
149994    8.682877
149995    9.159152
149996    8.922792
149997    8.517193
149998    8.455531
Name: price, Length: 149999, dtype: float64

将构建好的数据集划分为训练集<x_train, y_train>和验证集<x_train, y_train>两部分,并将划分前后的这些数据全部转储到文件中。

In [8]:
x_train, x_test, y_train, y_test = train_test_split(train, label, test_size=0.2)
joblib.dump(train, "dataset/train.csv") 
joblib.dump(label, "dataset/label.csv") 
joblib.dump(x_train, "dataset/x_train.csv") 
joblib.dump(x_test, "dataset/x_test.csv") 
joblib.dump(y_train, "dataset/y_train.csv") 
joblib.dump(y_test, "dataset/y_test.csv")
Out[8]:
['dataset/y_test.csv']

构建模型

在最初实现中,使用回归决策树模型进行预测,但是最终的效果还是不够好。所以进一步测试使用随机森林分类器和构建预测模型。

随机森林属于Bagging类算法,在训练阶段,随机森林使用bootstrap采样从输入训练数据集中采集多个不同的子训练数据集来依次训练多个不同决策树;在预测阶段,随机森林将内部多个决策树的预测结果取平均得到最终的结果。

XGBoost算法特征分裂来生长一棵树,每一轮学习一棵树,其实就是去拟合上一轮模型的预测值与实际值之间的残差。当我们训练完成得到k棵树时,我们要预测一个样本的分数,其实就是根据这个样本的特征,在每棵树中会落到对应的一个叶子节点,每个叶子节点就对应一个分数,最后只需将每棵树对应的分数加起来就是该样本的预测值。

  • 回归决策树
In [9]:
def train_decision_tree_regressor(x_train, y_train):
    dr = tree.DecisionTreeRegressor()
    dr.fit(x_train, y_train)
    return dr
  • 随机森林回归器
In [10]:
def train_random_forest_regressor(x_train, y_train):
    #rfr = RandomForestRegressor(n_estimators=80, max_depth=25 )
    rfr = RandomForestRegressor(n_estimators=80,)
    rfr.fit(x_train, y_train)
    return rfr
  • GBDT模型
In [11]:
def train_XGBRegressor(x_train, y_train):
    gbm= XGBRegressor()
    gbm.fit(x_train, y_train)
    return gbm

加载保存的x_train, y_train,训练模型

In [12]:
def load_dataset():
    paths = ["dataset/x_train.csv", "dataset/x_test.csv","dataset/y_train.csv", "dataset/y_test.csv"]
    x_train, x_test, y_train, y_test = [joblib.load(x) for x in paths]
    return x_train, x_test, y_train, y_test

x_train, x_test, y_train, y_test = load_dataset()

执行本目录下的algo2.py文件,开始训练模型,并将模型保存在models目录下。

In [14]:
if sys.argv[1] == "xgbt":
    drm = train_XGBRegressor(x_train, y_train)
    pkl_name = "XGBRegressor"
if sys.argv[1] == "tree":
    drm = train_decision_tree_regressor(x_train, y_train)
    pkl_name = "train_decision_tree_regressor"
if sys.argv[1] == "forest":
    drm = train_random_forest_regressor(x_train, y_train)
    pkl_name = "train_forest_regressor"
else:
    sys.exit(0)
t1 = time.time()
print("excute_time: %f"%round(t1 - t0, ndigits=4))
print("dumping model")
joblib.dump(drm, "models/"+pkl_name+".pkl") 

结果分析

载入训练后的模型,使用验证集<x_test,y_test>计算模型的MAE,MAE越小,说明模型预测得越准确。。

In [17]:
import joblib
dtr = joblib.load("models/train_decision_tree_regressor.pkl")
rfr = joblib.load("models/train_forest_regressor.pkl")
xgbr = joblib.load("models/XGBRegressor.pkl")
  • 回归决策树
In [6]:
x_train, x_test, y_train, y_test = load_dataset()
predict_test = dtr.predict(x_test)
mae_result = []
print(mean_absolute_error(predict_test, y_test))
print(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
mae_result.append(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
0.1862874276297354
857.6127744304264
In [27]:
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_test[i], y_test, color='red')
    plt.scatter(x_test[i], predict_test, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()
  • 随机森林回归器
In [7]:
predict_test = rfr.predict(x_test)
print(mean_absolute_error(predict_test, y_test))
print(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
mae_result.append(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
0.1304358534662892
590.0747423926608
In [29]:
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_test[i], y_test, color='red')
    plt.scatter(x_test[i], predict_test, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()
  • xgboost模型
In [8]:
predict_test = xgbr.predict(x_test)
print(mean_absolute_error(predict_test, y_test))
print(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
mae_result.append(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
0.13869179402663728
652.1383889745077
In [31]:
# 由于'v_12' ,'v_8', 'v_0'与price相关度较高所以进行可视化观察
for i in ['v_12' ,'v_8', 'v_0']:
    plt.scatter(x_test[i], y_test, color='red')
    plt.scatter(x_test[i], predict_test, color='blue')
    plt.xlabel(i)
    plt.ylabel('price')
    plt.show()

结果对比

In [9]:
#plt.figure(figsize=(8,4),dpi=80)
fig, ax = plt.subplots(figsize=(8,4))
x = ["decision_tree_regressor","random_forest_regressor","XGBRegressor"]
y = mae_result
for a, b in zip(x, y):
    ax.text(a, b+1, b, ha='center', va='bottom')
plt.bar(x, mae_result,label="2")
Out[9]:
<BarContainer object of 3 artists>

可以看出,相比于决策树,随机森林回归器和xgboost模型的MAE都得到显示降低,模型的预测性能得到提高。

得到最终的预测模型

最后,对随机森林回归树模型与xgbt回归模型进行简单的加权融合,计算MAE。

In [10]:
def Weighted_method(test_pre1,test_pre2,w=[1/2,1/2]):
    Weighted_result = w[0]*pd.Series(test_pre1)+w[1]*pd.Series(test_pre2)
    return Weighted_result

x_train, x_test, y_train, y_test = load_dataset()
m1 = rfr
m2 = xgbr
predict_1 = m1.predict(x_test)
predict_2 = m2.predict(x_test)
predict_test = Weighted_method(predict_1, predict_2)
print(mean_absolute_error(predict_test, y_test))
print(mean_absolute_error(np.expm1(predict_test), np.expm1(y_test)))
0.12778741339762678
585.0242469211836

MAE有所减小。因此,最终的模型使用随机森林回归树模型与xgbt回归模型预测的均值作为二手车价格的预测值。

使用最终模型在测试集data/test_data_v1.csv进行预测。

In [32]:
test_path = "data/test_data_v1.csv"
test_data = pd.read_csv(test_path,sep=" ")
test_data['used_time'] = (pd.to_datetime(test_data['creatDates'], format='%Y-%m-%d', errors='coerce') - 
                            pd.to_datetime(test_data['regDates'], format='%Y-%m-%d', errors='coerce')).dt.days
test_data_features = test_data[["bodyType","brand","kilometer",
              'model', 'notRepairedDamage', 'power',
              'v_0', 'v_1', 'v_10', 'v_11', 'v_12', 'v_13', 'v_14',
               'v_2', 'v_3', 'v_4', 'v_5', 'v_6', 'v_7', 'v_8', 'v_9', 
               'regDate_year',
              'name_count','used_time']]
model = joblib.load("models/train_forest_regressor.pkl")
model2 = joblib.load("models/XGBRegressor.pkl")

#预测
p1 = model.predict(test_data_features)
p2 = model2.predict(test_data_features)
price = Weighted_method(p1, p2)

#生成用于提交的csv文件
output = pd.DataFrame()
output['SaleID'] = test_data.SaleID
output['price'] = np.expm1(price)
output.to_csv('output/submmit_stack.csv',index=False)

测试集预测结果的MAE为578.4769

image.png

6. 总结

6.1 中期进展报告回顾

6.1.1 存在的问题

线性回归模型进行预测时对于同属性值的高price和低price预测较差,更倾向于预测为中间大小price,而回归决策树模型虽然预测效果更好一些,但还存在可提升空间。

6.1.2 解决方案

为达到更优的表现,将进行以下几种尝试:

  • 对模型的参数进行调试
  • 尝试使用其他模型进行建模预测

6.2 最终结果汇总

  1. 在中期进展报告中提到的线性回归模型、回归决策树模型两种模型基础上,又进行了六种模型的尝试,分别为:岭回归模型、Lasso回归模型、GDBT模型、LightGBM模型、随机森林回归器、XGBoost模型;
  1. 算法一对线性回归模型、岭回归模型、Lasso回归模型、GDBT模型、LightGBM模型五种模型进行了实验和可视化对比,从可视化中可以看出:
    • Lasso回归的预测price值全是中间值,高price和低price很少。可能是由于该方法是一种压缩估计,压缩了一些回归系数,得到的是一个较为精炼的模型。从MAE值也可以看出,Lasso回归是最高的。
    • LightGBM算法MAE值最小,即效果最好。从可视化也可以看出其预测结果很好。对于v_12属性,其他算法效果都不是很好,LightGBM算法则很好。
    • LightGBM的MAE值为:MAE = 0.1371255612762281
  1. 算法二对回归决策树模型、随机森林回归器、XGBoost模型三种模型进行了实验和可视化对比,从可视化中可以看出:
    • 相比于决策树,随机森林回归器和XGBT模型的MAE都得到明显降低,模型的预测性能得到提高。
    • 为得到更好的性能,算法二中对随机森林回归树模型与xgbt回归模型进行简单的加权融合,计算MAE,最终发现,使用随机森林回归树模型与xgbt回归模型预测的均值作为二手车价格的预测值,MAE值最小。
    • MAE = 0.12778741339762678
  1. 最好的结果为算法二中使用随机森林回归树模型与xgbt回归模型预测的均值作为二手车价格的预测值的方法。

6.3 项目整体流程

数据预处理->数据分析与可视化->算法一,二的实现->实验结果可视化与分析->得到最优算法模型。

详见项目报告PPT内容

6.4 致谢

感谢老师这一学期以来的付出和教导,尽管由于疫情的原因无法当面向您请教,但在您的指导和帮助下,我们都顺利地完成了课程的学习,并且收获很大,尤其是四次互评作业以及最终的大作业,很好地锻炼了我们代码的实际编写能力,这种方式的教学我们认为会比单纯的考试有用得多。通过您的课程,我们真的学到了很多,再一次感谢您!祝老师工作顺利、身体安康、阖家幸福!