Mímisbrunnr知恵の泉

← 時系列分析 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:カルマンフィルタローカルレベルとローカルトレンドモデル | 関連:時系列データと分解(成分の抽出)

要点(BLUF)

1. フィルタ vs 平滑化

カルマンフィルタ のフィルタは、時点 tt の状態を y1:ty_{1:t}(その時までの観測)で推定しました——リアルタイム向きのオンライン推定です。一方、解析が終わってデータが全部揃っているなら、時点 tt の状態を推定するのに未来の観測 yt+1,,yny_{t+1},\dots,y_n も使える。これが平滑化です。

固定区間スムーザー(RTS スムーザー)は、フィルタを最後まで回したあと後ろ向きに1回走らせて補正します。

atn=att+Ct(at+1nat+1t),Ct=PttTPt+1t1a_{t\mid n} = a_{t\mid t} + C_t\,(a_{t+1\mid n} - a_{t+1\mid t}), \qquad C_t = P_{t\mid t}\,T'\,P_{t+1\mid t}^{-1}

「未来から見て状態がこちらにズレていたら、その分を遡って今の推定にも反映する」という補正です。情報が増えるのでスムーザーの分散 PtnP_{t\mid n} はフィルタの PttP_{t\mid t} 以下になります。

直観的には、フィルタは系列の端(最新)で最も働き、過去ほど未来の観測を取りこぼす。スムーザーは全点で両側の観測を使えるので、系列の真ん中で特に有利です。

コード①:フィルタとスムーザーを真の隠れ状態と比べる

真の隠れ水準を仕込んだローカルレベル系列で、filtered_state(フィルタ)と smoothed_state(スムーザー)を取り出し、どちらが真の水準に近いかを RMSE と状態分散で比べます。

import warnings
warnings.simplefilter("ignore")
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from statsmodels.tsa.statespace.structural import UnobservedComponents

# 真のローカルレベル(隠れ水準を保存しておく)
np.random.seed(7)
n = 120
Q_true, H_true = 0.4, 6.0
alpha = np.zeros(n); alpha[0] = 5.0
for t in range(1, n):
    alpha[t] = alpha[t-1] + np.random.normal(0, np.sqrt(Q_true))
y = alpha + np.random.normal(0, np.sqrt(H_true), n)

# 真の分散を与えてフィルタ&スムーザーを実行
res = UnobservedComponents(y, level="local level").smooth([H_true, Q_true])
filt = res.filtered_state[0]          # 過去だけを使った推定
smooth = res.smoothed_state[0]        # 全データを使った推定
filt_var = res.filtered_state_cov[0, 0]
smooth_var = res.smoothed_state_cov[0, 0]

rmse_filt = np.sqrt(np.mean((filt - alpha)**2))
rmse_smooth = np.sqrt(np.mean((smooth - alpha)**2))
print(f"RMSE  フィルタ(過去のみ) = {rmse_filt:.3f}")
print(f"RMSE  スムーザー(全データ)= {rmse_smooth:.3f}(こちらが小さい)")
print(f"平均の状態分散  フィルタ={filt_var.mean():.3f}  スムーザー={smooth_var.mean():.3f}")
print(f"中央 t=60 の状態分散  フィルタ={filt_var[60]:.3f}  スムーザー={smooth_var[60]:.3f}")

fig, ax = plt.subplots(figsize=(9, 4.5))
ax.plot(y, color="0.75", lw=0.8, marker="o", ms=2.5, label="観測 y_t")
ax.plot(alpha, color="k", ls="--", lw=1.5, label="真の隠れ水準")
ax.plot(filt, color="C0", lw=1.5, label="フィルタ(過去のみ)")
ax.plot(smooth, color="C3", lw=1.8, label="スムーザー(全データ)")
ax.set_xlabel("時点 t"); ax.set_ylabel("値"); ax.legend()
ax.set_title("フィルタ vs 平滑化:スムーザーは未来も使い真の水準に近い")
plt.tight_layout(); plt.show()

出力:

RMSE  フィルタ(過去のみ) = 1.125
RMSE  スムーザー(全データ)= 0.893(こちらが小さい)
平均の状態分散  フィルタ=1.431  スムーザー=0.793
中央 t=60 の状態分散  フィルタ=1.362  スムーザー=0.768

出力の意味:スムーザーの RMSE は 0.8930.893 で、フィルタの 1.1251.125 より真の隠れ水準に近い。状態分散も平均 1.4310.7931.431\to0.793、系列中央(t=60t=60)で 1.3620.7681.362\to0.768スムーザーで縮みます——未来の観測ぶん情報が増えたから。図ではフィルタ(青)が観測のギザギザに少し遅れて追従するのに対し、スムーザー(赤)は両側の観測で均され、黒破線の真の水準に滑らかに張り付きます。過去を振り返って分析するなら、フィルタよりスムーザーを使うべき。ただしスムーザーは全データが要るので、リアルタイム予測には使えません(その時点で未来は無い)。

2. 欠測補間

状態空間のもう一つの強みは欠測への耐性です。観測 yty_t が無い時点では、カルマンの更新ステップを飛ばし、予測ステップだけで状態を運びます(ゲイン Kt=0K_t=0 と同じ)。

yt 欠測のとき:att=att1,Ptt=Ptt1y_t\ \text{欠測のとき}:\quad a_{t\mid t}=a_{t\mid t-1},\qquad P_{t\mid t}=P_{t\mid t-1}

観測で補正されないので、欠測が続くほど状態分散 PP が増え続けます=「埋めた値の自信が下がる」。観測が戻れば更新が再開し、分散が縮む。こうして欠測区間を不確実性つきで補間できます。スムーザーを併用すれば、欠測の両側の観測から内挿するので、さらに自然に埋まります。

コード②:欠測区間をスムーザーで不確実性つきに補間

系列の一部を np.nan にして欠測を作り、UnobservedComponents がそれを自動処理して補間する様子を見ます。欠測点の補間値が真の水準に近いか、95%区間が真値を捕らえるか、欠測点の標準誤差(SE)が観測点より大きいかを確かめます。

import warnings
warnings.simplefilter("ignore")
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from statsmodels.tsa.statespace.structural import UnobservedComponents

# 真のローカルレベル
np.random.seed(9)
n = 100
Q_true, H_true = 0.5, 3.0
alpha = np.zeros(n); alpha[0] = 8.0
for t in range(1, n):
    alpha[t] = alpha[t-1] + np.random.normal(0, np.sqrt(Q_true))
y_full = alpha + np.random.normal(0, np.sqrt(H_true), n)

# 一部を欠測(NaN)にする:40-49 と 70-74 を欠測
y_obs = y_full.copy()
missing_idx = list(range(40, 50)) + list(range(70, 75))
y_obs[missing_idx] = np.nan
print(f"欠測区間 = {missing_idx[0]}-{missing_idx[9]}{missing_idx[10]}-{missing_idx[-1]}(計{len(missing_idx)}点)")

# statsmodels は NaN を自動処理(欠測時は更新をスキップ=予測のみで状態を運ぶ)
res = UnobservedComponents(y_obs, level="local level").smooth([H_true, Q_true])
interp = res.smoothed_state[0]               # 欠測も含む平滑化水準
interp_se = np.sqrt(res.smoothed_state_cov[0, 0])

# 欠測点での補間 vs 真値
mi = np.array(missing_idx)
rmse_interp = np.sqrt(np.mean((interp[mi] - alpha[mi])**2))
covered = ((alpha[mi] >= interp[mi]-1.96*interp_se[mi]) &
           (alpha[mi] <= interp[mi]+1.96*interp_se[mi])).sum()
obs_mask = ~np.isnan(y_obs)
print(f"欠測点 補間値 RMSE vs 真の水準 = {rmse_interp:.3f}")
print(f"欠測点 95%区間が真値を捕捉 = {covered}/{len(mi)}")
print(f"平滑化SE  観測あり点の平均={interp_se[obs_mask].mean():.3f}  "
      f"欠測点の平均={interp_se[mi].mean():.3f}(欠測点は不確実で大きい)")

fig, ax = plt.subplots(figsize=(9, 4.5))
ax.plot(np.arange(n), alpha, color="k", ls="--", lw=1.4, label="真の隠れ水準")
ax.plot(np.arange(n), y_obs, color="0.6", lw=0, marker="o", ms=3, label="観測 y_t(欠測あり)")
ax.plot(np.arange(n), interp, color="C3", lw=1.8, label="スムーザー補間")
ax.fill_between(np.arange(n), interp-1.96*interp_se, interp+1.96*interp_se,
                color="C3", alpha=0.2, label="95%区間")
for a, b in [(40, 49), (70, 74)]:
    ax.axvspan(a, b, color="C1", alpha=0.12)
ax.set_xlabel("時点 t"); ax.set_ylabel("値"); ax.legend()
ax.set_title("欠測区間の補間:区間は欠測中に広がる(不確実性つき補間)")
plt.tight_layout(); plt.show()

出力:

欠測区間 = 40-49 と 70-74(計15点)
欠測点 補間値 RMSE vs 真の水準 = 1.222
欠測点 95%区間が真値を捕捉 = 14/15
平滑化SE  観測あり点の平均=0.797  欠測点の平均=1.198(欠測点は不確実で大きい)

出力の意味:欠測 15 点を一切手当てせず np.nan のまま渡すだけで、スムーザーが両側の観測から内挿して埋めました。補間値の RMSE は 1.2221.222、95%区間は 14/1514/15 で真の水準を捕捉。重要なのは標準誤差で、観測がある点の平均 SE 0.7970.797 に対し欠測点は 1.1981.198 と大きい——「埋めた値ほど自信がない」と区間が正直に語ります。図のオレンジ帯は欠測区間(40-49・70-74)で膨らみ、観測が戻ると締まります。平均値や線形内挿と違い、状態空間の補間は不確実性を定量化できるのが決定的な差です。欠測の中央が最も不確実(両端から最も遠い)という挙動も自然に出ます。

3. 予測は「未来が全部欠測」

ここまでで予測・フィルタ・平滑化・欠測補間が一つの枠に収まります。すべて「観測がある所では更新し、無い所では予測ステップだけで状態を運ぶ」同じカルマンの操作だからです。

flowchart LR
    F["フィルタ:今までの観測 y_(1..t) で現在を推定"] --> S["スムーザー:全観測 y_(1..n) で過去を改善"]
    S --> M["欠測補間:途中の欠測を両側から内挿"]
    M --> P["予測:未来 y_(t+1..) が全部欠測の特殊ケース"]

だから将来予測は「未来をすべて NaN にした欠測補間」と数学的に同じ。片側からしか情報が来ないぶん不確実性が大きく、予測区間はホライズンとともに広がりますローカルレベルとローカルトレンドモデルARMA・ARIMAモデル)。逆に言えば、過去にも未来にも観測がある欠測の方が、末尾の予測より埋めやすい——両側から挟めるからです。

4. 数式の直観

⚠️ よくある誤解

関連ノート