SARIMAモデルによる季節時系列予測 — 季節性を捉える拡張ARIMAの理論と実装

アイスクリームの売上は毎年夏に急増し、冬に落ち込みます。航空旅客数は毎年同じ月にピークを迎えます。電力需要は冬と夏に高まり、春と秋に低下します。こうした季節性(seasonality)を持つ時系列データは、ビジネスや科学の至るところに存在します。

通常のARIMAモデルは、トレンドや短期的な依存関係を捉えることには長けていますが、1年周期の季節パターンのような長期的な規則的変動を十分にモデル化できません。SARIMAモデルは、ARIMAモデルに季節成分を追加することで、この制約を克服します。

SARIMAモデルを理解することで、以下のような場面で威力を発揮します。

  1. 需要予測 — 小売業や製造業における月次・週次の季節パターンを考慮した在庫管理
  2. エネルギー需要予測 — 季節的な電力・ガス消費量の予測と計画
  3. 観光・交通量の予測 — 航空旅客数やホテル予約数の季節変動を捉えた経営計画

本記事の内容

  • ARIMAモデルの限界と季節性のある時系列への課題
  • 季節差分の導入とその数学的意味
  • SARIMA(p,d,q)(P,D,Q)$_s$ の記法と各パラメータの意味
  • 季節AR・季節MAの定式化
  • モデル選択手順(ACF/PACF、AIC)
  • Python(statsmodels)でAirPassengersデータの予測
  • 残差診断とモデル評価

前提知識

この記事を読む前に、以下の記事を読んでおくと理解が深まります。

ARIMAモデルの限界 — 季節性をどう扱うか

季節性とは

季節性とは、一定の周期 $s$ で繰り返されるパターンのことです。月次データであれば $s = 12$(1年周期)、四半期データであれば $s = 4$(1年周期)、日次データであれば $s = 7$(1週間周期)が典型的です。

季節性のある時系列 $\{x_t\}$ は、直感的に「今年の7月のデータは、昨年の7月のデータと似ている」という性質を持ちます。これは自己相関関数(ACF)で見ると、ラグ $s, 2s, 3s, \dots$ に有意なピークが現れるパターンとして観察されます。

通常のARIMAの問題点

通常のARIMAモデルで季節性を捉えようとすると、例えば月次データ($s = 12$)の場合、少なくともAR(12)やMA(12)が必要になります。これは12個もの自由パラメータを持つことを意味し、推定の効率が非常に悪くなります。

さらに深刻なのは、季節パターンでは「12期前」の影響が「1期前」や「2期前」の影響よりも強い場合が多いことです。しかし、通常のAR(12)モデルでは $\phi_1, \phi_2, \dots, \phi_{12}$ の全てが自由パラメータであり、季節パターンの構造的な特徴を利用していません。

この問題を解決するのが、季節差分季節ARMA項を組み合わせたSARIMAモデルです。

ARIMAモデルの基本を押さえたところで、SARIMAモデルの最初の構成要素である季節差分について見ていきましょう。

季節差分の導入

通常の差分と季節差分

ARIMAモデルでは、トレンドを除去するために1次差分を取りました。

$$ \nabla x_t = x_t – x_{t-1} = (1 – B) x_t $$

ここで $B$ はラグ演算子($Bx_t = x_{t-1}$)です。同様に、季節性を除去するためには季節差分を取ります。

$$ \begin{equation} \nabla_s x_t = x_t – x_{t-s} = (1 – B^s) x_t \end{equation} $$

季節差分の直感は明快です。「今年の7月の値から昨年の7月の値を引く」操作が季節差分です。もし季節パターンが毎年ほぼ同じであれば、この差分をとることで季節性が除去された系列が得られます。

差分の組み合わせ

実際のデータには、トレンドと季節性の両方が存在することが多いです。その場合、通常の差分と季節差分の両方を適用します。

トレンドと季節性を持つ時系列に対し、まず季節差分を $D$ 回、次に通常差分を $d$ 回適用した系列を考えます。

$$ \begin{equation} w_t = (1 – B)^d (1 – B^s)^D x_t \end{equation} $$

多くの実用ケースでは $d = 1$, $D = 1$ で十分です。月次データの場合($s = 12$)、

$$ w_t = (1 – B)(1 – B^{12}) x_t $$

まず $(1 – B^{12})$ で季節性を除去し、さらに $(1 – B)$ でトレンドを除去します。この操作を展開すると、

$(1 – B)(1 – B^{12})x_t$ の各項を具体的に書き下すと、

$$ w_t = x_t – x_{t-1} – x_{t-12} + x_{t-13} $$

となります。これは「今月と先月の差」から「12か月前の同じ差」を引いたもの、つまり「前年同月比の変化量の変化」を意味しています。

Pythonで季節差分を確認

季節差分の効果をAirPassengersデータで確認してみましょう。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.datasets import get_rdataset

# AirPassengersデータの読み込み
data = get_rdataset("AirPassengers").data
data.columns = ['time', 'passengers']
passengers = data['passengers'].values
n = len(passengers)
time_index = pd.date_range('1949-01', periods=n, freq='MS')

fig, axes = plt.subplots(4, 1, figsize=(12, 12))

# 元データ
axes[0].plot(time_index, passengers, linewidth=1.5)
axes[0].set_title('Original Data (AirPassengers)')
axes[0].set_ylabel('Passengers')

# 対数変換(分散安定化)
log_passengers = np.log(passengers)
axes[1].plot(time_index, log_passengers, linewidth=1.5, color='darkorange')
axes[1].set_title('Log Transformed')
axes[1].set_ylabel('log(Passengers)')

# 季節差分
seasonal_diff = log_passengers[12:] - log_passengers[:-12]
axes[2].plot(time_index[12:], seasonal_diff, linewidth=1.5, color='green')
axes[2].set_title('Seasonal Difference (d=0, D=1, s=12)')
axes[2].set_ylabel('Δ₁₂ log(Passengers)')
axes[2].axhline(y=0, color='gray', linestyle='--', alpha=0.5)

# 季節差分 + 1次差分
double_diff = seasonal_diff[1:] - seasonal_diff[:-1]
axes[3].plot(time_index[13:], double_diff, linewidth=1.5, color='purple')
axes[3].set_title('Seasonal + First Difference (d=1, D=1, s=12)')
axes[3].set_ylabel('Δ₁Δ₁₂ log(Passengers)')
axes[3].axhline(y=0, color='gray', linestyle='--', alpha=0.5)

plt.tight_layout()
plt.show()

この4段階の変換を上から順に追っていくと、各ステップの効果が明確にわかります。

  1. 元データ(1段目): 上昇トレンドと季節変動の振幅が増大するパターンが見られます。乗法的な季節性です。
  2. 対数変換(2段目): 対数をとることで、乗法的な季節性が加法的に変換されています。振幅がほぼ一定になっています。
  3. 季節差分(3段目): 12か月分の差分をとることで季節パターンが除去されています。ただし、まだ緩やかな上昇トレンドが残っています。
  4. 季節差分+1次差分(4段目): さらに1次差分をとることで、トレンドも除去され、概ね定常な系列が得られています。

この最後の系列 $w_t$ に対して定常時系列モデル(ARMA)を当てはめるのがSARIMAの基本方針です。次に、SARIMAモデルの完全な定式化を見ていきましょう。

SARIMAモデルの定式化

SARIMA(p,d,q)(P,D,Q)$_s$ の記法

SARIMAモデルは、ARIMAモデルに季節AR項と季節MA項を追加したもので、以下の7つのパラメータで特徴づけられます。

パラメータ 意味 範囲
$p$ 非季節AR次数 0, 1, 2, …
$d$ 非季節差分次数 0, 1, 2
$q$ 非季節MA次数 0, 1, 2, …
$P$ 季節AR次数 0, 1, 2, …
$D$ 季節差分次数 0, 1, 2
$Q$ 季節MA次数 0, 1, 2, …
$s$ 季節周期 4, 7, 12, …

数学的定式化

SARIMAモデルの完全な定式化は、ラグ演算子を使うとコンパクトに書けます。

まず、非季節成分と季節成分のそれぞれのAR多項式とMA多項式を定義します。

非季節AR多項式: $$ \phi(B) = 1 – \phi_1 B – \phi_2 B^2 – \cdots – \phi_p B^p $$

非季節MA多項式: $$ \theta(B) = 1 + \theta_1 B + \theta_2 B^2 + \cdots + \theta_q B^q $$

季節AR多項式: $$ \Phi(B^s) = 1 – \Phi_1 B^s – \Phi_2 B^{2s} – \cdots – \Phi_P B^{Ps} $$

季節MA多項式: $$ \Theta(B^s) = 1 + \Theta_1 B^s + \Theta_2 B^{2s} + \cdots + \Theta_Q B^{Qs} $$

これらを組み合わせて、SARIMAモデルは次のように定義されます。

$$ \begin{equation} \phi(B) \Phi(B^s) (1 – B)^d (1 – B^s)^D x_t = \theta(B) \Theta(B^s) \varepsilon_t \end{equation} $$

この式の左辺は「差分をとった後にARフィルタを通す」操作、右辺は「ホワイトノイズにMAフィルタを通す」操作を表しています。

各成分の解釈

この式を分解して、各成分の役割を理解しましょう。

差分部分 $(1-B)^d(1-B^s)^D$: トレンドと季節トレンドを除去して定常化する

非季節AR部分 $\phi(B)$: 短期的な自己回帰的依存(直前の数期の影響)を捉える

季節AR部分 $\Phi(B^s)$: 季節周期での自己回帰的依存(1年前、2年前、…の影響)を捉える

非季節MA部分 $\theta(B)$: 短期的なショックの影響を捉える

季節MA部分 $\Theta(B^s)$: 季節周期でのショックの影響を捉える

具体例:SARIMA(1,1,1)(1,1,1)$_{12}$

最もよく使われるモデルの一つである SARIMA(1,1,1)(1,1,1)$_{12}$ を展開してみましょう。

各多項式は、

$$ \phi(B) = 1 – \phi_1 B, \quad \theta(B) = 1 + \theta_1 B $$

$$ \Phi(B^{12}) = 1 – \Phi_1 B^{12}, \quad \Theta(B^{12}) = 1 + \Theta_1 B^{12} $$

モデル方程式は、

$$ (1 – \phi_1 B)(1 – \Phi_1 B^{12})(1 – B)(1 – B^{12}) x_t = (1 + \theta_1 B)(1 + \Theta_1 B^{12}) \varepsilon_t $$

右辺のMA部分を展開すると、

$$ (1 + \theta_1 B)(1 + \Theta_1 B^{12}) = 1 + \theta_1 B + \Theta_1 B^{12} + \theta_1 \Theta_1 B^{13} $$

これは、ホワイトノイズ $\varepsilon_t$ に加えて、1期前のショック($\theta_1 \varepsilon_{t-1}$)、12期前のショック($\Theta_1 \varepsilon_{t-12}$)、そして13期前のショック($\theta_1 \Theta_1 \varepsilon_{t-13}$)の影響が残ることを意味しています。

注目すべきは、13期前のショックの係数が $\theta_1 \Theta_1$ という積の形になっていることです。これは非季節MAと季節MAの相互作用を表しており、少ないパラメータ($\theta_1$ と $\Theta_1$ の2つだけ)で13次のラグ構造を表現できるという効率の良さを示しています。通常のMA(13)では13個のパラメータが必要であることと比較すると、SARIMAの構造的な利点が際立ちます。

SARIMAモデルの構造を理解したところで、次に実際のデータに対してモデルを構築する手順を見ていきましょう。

モデル同定の手順

SARIMAモデルのパラメータを決定するための体系的な手順を説明します。

ステップ1: データの前処理

  1. 分散安定化: 分散が時間とともに変化する場合、対数変換やBox-Cox変換を適用
  2. 定常性の確認: ADF検定等で定常性を検定

ステップ2: 差分次数 $d$, $D$ の決定

  1. 季節差分 $D$: 季節性のある非定常データに $D = 1$ を適用。通常 $D \leq 2$
  2. 通常差分 $d$: 季節差分後の系列にトレンドが残れば $d = 1$ を適用

差分をとりすぎると(over-differencing)、不必要な複雑さが生じるので注意が必要です。ADF検定や系列の目視確認で判断します。

ステップ3: ACF/PACFによる次数の同定

差分後の系列 $w_t$ に対してACFとPACFを計算し、以下のパターンを確認します。

非季節次数の同定(ラグ1, 2, 3, … のパターンを見る):

  • PACFがラグ $p$ で打ち切り → 非季節ARに $p$ を採用
  • ACFがラグ $q$ で打ち切り → 非季節MAに $q$ を採用

季節次数の同定(ラグ $s, 2s, 3s, \dots$ のパターンを見る):

  • PACFがラグ $s$ で打ち切り → 季節AR $P = 1$
  • ACFがラグ $s$ で打ち切り → 季節MA $Q = 1$

ステップ4: AICによる自動選択

ACF/PACFによる同定は主観が入りやすいため、情報量基準(AIC/BIC)を用いた自動的なモデル選択も併用します。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.datasets import get_rdataset
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf

# データの準備
data = get_rdataset("AirPassengers").data
data.columns = ['time', 'passengers']
passengers = data['passengers'].values
time_index = pd.date_range('1949-01', periods=len(passengers), freq='MS')

# 対数変換 + 季節差分 + 1次差分
log_pass = np.log(passengers)
seasonal_diff = log_pass[12:] - log_pass[:-12]
double_diff = seasonal_diff[1:] - seasonal_diff[:-1]

# ADF検定
adf_result = adfuller(double_diff)
print(f"ADF Statistic: {adf_result[0]:.4f}")
print(f"p-value: {adf_result[1]:.6f}")
print(f"=> {'Stationary' if adf_result[1] < 0.05 else 'Non-stationary'}")

# ACF/PACF
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

plot_acf(double_diff, lags=36, ax=axes[0])
axes[0].set_title('ACF of Δ₁Δ₁₂ log(Passengers)')

plot_pacf(double_diff, lags=36, ax=axes[1])
axes[1].set_title('PACF of Δ₁Δ₁₂ log(Passengers)')

plt.tight_layout()
plt.show()

ACF/PACFの結果から、以下のことが読み取れます。

  1. ADF検定: p値が0.05を大幅に下回っており、差分後の系列は定常であることが確認できます。$d=1, D=1$ の差分で十分です。
  2. ACF: ラグ1に有意な負の自己相関が見られ(非季節MA成分を示唆)、ラグ12にも有意なスパイクがあります(季節MA成分を示唆)。ラグ2以降は急速に減衰しており、$q=1, Q=1$ が候補です。
  3. PACF: ラグ1に有意なスパイクがあり、ラグ12にも有意なスパイクが見られます。$p=1, P=1$ が候補です。

これらの情報を総合すると、SARIMA(1,1,1)(1,1,1)$_{12}$ やSARIMA(0,1,1)(0,1,1)$_{12}$ が有力な候補となります。次に、AICで最適なモデルを選択してみましょう。

PythonによるSARIMAモデルの実装

AICによるモデル選択

複数のSARIMAモデルのAICを比較して最適なモデルを選択します。

import numpy as np
import pandas as pd
import warnings
from statsmodels.datasets import get_rdataset
from statsmodels.tsa.statespace.sarimax import SARIMAX
import itertools

warnings.filterwarnings('ignore')

# データ準備
data = get_rdataset("AirPassengers").data
data.columns = ['time', 'passengers']
passengers = data['passengers'].values
log_pass = np.log(passengers)
ts = pd.Series(log_pass, index=pd.date_range('1949-01', periods=len(passengers), freq='MS'))

# パラメータの候補
p_range = range(0, 3)
q_range = range(0, 3)
P_range = range(0, 2)
Q_range = range(0, 2)

# d=1, D=1, s=12 は固定
d, D, s = 1, 1, 12

results = []
for p, q, P, Q in itertools.product(p_range, q_range, P_range, Q_range):
    try:
        model = SARIMAX(ts, order=(p, d, q),
                        seasonal_order=(P, D, Q, s),
                        enforce_stationarity=False,
                        enforce_invertibility=False)
        fit = model.fit(disp=False, maxiter=200)
        results.append({
            'order': f'({p},{d},{q})({P},{D},{Q})_{s}',
            'p': p, 'q': q, 'P': P, 'Q': Q,
            'AIC': fit.aic,
            'BIC': fit.bic
        })
    except Exception:
        continue

results_df = pd.DataFrame(results).sort_values('AIC').reset_index(drop=True)
print("Top 10 Models by AIC:")
print(results_df.head(10).to_string(index=False))

AICの比較結果から、最も良いモデル(AICが最小)を確認できます。通常、SARIMA(0,1,1)(0,1,1)$_{12}$ や SARIMA(1,1,1)(0,1,1)$_{12}$ が上位に来ることが多いです。

最適モデルのフィッティングと予測

AICで選ばれた最適モデルを使って、実際に予測を行います。ここでは訓練データとテストデータに分割し、予測精度を評価します。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.datasets import get_rdataset
from statsmodels.tsa.statespace.sarimax import SARIMAX
import warnings
warnings.filterwarnings('ignore')

# データ準備
data = get_rdataset("AirPassengers").data
data.columns = ['time', 'passengers']
passengers = data['passengers'].values
log_pass = np.log(passengers)
time_index = pd.date_range('1949-01', periods=len(passengers), freq='MS')
ts = pd.Series(log_pass, index=time_index)

# 訓練/テスト分割(最後の24か月をテスト)
train = ts[:'1958-12']
test = ts['1959-01':]

# SARIMA(0,1,1)(0,1,1)_12 のフィッティング
model = SARIMAX(train, order=(0, 1, 1),
                seasonal_order=(0, 1, 1, 12),
                enforce_stationarity=False,
                enforce_invertibility=False)
result = model.fit(disp=False)

print(result.summary())

# 予測(テスト期間 + 12か月先)
n_forecast = len(test) + 12
forecast = result.get_forecast(steps=n_forecast)
forecast_mean = forecast.predicted_mean
forecast_ci = forecast.conf_int(alpha=0.05)

# 元のスケール(指数変換)に戻す
train_orig = np.exp(train)
test_orig = np.exp(test)
forecast_orig = np.exp(forecast_mean)
ci_lower = np.exp(forecast_ci.iloc[:, 0])
ci_upper = np.exp(forecast_ci.iloc[:, 1])

fig, axes = plt.subplots(2, 1, figsize=(12, 8))

# 対数スケールでのプロット
axes[0].plot(train.index, train, label='Train', linewidth=1.5)
axes[0].plot(test.index, test, label='Test', linewidth=1.5, color='darkorange')
axes[0].plot(forecast_mean.index, forecast_mean, label='Forecast',
             linewidth=1.5, color='red', linestyle='--')
axes[0].fill_between(forecast_ci.index,
                     forecast_ci.iloc[:, 0], forecast_ci.iloc[:, 1],
                     color='red', alpha=0.1)
axes[0].set_title('SARIMA(0,1,1)(0,1,1)₁₂ — Log Scale')
axes[0].set_ylabel('log(Passengers)')
axes[0].legend()

# 元スケールでのプロット
axes[1].plot(train_orig.index, train_orig, label='Train', linewidth=1.5)
axes[1].plot(test_orig.index, test_orig, label='Test', linewidth=1.5, color='darkorange')
axes[1].plot(forecast_orig.index, forecast_orig, label='Forecast',
             linewidth=1.5, color='red', linestyle='--')
axes[1].fill_between(ci_lower.index, ci_lower, ci_upper,
                     color='red', alpha=0.1)
axes[1].set_title('SARIMA(0,1,1)(0,1,1)₁₂ — Original Scale')
axes[1].set_ylabel('Passengers')
axes[1].legend()

plt.tight_layout()
plt.show()

# 予測精度(テスト期間のみ)
test_forecast = forecast_orig[:len(test)]
mae = np.mean(np.abs(test_orig.values - test_forecast.values))
mape = np.mean(np.abs((test_orig.values - test_forecast.values) / test_orig.values)) * 100
rmse = np.sqrt(np.mean((test_orig.values - test_forecast.values) ** 2))

print(f"\n予測精度(テスト期間: 24か月):")
print(f"MAE  = {mae:.2f}")
print(f"MAPE = {mape:.2f}%")
print(f"RMSE = {rmse:.2f}")

予測結果のグラフから、SARIMAモデルの能力が読み取れます。

  1. 季節パターンの再現: 予測値(赤い破線)がテストデータ(オレンジ)の季節パターンをよく捉えています。毎年夏にピーク、冬に谷というパターンが正確に再現されています。
  2. トレンドの追従: 右肩上がりのトレンドも予測に反映されています。
  3. 予測区間の広がり: 予測が遠くなるほど信頼区間(赤い帯)が広がっており、予測の不確実性が適切に表現されています。
  4. MAPE: 平均絶対パーセント誤差が数%程度であれば、実用的に十分な予測精度と言えます。

残差診断

モデルの適合度を残差分析で確認しましょう。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.datasets import get_rdataset
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.graphics.tsaplots import plot_acf
from statsmodels.stats.diagnostic import acorr_ljungbox
from scipy import stats
import warnings
warnings.filterwarnings('ignore')

# データ準備(訓練データのみ)
data = get_rdataset("AirPassengers").data
data.columns = ['time', 'passengers']
passengers = data['passengers'].values
log_pass = np.log(passengers)
time_index = pd.date_range('1949-01', periods=len(passengers), freq='MS')
ts = pd.Series(log_pass, index=time_index)
train = ts[:'1958-12']

# フィッティング
model = SARIMAX(train, order=(0, 1, 1),
                seasonal_order=(0, 1, 1, 12),
                enforce_stationarity=False,
                enforce_invertibility=False)
result = model.fit(disp=False)

# 残差診断
residuals = result.resid

fig, axes = plt.subplots(2, 2, figsize=(12, 8))

# 残差の時系列
axes[0, 0].plot(residuals, linewidth=0.8)
axes[0, 0].axhline(y=0, color='red', linestyle='--', alpha=0.5)
axes[0, 0].set_title('Residuals')

# 残差のACF
plot_acf(residuals, lags=36, ax=axes[0, 1])
axes[0, 1].set_title('ACF of Residuals')

# Q-Qプロット
stats.probplot(residuals, dist="norm", plot=axes[1, 0])
axes[1, 0].set_title('Q-Q Plot')

# 残差のヒストグラム
axes[1, 1].hist(residuals, bins=25, density=True, alpha=0.7, color='steelblue')
x_range = np.linspace(residuals.min(), residuals.max(), 100)
axes[1, 1].plot(x_range,
                stats.norm.pdf(x_range, residuals.mean(), residuals.std()),
                'r-', linewidth=2)
axes[1, 1].set_title('Distribution of Residuals')

plt.tight_layout()
plt.show()

# Ljung-Box検定
lb = acorr_ljungbox(residuals, lags=[12, 24, 36], return_df=True)
print("Ljung-Box Test:")
print(lb)

残差診断の結果から、モデルの適切さを評価できます。

  1. 残差の時系列: 残差に明確なパターンやトレンドが見られなければ、モデルがデータの構造を十分に捉えていると判断できます。
  2. 残差のACF: 全てのラグ(特にラグ12, 24, 36の季節ラグ)で自己相関が信頼区間内に収まっていれば、季節パターンが適切に除去されていることを意味します。
  3. Q-Qプロット: データ点がほぼ直線上に並んでいれば正規性が保たれています。裾が重い場合は、外れ値の影響を受けている可能性があります。
  4. Ljung-Box検定: p値が0.05を上回れば、残差に有意な自己相関がないことを示します。

SARIMAモデルのパラメータの解釈

フィッティングされたモデルのパラメータがどのような意味を持つか、もう少し踏み込んで考えてみましょう。

MA(1)パラメータ $\theta_1$

非季節MA項の係数 $\theta_1$ は、直前の期のショック(予期しない変動)が現在の値にどれだけ影響するかを表します。$\theta_1$ が負の値(例えば $-0.4$)であれば、前期に正のショックがあった場合、今期はそれを打ち消す方向に動く傾向があることを意味します。

季節MA(1)パラメータ $\Theta_1$

季節MA項の係数 $\Theta_1$ は、12か月前のショックが現在の値にどれだけ影響するかを表します。$\Theta_1$ が負の値であれば、去年の同月に正のショックがあった場合、今年はそれを修正する方向に動く傾向を示します。

乗法的構造の意味

SARIMAモデルの乗法的構造は、非季節成分と季節成分が独立に作用することを仮定しています。例えば、MA部分の展開で現れる $\theta_1 \Theta_1 \varepsilon_{t-13}$ は、「13か月前のショックの影響」が「短期MA効果」と「季節MA効果」ので決まることを意味します。

この乗法的構造のおかげで、SARIMAモデルは少ないパラメータで複雑な季節パターンを効率的に表現できます。もし非乗法的な構造が必要な場合は、一般的なARIMAモデルで高次のARやMAを直接指定する方法もありますが、パラメータ数が増えるため推定が不安定になりやすいです。

ここまでSARIMAモデルの理論と実装を見てきましたが、実務ではモデル構築の各ステップでいくつかの注意点があります。次のセクションでは、よくある落とし穴と対処法をまとめます。

実務上の注意点

差分のとりすぎ(Over-differencing)

差分を必要以上にとると、系列に人工的なMA構造が導入され、モデルが不必要に複雑になります。ADF検定のp値だけでなく、差分後の系列の分散が差分前より大きくなっていないかも確認しましょう。分散が増えていれば、差分のとりすぎの可能性があります。

対数変換の必要性

時系列の振幅(季節変動の大きさ)が時間とともに増加している場合、乗法的な季節性が存在します。このとき対数変換を適用して加法的な季節性に変換すると、SARIMAモデルの仮定に合致しやすくなります。AirPassengersデータがまさにこのケースでした。

パラメータ数の抑制

SARIMAモデルのパラメータ $(p, d, q, P, D, Q)$ は、合計7つあり、組み合わせが膨大になります。実務では以下のガイドラインが有効です。

  • $d$ と $D$ はそれぞれ0か1(まれに2)
  • $p, q$ はそれぞれ0〜3程度
  • $P, Q$ はそれぞれ0か1(まれに2)
  • AICやBICの最小化で自動選択する

予測区間の解釈

SARIMAの予測区間は正規性の仮定に基づいています。残差が正規分布から大きく逸脱する場合、予測区間の信頼性が低下します。また、予測ホライズンが長くなるほど不確実性が増大するのは当然ですが、その増大速度がモデルの性質に依存することも理解しておくべきです。

まとめ

本記事では、SARIMAモデルの理論と実装について詳しく解説しました。

  • 季節差分: $(1 – B^s)$ 演算子により、周期 $s$ の季節パターンを除去できる
  • SARIMAの構造: 非季節成分 $(p,d,q)$ と季節成分 $(P,D,Q)_s$ の乗法的組み合わせにより、少ないパラメータで複雑な季節時系列をモデル化できる
  • モデル同定: ACF/PACFのパターンとAIC/BICによる情報量基準を併用して、最適な次数を決定する
  • 予測と診断: モデルの予測精度はMAE/MAPE/RMSEで評価し、残差のホワイトノイズ性をLjung-Box検定で確認する

SARIMAモデルは季節時系列の「ベースライン」として非常に有用ですが、指数平滑法やProphetなどの代替手法と比較することも実務では重要です。

次のステップとして、以下の記事も参考にしてください。