kaggle——銷量預測 Top1%

這個比賽當時是在jupyter notebook上編程的,這篇博客是之前自己整理的代碼和流程記錄。

但是很可惜,notebook轉markdown顯示效果很不好,下面給出目錄和代碼。

在這裏插入圖片描述


# coding: utf-8

# # 數據分析

# In[59]:


# 一般一起用纔會管用,否則可能會顯示混亂
get_ipython().run_line_magic('config', "ZMQInteractiveShell.ast_node_interactivity='all'")
get_ipython().run_line_magic('pprint', '')


# In[60]:


# coding: utf-8
 
# 開發環境:windows10, Anacoda3.5 , jupyter notebook ,python3.6 
# 庫: numpy,pandas,matplotlib,seaborn,xgboost,time
# 運行時間:CPU: i7-6700HQ,約8h
 
# 導入所需要的庫
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
import xgboost as xgb
from time import time

import warnings
warnings.filterwarnings("ignore")


# ## 讀取數據

# In[61]:


# 讀取數據
train = pd.read_csv('train.csv', parse_dates=[2])
test = pd.read_csv('test.csv', parse_dates=[3])
store = pd.read_csv('store.csv')


# ##  查看數據基本信息

# ### 讀取各csv的前幾行和尾幾行查看

# In[62]:


# 查看訓練集、測試集和店鋪信息
display(train.head().append(train.tail()), test.head().append(test.tail()), store.head().append(store.tail()))


# ### info() 快速查看數據的描述

# In[63]:


# info() 可以快速查看數據的描述,特別是總行數,每個屬性的類型(數值型/非數值型)和非空值(或缺失值)的數量
train.info()  # 可以看到有缺失值(後面需要處理)


# In[64]:


test.info()  


# In[65]:


store.info()


# ### describe() 描述了數值屬性的概括

# In[66]:


# describe() 描述了數值屬性的概括
train.describe()  # 注意:空值被忽略了(不計數)


# ### 對類別型的Series執行value_counts()

# 我們發現`StateHoliday`爲非數值型數據,因爲是從csv中讀取的,所以肯定是文本類型。這是一個表示類別的屬性,可以使用 `value_counts` 的方法查看該項中有哪些類別,以及每種類別的數量。

# In[67]:


# 我們發現StateHoliday爲非數值型數據,因爲是從csv中讀取的,所以肯定是文本類型。這是一個表示類別的屬性,
# 可以使用 value_counts() 的方法查看該項中有哪些類別,以及每種類別的數量。
train["StateHoliday"].value_counts()


# ### 還有一個快速瞭解數值屬性的方式(畫餅狀圖)
# 但是這個比賽,畫柱狀圖好像並不是很合適。

# In[68]:


get_ipython().run_line_magic('matplotlib', 'inline')
import matplotlib.pyplot as plt
train.hist(bins=50, figsize=(20,15))
plt.show()


# ## 缺失數據分析
# 需要逐條分析上面有缺失值的屬性,以此決定處理缺失值的方法。

# ### 查看數據缺失情況

# In[69]:


display(train.isnull().sum(),test.isnull().sum(),store.isnull().sum())


# ### 訓練集缺失數據

# 訓練集沒有缺失數據,不用處理

# In[70]:


display(train.isnull().sum(),test.isnull().sum(),store.isnull().sum())


# ### 測試集缺失數據

# In[71]:


# 測試缺失數據 Open    11條
test[pd.isnull(test.Open)]


# 缺失數據都來自於622店鋪,從周1到周6而且沒有假期,所以我們認爲這個店鋪的狀態應該是正常營業的。(後面可填充1)

# ### 店鋪集缺失數據

# In[72]:


# 店鋪集缺失數據 CompetitionDistance   3條
store[pd.isnull(store.CompetitionDistance)]


# 這個缺失信息啥也分析不出來,看不出爲嘛缺失

# In[73]:


# 店鋪集缺失數據 CompetitionOpenSinceMonth / CompetitionOpenSinceYear   均爲354條
store[pd.isnull(store.CompetitionOpenSinceMonth)].head(10)


# 根據上面的信息,`CompetitionOpenSinceMont` 和 `CompetitionOpenSinceYear`,缺失數量是一樣的,也是同時缺失的。暫時分析不出什麼有用信息

# **店鋪競爭數據缺失的原因不明,且數量比較多,我們可以用中值或者0來填充,後續的實驗發現以0填充的效果更好**

# In[74]:


# 查看是否Promo2系列的缺失是否是因爲沒有參加促銷(Promo2SinceWeek,Promo2SinceYear,PromoInterval)  均爲544條
NoPW = store[pd.isnull(store.Promo2SinceWeek)]
NoPW[NoPW.Promo2 != 0].shape


# 由此可知假設成立:Promo2系列的缺失確實是因爲沒有參加促銷。
# 
# **店鋪促銷信息的缺失是因爲沒有參加促銷活動,所以我們以0填充**

# ## 看各屬性隨時間的變化趨勢

# ### 分析店鋪銷量隨時間的變化(折線圖)

# In[75]:


strain = train[train.Sales>0]
# strain.head()
# strain.head(10000).loc[strain['Store']==1 ,['Date','Sales']]
strain.loc[strain['Store']==1 ,['Date','Sales']].plot(x='Date', y='Sales', title='Store_1', figsize=(16 ,4))


# ### 分析店鋪6-9月份的銷量變化

# In[76]:


strain = train[train.Sales>0]
strain.loc[strain['Store']==1 ,['Date','Sales']].plot(x='Date',y='Sales',title='Store_1',figsize=(8,2),xlim=['2014-6-1','2014-7-31'])
strain.loc[strain['Store']==1 ,['Date','Sales']].plot(x='Date',y='Sales',title='Store_1',figsize=(8,2),xlim=['2014-8-1','2014-9-30'])


# - 從上圖的分析中,我們可以看到店鋪的銷售額是有週期性變化的,一年之中11,12月份銷量要高於其他月份,可能有季節因素或者促銷等原因.
# - 此外從對2014年6月-9月份的銷量來看,6,7月份的銷售趨勢與8,9月份類似,因爲我們需要預測的6周在2015年8,9月份(8.1—9.17),因此我們可以把2015年6,7月份最近的6週數據作爲hold-out數據集,用於模型的優化和驗證。

# # 數據預處理

# ## 缺失值處理

# ### 測試集缺失值處理

# In[77]:


#我們將test中的open數據補爲1,即營業狀態
test.fillna(1, inplace=True)


# ### 店鋪集缺失值處理

# In[78]:


# store['CompetitionDistance'].fillna(store['CompetitionDistance'].median(), inplace = True)
# store['CompetitionOpenScinceYear'].fillna(store['CompetitionDistance'].median(), inplace = True)
# store['CompetitionOPenScinceMonth'].fillna(store['CompetitionDistance'].median(), inplace = True)

# store中的缺失數據大多與競爭對手和促銷有關,在實驗中我們發現競爭對手信息的中值填充效果並不好,所以這裏統一採用0填充
store.fillna(0, inplace=True)


# ### 查看是否還存在缺失值

# In[79]:


display(train.isnull().sum(),test.isnull().sum(),store.isnull().sum())


# so....缺失值處理完成

# ## 合併表單

# In[80]:


train = pd.merge(train, store, on='Store')
test = pd.merge(test, store, on='Store')


# ## 切分測試集

# In[81]:


# 留出最近的6週數據作爲hold_out數據集進行測試
train = train.sort_values(['Date'], ascending = False)
ho_test = train[: 6*7*1115]
ho_train = train[6*7*1115: ]


# ## 去掉銷量爲0的數據(題目要求)

# In[82]:


# 因爲銷售額爲0的記錄不計入評分,所以只採用店鋪爲開,且銷售額大於0的數據進行訓練
ho_test = ho_test[ho_test["Open"] != 0]
ho_test = ho_test[ho_test["Sales"] > 0]
ho_train = ho_train[ho_train["Open"] != 0]
ho_train = ho_train[ho_train["Sales"] > 0]


# # 特徵工程

# ## 定義特徵處理函數(特徵處理與轉化)

# In[83]:


def features_create(data):
    
    # 將存在其他字符表示分類的特徵轉化爲數字
    mappings = {'0':0, 'a':1, 'b':2, 'c':3, 'd':4}
    data.StoreType.replace(mappings, inplace=True)
    data.Assortment.replace(mappings, inplace=True)
    data.StateHoliday.replace(mappings, inplace=True)
    
    #將時間特徵進行拆分和轉化,並加入'WeekOfYear'特徵
    data['Year'] = data.Date.dt.year
    data['Month'] = data.Date.dt.month
    data['Day'] = data.Date.dt.day
    data['DayOfWeek'] = data.Date.dt.dayofweek
    data['WeekOfYear'] = data.Date.dt.weekofyear
    
    
    # 新增'CompetitionOpen'和'PromoOpen'特徵,計算某天某店鋪的競爭對手已營業時間和店鋪已促銷時間,用月爲單位表示
    data['CompetitionOpen'] = 12 * (data.Year - data.CompetitionOpenSinceYear) + (data.Month - data.CompetitionOpenSinceMonth)
    data['PromoOpen'] = 12 * (data.Year - data.Promo2SinceYear) + (data.WeekOfYear - data.Promo2SinceWeek) / 4.0
    data['CompetitionOpen'] = data.CompetitionOpen.apply(lambda x: x if x > 0 else 0)        
    data['PromoOpen'] = data.PromoOpen.apply(lambda x: x if x > 0 else 0)
    
    
    # 將'PromoInterval'特徵轉化爲'IsPromoMonth'特徵,表示某天某店鋪是否處於促銷月,1表示是,0表示否
    month2str = {1:'Jan', 2:'Feb', 3:'Mar', 4:'Apr', 5:'May', 6:'Jun', 7:'Jul', 8:'Aug', 9:'Sept', 10:'Oct', 11:'Nov', 12:'Dec'}
    data['monthStr'] = data.Month.map(month2str)
    data.loc[data.PromoInterval == 0, 'PromoInterval'] = ''
    data['IsPromoMonth'] = 0
    for interval in data.PromoInterval.unique():
        if interval != '':
            for month in interval.split(','):
                data.loc[(data.monthStr == month) & (data.PromoInterval == interval), 'IsPromoMonth'] = 1
 
    return data


# ## 對訓練,保留以及測試數據集進行特徵轉化

# In[84]:


features_create(ho_train)
features_create(ho_test)
features_create(test)
print('Features creation finished')


# ## 刪掉訓練和保留數據集中不需要的特徵

# In[85]:


ho_train.drop(['Date','Customers','Open','PromoInterval','monthStr'],axis=1,inplace =True)
ho_test.drop(['Date','Customers','Open','PromoInterval','monthStr'],axis=1,inplace =True)


# ## 分析訓練數據集中特徵相關性以及特徵與'Sales'標籤的相關性

# In[86]:


plt.subplots(figsize=(24,20))
sns.heatmap(ho_train.corr(), annot=True, vmin=-0.1, vmax=0.1, center=0)


# ## 拆分特徵與標籤,並將標籤取對數處理

# In[87]:


# log1p 平滑處理
ho_xtrain = ho_train.drop(['Sales'],axis=1 )
ho_ytrain = np.log1p(ho_train.Sales)
ho_xtest = ho_test.drop(['Sales'],axis=1 )
ho_ytest = np.log1p(ho_test.Sales)


# ## 刪掉測試集中對應的特徵與訓練集保持一致

# In[88]:


xtest =test.drop(['Id','Date','Open','PromoInterval','monthStr'],axis = 1)


# # 定義評價函數

# In[89]:


#定義評價函數rmspe
def rmspe(y, yhat):
    return np.sqrt(np.mean((yhat/y-1) ** 2))
 
def rmspe_xg(yhat, y):
    y = np.expm1(y.get_label())
    yhat = np.expm1(yhat)
    return "rmspe", rmspe(y,yhat)


# # 模型構建

# ## 參數設定

# In[95]:


#參數設定
params = {"objective": "reg:linear",
          "booster" : "gbtree",
          "eta": 0.03,
          "max_depth": 10,
          "subsample": 0.9,
          "colsample_bytree": 0.7,
          "silent": 1,
          "seed": 10
          }
num_boost_round = 2800 # 原來是6000
 
 
dtrain = xgb.DMatrix(ho_xtrain, ho_ytrain)
dvalid = xgb.DMatrix(ho_xtest, ho_ytest)
watchlist = [(dtrain, 'train'), (dvalid, 'eval')]


# ## 模型訓練

# In[96]:


print("Train a XGBoost model")
start = time()
gbm = xgb.train(params, dtrain, num_boost_round, evals=watchlist, early_stopping_rounds=100, feval=rmspe_xg, verbose_eval=True)
end = time()
print('Training time is {:2f} s.'.format(end-start))
 
#採用保留數據集進行檢測
print("validating:")
ho_xtest.sort_index(inplace=True) 
ho_ytest.sort_index(inplace=True) 
yhat = gbm.predict(xgb.DMatrix(ho_xtest))
error = rmspe(np.expm1(ho_ytest), np.expm1(yhat))
 
print('RMSPE: {:.6f}'.format(error))


# # 結果分析

# ## 構建保留數據集預測結果

# In[97]:


res = pd.DataFrame(data = ho_ytest)
res['Prediction'] = yhat
res = pd.merge(ho_xtest, res, left_index= True, right_index=True)
res['Ratio'] = res.Prediction/res.Sales
res['Error'] =abs(res.Ratio-1)
res['Weight'] = res.Sales/res.Prediction
res.head()


# ## 分析保留數據集中任意三個店鋪的預測結果

# In[98]:


col_1 = ['Sales','Prediction']
col_2 = ['Ratio']
L = np.random.randint( low=1, high = 1115, size = 3 ) 
print('Mean Ratio of predition and real sales data is {}: store all'.format(res.Ratio.mean()))
for i in L:
    s1 = pd.DataFrame(res[res['Store'] == i], columns = col_1)
    s2 = pd.DataFrame(res[res['Store'] == i], columns = col_2)
    s1.plot(title = 'Comparation of predition and real sales data: store {}'.format(i), figsize=(12,4))
    s2.plot(title = 'Ratio of predition and real sales data: store {}'.format(i), figsize=(12,4))
    print('Mean Ratio of predition and real sales data is {}: store {}'.format(s2.Ratio.mean(), i))


# ## 分析偏差最大的10個預測結果

# In[99]:


res.sort_values(['Error'],ascending=False,inplace= True)
res[:10]


# 從分析結果來看,我們的初始模型已經可以比較好的預測hold-out數據集的銷售趨勢,但是相對真實值,我們的模型的預測值整體要偏高一些。從對偏差數據分析來看,偏差最大的3個數據也是明顯偏高。因此我們可以以hold-out數據集爲標準對模型進行偏差校正。

# # 模型優化

# ## 偏差整體校正優化

# In[100]:


print("weight correction")
W=[(0.990+(i/1000)) for i in range(20)]
S =[]
for w in W:
    error = rmspe(np.expm1(ho_ytest), np.expm1(yhat*w))
    print('RMSPE for {:.3f}: {:.6f}'.format(w, error))
    S.append(error)
Score = pd.Series(S, index=W)
Score.plot()
BS = Score[Score.values == Score.values.min()]
print ('Best weight for Score:{}'.format(BS))


# - 當校正係數爲0.995時,hold-out集的RMSPE得分最低:0.118889,相對於初始模型 0.125453得分有很大的提升。(這是迭代6000輪的結果)
# - 因爲每個店鋪都有自己的特點,而我們設計的模型對不同的店鋪偏差並不完全相同,所以我們需要根據不同的店鋪進行一個細緻的校正。

# ## 細緻校驗

# ### 分組計算校正係數

# 以不同的店鋪分組進行細緻校正,每個店鋪分別計算可以取得最佳RMSPE得分的校正係數

# In[101]:


L=range(1115)
W_ho=[]
W_test=[]
for i in L:
    s1 = pd.DataFrame(res[res['Store']==i+1],columns = col_1)
    s2 = pd.DataFrame(xtest[xtest['Store']==i+1])
    W1=[(0.990+(i/1000)) for i in range(20)]
    S =[]
    for w in W1:
        error = rmspe(np.expm1(s1.Sales), np.expm1(s1.Prediction*w))
        S.append(error)
    Score = pd.Series(S,index=W1)
    BS = Score[Score.values == Score.values.min()]
    a=np.array(BS.index.values)
    b_ho=a.repeat(len(s1))
    b_test=a.repeat(len(s2))
    W_ho.extend(b_ho.tolist())
    W_test.extend(b_test.tolist())


# ### 計算校正後整體數據的RMSPE得分:

# In[111]:


yhat_new = yhat*W_ho
error = rmspe(np.expm1(ho_ytest), np.expm1(yhat_new))
print ('RMSPE for weight corretion {:6f}'.format(error))


# 細緻校正後的hold-out集的得分爲0.112010,相對於整體校正的0.118889的得分又有不小的提高

# ### 用初始和校正後的模型對訓練數據集進行預測

# In[112]:


print("Make predictions on the test set")
dtest = xgb.DMatrix(xtest)
test_probs = gbm.predict(dtest)


# #### 初始模型

# In[113]:


result = pd.DataFrame({"Id": test['Id'], 'Sales': np.expm1(test_probs)})
result.to_csv("./top1_pre_result/Rossmann_submission_1.csv", index=False)


# #### 整體校正模型

# In[114]:


result = pd.DataFrame({"Id": test['Id'], 'Sales': np.expm1(test_probs*0.995)})
result.to_csv("./top1_pre_result/Rossmann_submission_2.csv", index=False)


# #### 細緻校正模型

# In[115]:


result = pd.DataFrame({"Id": test['Id'], 'Sales': np.expm1(test_probs*W_test)})
result.to_csv("./top1_pre_result/Rossmann_submission_3.csv", index=False)


# 然後我們用不同的seed訓練10個模型,每個模型單獨進行細緻偏差校正後進行融合.

# ## 訓練融合模型

# ### 訓練10個xgb(耗時太大,略過)

# In[ ]:


print("Train an new ensemble XGBoost model")
start = time()
rounds = 10
preds_ho = np.zeros((len(ho_xtest.index), rounds))
preds_test = np.zeros((len(test.index), rounds))
B=[]
for r in range(rounds):
    print('round {}:'.format(r+1))
    
    params = {"objective": "reg:linear",
          "booster" : "gbtree",
          "eta": 0.03,
          "max_depth": 10,
          "subsample": 0.9,
          "colsample_bytree": 0.7,
          "silent": 1,
          "seed": r+1
          }
    num_boost_round = 6000
    gbm = xgb.train(params, dtrain, num_boost_round, evals=watchlist, 
                    early_stopping_rounds=100, feval=rmspe_xg, verbose_eval=True)
    
    yhat = gbm.predict(xgb.DMatrix(ho_xtest))
    
    L=range(1115)
    W_ho=[]
    W_test=[]
    for i in L:
        s1 = pd.DataFrame(res[res['Store']==i+1],columns = col_1)
        s2 = pd.DataFrame(xtest[xtest['Store']==i+1])
        W1=[(0.990+(i/1000)) for i in range(20)]
        S =[]
        for w in W1:
            error = rmspe(np.expm1(s1.Sales), np.expm1(s1.Prediction*w))
            S.append(error)
        Score = pd.Series(S,index=W1)
        BS = Score[Score.values == Score.values.min()]
        a=np.array(BS.index.values)
        b_ho=a.repeat(len(s1))
        b_test=a.repeat(len(s2))
        W_ho.extend(b_ho.tolist())
        W_test.extend(b_test.tolist())
    
 
    yhat_ho = yhat*W_ho
    yhat_test =gbm.predict(xgb.DMatrix(xtest))*W_test
    error = rmspe(np.expm1(ho_ytest), np.expm1(yhat_ho))
    B.append(error)
    preds_ho[:, r] = yhat_ho
    preds_test[:, r] = yhat_test
    print('round {} end'.format(r+1))
    
end = time()
time_elapsed = end-start
print('Training is end')
print('Training time is {} h.'.format(time_elapsed/3600))   


# ### 分析不同模型的相關性

# In[ ]:


preds = pd.DataFrame(preds_ho)
sns.pairplot(preds)


# 模型融合可以採用簡單平均或者加權重的方法進行融合。從上圖來看,這10個模型相關性很高,差別不大,所以權重融合我們只考慮訓練中單獨模型在hold-out模型中的得分情況分配權重。

# ### 模型融合在hold-out數據集上的表現

# #### 簡單平均融合

# In[ ]:


print ('Validating')

bagged_ho_preds1 = preds_ho.mean(axis = 1)
error1 = rmspe(np.expm1(ho_ytest), np.expm1(bagged_ho_preds1))
print('RMSPE for mean: {:.6f}'.format(error1))


# #### 加權融合

# In[ ]:


R = range(10)   
Mw = [0.20,0.20,0.10,0.10,0.10,0.10,0.10,0.10,0.00,0.00] 
A = pd.DataFrame()
A['round']=R
A['best_score']=B
A.sort_values(['best_score'],inplace = True)
A['weight']=Mw
A.sort_values(['round'],inplace = True)
weight=np.array(A['weight'])
preds_ho_w=weight*preds_ho
bagged_ho_preds2 = preds_ho_w.sum(axis = 1)
error2 = rmspe(np.expm1(ho_ytest), np.expm1(bagged_ho_preds2))
print('RMSPE for weight: {:.6f}'.format(error2))


# 權重模型較均值模型有比較好的得分

# ### 用均值融合和加權融合後的模型對訓練數據集進行預測

# In[ ]:


#均值融合
print("Make predictions on the test set")
bagged_preds = preds_test.mean(axis = 1)
result = pd.DataFrame({"Id": test['Id'], 'Sales': np.expm1(bagged_preds)})
result.to_csv("Rossmann_submission_4.csv", index=False)

#加權融合
bagged_preds = (preds_test*weight).sum(axis = 1)
result = pd.DataFrame({"Id": test['Id'], 'Sales': np.expm1(bagged_preds)})
result.to_csv("Rossmann_submission_5.csv", index=False)


# # 模型特徵重要性及最佳模型結果分析

# ## 模型特徵重要性

# In[116]:


xgb.plot_importance(gbm)


# - 從模型特徵重要性分析,比較重要的特徵有四類包括
#     1. 週期性特徵'Day','DayOfWeek','WeekOfYear','Month'等,可見店鋪的銷售額與時間是息息相關的,尤其是週期較短的時間特徵;
#     2. 店鋪差異'Store'和'StoreTyp'特徵,不同店鋪的銷售額存在特異性;
#     3. 短期促銷(Promo)情況:'PromoOpen'和'Promo'特徵,促銷時間的長短與營業額相關性比較大;
#     4. 競爭對手相關特徵包括:'CompetitionOpen',‘CompetitionDistance','CompetitionOpenSinceMoth'以及'CompetitionOpenScinceyear',競爭者的距離與營業年限對銷售額有影響。
# - 作用不大的特徵主要兩類包括:
#     1. 假期特徵:'SchoolHoliday'和'StateHoliday',假期對銷售額影響不大,有可能是假期店鋪大多不營業,對模型預測沒有太大幫助。
#     2. 持續促銷(Promo2)相關的特徵:'Promo2','Prom2SinceYear'以及'Prom2SinceWeek'等特徵,有可能持續的促銷活動對短期的銷售額影響有限。

# ## 採用新的權值融合模型構建保留數據集預測結果

# In[ ]:


res1 = pd.DataFrame(data = ho_ytest)
res1['Prediction']=bagged_ho_preds2
res1 = pd.merge(ho_xtest,res1, left_index= True, right_index=True)
res1['Ratio'] = res1.Prediction/res.Sales
res1['Error'] =abs(res1.Ratio-1)
res1.head()


# ## 分析偏差最大的10個預測結果與初始模型差異

# In[ ]:


res1.sort_values(['Error'],ascending=False,inplace= True)
res['Store_new'] = res1['Store']
res['Error_new'] = res1['Error']
res['Ratio_new'] = res1['Ratio']
col_3 = ['Store','Ratio','Error','Store_new','Ratio_new','Error_new']
com = pd.DataFrame(res,columns = col_3)
com[:10]


# 從新舊模型預測結果最大的幾個偏差對比的情況來看,最終的融合模型在這幾個預測值上大多有所提升,證明模型的校正和融合確實有效。

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