本ページでは、 時系列分析を統合的に解説します。 定常性・ACF/PACF・ARIMA・季節調整・状態空間モデル・Prophet・予測評価を一気通貫で扱います。
時系列データは「時間的な順序が重要」「過去が未来に影響」という特徴があり、 通常の機械学習とは異なる前提・手法が必要です。 SSDSE-B は時系列でないですが、 多くの公的統計(気象・経済・売上)は時系列です。
論文記事から各用語のリンクをクリックすると、 該当箇所が開きます:
時間順に並んだ観測値の列 $\{y_t\}_{t=1}^T$。 観測の順序と時間間隔に意味がある。 通常の機械学習の独立同分布仮定が成り立たない。
加法モデル:$y_t = T_t + S_t + R_t$(トレンド + 季節 + 残差)
乗法モデル:$y_t = T_t \cdot S_t \cdot R_t$
1 2 3 4 5 6 7 8 | import pandas as pd from statsmodels.tsa.seasonal import seasonal_decompose # 例:気象庁公開の月次気温データを想定(実データURL読み替え可) ts = pd.read_csv('data/raw/temperature_monthly.csv', index_col='date', parse_dates=True)['temp'] res = seasonal_decompose(ts, model='additive', period=12) res.plot() |
季節性が時間変動する場合に有効。 ロバストオプション付き。
定常時系列:平均・分散・自己相関が時間によらず一定。 多くのモデルが定常性を要求する。
帰無仮説:単位根あり(非定常)。 p < 0.05 で棄却=定常。
1 2 3 | from statsmodels.tsa.stattools import adfuller res = adfuller(ts) print(f'ADF統計量: {res[0]:.3f}, p値: {res[1]:.4f}') |
1 2 | ts_diff = ts.diff().dropna() ts_log_diff = ts.apply(lambda x: x).pipe(lambda s: s.apply(__import__('numpy').log)).diff().dropna() |
ADF と逆で、 帰無仮説が定常。 両方を併用するのが推奨(Hyndman 流)。
$$\rho_k = \mathrm{Corr}(y_t, y_{t-k})$$
ACF は「ラグ k での相関」、 PACF は「他のラグの影響を取り除いた」相関。
| パターン | 推定モデル |
|---|---|
| PACF が p 次で切れる、 ACF が減衰 | AR(p) |
| ACF が q 次で切れる、 PACF が減衰 | MA(q) |
| ACF/PACF とも減衰 | ARMA |
1 2 3 4 5 6 | from statsmodels.graphics.tsaplots import plot_acf, plot_pacf import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(14, 4)) plot_acf(ts_diff, lags=24, ax=axes[0]) plot_pacf(ts_diff, lags=24, ax=axes[1]) plt.show() |
$$y_t = c + \phi_1 y_{t-1} + \cdots + \phi_p y_{t-p} + \varepsilon_t$$
$\phi_i$ は「ファイ・サブ・アイ」自己回帰係数。 過去の自分を線形結合。
$$y_t = \mu + \varepsilon_t + \theta_1 \varepsilon_{t-1} + \cdots + \theta_q \varepsilon_{t-q}$$
$\theta_j$ は「シータ・サブ・ジェイ」移動平均係数。 過去のショックを線形結合。
AR と MA の組合せ。 定常データ向け。
d 階差分してから ARMA を適用。 非定常データに対応。
1 2 3 4 5 6 | from statsmodels.tsa.arima.model import ARIMA model = ARIMA(ts, order=(1, 1, 1)) fit = model.fit() print(fit.summary()) forecast = fit.forecast(steps=12) print(forecast) |
季節成分を別の (P,D,Q) で表現。 s は季節周期(月次なら 12、 週次なら 7)。
1 2 3 4 | from statsmodels.tsa.statespace.sarimax import SARIMAX model = SARIMAX(ts, order=(1, 1, 1), seasonal_order=(1, 1, 1, 12)) fit = model.fit(disp=False) print(fit.aic) |
1 2 3 4 | import pmdarima as pm auto = pm.auto_arima(ts, seasonal=True, m=12, stepwise=True, suppress_warnings=True) print(auto.summary()) forecast = auto.predict(n_periods=12) |
古典的な予測手法。 過去ほど指数的に重みを下げる。
1 2 3 | from statsmodels.tsa.holtwinters import ExponentialSmoothing hw = ExponentialSmoothing(ts, trend='add', seasonal='add', seasonal_periods=12).fit() forecast = hw.forecast(12) |
観測 $y_t = Z \alpha_t + \varepsilon_t$、 状態 $\alpha_{t+1} = T\alpha_t + \eta_t$。 Kalman フィルタで隠れ状態を再帰的に推定。
1 2 3 4 | from statsmodels.tsa.statespace.structural import UnobservedComponents ucm = UnobservedComponents(ts, level='local linear trend', seasonal=12) fit = ucm.fit() print(fit.summary()) |
「トレンド + 季節 + 休日効果」のシンプルな加法モデル。 欠損・外れ値に頑健。 ビジネス時系列で人気。
1 2 3 4 5 6 7 | from prophet import Prophet df_p = pd.DataFrame({'ds': ts.index, 'y': ts.values}) m = Prophet(yearly_seasonality=True, weekly_seasonality=False) m.fit(df_p) future = m.make_future_dataframe(periods=12, freq='M') fcst = m.predict(future) m.plot(fcst) |
複数時系列を同時にモデル化:$\mathbf{y}_t = \mathbf{c} + \sum_i \mathbf{A}_i \mathbf{y}_{t-i} + \boldsymbol{\varepsilon}_t$。
応用:因果関係(Granger因果)、 インパルス応答、 ショック分解。
1 2 3 4 | from statsmodels.tsa.api import VAR multi_ts = pd.DataFrame({'temp': ts, 'humidity': ts2}) var = VAR(multi_ts).fit(maxlags=12, ic='aic') print(var.summary()) |
1 2 3 4 5 6 7 8 9 10 | from sklearn.model_selection import TimeSeriesSplit import numpy as np tscv = TimeSeriesSplit(n_splits=5, test_size=12) errors = [] y = ts.values for tr, te in tscv.split(y): model = ARIMA(y[tr], order=(1,1,1)).fit() pred = model.forecast(steps=len(te)) errors.append(np.mean(np.abs(y[te] - pred))) print(f'CV MAE: {np.mean(errors):.3f}') |
| 手法 | 短期予測 | 長期予測 | 解釈性 | 特徴 |
|---|---|---|---|---|
| ARIMA | 良 | 中 | 高 | 古典・標準 |
| 指数平滑 | 良 | 中 | 高 | 高速 |
| Prophet | 良 | 良 | 高 | 休日・イベント考慮 |
| 状態空間 | 良 | 良 | 中 | 欠損対応 |
| LSTM/RNN | 中 | 中 | 低 | 複雑パターン |
| LightGBM | 良 | 中 | 中 | 外部特徴量強 |
| 落とし穴 | 対処 |
|---|---|
| 通常 k-fold で予測評価 | TimeSeriesSplit を使う。 未来→過去のリーク厳禁。 |
| 差分前の系列にAR検定 | ADF/KPSS で定常化してから ACF/PACF を見る。 |
| R² で予測評価 | 時系列では MAE/RMSE/MASE。 R² は誤解を生む。 |
| 季節性を無視 | SARIMA・季節ダミー・Prophet で季節性を明示。 |
| 未来情報の特徴量化 | 予測時に得られない情報は使わない。 ラグ系のみ。 |
| 短期データに複雑モデル | サンプル数が少ないなら ARIMA や指数平滑で十分。 |
| 予測区間を出さない | 点予測 + 80%/95% 予測区間を必ず併記。 |
注意:SSDSE-B は時系列ではないので、 別の時系列実データ(例:気象庁公開の日次気温、 e-Stat の月次小売販売、 政府統計局の月次失業率など)を使用してください。
1 2 3 | from statsmodels.tsa.seasonal import STL res = STL(ts, period=12, robust=True).fit() res.plot() |
1 2 3 | from statsmodels.tsa.stattools import adfuller, kpss print('ADF p:', adfuller(ts)[1]) print('KPSS p:', kpss(ts, nlags='auto')[1]) |
1 2 3 4 5 6 7 8 | import pmdarima as pm import numpy as np train, test = ts[:-12], ts[-12:] auto = pm.auto_arima(train, seasonal=True, m=12) pred = auto.predict(n_periods=12) naive = train.iloc[-12:].values # 直近1年で繰り返し mase = np.mean(np.abs(test - pred)) / np.mean(np.abs(np.diff(train))) print(f'MASE = {mase:.3f}') |
「ARIMA を当てはめたら良い予測ができました。」
「月次データ(2015-01 から 2025-12、 N=132)に対し、 1階差分で定常化(ADF p < .001)。 ACF/PACF と AIC 最小化により SARIMA(1,1,1)(1,1,1,12) を選択。 12-期 rolling-origin バックテストで MASE = 0.81(< 1 でナイーブより優れる)、 95% 予測区間のカバレッジ 92%(名目 95%)。 比較として Prophet(MASE=0.85)、 ETS(0.78)、 LightGBM with lag features(0.74)を実施し、 LightGBM を採用。」
| 用途 | 関数・クラス |
|---|---|
| ARIMA / SARIMA | statsmodels.tsa.arima.ARIMA, SARIMAX, pmdarima.auto_arima |
| 指数平滑 / ETS | statsmodels.tsa.holtwinters.ExponentialSmoothing |
| 分解 | seasonal_decompose, STL |
| 単位根検定 | adfuller, kpss |
| ACF/PACF | plot_acf, plot_pacf, acf, pacf |
| VAR / VECM | statsmodels.tsa.api.VAR, VECM |
| 状態空間 | UnobservedComponents, DynamicFactor |
| Prophet | prophet.Prophet |
| 時系列CV | sklearn.model_selection.TimeSeriesSplit |
| 深層学習 | torch.nn.LSTM/GRU, Darts, NeuralProphet |
| 変化点検出 | ruptures, bocpd |
ARIMA 等のフィット後、 残差が「ホワイトノイズ」になっているかを確認。 残差にパターンが残っていれば、 モデルが情報を取り切れていない。
残差の自己相関がすべて 0 か? 帰無仮説:H₀ = 自己相関なし。 p > 0.05 で「ホワイトノイズ」と判断可。
1 2 3 | from statsmodels.stats.diagnostic import acorr_ljungbox lb = acorr_ljungbox(fit.resid, lags=[12], return_df=True) print(lb) |
SSDSE-B のパネル構造(47都道府県 × 2003-2020年)から、 例えば東京都の「人口」や沖縄県の「合計特殊出生率」を取り出すと、 単一都道府県の年次時系列が得られます。 ここでは東京都の総人口(千人)を題材に、 ARIMA・指数平滑・トレンド分解の実値計算を見ます。
1 2 3 4 5 6 7 8 9 | import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2023.csv', encoding='shift_jis', header=[0,1]) df.columns = ['_'.join(c).strip() for c in df.columns] tokyo = df[df['都道府県_Prefecture'] == '東京都'].set_index('年度_Year') y = tokyo['総人口_Total population'] / 1000 # 千人単位 print(y.head()) # 2003 年: 12,420 千人 # 2010 年: 13,160 千人 # 2020 年: 14,048 千人 — 緩やかに増加 |
年次データなので季節周期はないが、 トレンドと残差の分解は意味がある:
1 2 3 4 5 | from statsmodels.tsa.seasonal import STL # 年次データなのでトレンド + 残差のみ(周期なし) stl = STL(y, period=None, robust=True).fit() print(f'トレンド成分: 2003={stl.trend.iloc[0]:.0f}, 2020={stl.trend.iloc[-1]:.0f}') print(f'残差 SD: {stl.resid.std():.1f}') |
典型出力:トレンドは 12,400 → 14,000 へ単調増加、 残差は 50 程度(年率0.4%以内の変動)。
1 2 3 4 5 6 7 8 9 | from statsmodels.tsa.stattools import adfuller res = adfuller(y) print(f'ADF stat = {res[0]:.3f}, p = {res[1]:.4f}') # 例:ADF = -0.5, p = 0.88 → 単位根あり(非定常) # 1階階差で再検定 res2 = adfuller(y.diff().dropna()) print(f'diff ADF stat = {res2[0]:.3f}, p = {res2[1]:.4f}') # 例:ADF = -3.2, p = 0.02 → 階差で定常 → I(1) → ARIMA(p,1,q) 候補 |
1 2 3 4 5 6 7 8 9 10 11 | from statsmodels.tsa.arima.model import ARIMA model = ARIMA(y, order=(1, 1, 0)).fit() print(model.summary()) # AR(1) 係数 ≈ 0.4, σ² ≈ 1500 程度 forecast = model.get_forecast(steps=5) # 2021-2025 を予測 ci = forecast.conf_int(alpha=0.05) print(forecast.predicted_mean) print(ci) # 2021: 14,090 千人 [13,990, 14,190](CI 幅 ±100) # 2025: 14,180 千人 [13,890, 14,470](先になるほど CI 拡大) |
1 2 3 4 5 | from statsmodels.tsa.holtwinters import ExponentialSmoothing hw = ExponentialSmoothing(y, trend='add', seasonal=None).fit() print(f'α (level) = {hw.params["smoothing_level"]:.3f}') print(f'β (trend) = {hw.params["smoothing_trend"]:.3f}') print(hw.forecast(5)) # ARIMA とほぼ同じ予測値が得られる |
同じデータで複数モデルを試し、 AIC・BIC・CV-RMSEで比較するのが現代的なベストプラクティス。
時系列ライブラリは選択肢が豊富。 用途・速度・拡張性で使い分けます。
1 2 3 4 | from statsmodels.tsa.arima.model import ARIMA from statsmodels.tsa.statespace.sarimax import SARIMAX from statsmodels.tsa.holtwinters import ExponentialSmoothing # 古典的 ARIMA・SARIMAX・状態空間モデルが揃う |
1 2 3 4 | from pmdarima import auto_arima model = auto_arima(y, seasonal=False, stepwise=True, trace=True) print(model.summary()) # (p,d,q) を AIC 最小で自動探索 |
1 2 3 4 5 6 | from prophet import Prophet df_p = pd.DataFrame({'ds': pd.to_datetime(y.index, format='%Y'), 'y': y.values}) m = Prophet(yearly_seasonality=False, daily_seasonality=False).fit(df_p) future = m.make_future_dataframe(periods=5, freq='Y') forecast = m.predict(future) print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail(5)) |
1 2 3 4 5 6 7 | from sktime.forecasting.arima import ARIMA as SKARIMA from sktime.forecasting.model_selection import temporal_train_test_split y_train, y_test = temporal_train_test_split(y, test_size=3) forecaster = SKARIMA(order=(1,1,0)) forecaster.fit(y_train) y_pred = forecaster.predict(fh=[1, 2, 3]) print(y_pred) |
1 2 3 4 5 | from darts import TimeSeries from darts.models import ExponentialSmoothing as DartsES, NBEATSModel ts = TimeSeries.from_series(y) es = DartsES(); es.fit(ts); print(es.predict(5)) # NBEATSModel など深層学習モデルも同じインターフェース |
1 2 3 4 5 6 | from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import mean_absolute_error tscv = TimeSeriesSplit(n_splits=5) for train_idx, test_idx in tscv.split(y): y_tr, y_te = y.iloc[train_idx], y.iloc[test_idx] # 各 fold でモデル訓練・評価。 通常CV と違い「過去→未来」順を維持 |
TimeSeriesSplit(rolling/expanding window)を使い、 「訓練は過去、 テストは未来」を厳守。 これを破ると訓練時の精度が異常に高くなり、 本番運用で激しく性能が落ちる。 機械学習プロジェクトの典型的失敗パターン。changepoint_prior_scale やベイズ的変化点モデル(PyMC)も有効。時系列とは「同じ対象を時間順に観測した値の並び」です。 SSDSE-B-2026 の都道府県別 合計特殊出生率は年度ごとに変化しており、 北海道は 2021 年の 1.20 から 2023 年の 1.06 へ低下傾向です。 こうした「トレンド・季節・残差」を分解して未来を予測するのが時系列分析の本質。
直感で全体像を掴んだら、 次は厳密な定義を見ます。 数式は短いものでも、 「何を入力にして、 何を出力するのか」を意識して読むと早く慣れます。
上の数式に出てくる各記号が何を表すかを、 言葉で翻訳します。 1 つずつ自分の言葉で言い換えられるようになると、 論文や教科書のスピードが一気に上がります。
| 記号 | 意味(言葉での説明) |
|---|---|
| $y_t$ | 時刻 $t$ の観測値(出生率・死亡数 等) |
| $T_t$ | トレンド成分(長期傾向) |
| $S_t$ | 季節成分(周期的変動) |
| $R_t$ | 残差(説明できない揺らぎ) |
数式だけでは「分かった気になる」だけで終わりがち。 ここで SSDSE-B-2026(教育用標準データセット — 47 都道府県 × 100+ 指標、 2018-2023 年度)の実値を当てはめて、 時系列 の挙動を電卓的に追体験します。
SSDSE-B-2026 は 統計センターの SSDSE 配布ページ から CSV を直接ダウンロードできます。 本サイトでは data/raw/SSDSE-B-2026.csv に配置している前提でコードを書いています。
以下のコードは最小限の構成です。 pd.read_csv('data/raw/SSDSE-B-2026.csv') を直書きしているので、 同じ階層に CSV を置けばそのまま動きます。 変数化しないのは、 初学者が「パスをどこに書くべきか」で迷わないようにするためです。
# 時系列 を SSDSE-B-2026 で確かめる最小コード
import pandas as pd
import numpy as np
# 1) SSDSE-B-2026(教育用標準データセット)を読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
print('shape:', df.shape) # (564, 112) — 47 都道府県 × 6 年度
print('cols head:', list(df.columns[:8]))
# 2) 直近年度(2023 年度)に絞る
df23 = df[df['年度'] == 2023].copy()
print('rows in 2023:', len(df23))
# 3) 時系列 を動かすために必要な列だけ取り出す
y = df23['合計特殊出生率'].astype(float)
x = df23['総人口'].astype(float)
print('y stats:', y.describe().round(3).to_dict())
print('x stats:', x.describe().round(0).to_dict())
# 4) 時系列 の本処理(このページの主題)
# — 具体実装は同カテゴリの個別ページにも掲載
print('---- 時系列 結果 ----')
print('mean y:', y.mean().round(3), '/ std y:', y.std().round(3))
print('mean x:', x.mean().round(0), '/ std x:', x.std().round(0))
print('corr(x, y):', y.corr(x).round(3))
うまく動かないときは ①data/raw/SSDSE-B-2026.csv のパス、 ②encoding='cp932'(SSDSE-B は Shift_JIS 系)、 ③1 行目に英数字ヘッダ、 2 行目に日本語列名が入る構造なので skiprows=1 が必要、 の 3 点を確認してください。
この用語を実務で使うときにつまずきやすい点を、 失敗パターン別に整理しました。 1 度経験すれば回避できるものばかりですが、 先に知っておくと事故が大幅に減ります。
時系列 と一緒に覚えておくと選択肢が広がる関連手法。 状況によって使い分けが必要なので、 それぞれの強みと弱みを 1 行で言えるようにしておきましょう。
表中の各手法は本サイト内に個別ページが用意されているものが多いです。 興味を持った概念は、 横展開的に読むと体系的な理解が早く進みます。