利用SARIMAX進行銷量預測

本文從傳統的時間序列SARIMAX算法講解銷量預測模型。
主要涉及到python的pandas、statsmodels、joblib等模塊,通過對多個模型進行並行網格搜索尋找評價指標MAPE最小的模型參數,雖然供應鏈銷量預測可供使用的模型非常多,但是作爲計量經濟學主要內容之一,時間序列因爲其強大成熟完備的理論基礎,應作爲我們處理帶有時序效應數據時首要嘗試的模型類型,且往往效果不錯。本文只是從代碼的角度講解其中statsmodels中SARIMAX的用法。剖析代碼細節,以便於照例上手實際操作。

  1. 參數解釋

SARIMAX是在差分移動自迴歸模型(ARIMA)的基礎上加上季節(S,Seasonal)和外部因素(X,eXogenous)。也就是說以ARIMA基礎加上週期性和季節性,適用於時間序列中帶有明顯週期性和季節性特徵的數據。由於參數衆多,所以下面簡單整理出其含義,以免誤用或遺漏。

由於參數衆多,所以下面簡單介紹其含義,免得勿用或者遺漏。

參數 含義 是否必須
endog 觀察(自)變量 y
exog 外部變量
order 自迴歸,差分,滑動平均項 (p,d,q)
seasonal_order 季節因素的自迴歸,差分,移動平均,週期 (P,D,Q,s)
trend 趨勢,c表示常數,t:線性,ct:常數+線性
measurement_error 自變量的測量誤差
time_varying_regression 外部變量是否存在不同的係數
mle_regression 是否選擇最大似然極大參數估計方法
simple_differencing 簡單差分,是否使用部分條件極大似然
enforce_stationarity 是否在模型種使用強制平穩
enforce_invertibility 是否使用移動平均轉換
hamilton_representation 是否使用漢密爾頓表示
concentrate_scale 是否允許標準誤偏大
trend_offset 是否存在趨勢
use_exact_diffuse 是否使用非平穩的初始化
**kwargs 接受不定數量的參數,如空間狀態矩陣和卡爾曼濾波

以上我們列出了該函數中所有可能用到的參數,很多參數不是必須,比如甚至只是需要給定endog(觀察變量),程序就可以運行起來,也正是這樣,SARIMAX具有極大的靈活性。

  1. 如果不指定seasonal_order或者季節性參數都設定爲0,那麼就是普通的ARIMA模型,exog外部因子沒有也可以不用指定;
  2. 其他的參數如無必要,則不需要修改,因爲函數默認的參數在大多數時候是最優的;
  3. 上表多次提到初始化,我們知道模型擬合所用迭代算法,是需要提供一個初始值的,在初始值的基礎上不斷迭代,一般情況下是隨機指定一個值,或者給出一套包含某種分佈的值,如正態分佈。在大多數梯度下降算法陷入局部最優的時候,可以嘗試更改初始值,和上條一樣如無特殊需求,勿動;
  4. 關於擬合算法,我們一般都是假定給定的數據滿足正態分佈,所有使用極大似然算法求解最優參數;
  5. 關於是否強制平穩和移動平均轉換,一般設置爲False,保持靈活性。

總的來說,SARIMA 模型通過(p,d,q) (P,D,Q,m)不同的組合,囊括了ARIMA, ARMA, AR, MA模型,使用指定的模型評估準則,選擇最優模型,目前用python的statsmodels進行時間序列分析時,用SARIMAX就好。

  1. 模型選擇標準

模型選擇標準,提供以下三種方案:
2.1使用AIC信息準則

通過Akaike information criteria (AIC)進行模型選擇,用極大似然函數擬合模型,雖然我們的目標最大化似然函數,但同時需要考慮模型複雜度,所以常常使用AIC和BIC作爲模型優劣的衡量標準,該標準只能說在這麼多備選模型中,最小AIC的模型刻畫的真實數據表達的信息損失最小,是相對指標。

AIC=-2 ln(L) + 2 k

BIC=-2 ln(L) + ln(n)*k

AIC在樣本容量很大時,擬合所得數值會因爲樣本容量而放大(通常超過1000的樣本稱之爲大樣本容量)。AIC和BIC作爲模型選擇的準則,可以有效彌補根據自相關圖和偏自相關圖定階的主觀性。

2.2使用 Box-Jenkins檢驗準則

Box-Jenkins 建模流程如下,建立在反覆驗證是否滿足假設前提,並對參數調整。
在這裏插入圖片描述
2.3.使用準確率作爲模型評估準則

比如,MAPE(平均絕對誤差百分比),從總體上評估模型預測準確率:
在這裏插入圖片描述
MAPE也是反映誤差大小的相對值,不同模型對同一數據的模型進行評估纔有比較的意義。

以上列舉了三種建模評估標準,最好綜合考慮,AIC從信息論的角度度量信息損失大小, Box-Jenkins是傳統的層層假設之下的時間序列統計建模準則,在應對單一序列模型,通過眼觀圖形和檢驗參數的顯著性來判定,效果佳。而如果我們關注的是模型準確率,那麼最好的當然是MAPE等關注預測準確性的指標,此時不能無腦的用函數原始的定義或者AIC,這點尤其值得關注,包括XGBoost模型中,目標損失函數不建議直接使用RMSE。

  1. 代碼實現

3.1導入模塊


import time
from itertools  import product
import numpy as np
import pandas as pd
from joblib import Parallel,delayed
import warnings
warnings.filterwarnings('ignore')
from warnings import catch_warnings,filterwarnings
from statsmodels.tsa.statespace.sarimax import SARIMAX

3.2 定義模型

該部分的關鍵參數已經在上文全部羅列,因爲重要所以從原文檔中全部總結翻譯過來。

#傳入數據和參數,輸出模型預測
def model_forecast(history,config):
    order, sorder, trend = config
    model = SARIMAX(history, order=order,      seasonal_order=sorder,trend=trend,enforce_stationarity=False, enforce_invertibility=False)
    model_fit = model.fit(disp=False)
    yhat = model_fit.predict(len(history), len(history))
    return yhat[0]

3.3模型評估函數和數據劃分

#模型評估指標,mape
def mape(y_true, y_pred):
    y_true, y_pred = np.array(y_true), np.array(y_pred)
    return np.mean(np.abs((y_true - y_pred) / y_true)) * 100    

#劃分訓練集和測試集
def train_test_split(data, n_test):
    return data[:-n_test], data[-n_test:]

3.4 定義滾動預測

使用one-step滾動向前預測法,每次預測值再加入數據中,接着預測下一個值,而不是一次預測多個值。依據經驗,我們可以知道多數情況下,其實滾動逐步預測比多步預測效果更佳,所以應該嘗試滾動預測。

#one-step滾動向前預測
def forward_valid(data, n_test, cfg):
    predictions = list()
    train, test = train_test_split(data, n_test)
    history = [x for x in train]
    for i in range(len(test)):
        yhat = model_forecast(history, cfg)
        predictions.append(yhat)
        history.append(test[i])
    error = mape(test, predictions)
    return error

當模型的移動平均或者自迴歸階數較高,模型計算極大似然的時候可能會拋出很多警告,比如常見的自由度爲0,模型非正定,還如AIC有時會得到NaN值,所以這裏需要設置忽視警告,我們需要用到python中的try-except異常處理控制流,把可能報錯的語句放置在try中,以免程序報錯中斷。如果需要查看警告或者調試,則debug這裏可以設置爲True,但是大概率程序會報錯退出。對數據進行標準化等處理,其實在一定程度上是可以避免一些計算方面的問題,同時也會提高計算求解效率。

3.5 模型評估

#模型評估
def score_model(data,n_test,cfg,debug=False):
    result = None
    key = str(cfg)
    if debug:
        result = forward_valid(data, n_test, cfg)
    else:
        try:
            with catch_warnings():
                filterwarnings("ignore")
                result = forward_valid(data, n_test, cfg)
        except:
            error = None
            
    return (key, result)

3.6 網格搜索

網格搜索非常耗時,時間複雜度非常高,爲指數型。在條件允許的情況下,尤其是PC和服務器計算力大大的得到提高的當下,通常的做法都是用空間換時間,壓榨計算資源,使用多線程並行,以便可以在短時間內得到計算結果。

所以,我們使用Joblib模塊中的Parallel和delayed函數並行多個模型,Joblib模塊也是常常在機器學習任務grid search和Cross validation中爲了提高計算速度需要必備的模塊。

#網格搜索
def grid_search(data, cfg_list, n_test, parallel=True):
    scores = None
    if parallel:
        #使用計算機全部的cpu核數多進程並行
        executor = Parallel(n_jobs=-1, backend='multiprocessing')
        tasks = (delayed(score_model)(data, n_test, cfg) for cfg in cfg_list)
        scores = executor(tasks)
        
    else:
        scores = [score_model(data, n_test, cfg) for cfg in cfg_list]
    scores = [r for r in scores if r[1] != None]
    scores.sort(key=lambda x: x[1])
    return scores

#生成參數列表
def sarima_configs(seasonal=[0]):   
    p = d = q = [0,1,2]
    pdq = list(product(p, d, q))
    s = 0
    seasonal_pdq = [(x[0], x[1], x[2], s) for x in list(product(p, d, q))]
    t=['n','c','t','ct']
    return list(product(pdq,seasonal_pdq,t))

還要嘮叨的是,本文引進的itertools模塊中的product是對迭代對象創建笛卡爾積的函數,窮盡迭代對象的所有組合,返回迭代對象組合的元組。解釋一下 ,上面sarima_configs函數中的,p、 d、 q都是0、1、2,因爲更高階的並不多見,且高階會導致模型非常複雜,往往0,1,2也就夠了,關於季節性設置了一個默認的0,是因爲本文使用的是周這樣的彙總時間點,通過前期數據探索作圖看出設置爲4,或者12都沒有意義,所以爲了節省計算資源,指定爲0。以上函數自己可以寫嵌套循環,但是python內置的模塊和成熟的模塊在計算性能和規範上會比自己手寫的優很多,這也是不要重複造輪子的理念,除非自己造的輪子更好,能解決一個新需求。既然講到計算性能,本文涉及到了很多循環迭代,那麼如果可以的話,建議使用Profile 這個內置的模塊分析每個函數花費的時間。

以下爲模型訓練函數,n_test=3表示預測3個值,因爲我個人使用的場景比較固定,所以就直接寫在函數內部作爲局部變量了,爲了保持函數的靈活性,作爲全局參數或者函數的形參當然是更好,另,下面這種列表元素append追加似乎不太優雅。(過早優化是萬惡之源,emmm,就這樣子了,逃)


#模型訓練
def train_model(sale_df):
    
    n_test = 3
    p_b,d_b,q_b=[],[],[]
    P_b,D_b,Q_b=[],[],[]
    m_b,t_b=[],[]
    model_id,error=[],[]
    for i in sale_df['store_code'].unique():
        data=sale_df[sale_df['store_code']==i]['y']
        data=[i for i in data]
        cfg_list = sarima_configs()
        scores = grid_search(data,cfg_list,n_test,parallel=True)
        p_b.append(int(scores[0][0][2]))
        d_b.append(int(scores[0][0][5]))
        q_b.append(int(scores[0][0][8]))
        P_b.append(int(scores[0][0][13]))
        D_b.append(int(scores[0][0][16]))
        Q_b.append(int(scores[0][0][19]))
        m_b.append(int(scores[0][0][22]))
        t_b.append(str(scores[0][0][27]))
        model_id.append(i)
        error.append(scores[1][-1])
        params_df=pd.DataFrame({'store_code': model_id, 'map': error,'p':p_b,'d':d_b,'q':q_b,'P':P_b,'D':D_b,'Q':Q_b,'m':m_b,'t':t_b})
    return params_df

3.7 獲得最優參數,滾動預測未來值

通過模型訓練得到的最優參數,滾動預測四個時間點。

#定義預測函數,傳入數據和參數,返回預測值
def one_step_forecast(data,order,seasonal_order,t,h_fore):
    predictions=list()
    data=[i for i in data]
    for i in range(h_fore):
        model = SARIMAX(data, order=order, seasonal_order=seasonal_order,trend=t,enforce_stationarity=False, enforce_invertibility=False)
        model_fit = model.fit(disp=False)
        yhat = model_fit.predict(len(data), len(data))
        data.append(yhat[0])
        predictions.append(yhat[0])
    return predictions


#用for循環,多個序列預測
def forecast_model(sale_df,params_df):
    h_fore=4
    fore_list=[]
    model_id=[]
    for i in sale_df['store_code'].unique():
        #params_list=params_df[params_df['store_code']==i]
        data=sale_df[sale_df['store_code']==i]['y']
        p=params_df[params_df['store_code']==i].iloc[:,2].values[0]
        d=params_df[params_df['store_code']==i].iloc[:,3].values[0]
        q=params_df[params_df['store_code']==i].iloc[:,4].values[0]
        P=params_df[params_df['store_code']==i].iloc[:,5].values[0]
        D=params_df[params_df['store_code']==i].iloc[:,6].values[0]
        Q=params_df[params_df['store_code']==i].iloc[:,7].values[0]
        m=params_df[params_df['store_code']==i].iloc[:,8].values[0]
        t=params_df[params_df['store_code']==i].iloc[:,9].values[0]
        order=(p, d, q)
        seasonal_order=(P,D,Q,m)
        all_fore=one_step_forecast(data,order,seasonal_order,t,h_fore)
        fore_list.append(all_fore)
        
        #以下爲,多步預測,如果不使用滾動預測,則不調one_step_forecast函數
        #model=SARIMAX(data, order=order,seasonal_order=seasonal_order,trend=t,enforce_stationarity=False,
        #                                                enforce_invertibility=False)
        #forecast_=model.fit(disp=-1).forecast(steps=h_fore)
        #fore_list_flatten = [x for x in forecast_]
        #fore_list.append(fore_list_flatten)
        model_id.append(i)
    df_forecast = pd.DataFrame({'store_code': model_id, 'fore': fore_list})
    return df_forecast

main函數


if __name__ == '__main__':
    start_time=time.time()
    sale_df=pd.read_excel('/home/test01/store_forecast/sale_df.xlsx')
    params_df=train_model(sale_df)
    forecast_out=forecast_model(sale_df,params_df)
    end_time=time.time()
    use_time=(end_time-start_time)//60
    print('finish the process use',use_time,'mins')

3.8 模型結果

以下爲本次模型所得結果,每個門店一個序列模型。

store_code mape p d q P D Q m t
1F 6.305378 1 0 2 2 0 1 0 c
62 1.889192 0 2 2 0 0 2 0 t
CS 1.425515 1 2 2 0 0 0 0 c
2H 2.144674 0 1 2 1 0 2 0 c
32 5.289745 0 2 2 0 0 0 0 t

五個模型總體MAPE爲3.4%,效果還不錯,本文還沒有用到SARIMAX中的X,也就是eXogenous外部因素,模型得到的MAPE已經降到3.4%,也即準確率到了96.6%,限於時間的關係,暫時沒有加入如天氣/客流等外部因素。

4.總結

本文考慮多種參數組合的時間序列模型,通過並行網格搜索,回測得到MAPE最小的模型參數,並把最優參數作爲模型預測未來值的參數滾動預測未來4個時間點。以上就是結合自己經驗和體會以及查閱的資料針對幾個關鍵點進行闡述而寫就。如有誤,歡迎指正留言。完整的程序和數據放在Github

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