本文章主要根据该比赛冠军的开源代码进行梳理,总结了冠军的两个解题方案,并对代码进行详细的注释。
1. 赛题出处
冠军报告【https://zhuanlan.zhihu.com/p/98926322 】
代码 【https://github.com/cxq80803716/2019-CCF-BDCI-Car_sales/tree/master/fusaicar 】
2. 赛题介绍
2.1 数据集
本次赛题给出2016.1 ~ 2017.12的省份、车型、车身、销量、搜索量、评论量、评价量等特征,要求预测2018.1~2018.4的汽车销量。
训练集
待预测的数据集
2.2 评估指标
均方误差-MSE
3. 冠军方案解读
3.1 方案1
- 先进行预处理,将原始输入的数据集中的离散型属性转化为数值特征。
- 将原始的数据放入函数def get_stat_feature(df_, month),可以加工原始特征得到复合特征。
- 调用函数def get_lgb_ans(input_data),使用LightGBM进行预测
1) 数据预处理
def prepare(data):
input:
data, DataFrame, 输入的数据集
return:
data, DataFrame, 经过预处理后的输出数据集
功能:
将原始input的data中的离散型属性转化为数值特征:
data['province']:对每个省份进行编号,比如广东用1表示,上海用2表示
data['model_id']:对车的型号进行编号
data['bodyType']:对车的类型进行编号,其中不同型号可能属于同一种类型
data['time_id']:数据集中需要根据2016.1 ~ 2017.12共24个月的数据,预测
2018.1~2018.4共4个月的数据,因此month_id分别对这28个月
份月进行编码,比如2017.1编码为13
data['sales_year']:该样本的记录年份,可以为2016、2017、2018
data['month_id']:月份,区间为[1,12]的整数
2) 特征提取
将原始的数据放入函数def get_stat_feature(df_,month)
,可以加工原始特征得到复合特征,具体注释如下:
def get_stat_feature(df_, month):
input:
df_, DataFrame, 经过数据预处理后的DataFrame
month, int, 待预测的月份,分别为25,26,27,28
return:
data, DataFrame, 在输入的df_的基础上新增了若干列的特征,它们都属于复合特征
new_stat_feat, List, 用于存放新增特征列的名称
功能:
根据df_中原有的特征,组合出以下复合特征:
data['last_X_sale']:该样本前X个月的销量,该特征共有16个,即存在'last_1_sale'~'last_16_sale'
data['last_X_popularity']:该样本前X个月的热门量,该特征共有16个,即存在'last_1_popularity'~'last_16_popularity'
# 半年销量等统计特征
data['1_6_sum']、data['1_6_mea']、data['1_6_max']、data['1_6_min']:该样本前半年(6个月)销量的总量、均值、最大值、最小值
data['jidu_1_3_sum']、data['jidu_4_6_sum']:该样本前1~3个月、前4~6个月的销售总量
data['jidu_1_3_mean']、data['jidu_4_6_mean']:该样本前1~3个月、前4~6个月的销售均值
data['1_2_diff']
# model_pro趋势特征
data['1_2_diff']:该样本前1个月与前2个月的销量之差
data['1_3_diff']: 该样本前1个月与前3个月的销量之差
data['2_3_diff']:同理
data['2_4_diff']:同理
data['3_4_diff']:同理
data['3_5_diff']:同理
data['jidu_1_2_diff']:该样本前1~3个月(第1季度) 与 前4~6个月的销售总量(第2季度) 之差
# 是否沿海城市、是否'春节月'的前后一个月
data['is_yanhai']:如果为1,表示该样本为沿海城市,反之为0
data['is_chunjie']:表示该样本是否为春节月,春节月分的编号为2,13,26
data['is_chunjie_before']:表示该样本是否为春节月的前一个月
data['is_chunjie_late']:表示该样本是否为春节月的后一个月
# 两个月销量差值
data['model_1_2_diff_sum']:全国省份两年期间,各模型前1月和前2月的销量之差的和
data['pro_1_2_diff_sum']:两年期间,各省全部汽车前1月和前2月的销量之差的和
data['model_pro_1_2_diff_sum']:两年期间,在各个省份中,各模型前1月和前2月的销量之差的和
data['model_pro_1_2_diff_mean']:两年期间,在各个省份中,各模型前1月和前2月的销量之差的均值
# 环比
data['huanbi_1_2']:各条样本,前1个月销量和前2个月销量的比值
data['huanbi_2_3']:各条样本,前2个月销量和前3个月销量的比值
data['huanbi_3_4']:各条样本,前3个月销量和前4个月销量的比值
data['huanbi_4_5']:各条样本,前4个月销量和前5个月销量的比值
data['huanbi_5_6']:各条样本,前5个月销量和前6个月销量的比值
# 环比的比
data['huanbi_1_2_2_3']:各条样本,data['huanbi_1_2']和data['huanbi_2_3']的比值
data['huanbi_2_3_3_4']:各条样本,data['huanbi_2_3']和data['huanbi_3_4']的比值
data['huanbi_3_4_4_5']:各条样本,data['huanbi_3_4']和data['huanbi_4_5']的比值
data['huanbi_4_5_5_6']:各条样本,data['huanbi_4_5']和data['huanbi_5_6']的比值
# 该月该省份bodytype销量的占比与涨幅
data['pro_body_last_X_sale_sum']:该月该省份类型为bodytype的车辆(同一种bodytype下,可以
有多个不同型号的车辆),前X个月的销量之和,X属于[1,6]
data['data['last_X_sale_ratio_pro_body_last_X_sale_sum']:某月某省份的样本前X个月的销量,与data['pro_body_last_X_sale_sum']的比值
data['model_last_X_X-1_sale_pro_diff']:data['last_X-1_sale_ratio_pro_body_last_X-1_sale_sum'] 与
data['last_X_sale_ratio_pro_body_last_X_sale_sum']之差
# 该月该省份总销量占比与涨幅
data['pro_last_X_sale_sum']:该月该省份全部车辆前X个月的销量之和
data['last_X_sale_ratio_pro_body_last_X_sale_sum']:某月某省份的样本前X个月的销量,与data['pro_last_X_sale_sum']的比值
data['model_last_X-1_X_sale_pro_diff']:data['last_X-1_sale_ratio_pro_body_last_X-1_sale_sum'] 与
data['last_X_sale_ratio_pro_body_last_X_sale_sum'] 之差
# popularity的涨幅占比
data['huanbi_1_2popularity']: (data['last_1_popularity'] - data['last_2_popularity']) / data['last_2_popularity']
data['huanbi_2_3popularity']: (data['last_2_popularity'] - data['last_3_popularity']) / data['last_3_popularity']
data['huanbi_3_4popularity']: (data['last_3_popularity'] - data['last_4_popularity']) / data['last_4_popularity']
data['huanbi_4_5popularity']: (data['last_4_popularity'] - data['last_5_popularity']) / data['last_5_popularity']
data['huanbi_5_6popularity']: (data['last_5_popularity'] - data['last_6_popularity']) / data['last_6_popularity']
# 以车型model_id为主键,统计popularity总量与占比
data['model__last_X_popularity_sum']:统计每个月中,每一种车型的上X个月的热门量
data['last_X_popularity_ratio_model_last_X_popularity_sum']:该样本上X个月的热门量 与 data['model__last_X_popularity_sum'] 的比值
# 以车类型body_id为主键,统计popularity总量与占比
data['body_last_X_popularity_sum']:统计每个月中,每一种车类别的上X个月的热门量
data['last_X_popularity_ratio_model_last_X_popularity_sum']:该样本上X个月的热门量 与 data['body_last_{0}_popularity_sum'] 的比值
data['last_X-1_X_popularity_body_diff']:(data['last_X-1_popularity_ratio_body_last_X-1_popularity_sum']-
data['last_X_popularity_ratio_body_last_X_popularity_sum'])/data['last_X_popularity_ratio_body_last_X_popularity_sum']
# 同比一年前的增长
data["increase16_4"]:(data["last_16_sale"] - data["last_4_sale"]) / data["last_16_sale"]
data['mean_province']:在每个model_id下,分别针对两年共24个月的每个月下,统计12个月前(1年前)平均各省份销售额
data['min_province']:在每个model_id下,分别针对两年共24个月的每个月下,统计12个月前(1年前)最小的省份销售额
# 前4个月车型的平均省销量占比
X属于[1,4]
data['mean_province_X']:在每个model_id下,分别针对两年共24个月的每个月下,统计X个月前平均各省份销售额
data['mean_province_X+12']:在每个model_id下,分别针对两年共24个月的每个月下,统计X+12个月前平均各省份销售额
data["increase_mean_province_14_2"]: (data["mean_province_14"] - data["mean_province_2"]) / data["mean_province_14"]
data["increase_mean_province_13_1"]: (data["mean_province_13"] - data["mean_province_1"]) / data["mean_province_13"]
data["increase_mean_province_16_4"]: (data["mean_province_16"] - data["mean_province_4"]) / data["mean_province_16"]
data["increase_mean_province_15_3"]: (data["mean_province_15"] - data["mean_province_3"]) / data["mean_province_15"]
3) 训练LightGBM并进行预测
模型LightGBM于2017年由微软提出,是Xgboost的升级版。通过直接调用函数def get_lgb_ans(input_data)
,可以使用模型LightGBM进行预测。
代码中的详细实现如下注释所示:
def get_train_model(df_, m, features, num_feat, cate_feat):
input:
df_, DataFrame, 经过特征提取后的数据集
m, int, 待预测的月份id, 分别为25,26,27,28
num_feat, List, 模型训练时所用到的特征名称
categorical_feature, List,输入模型中的类别特征,该代码的类别特征固定为['pro_id','body_id','model_id','month_id','jidu_id']
return:
sub, DataFrame, 存放模型的预测结果。 共有两列,sub['id']预测样本的id, sub['forecastVolum']样本的预测销售量
功能:
根据输入的数据集df_,取出第7~(m-1)个月的数据作为训练集,要求模型预测第m个月的销售量并返回。
def LGB(input_data,is_get_82_model):
input:
input_data, DataFrame, 未经过特征提取的数据集
is_get_82_model, int, 如果同时使用初赛的60类车型和复赛新加入的22类车型(共82类),则为1。如果只使用初赛的60类车型,则为0.
return:
sub, DataFrame, 存放模型的预测结果。 共有两列,sub['id']预测样本的id, sub['forecastVolum']样本的预测销售量
功能:
根据is_get_82_model的值,返回包含特定车型的数据集。
对历史的销售量进行平滑化处理 y=math.log(x+1,2)。
使用函数def get_train_model(df_, m, features, num_feat, cate_feat),分别预测月份编号为
25,26,27,28的销售量(共4列数据),并将4列数据都合并到未经过特征提取的数据集input_data中。
对input_data中的4列预测数据都取消平滑化处理 x=(2**y)-1,对月份编号为26,27,28,29的预测数据分别乘上权值0.95,0.98,0.90.
对input_data中的4列预测数据都进行四舍五入。
def get_lgb_ans(input_data):
input:
input_data, DataFrame, 未经特征提取的数据集
return:
在input_data中新增一列input_data['forecastVolum'],存放预测结果
功能:
基于函数def LGB(input_data,is_get_82_model),基于函数使用预赛的60类车型进行预测月
份编号为25,26,27,28的销售量,得到DataFrame X。
基于函数def LGB(input_data,is_get_82_model),使用预赛和复赛共82类车型进行预测月份
编号为25,26,27,28的销售量,得到DataFrame Y。
将X和Y合并到未经特征处理的input_data中。
在input_data中新增一列存放最终预测值:当且仅当X的预测值非空,则为X,反之为Y。
3.2 方案2
- 根据
1~24
个月份的历史销量(2016.21~2017.12),使用指数平滑法(指数平滑法的详细介绍见这里)来预测月份编号为25、26
的销量。 - 结合特定的人工规则,基于月份编号为
25、26
的销量,进行简单的加权组合来预测月份27、28的销量。
1) 定义指数平滑法的函数
def exp_smooth(df,alpha=0.97,base=50,start=1,win_size=3,t=24):
input:
df_, DataFrame, 其列名为:[省名,车型,1,2,3,4,5,6,7,8,9,....,24],其中列名为21是月份编号为21的销量
return:
将预测得到的月份编号为25,26的销量合并入输入的df_
功能:
使用指数平滑法,根据输入的历史月份(1~24个月)销售量,来预测编号为25、26月份的销售量
2) 根据指数平滑法的结果,基于特定规则再进行修正
def pre_rule():
input:
无
return:
df, DataFrame,预测结果,包含两列,id和预测销量。
功能:
使用16和17年的数据,计算下半年趋势因子:df['after_factor'] = (各个省份中,每种车型在17年下半年6个月内的销量均值)/(各个省份中,每种车型在16年下半年6个月内的销量均值)
使用16和17年的数据,计算上半年趋势因子:df['after_factor'] = (各个省份中,每种车型在17年上半年6个月内的销量均值)/(各个省份中,每种车型在16年上半年6个月内的销量均值)
总体趋势df['factor'] = 0.35 * df['front_factor'] + 0.65 * df['after_factor']
在省份-车型作为主键的情况下,取出16年和17年的销量数据,共24个月,存于一个DataFrame中,其列名为:[省名,车型,1,2,3,4,5,6,7,8,9,....,24]
使用指数平滑法,预测月份25,26的销量。
根据以下规则,来修正月份25、26的销量,并以线性组合预测月份27、28的销量:
trend_factor = [0.985,0.965,0.99,0.985]
for i,m in enumerate([25,26,27,28]):
#以省份-车型作为主键,计算前年,去年,最近几个月的值,然后加权得到一个当前月份的预测值
last_year_base = 0.2 * df[m-13].values + 0.6 * df[m-12].values + 0.2 * df[m-11].values
if m == 25:
last_last_year_base = 0.8 * df[m-24] + 0.2 * df[m-23]
else:
last_last_year_base = 0.2 * df[m-25] + 0.6 * df[m-24] + 0.2 * df[m-23]
if m <=26:
near_base = 0.2 * df[m-3] + 0.2 * df[m-2] + 0.3 * df[m-1] + 0.3 * df[m]
else:
near_base = 0.2 * df[m-3] + 0.2 * df[m-2] + 0.6 * df[m-1]
base = (last_year_base + near_base + last_last_year_base) / 3
df[m] = base * df['factor'] * trend_factor[i] # 计算最终预测结果
3.3 融合方案1和2的预测结果
def fusion(sub,sub_rule,sub_lgb):
input:
sub, DataFrame, 待预测的数据集,列名为[省名、车型、车型类别、年份、月份]
sub_rule, DataFrame, 待预测的数据集的销量预测结果(基于指数平滑法和规则的预测结果),共有两列,sub_rule['id']为预测样本的id, sub_rule['forecastVolum']为样本的预测销售量
sub_lgb, DataFrame, 待预测的数据集的销量预测结果(LightGBM的预测结果),共有两列,sub_lgb['id']为预测样本的id, sub_lgb['forecastVolum']为样本的预测销售量
return:
sub_rule和sub_lgb的几何加权的结果
功能:
基于以下规则,对LightGBM的预测结果和指数平滑法的预测结果进行几何加权:
sub['rule'] = sub_rule['forecastVolum'].values
sub['lgb'] = sub_lgb['forecastVolum'].values
'60个车型1-4月融合'
sub['forecastVolum'] = -1
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.40) * math.pow(y,0.60)) if z==0 and m==25 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.40) * math.pow(y,0.60)) if z==0 and m==26 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.50) * math.pow(y,0.50)) if z==0 and m==27 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.40) * math.pow(y,0.60)) if z==0 and m==28 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
'22个车型1-4月融合'
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.35) * math.pow(y,0.65)) if z==1 and m<=26 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.40) * math.pow(y,0.60)) if z==1 and m==27 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
sub['forecastVolum'] = list(map(lambda x,y,z,m,f:(math.pow(x,0.40) * math.pow(y,0.60)) if z==1 and m==28 else f,sub['rule'],sub['lgb'],sub['new_model'],sub['time_id'],sub['forecastVolum']))
sub = sub[['id','forecastVolum']]
sub['id'] = sub['id'].map(int)
sub['forecastVolum'] = sub['forecastVolum'].map(int)