【競賽解讀】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的汽車銷量。

訓練集在這裏插入圖片描述
待預測的數據集在這裏插入圖片描述

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並進行預測

  模型1是LightGBM,於2017年由微軟提出,是Xgboost的升級版。通過直接調用函數def get_lgb_ans(input_data),可以使用模型1進行預測。

  代碼中的詳細實現如下注釋所示:

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)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章