背景
前段时间参与了一个快消行业需求预测的项目。其中,用到了移动平均法、ARIMA、Xgboost等方法进行预测,现在打算总结一下ARIMA。
因为项目的销售数据属于私密数据,这里用网上找的一份案例数据用于展示。
构建ARIMA模型可以用到statsmodels库和pmdarima库。我这里用的是pmdarima库,这个库有一个优点是能够自动地对ARIMA模型的参数进行自动确定。
1、建模步骤
2、季节性ARIMA模型
A R I M A ( p , d , q ) ( P , D , Q ) m ARIMA (p,d,q) (P,D,Q)_mARIMA(p,d,q)(P,D,Q)m 该模型需要提供的参数有如下两类:
(1)趋势参数
p:趋势自回归阶数。
d:趋势差分阶数。
q:趋势移动平均阶数。
(2)季节性参数
P:季节性自回归阶数。
D:季节性差分阶数。
Q:季节性移动平均阶数。
m:单个季节期间的时间步数。
一、模块导入及数据读取
1、模块及数据
导入相关模块
from model.arimaModel import * # 导入自定义的方法
import pandas as pd
import matplotlib.pyplot as plt
import pmdarima as pm
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf # 画图定阶
from statsmodels.tsa.stattools import adfuller # ADF检验
from statsmodels.stats.diagnostic import acorr_ljungbox # 白噪声检验
import warnings
warnings.filterwarnings("ignore") # 选择过滤警告
读取本地数据,需要数据可以留言。这里我用的数据是月份数据,如果有的数据集是日的数据,当发生太大波动性性很难发现趋势时,可以使用resample()进行每月重采样。
io = 'D:/PythonProject/ARIMA/AirPassengers.csv'
time_series = pd.DataFrame(pd.read_csv(io, header=0, index_col='Month'))
# 将字符串索引转换成时间格式
time_series.index = pd.to_datetime(time_series.index)
# 输出前5个数据,此时Date为时间索引
print("样本量为{}个".format(len(time_series)))
print(time_series.head())
2、季节性分析
# 时间序列图生成函数
def sequencePlot(data, sequencePlot_name):
data.plot(marker='o')
# 设置坐标轴标签
plt.xlabel("时间", fontsize=15)
plt.ylabel("销量", fontsize=15)
# 设置图表标题
plt.title("{}时间序列图".format(sequencePlot_name), fontsize=15)
# 图例位置
plt.legend(loc='best')
# 生成网格
plt.grid(linestyle='--')
plt.show()
orign_seqname = "原始"
sequencePlot(time_series, orign_seqname)
用matplotlib输出原始时间序列图如下。明显地,我们可以看到数据有上升的趋势,可以定性地判断该序列非平稳。另外,该时间序列有比较明显的季节性趋势,即基本上每一年数据都呈现先上升再下降。
同时,可以使用seasonal_decompose()进行分析,将时间序列分解成长期趋势项(Trend)、季节性周期项(Seansonal)和残差项(Resid)这三部分。该方法的原理见这篇文章 。
from statsmodels.tsa.seasonal import seasonal_decompose
# 分解数据查看季节性 freq为周期
ts_decomposition = seasonal_decompose(time_series['Passengers'], freq=12)
ts_decomposition.plot()
plt.show()
由下子图,确实每年的数据呈现先升后降的特征。
二、平稳性检验
1、Augmented Dickey-Fuller Test(ADF检验)
ARIMA模型要求时间序列是平稳的。所谓平稳性,其基本思想是:决定过程特性的统计规律不随着时间的变化而变化。由平稳性的定义:对于一切t , s t,st,s,Y t Y_tYt和Y s Y_sYs的协方差对于时间的依赖之和时间间隔∣ t − s ∣ |t-s|∣t−s∣有关,而与实际的时刻t和s无关。因此,平稳过程可以简化符号,其中C o v CovCov为自协方差函数,C o r r CorrCorr为自相关函数,记为:γ t = C o v ( Y t , Y t − k ) , ρ k = C o r r ( Y t , Y t − k ) \gamma_t=Cov(Y_t,Y_{t-k}) ,\rho_k=Corr(Y_t,Y_{t-k})γt=Cov(Yt,Yt−k),ρk=Corr(Yt,Yt−k)另外,平稳分为严平稳和宽平稳。定义如下:
(1)严平稳:特别地,如果对一切时滞k kk和时点点t 1 , t 2 , . . . , t k t_1,t_2,...,t_kt1,t2,...,tk,都有Y t 1 , Y t 2 , . . . , Y t n Y_{t_1},Y_{t_2},...,Y_{t_n}Yt1,Yt2,...,Ytn与Y t 1 − k , Y t 2 − k , . . . , Y t n − k Y_{t_1-k},Y_{t_2-k},...,Y_{t_n-k}Yt1−k,Yt2−k,...,Ytn−k的联合分布相同,则称过程{Y t Y_tYt}为严平稳的。
(2)宽平稳:满足1)均值函数在所有时间上恒为常数;2)γ t , t − k = γ 0 , k \gamma_{t,t-k}=\gamma_{0,k}γt,t−k=γ0,k,对所有的时间t tt和滞后k kk。
如果通过时间序列图来用肉眼观看的话可能会存在一些主观性。ADF检验(又称单位根检验)是一种比较常用的严格的统计检验方法。
ADF检验主要是通过判断时间序列中是否含有单位根:如果序列平稳,就不存在单位根;否则,就会存在单位根。
详细的介绍可以看这篇博客
https://blog.csdn.net/FrankieHello/article/details/86766625/
2、Python实现
from statsmodels.tsa.stattools import adfuller # ADF检验
# 该函数参考了其他文章
def stableCheck(timeseries):
# 移动12期的均值和方差
rol_mean = timeseries.rolling(window=12).mean()
rol_std = timeseries.rolling(window=12).std()
# 绘图
fig = plt.figure(figsize=(12, 8))
orig = plt.plot(timeseries, color='blue', label='Original')
mean = plt.plot(rol_mean, color='red', label='Rolling Mean')
std = plt.plot(rol_std, color='black', label='Rolling Std')
plt.legend(loc='best')
plt.title('Rolling Mean & Standard Deviation')
plt.show()
# 进行ADF检验
print('Results of Dickey-Fuller Test:')
dftest = adfuller(timeseries, autolag='AIC')
# 对检验结果进行语义描述
dfoutput = pd.Series(dftest[0:4], index=['Test Statistic', 'p-value', '#Lags Used', 'Number of Observations Used'])
for key, value in dftest[4].items():
dfoutput['Critical Value (%s)' % key] = value
print('ADF检验结果:')
print(dfoutput)
stableCheck_result1 = stableCheck(time_series)
检验结果如下。ADF检验的H 0 H_0H0假设就是存在单位根,如果得到的显著性检验统计量小于三个置信度(10%,5%,1%),则对应有(90%,95,99%)的把握来拒绝原假设。有以下两种方式来判断:
(1)这里将Test Statistic与1%、%5、%10不同程度拒绝原假设的统计值进行比较。当ADF Test result 同时小于 1%、5%、10%即说明非常好地拒绝该假设。本数据中,0.815369并没有都小于三个level的统计值,所以判断为该时间序列time series非平稳。
(2)观察p-value,是否非常接近于0。这里p-value约为0.99>0.05,因此不能拒绝原假设。
三、序列平稳化
差分法
对于非平稳时间序列,可以通过d阶差分运算使变成平稳序列。从统计学角度来讲,只有平稳性的时间序列才能避免“伪回归”的存在,才有经济意义。
在这里,我先进行了一阶差分,再进行了一次季节性差分,才通过ADF检验。
# 差分处理非平稳序列,先进行一阶差分
time_series_diff1 = time_series.diff(1).dropna()
# 在一阶差分基础上进行季节性差分差分
time_series_diff2 = time_series_diff1.diff(12).dropna()
stableCheck_result2 = stableCheck(time_series_diff2)
结果如下,p-value小于0.05,且Test Statistic远小于Critical Value (5%)的值,可以有95%的概率认为时间序列平稳。
因此,我们可以确定季节性ARIMA模型的d = 1 , D = 1 , m = 12 d=1,D=1,m=12d=1,D=1,m=12。
四、白噪声检验
接下来,对d阶差分后的的平稳序列进行白噪声检验,若该平稳序列为非白噪声序列,则可以进行第五步。
1、白噪声定义
对于一序列e 1 , e 2 , e 3 , . . . , e t e_1,e_2,e_3,...,e_te1,e2,e3,...,et,若满足:E ( e t ) = 0 E(e_t)=0E(et)=0V a r ( e t ) = σ 2 Var(e_t)=σ^2Var(et)=σ2C o v ( e t , e t + k ) = 0 ( k ≠ 0 ) Cov(e_t,e_{t+k})=0 (k≠0)Cov(et,et+k)=0(k=0) 那么该序列是一个白噪声序列。
详细的介绍可以看这篇博客
https://www.cnblogs.com/travelcat/p/11400307.html
2、Ljung-Box检验
Ljung-Box检验即LB检验,是时间序列分析中检验序列自相关性的方法。LB检验的Q统计量为:
Q ( m ) = T ( T + 2 ) ∑ l = 1 m ρ ^ l 2 T − l Q(m)=T(T+2) \sum_{l=1}^m \frac{\hat{ρ}_l^2}{T-l} \quadQ(m)=T(T+2)l=1∑mT−lρ^l2 用来检验m mm阶滞后范围内序列的自相关性是否显著,或序列是否为白噪声,Q统计量服从自由度为m mm的卡方分布。
3、Python实现
def whiteNoiseCheck(data):
result = acorr_ljungbox(data, lags=1)
temp = result[1]
print('白噪声检验结果:', result)
# 如果temp小于0.05,则可以以95%的概率拒绝原假设,认为该序列为非白噪声序列;否则,为白噪声序列,认为没有分析意义
print(temp)
return result
ifwhiteNoise = whiteNoiseCheck(time_series_diff2)
检验结果如下:返回的是统计量和p-value,其中,延迟1阶的p-value约为0.00033<0.05,可以以95%的概率拒绝原假设,认为该序列为非白噪声序列。
五、时间序列定阶
def draw_acf(data):
# 利用ACF判断模型阶数
plot_acf(data)
plt.title("序列自相关图(ACF)")
plt.show()
def draw_pacf(data):
# 利用PACF判断模型阶数
plot_pacf(data)
plt.title("序列偏自相关图(PACF)")
plt.show()
def draw_acf_pacf(data):
f = plt.figure(facecolor='white')
# 构建第一个图
ax1 = f.add_subplot(211)
# 把x轴的刻度间隔设置为1,并存在变量里
x_major_locator = MultipleLocator(1)
plot_acf(data, ax=ax1)
# 构建第二个图
ax2 = f.add_subplot(212)
plot_pacf(data, ax=ax2)
plt.subplots_adjust(hspace=0.5)
# 把x轴的主刻度设置为1的倍数
ax1.xaxis.set_major_locator(x_major_locator)
ax2.xaxis.set_major_locator(x_major_locator)
plt.show()
draw_acf_pacf(time_series_diff2)
具体怎么对季节性ARIMA模型进行定阶数可以看这里。
(1)确定d dd和D DD的阶数:当对原序列进行了d dd阶差分和l a g laglag为m mm的D DD阶差分后序列为平稳序列,则可以确定d dd,D DD,m mm的值。
(2)确定p pp,q qq和P PP,Q QQ的阶数:1)首先对平稳化后的时间序列绘制ACF和PACF图;2)然后,可以通过观察季节性l a g laglag处的拖尾/截尾情况来确定P , Q P,QP,Q的值;3)观察短期非季节性l a g laglag处的拖尾/截尾情况来确定p , q p,qp,q的值。
之前已经在步骤三处确定了d dd,D DD,m mm。对于剩下的参数,将依据如下的ACF和PACF图确定。下图的横坐标l a g laglag是以月份为单位。
非季节性部分:对于p pp,在l a g = 1 lag=1lag=1后,ACF图拖尾,PACF图截尾。同样地,对于q qq,在l a g = 1 lag=1lag=1后,ACF图截尾,PACF图拖尾。
季节性部分:P , Q P,QP,Q的确定和非季节性一样,不过需要记得滞后的间隔为12。
【补充参考】
AR模型:自相关系数拖尾,偏自相关系数截尾;
MA模型:自相关系数截尾,偏自相关函数拖尾;
ARMA模型:自相关函数和偏自相关函数均拖尾。
六、构建ARIMA模型及预测
这里使用了pmdarima.auto_arima()方法。这个方法可以帮助我们自动确定 A R I M A ( p , d , q ) ( P , D , Q ) m ARIMA(p,d,q)(P,D,Q)_mARIMA(p,d,q)(P,D,Q)m的参数,也就是可以直接跳过上述的步骤2~5,直接输入数据,设置auto_arima()中的参数则可。
之前我们是通过观察ACF、PACF图的拖尾截尾现象来定阶,但是这样可能不准确。实际上,往往需要结合图像拟合多个模型,通过模型的AIC、BIC值以及残差分析结果来选择合适的模型。
1、构建模型
import pmdarima as pm
将数据分为训练集data_train和测试集data_test 。
split_point = int(len(time_series) * 0.85)
# 确定训练集/测试集
data_train, data_test = time_series[0:split_point], time_series[split_point:len(time_series)]
# 使用训练集的数据来拟合模型
built_arimamodel = pm.auto_arima(data_train,
start_p=0, # p最小值
start_q=0, # q最小值
test='adf', # ADF检验确认差分阶数d
max_p=5, # p最大值
max_q=5, # q最大值
m=12, # 季节性周期长度,当m=1时则不考虑季节性
d=None, # 通过函数来计算d
seasonal=True, start_P=0, D=1, trace=True,
error_action='ignore', suppress_warnings=True,
stepwise=False # stepwise为False则不进行完全组合遍历
)
print(built_arimamodel.summary())
相关结果如下,从Model可以看出确定的参数与之前确定的参数大致相同。
2、模型识别
built_arimamodel.plot_diagnostics()
plt.show()
3、需求预测
# 进行多步预测,再与测试集的数据进行比较
pred_list = []
for index, row in data_test.iterrows():
# 输出索引,值
# print(index, row)
pred_list += [built_arimamodel.predict(n_periods=1)]
# 更新模型,model.update()函数,不断用新观测到的 value 更新模型
built_arimamodel.update(row)
# 预测时间序列以外未来的一次
predict_f1 = built_arimamodel.predict(n_periods=1)
print('未来一期的预测需求为:', predict_f1[0])
3、结果评估
对预测结果进行评价,指标解释看这。
# ARIMA模型评价
def forecast_accuracy(forecast, actual):
mape = np.mean(np.abs(forecast - actual)/np.abs(actual))
me = np.mean(forecast - actual)
mae = np.mean(np.abs(forecast - actual))
mpe = np.mean((forecast - actual)/actual)
# rmse = np.mean((forecast - actual)**2)**0.5 # RMSE
rmse_1 = np.sqrt(sum((forecast - actual) ** 2) / actual.size)
corr = np.corrcoef(forecast, actual)[0, 1]
mins = np.amin(np.hstack([forecast[:, None], actual[:, None]]), axis=1)
maxs = np.amax(np.hstack([forecast[:, None], actual[:, None]]), axis=1)
minmax = 1 - np.mean(mins/maxs)
return ({'mape': mape,
'me': me,
'mae': mae,
'mpe': mpe,
'rmse': rmse_1,
'corr': corr,
'minmax': minmax
})
# 画图观测实际与测试的对比
test_predict = data_test.copy()
for x in range(len(test_predict)):
test_predict.iloc[x] = pred_list[x]
# 模型评价
eval_result = forecast_accuracy(test_predict.values, data_test.values)
print('模型评价结果\n', eval_result)
# 画图显示
plt.plot(origin_timeseries, 'b-', lw=1, label='True Data', marker='*')
plt.plot(test_predict, 'r-', lw=2, label='Prediction', marker='o')
plt.title('RMSE:{}'.format(eval_result['rmse']))
plt.legend(loc='best')
plt.grid() # 生成网格
plt.show()
红色的点为预测结果,蓝色的点为原来的数据。
RMSE约为16.4,均方根误差(Root Mean Square Error,RMSE)是预测值与真实值偏差的平方与观测次数n比值的平方根指标,衡量观测值与真实值之间的偏差。一般是越小越好,但是实际上需要进行结果对比才能判断。
七、小结
在做项目时候,发现ARIMA模型对数据的要求还是挺高的。其中,数据量太少不行,当时做的时候发现一些门店的数据量太少,而且只有一年左右的跨度,很难发现一些周期规律。另外就是对平稳性的要求。
目前对时间序列分析还是初学阶段,很多地方的解释不够深入,后续还需要继续学习、完善。