最终成绩:62 / 2776
比赛地址:https://tianchi.aliyun.com/competition/entrance/231784/introduction
源码:https://github.com/LogicJake/competition_baselines/tree/master/competitions/tianchi_car_sale/final

比赛介绍

本次新人赛是Datawhale与天池联合发起的0基础入门系列赛事第一场 —— 零基础入门数据挖掘之二手车交易价格预测大赛。

赛题以二手车市场为背景,要求选手预测二手汽车的交易价格,这是一个典型的回归问题。通过这道赛题来引导大家走进AI数据竞赛的世界,主要针对于于竞赛新人进行自我练习、自我提高。

为了更好的引导大家入门,我们同时为本赛题定制了系列学习方案,其中包括数据科学库、通用流程和baseline方案学习三部分。通过对本方案的完整学习,可以帮助掌握数据竞赛基本技能。同时我们也将提供专属的视频直播学习通道。

EDA

详细的 eda 可以参考天才儿童在天池的分享: https://tianchi.aliyun.com/notebook-ai/detail?postId=95276

数据集的格式如下:

数据格式
数据格式

特征可以分成三类:

  • 日期特征: regDate, creatDate
  • 类别特征: name, model, brand, bodyType, fuelType, gearbox, notRepairedDamage, regionCode, seller, offerType
  • 数值特征: power, kilometer和15个匿名特征

这里主要关注特征的缺失率和 nunique 信息,主要是看有没有缺失过多或 nunique 太少的特征,一般情况下这两种特征对模型学习起不到作用。数值特征 power 和 kilometer nunique 值比较少,也不知道是不是数据做了处理,抹去了精度。seller 和 offerType 只有两个甚至1个不同的值,所以可以删去, 对模型学习起不到作用,模型的特征重要性也为0。

数据统计
数据统计

匿名特征的分布见下图,匿名特征在最后的模型重要性都挺高的,可以好好挖掘一下。

匿名特征
匿名特征

数据处理

缺失值处理

缺失值主要集中在bodyType,fuelType,gearbox,我的思路是汽车的指标往往和其所属的品牌和车型有较大关系,所以采用该品牌车型下的众数来填补缺失值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from scipy import stats

cols = ['bodyType', 'fuelType', 'gearbox']
df_feature['gp'] = df_feature['brand'].astype(
'str') + df_feature['model'].astype('str')
gp_col = 'gp'

df_na = df_feature[cols].isna()
df_mode = df_feature.groupby(gp_col)[cols].agg(
lambda x: stats.mode(x)[0][0])

for col in cols:
na_series = df_na[col]
names = list(df_feature.loc[na_series, gp_col])

t = df_mode.loc[names, col]
t.index = df_feature.loc[na_series, col].index

df_feature.loc[na_series, col] = t

del df_feature['gp']
df_feature[cols].isnull().sum()

目标变量分布变换

一般来说对于回归问题,目标变量正态化对模型预测有帮助,下图展示了使用 log1p 前后的价格分布情况。

价格分布
价格分布

无效特征删除

seller 和 offerType 只有两个甚至1个不同的值,所以可以删去, 对模型学习起不到作用,模型的特征重要性也为0。

特征工程

基础特征

对于两个日期特征汽车注册日期和开始售卖时间,可以二者做差值计算汽车售卖时的使用时间,我这里使用了年和天来刻画。除此以外,汽车是哪一年注册的对价格的影响也挺大。数据中存在一些异常日期数据:月份为0,处理的时候将其置为1即可。

1
2
df_feature['car_age_day'] = ( df_feature['creatDate'] - df_feature['regDate']).dt.days
df_feature['car_age_year'] = round(df_feature['car_age_day'] / 365, 1)

对于类别特征, 可以计算count属性, 反应销售热度。

1
df_feature['name_count'] = df_feature.groupby(['name'])['SaleID'].transform('count')

数值特征往往结合类别特征进行统计。比如可以统计不同汽车品牌下匿名特征的统计特征:mean, std, max, min。

1
2
3
4
5
l = ['name', 'model', 'brand', 'bodyType']
for f1 in tqdm(l):
for f2 in v_cols:
df_feature = stat(df_feature, df_feature, [f1], {
f2: ['mean', 'max', 'min', 'std']})

目标变量 price 也是数值特征,所以也可以结合类别进行统计,比如计算某品牌,某车型的平均交易价格,这种做法称为目标编码。但需要注意的是,假如使用全局标签信息统计会出现标签泄露的问题,所以一般使用五折统计法,用四折的标签数据做统计给另外一折的数据做特征。

匿名特征

简单一点,可以直接统计每辆车15个匿名特征的统计值,得到v_mean,v_max,v_min和v_std。然后再统计汽车交易名称下这四个特征的统计值,这道题,汽车交易名称也是一个很重要的特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v_cols = ['v_'+str(i) for i in range(15)]

df_feature['v_mean'] = df_feature[v_cols].mean(axis=1)
df_feature['v_max'] = df_feature[v_cols].max(axis=1)
df_feature['v_min'] = df_feature[v_cols].min(axis=1)
df_feature['v_std'] = df_feature[v_cols].std(axis=1)

for col in ['v_mean', 'v_max', 'v_min', 'v_std']:
df_feature[f'name_{col}_mean'] = df_feature.groupby('name')[
col].transform('mean')
df_feature[f'name_{col}_std'] = df_feature.groupby('name')[
col].transform('std')
df_feature[f'name_{col}_max'] = df_feature.groupby('name')[
col].transform('max')
df_feature[f'name_{col}_min'] = df_feature.groupby('name')[
col].transform('min')

匿名特征无法知道具体的业务含义,所以只能梭哈操作,写了个程序对匿名特征进行二阶或三阶组合,计算相加和相减,最后筛选保留以下特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
df_feature['v_0_add_v_4'] = df_feature['v_0'] + df_feature['v_4']
df_feature['v_0_add_v_8'] = df_feature['v_0'] + df_feature['v_8']
df_feature['v_1_add_v_3'] = df_feature['v_1'] + df_feature['v_3']
df_feature['v_1_add_v_4'] = df_feature['v_1'] + df_feature['v_4']
df_feature['v_1_add_v_5'] = df_feature['v_1'] + df_feature['v_5']
df_feature['v_1_add_v_12'] = df_feature['v_1'] + df_feature['v_12']
df_feature['v_2_add_v_3'] = df_feature['v_2'] + df_feature['v_3']
df_feature['v_4_add_v_11'] = df_feature['v_4'] + df_feature['v_11']
df_feature['v_4_add_v_12'] = df_feature['v_4'] + df_feature['v_12']
df_feature['v_0_add_v_12_add_v_14'] = df_feature['v_0'] + \
df_feature['v_12'] + df_feature['v_14']

df_feature['v_4_add_v_9_minu_v_13'] = df_feature['v_4'] + \
df_feature['v_9'] - df_feature['v_13']
df_feature['v_2_add_v_4_minu_v_11'] = df_feature['v_2'] + \
df_feature['v_4'] - df_feature['v_11']
df_feature['v_2_add_v_3_minu_v_11'] = df_feature['v_2'] + \
df_feature['v_3'] - df_feature['v_11']

尝试过的无用特征

论坛分享了不少结合业务的特征,但我自己尝试后发现效果都不行,这也是让我很困惑的地方。官方赛题分享提到可以截取regionCode,提取城市信息,但 regionCode 已经被编码脱敏成0~8121的数字,已经无法进行信息提取。

模型

两个树模型:lgb 和 xgb 分别预测,然后根据得分进行简单的加权,按照 0.45*xgb_pred+0.55 *lgb_pred 得到最后的汽车预测价格。最后线上得分433,rank:62 / 2776。