【竞赛解读】2019-CCF BDCI 车辆销量预测

  本文章主要根据该比赛冠军的开源代码进行梳理,总结了冠军的两个解题方案,并对代码进行详细的注释。

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的汽车销量。

训练集在这里插入图片描述
待预测的数据集【竞赛解读】2019-CCF BDCI 车辆销量预测_第1张图片

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.1224个月的数据,预测
                            2018.1~2018.44个月的数据,因此month_id分别对这28个月
                            份月进行编码,比如2017.1编码为13
        data['sales_year']:该样本的记录年份,可以为201620172018
        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个月)销售量,来预测编号为2526月份的销售量
    
2) 根据指数平滑法的结果,基于特定规则再进行修正
def pre_rule():
    input:return:
        df, DataFrame,预测结果,包含两列,id和预测销量。
    功能:
        使用1617年的数据,计算下半年趋势因子:df['after_factor'] = (各个省份中,每种车型在17年下半年6个月内的销量均值)/(各个省份中,每种车型在16年下半年6个月内的销量均值)
        使用1617年的数据,计算上半年趋势因子: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的销量。

        根据以下规则,来修正月份2526的销量,并以线性组合预测月份2728的销量:
            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)

你可能感兴趣的:(竞赛)