月間航空旅客数のデータを使用する。
y <- c(AirPassengers) # 月間航空旅客数
n <- length(y) # 月数
n.tr <- 120 # 訓練月数
n.te <- n - n.tr # テスト月数
t <- 1:n # 月
ii.tr <- 1:120 # 訓練期間インデックス
ii.te <- 121:n # テスト期間インデックス
matplot(y, type = 'o', pch = 1, col = 2)
fq <- 12
グラフから周期(\(fq = 12\))の周期性があることが分かる。
時間の経過とともに上昇するトレンドが有り振幅も徐々に大きくなっていることから,この時系列は非定常であることが分かる。 定常時系列にするためにはトレンド除去や変数変換が必要となる。
BoxCox変換は次式で\(x\)を変換し\(y^{(\lambda)}\)を得る変数変換法である。 \(\lambda\)が0のときは定義により(自然)対数変換となる。
\[y^{(\lambda)} = \begin{cases}\frac{x^\lambda-1}{\lambda}\quad (\lambda \ne 0)\\ \ln(x)\quad (\lambda = 0)\end{cases}\]
library(MASS)
bc <- boxcox(y ~ t, data = data.frame(y = y[ii.tr], t = t[ii.tr]))
lambda <- round(bc$x[which.max(bc$y)], 1)
lambda
## [1] 0
このデータでは\(\lambda\)は0のとき対数尤度(たいすうゆうど;log-likelihood)が最大になっている。
library(forecast)
## Registered S3 method overwritten by 'quantmod':
## method from
## as.zoo.data.frame zoo
y.bc <- BoxCox(y[ii.tr], lambda)
matplot(y.bc, type = 'o', pch = 1, col = 2, ylab = 'y')
振幅がほぼ一定になった。
1次のトレンド(直線的傾向変動)があることから, 1次の階差をとりトレンド除去する。 2次のトレンド(曲線的傾向変動)がある場合は2次階差をとるなど, トレンドがなくなるまで階差を取り続ける(階差を取ることは微分することに相当)。
d <- 1
dy <- diff(y.bc, diff = d)
matplot(dy, type = 'o', pch = 1, col = 2, ylab = 'y')
1次の階差をとることで,トレンドが消えほぼ定常化できた。
定常化のためにBoxCox変換(\(\lambda = 0\))と1次階差(\(d = 1\))が必要となる ので,ARIMAモデルをフィッティングする関数(forecast::Arima)のオプションを設定する。 なお,1次階差をとる前の時系列は定常時系列から見ると和分(差分の反対) した時系列となる。
原系列とラグ系列の自己相関
原系列とラグ系列の本質的な自己相関(自己相関から他の短いラグ系列の影響を取り除いたもの)。
最大次数が分かる。
AR,MAの次数p,qをACF,PACFグラフを見て決める。
モデル | ACF | PACF |
---|---|---|
AR | 減衰 | 有意なp次ラグ |
MA | 有意なq次ラグ | 減衰 |
ARMA | 減衰 | 減衰 |
ys <- ts(10 * sin(2 * pi * t / 10) + rnorm(n))
dir.create('acf_lag', showWarnings = F)
for (k in 1:25)
{
png(paste0('acf_lag/acf_lag', k, '.png'))
ys.lag <- lag(ys, k = -k)
ts2 <- cbind(ys, ys.lag)
par(mfrow = c(3, 1), mar = c(2, 4, 1, 1))
matplot(ts2, type = 'o', pch = 1, col = c(1, 4),
xlab = '', ylab = '', xlim = c(0, 120))
abline(v = c(0, k), col = 2)
arrows( 0, 0, k, 0, length = 0.1, col = 2)
mtext(paste(k), side = 1, at = k, line = 1, col = 2)
legend('topright', col = c(1, 4), bg = 'white', lty = 1,
legend = c('原系列(周期10の正弦波+ノイズ)', paste0(k, '次ラグ系列')))
grid()
acf(ys, lag.max = k, xlim = c(0, 120), ylim = c(-1, 1), main = '')
pacf(ys, lag.max = k, xlim = c(0, 120), ylim = c(-1, 1), main = '')
dev.off()
}
#system('bash -c 'convert -delay 100 -loop 0 ./acf_lag/acf_lag{1..25}.png ./acf_lag/acf_lag.gif'')
\[Y_t = 10\sin\left(2\pi \frac{t}{10}\right) + \epsilon_t, \quad \epsilon_t \sim \mathbf{N}(0, 1)\]
【注意】青点線で囲まれた範囲は有意ではない。
周期性がある場合は周期内で有意な最大ラグ次数を選ぶ。
a <- acf(dy, lag.max = 50) # MA(q)
grid()
abline(v = fq, col = 3, lty = 3)
q <- 12
pa <- pacf(dy, lag.max = 50) # AR(p)【注意】acfと違いグラフが1次ラグから始まる。
grid()
abline(v = fq, col = 3, lty = 3)
p <- 12
Arima関数のオプション設定 order = c(p, d,
q)にAR,和分,MAの次数p,d,qを与える。
frequencyには季節周期を入力する。
seasonalには季節周期を単位時間とみなしたときの周期点系列のモデル次数を設定する。
変数変換(BoxCox変換)を自動で行うオプション設定(lambda =
‘auto’)を使用。
【注意】次数が大きいと計算時間が掛かる。また,訓練データは大きなサイズが必要となる。
fit.arima <- Arima(ts(y[ii.tr], frequency = fq), lambda = 'auto',
order = c(p, d, q),
seasonal = c(1, 1, 0)) # SARIMA
# c.f.
#fit.arima <- auto.arima(ts(y[ii.tr], frequency = fq), lambda = 'auto')
fit.arima
## Series: ts(y[ii.tr], frequency = fq)
## ARIMA(12,1,12)(1,1,0)[12]
## Box Cox transformation: lambda= -0.3096643
##
## Coefficients:
## ar1 ar2 ar3 ar4 ar5 ar6 ar7 ar8
## -0.2542 0.0940 -0.0545 0.0567 -0.0594 -0.0297 0.1077 -0.0028
## s.e. 0.1417 0.1441 0.1368 0.1352 0.1335 0.1306 0.1425 0.1334
## ar9 ar10 ar11 ar12 ma1 ma2 ma3 ma4
## 0.1405 0.0729 -0.2619 -0.2848 -0.0736 -0.1363 -0.0955 -0.2205
## s.e. 0.1335 0.1311 0.1333 0.1807 0.1472 0.1384 0.1264 0.1472
## ma5 ma6 ma7 ma8 ma9 ma10 ma11 ma12
## 0.1913 0.2118 -0.1829 -0.0367 -0.1446 0.0096 0.3593 -0.6336
## s.e. 0.1458 0.1479 0.1603 0.1385 0.1426 0.1516 0.1519 0.1503
## sar1
## 0.1202
## s.e. 0.2297
##
## sigma^2 = 5.446e-05: log likelihood = 381.33
## AIC=-710.67 AICc=-693.12 BIC=-641.17
ACFで有意なものがない(棒が青点線をでていない)こと, 残差分布が0点回りに左右対称(釣り鐘状)になっていることを確認する。
Ljung-Box(リュング・ボックス)検定は残差系列の自己相関の有無を判定する。
p値が0.05以下であると5%水準で有意な自己相関がある。
帰無仮説:残差は独立に分布している(ホワイトノイズ)。
対立仮設:残差には自己相関がある。
残差分析の結果が思わしくないときはモデルを見直す。 また,気温などの外的要因の影響があるときは,外生変数(説明変数;xregオプションで与える)の導入を検討する。
checkresiduals(fit.arima) # 残差分析グラフ
##
## Ljung-Box test
##
## data: Residuals from ARIMA(12,1,12)(1,1,0)[12]
## Q* = 6.3161, df = 3, p-value = 0.09721
##
## Model df: 25. Total lags used: 28
pred <- forecast(fit.arima, h = n.te, level = 95)
matplot(t, y, type = 'o', pch = 16, col = 2, main = pred$method)
grid()
abline(v = n.tr + 1, lty = 3, col = 3)
matlines(t[ii.tr], fit.arima$fitted, type = 'o', pch = 16, lty = 2, col = 4)
matlines(t[ii.te], pred$mean, type = 'o', pch = 1, lty = 2, col = 4)
matlines(t[ii.te], pred$lower, lty = 3, col = 'gray')
matlines(t[ii.te], pred$upper, lty = 3, col = 'gray')
legend('topleft', bg = 'white', lty = c(1, 2, 2, 3),
col = c(2, 4, 4, 'gray'), pch = c(16, 16, 1, NA),
legend = c('原系列', '1期先予測値', '予測値', '95%予測区間'))
get.accuracy <- function(yhat, y)
{
data.frame(MBE = mean(yhat - y),
MAE = mean(abs(yhat - y)),
MAPE = mean(abs((yhat - y) / y)) * 100,
RMSE = sqrt(mean((yhat - y)^2))
)
}
get.accuracy(yhat = pred$mean, y = y[ii.te])
## MBE MAE MAPE RMSE
## 1 -14.07091 14.92617 3.421171 17.18222
Rの時系列解析ライブラリforecastには,auto.arimaと呼ばれる自動でARIMAモデル
の選択,フィッティングを行う関数がある。
この関数の利用は周期性を有する時系列の長期予測には以下の理由により推奨しない。
AICは期待平均対数尤度の不偏推定量であり, 最大対数尤度とパラメータ数を明示的に含んだ関数である。 AICではパラメータ数は罰則として機能しモデル選定でよく使用される。 重回帰モデルとは違い,時系列モデルにおいてはARの最尤計算などで 使用する誤差は1期先予測誤差であり, 本来時系列に内包する周期性を表現するため高次のモデルが必要な場合であっても, 低次のモデルでも1期先予測誤差は十分小さくなる場合がある。このため, モデル選択は比較的低次のモデルになりやすいという欠点がある。
周期性を有する時系列の長期予測のためには周期分の繰り返しを再現できる パラメータの数を十分確保できていることをよく確認すること。 長期予測のためには検証データ(validation data set)をテストデータとは 別に用意して,Arima関数を使って手動でパラメータ数を設定したほうが良い。 時系列データを訓練用,検証用,テスト用の3分割し,訓練と検証を繰り返し, 選定された最適なモデルで,最後にテストデータで精度評価する。 テストデータでパラメータ調整を行うと訓練データとテストデータに過適合するので, 必ず検証データで行うこと。
次の時系列モデルで生成したデータを用いてARIMAモデルによる予測を行え。 (BoxCox変換やgif動画作成は実施しなくて良い。)
\[Y_t = 100 + 0.1t + 0.02t^2 + 100\sin\left(2\pi \frac{t}{24}\right) + \epsilon_t, \quad \epsilon_t \sim \mathbf{N}(0, 5^2)\]
n <- 24*7*2 # 時間数
t <- 1:n # 時間
n.tr <- floor(n * 0.8) # 訓練期間数
n.te <- n - n.tr # テスト期間数
ii.tr <- 1:n.tr # 訓練期間インデックス
ii.te <- (n.tr+1):n # テスト期間インデックス
set.seed(5)
y <- 100 + 0.1 * t + 0.02 * t^2 + 100 * sin(2 * pi * t / 24) + rnorm(n, sd = 5)
matplot(y, type = 'o', pch = 1, col = 2)