Pythonで学ぶ時系列予測 ― e-learningで基礎から実践まで Part 3
はじめに
Part 2では、CSVを読み込んでから、時系列解析や予測へ渡しやすい形に整えるところまで進みました。
最後に確認した流れは、次のようなものでした。
ここまでで、データを扱うための土台はできました。
Part 3では、そのデータをもう一段深く見ていきます。
テーマはTemporal Structure ― 時系列の時間構造です。
同じように時間順に並んでいるデータでも、中身はまったく同じではありません。
例えば、
ほとんど規則性がない系列
前の値を引きずりながら動く系列
長期的に上昇している系列
一定周期で上下する系列
平均や分散があまり変わらない系列
があります。
Part 3では、こうした違いをグラフとPythonで確認します。
今回もPart 2と同じく、
説明を読む
↓
Notebookで試す
↓
グラフを見る
↓
条件を少し変える
↓
確認問題
という流れで進めます。
1. Part 2からPart 3へ
Part 2では、主に「データを整える」ことを学びました。
Part 3では、整えた系列に対して、
この時系列には、どんな時間的な構造が残っているのか
を調べます。
Part 3が終わるころには、グラフを見て「何となく上下している」で終わらず、
Trendがある
7日周期のSeasonalityがある
Random Walkに近い
差分すると安定する
ADFでは単位根を棄却できる
といった形で、時系列の特徴を言葉にできるようにします。
2. Part 3 ― Temporal Structure
原書のPart 3はChapter 10からChapter 15までです。
このe-learningでも6つのLessonとして進めます。
| Lesson | 内容 | 主に使うもの |
|---|---|---|
| Lesson 10 | White Noise | line plot / histogram / ACF |
| Lesson 11 | Random Walk |
cumsum() / diff() / ACF |
| Lesson 12 | 時系列分解 | seasonal_decompose() |
| Lesson 13 | Trend | 線形Trend / diff()
|
| Lesson 14 | Seasonality | 周期 / 季節差分 |
| Lesson 15 | StationarityとADF検定 | 区間統計量 / adfuller()
|
Part 3全体を一枚にすると、次の流れになります。
順番に見ていくと、話はつながっています。
White Noiseは「予測に使える規則性がほとんどない系列」です。
Random Walkは、ランダムではありますが、直前の値を強く引きずります。
その後、実際の時系列をTrendやSeasonalityへ分解し、最後にStationarityとADF検定へ進みます。
3. Part 3のゴール
Part 3を終えた時点で、次のことができれば十分です。
- White Noiseがなぜ予測しにくいのか説明できる
- White Noiseと自己相関の関係を説明できる
- Random Walkをランダムな増分の累積として理解できる
- Random Walkに1階差分を適用すると何が起きるか確認できる
- 時系列をTrend、Seasonality、Residualへ分けて考えられる
- Trendをグラフや線形モデルで確認できる
-
diff()でTrendの影響を弱められる - 周期的なSeasonalityを確認できる
- 季節差分を作れる
- Stationarityの意味を説明できる
- 前半と後半の平均・分散を比較できる
- ADF検定の帰無仮説とp値を説明できる
- ADF検定だけに頼らず、グラフと統計量を合わせて判断できる
ここでも、まだ予測モデルの性能比較はしません。
Part 3は、モデルを選ぶ前にデータの性質を理解するPartです。
4. 学習環境はPart 2をそのまま使う
Part 3でもDocker + JupyterLabをそのまま使います。
Windowsのブラウザー
↓
JupyterLab
↓
Notebook
↓
Python
↓
pandas / NumPy / Matplotlib / statsmodels
Part 2までのプロジェクトへNotebookを追加します。
notebooks/
├── 01_environment.ipynb
├── 02_load_explore.ipynb
├── 03_feature_engineering.ipynb
├── 04_visualization.ipynb
├── 05_resampling_interpolation.ipynb
├── 06_power_transforms.ipynb
├── 07_moving_average.ipynb
├── 08_white_noise.ipynb
├── 09_random_walk.ipynb
├── 10_decomposition.ipynb
├── 11_trend.ipynb
├── 12_seasonality.ipynb
└── 13_stationarity.ipynb
Part 1、Part 2のNotebookは消しません。
e-learning全体を1つのプロジェクトとして積み上げていきます。
起動方法も変わりません。
docker compose up
初回や依存関係を作り直す場合は、
docker compose up --build
を使います。
5. Part 3で使うデータ
Part 2で使った sample_daily.csv を引き続き使います。
この系列には、教材として分かりやすいように、
- 緩やかなTrend
- 7日周期
- ゆっくりした周期変動
- 小さなNoise
を入れています。
Part 3では、これに加えて3つの人工データを使います。
data/
├── sample_daily.csv
├── part3_white_noise.csv
├── part3_random_walk.csv
└── part3_stationarity.csv
part3_white_noise.csv はWhite Noise確認用です。
part3_random_walk.csv には、Random Walk本体と、1ステップごとのランダム増分を保存しています。
part3_stationarity.csv には、定常なAR(1)系列と、ドリフトを持つ非定常なRandom Walkを入れています。
どれも実在の観測データではありません。
仕組みを理解しやすくするための教材用データです。
Lesson 10 ― White Noise
学習目標
このLessonでは、次の内容を確認します。
- White Noiseとは何か
- なぜWhite Noiseは予測しにくいのか
- 時系列グラフを見る
- 平均と分散を見る
- 自己相関を見る
- 予測モデルの残差とWhite Noiseの関係を知る
6.1 White Noiseとは
White Noiseは、簡単に言えば、時間方向に利用できる規則性がほとんどない系列です。
理想化すると、各時刻の値は独立で、同じ分布から発生し、平均や分散が時間とともに大きく変化しません。
例えば、次のような系列です。
0.5
-1.2
0.1
1.1
-0.7
0.4
...
前の値が大きかったから次も大きい、という関係がほとんどありません。
ここで重要なのは、White Noiseが単に「見た目がガタガタしている」という意味ではないことです。
時系列にはNoiseが含まれていても、その下にTrendやSeasonalityが隠れている場合があります。
6.2 なぜWhite Noiseが重要なのか
White Noiseは2つの意味で重要です。
1つ目は、元の時系列がWhite Noiseなら、過去値を使った予測に大きな期待をしにくいことです。
2つ目は、予測モデルの誤差がWhite Noiseに近いかどうかです。
観測値
=
モデルで説明できる信号
+
説明しきれないNoise
モデルを作ったあと、残差にまだ自己相関やTrendが残っているなら、モデルが利用しきれていない構造が残っている可能性があります。
この話はPart 4の残差解析で改めて扱います。
6.3 Notebookで確認する
08_white_noise.ipynb を開きます。
from statsmodels.graphics.tsaplots import plot_acf
frame = pd.read_csv(
ROOT / "data" / "part3_white_noise.csv",
parse_dates=["timestamp"],
).set_index("timestamp")
series = frame["white_noise"]
print(series.describe())
print("lag 1 autocorrelation:", series.autocorr(lag=1))
そのあと、時系列グラフ、Histogram、ACFを描きます。
plot_acf(series, lags=30, zero=False)
plt.title("White Noise - autocorrelation")
plt.show()
ACFは Autocorrelation Function の略です。
Lag 1、Lag 2、Lag 3…と、過去値との相関を順番に見ます。
White Noiseなら、0以外のLagで強い自己相関が連続して残ることは期待しません。
6.4 区間ごとの平均と分散を見る
時系列全体の平均だけを見ると、途中で性質が変わっていても気付かないことがあります。
そこで系列を時間順にいくつかの区間へ分けます。
block_statistics(series, blocks=4)
ここでランダムに4分割しないのがポイントです。
時系列では、
最初の区間
次の区間
次の区間
最後の区間
という連続した時間区間で比べます。
6.5 条件を変えてみる
次は sample_daily.csv の observed_value で同じACFを描いてみます。
White Noiseとは違い、近いLagや7日周期付近に構造が現れるはずです。
この違いを自分の目で確認してみます。
確認問題
White Noiseの系列に、はっきりしたLag 1の自己相関が長期間残っていた場合、その系列をWhite Noiseと考えてよいでしょうか。
答えと解説を見る
そのままWhite Noiseと考えるのは難しくなります。
White Noiseでは、時刻どうしの相関が基本的に残らないことを想定します。
Lag 1に強い自己相関があるなら、前の値と現在値の間に利用できる時間構造が残っている可能性があります。
Lesson 11 ― Random Walk
学習目標
- Random Walkの作り方を理解する
- ランダムな系列とRandom Walkの違いを見る
- 1階差分の意味を確認する
- Random Walkで現在値が重要になる理由を理解する
7.1 Random Walkは「ランダム値そのもの」ではない
Random Walkは、毎時刻のランダムな変化量を累積して作ります。
イメージは次です。
x(t) = x(t-1) + ε(t)
ε(t) はランダムな増分です。
例えば、
現在値 20.0
+0.8 → 20.8
-0.3 → 20.5
+1.1 → 21.6
-0.4 → 21.2
となります。
一つ一つの増分はランダムでも、系列本体は前の値を引きずります。
7.2 NotebookでRandom Walkを見る
09_random_walk.ipynb を開きます。
frame = pd.read_csv(
ROOT / "data" / "part3_random_walk.csv",
parse_dates=["timestamp"],
).set_index("timestamp")
walk = frame["random_walk"]
steps = frame["step"]
walk と steps を上下に描くと、見た目がかなり違います。
Random Walk本体は長く上昇したり下降したりすることがあります。
一方、増分側は0付近を中心にランダムに動きます。
7.3 差分すると何が起きるか
Part 3で何度も使うのが差分です。
walk_diff = walk.diff().dropna()
Random Walkでは、
x(t) - x(t-1)
を計算すると、その時刻に加えられたランダム増分に戻ります。
Notebookでは、CSVに保存した元の step と diff() の結果を並べて確認します。
7.4 Random Walkの簡単な予測
Random Walkに対して、非常に強い基準になる予測があります。
次の値 ≒ 現在値
です。
Pythonでは、
naive_forecast = walk.shift(1)
と書けます。
ここではまだ精度を評価しません。
どの予測が良いのかを正しく比較する方法はPart 4で扱います。
確認問題
Random Walk本体と、その1階差分では、一般にどちらの方が長期的な水準移動が見えにくくなるでしょうか。
答えと解説を見る
1階差分です。
Random Walk本体はランダムな増分を累積しているため、水準が長期間移動します。
1階差分を取ると、その累積を外して1ステップごとの増分を見る形になります。
Lesson 12 ― 時系列分解
学習目標
- 時系列を複数の成分に分けて考える
- Level、Trend、Seasonality、Residualを理解する
-
seasonal_decompose()を使う - Additiveな分解のイメージをつかむ
8.1 時系列を一つの線として見ない
時系列には複数の動きが重なっています。
例えば、
長期的には上昇
毎週同じような波
その上に小さなランダム変動
というケースがあります。
これを一つの線だけで見ていると、何が原因で動いているのか分かりにくくなります。
そこで時系列を成分へ分けます。
Additiveな考え方なら、
Observed
= Trend
+ Seasonality
+ Residual
のように考えます。
値が大きくなるほど季節変動の振れ幅も大きくなるような系列では、Multiplicativeな考え方が合う場合もあります。
8.2 sample_daily.csvを分解する
10_decomposition.ipynb を開きます。
from statsmodels.tsa.seasonal import seasonal_decompose
series = frame["observed_value"].asfreq("D")
result = seasonal_decompose(
series,
model="additive",
period=7,
extrapolate_trend="freq",
)
今回の人工データには7日周期を入れているので、period=7 とします。
period を機械的に7にするのではありません。
データの観測間隔と、想定している季節周期から決めます。
例えば、
1時間データで1日周期 → 24
日次データで1週間周期 → 7
月次データで1年周期 → 12
です。
8.3 Residualを見る
分解したあと、TrendとSeasonalityを取り除いて残るのがResidualです。
Residualが完全にWhite Noiseになるとは限りません。
もしResidualにまだ規則性が残っているなら、
別の周期がある
Trendが単純ではない
外生要因がある
分解方法が合っていない
可能性があります。
ここでLesson 10のWhite Noiseの話につながります。
確認問題
日次データに「毎週同じ曜日に似た動き」がある場合、最初に試すSeasonalityのperiodはいくつでしょうか。
答えと解説を見る
7です。
日次データで1週間は7観測だからです。
ただし、実際のデータでは7日周期だけとは限らないため、グラフやACFなども合わせて確認します。
Lesson 13 ― Trendを使う・除く
学習目標
- Trendの意味を理解する
- 線形Trendを推定する
- Trendを引いた系列を見る
- 1階差分でTrendの影響を弱める
9.1 Trendとは
Trendは、時系列の水準が長期的に増加または減少する動きです。
短期的には上下する
↓
長期的には少しずつ上がっている
という系列にもTrendがあります。
9.2 線形Trendを推定する
Notebookでは、学習用として単純な1次式を当てます。
trend = fit_linear_trend(series)
内部では、経過時刻を
0, 1, 2, 3, ...
として、直線を最小二乗で当てています。
そのTrendを元系列から引けば、
detrended = series - trend
となります。
9.3 1階差分
もう一つの方法が、隣り合う値の差を取ることです。
first_diff = series.diff().dropna()
数式では、
y'(t) = y(t) - y(t-1)
です。
一定の傾きで増えている系列なら、水準そのものではなく「増減量」に変わります。
9.4 DetrendとDifferenceは同じではない
どちらもTrendの影響を弱めることがありますが、同じ処理ではありません。
線形Trendを引く方法は、推定した長期傾向からのずれを見る方法です。
差分は、隣り合う時刻の変化量を見る方法です。
目的に応じて使い分けます。
確認問題
元の値が 10, 12, 14, 16 と一定幅で増えている場合、1階差分はどうなるでしょうか。
答えと解説を見る
2, 2, 2
になります。
水準の上昇は消え、「毎時刻2ずつ増えている」という変化量へ変わります。
Lesson 14 ― Seasonalityを使う・除く
学習目標
- Seasonalityと単なる不規則なCycleを区別する
- 曜日別など周期ごとの特徴を見る
- 季節差分を作る
-
periodの意味を理解する
10.1 Seasonalityとは
Seasonalityは、一定の周期で繰り返すパターンです。
例えば、
毎日
毎週
毎月
毎年
などがあります。
重要なのは周期が固定されていることです。
単に大きな波が何度か出ているだけでは、必ずしもSeasonalityとは言えません。
10.2 曜日ごとの平均を見る
日次データに7日周期がありそうなら、曜日ごとに平均を見てみます。
weekday_mean = series.groupby(series.index.dayofweek).mean()
これは難しいモデルではありませんが、周期構造を理解するのに役立ちます。
10.3 季節差分
7日周期を弱めたい場合、現在値から7日前の値を引きます。
Pythonでは、
seasonal_diff = series.diff(7).dropna()
です。
今回の補助関数なら、
seasonal_diff = seasonal_difference(series, period=7)
と書けます。
10.4 季節差分を使えば何でもよいわけではない
Seasonalityがない系列へ無理に季節差分を適用すると、不要な変換になります。
まず、
グラフ
曜日・月ごとの集計
自己相関
ドメイン知識
から周期を考えます。
その後で差分を使います。
確認問題
1時間ごとのデータに24時間周期がある場合、季節差分のLagはいくつでしょうか。
答えと解説を見る
24です。
1時間 × 24観測で1日になるためです。
Lesson 15 ― StationarityとADF検定
学習目標
- Stationarityの意味を理解する
- 定常・非定常のグラフを見比べる
- 前半・後半の平均と分散を比べる
- ADF検定を実行する
- 帰無仮説とp値を読む
- 差分前後のADF結果を比較する
11.1 Stationarityとは
時系列でいうStationarityは、時間が進んでも系列の統計的な性質が大きく変わらない状態を指します。
入門では、まず次の点を見ます。
平均が大きく移動しない
分散が大きく変わらない
強いTrendがない
強いSeasonalityが残っていない
統計学ではより厳密な定義がありますが、最初はこの感覚をつかみます。
11.2 定常系列と非定常系列を見比べる
13_stationarity.ipynb では、同じ日時軸に2つの人工系列を入れています。
stationary = frame["stationary"]
nonstationary = frame["nonstationary"]
stationary はAR(1)過程です。
今回の生成式は概念的に、
x(t) = 0.55 x(t-1) + ε(t)
です。
係数の絶対値が1より小さいため、ショックの影響が時間とともに弱まり、0付近へ戻ろうとします。
nonstationary はドリフト付きRandom Walkです。
こちらは水準が時間とともに移動します。
11.3 前半と後半の平均・分散
いきなりADF検定を実行する前に、簡単な確認をします。
block_statistics(stationary, blocks=2)
block_statistics(nonstationary, blocks=2)
前半と後半で平均や分散が大きく変わるなら、非定常を疑う材料になります。
これは厳密な検定ではありません。
まずデータを見るための診断です。
11.4 ADF検定
ADFは Augmented Dickey-Fuller test です。
この検定では、帰無仮説を次のように考えます。
H0: 単位根を持つ
→ 非定常と考える方向
H1: 単位根を持たない
Notebookでは、
result = adf_test(series)
print(result.p_value)
のように確認します。
例えば有意水準を5%とした場合、
p < 0.05
ならH0を棄却する材料になります。
11.5 p値だけを見ない
ここは重要です。
ADF検定で p < 0.05 だったからといって、
この系列はどんな意味でも完全に定常である
とまでは言えません。
ADFは単位根を調べる検定です。
そのため、実際には、
グラフ
Trend
Seasonality
区間ごとのMean / Variance
ADF検定
を合わせて見ます。
11.6 非定常系列へ差分を適用する
Random Walk系の非定常系列に1階差分を適用します。
nonstationary_diff = nonstationary.diff().dropna()
そのあと、もう一度ADF検定を行います。
adf_test(nonstationary_diff)
Part 3の前半で学んだRandom Walkと差分が、ここでStationarityの話につながります。
確認問題
ADF検定で p = 0.01 だったとします。有意水準5%で、帰無仮説「単位根を持つ」に対してどう考えるでしょうか。
答えと解説を見る
0.01 < 0.05 なので、5%水準では帰無仮説を棄却する材料になります。
したがって、単位根を持たないと判断する方向です。
ただし、ADF検定だけで時系列のすべての性質を判断せず、グラフやTrend、Seasonality、区間統計量も合わせて確認します。
12. Part 3でつながったこと
ここまで学んだ内容は、個別の用語ではなく一つの流れになっています。
モデルを作る前に、この流れを一度通しておくと、後でARIMAへ進んだときの d、つまり差分の意味が理解しやすくなります。
13. Part 3で覚えておきたいポイント
1. ランダムに見えることとWhite Noiseは同じではない
まずグラフだけで決めず、自己相関や区間統計量も見ます。
2. Random Walkは前の値を引きずる
ランダムな増分を累積しているので、系列本体には強い時間依存が生まれます。
3. 差分は重要
Random Walk → 1階差分
Seasonality → 季節差分
のように、差分は非定常な構造を弱める基本操作です。
4. TrendとSeasonalityは別に考える
長期的な方向と、固定周期の繰り返しは別の構造です。
5. Stationarityはモデル作りの前提として重要
特に古典的な統計時系列モデルでは、Stationarityを意識する場面が増えます。
6. ADF検定だけで決めない
p値は重要ですが、グラフやデータの意味と一緒に読みます。
14. Part 3の全体像
Part 1では、
何を入力にして、何を未来として予測するか
を考えました。
Part 2では、
そのためのデータをどう整えるか
を学びました。
Part 3では、
整えたデータの中に、どんな時間構造があるのか
を確認しました。
次からは、いよいよ「予測モデルをどう評価するか」へ進みます。
15. 次はPart 4 ― Evaluate Models
Part 4では、予測モデルを作る前に評価方法を決めます。
通常の機械学習では、データをランダムにTrain/Testへ分割することがあります。
しかし、時系列で未来と過去を混ぜると、本来の予測とは違う問題になってしまいます。
Part 4では、次の流れを扱います。
ここで初めて、
この予測モデルは本当に良いのか
を比較できるようになります。
参考資料
本記事は、次の書籍のPart 3を参考に、現在のPython環境とDocker + JupyterLabを使ったe-learning形式へ組み直しています。
-
Jason Brownlee, Introduction to Time Series Forecasting with Python, Part III: Temporal Structure
- Chapter 10: A Gentle Introduction to White Noise
- Chapter 11: A Gentle Introduction to the Random Walk
- Chapter 12: Decompose Time Series Data
- Chapter 13: Use and Remove Trends
- Chapter 14: Use and Remove Seasonality
- Chapter 15: Stationarity in Time Series Data
原書のPythonコードは刊行当時のpandasやstatsmodels APIを前提としている箇所があります。
この教材では、Part 1・Part 2から継続して現在の環境で実行しやすい形へ書き換えています。











