本文章主要根據該比賽冠軍的開源代碼進行梳理,總結了冠軍的兩個解題方案,並對代碼進行詳細的註釋。
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並進行預測
模型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個月)銷售量,來預測編號爲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)