Day 122 — Prophet入門 — トレンド・季節性・祝日効果の分解

2026-08-12 紫 / Phase 5 理論→コーディング Prophet / 加法モデル / holidays / changepoint

📚 背景知識(読んでから問題へ)

🔀
Day121で学んだLSTMは「モデルに時間の流れを丸ごと読ませて、内部でよしなに処理してもらう」ブラックボックス寄りのアプローチでした。今日学ぶProphet(Meta/旧Facebookが開発した時系列予測ライブラリ)は真逆の発想で、時系列を「トレンド・季節性・イベント効果」という3つの成分に明示的に分解してから、それぞれを別々に予測し、最後に足し合わせます。
加法モデル(Additive Model): Prophetは y(t) = トレンド(t) + 季節性(t) + イベント効果(t) + 誤差 という足し算で時系列を表現します。「今日の売上 = ①長期的な右肩上がり/下がり + ②曜日や季節で毎回繰り返すパターン + ③セール日など特別な日の上乗せ + ④説明しきれない揺れ」という考え方です。「なぜこの日にこの値を予測したのか」を成分ごとに分解して人に説明しやすいのが最大の強みです。
🎚️
changepoint_prior_scale — 「トレンドの折れ具合をどれだけ許すか」のダイヤル: トレンドの変化点をどれだけ柔軟に折り曲げてよいかを決めるハイパーパラメータ。値を大きくすると細かく折れ曲がるトレンドを許容してtrainに敏感にフィットするが過学習しやすく、小さくするとなめらかで動きにくいトレンドになる。Day116(過学習の兆候)の「柔軟性と過学習のトレードオフ」が、Prophetでは「トレンドの折れやすさ」という形で現れる。
🔑
キーワード
ds / y: Prophetが要求する入力列名(ds=日付、y=予測したい値)。列名を変えると動かない
changepoint: トレンドの傾きが変わる地点
Fourier order: 季節性を表すサイン・コサインの本数(多いほど複雑な周期形状を表現できるが過学習しやすい)
holidays: 特定の日にだけ効く補正項を定義するためのDataFrame

3つの成分の作り方 — 「トレンド」「季節性」「イベント効果」

📈

トレンド (trend)

区間ごとに傾きが変わる折れ線。傾きが変わる地点(changepoint)を自動検出し、その間は直線的に伸ばす

🌊

季節性 (seasonality)

毎週・毎年、同じ形で繰り返す波。サイン・コサインを何本も重ね合わせ(フーリエ級数)、複雑な周期も表現する

🏷️

イベント/祝日効果 (holidays)

セール日・祝日などカレンダー上の特別な日を指定すると、その日だけの補正値を学習する

🔍 加法モデルの分解図 — 3つの波を足し合わせて1本の予測にする

① トレンド trend(t) ② 週次季節性 seasonality(t) ③ イベント効果 holidays(t)(30日おきのセール日だけスパイク) = 予測 y(t)(3成分の足し算)

トレンド(右肩上がりの直線)、週次季節性(規則正しいサイン波)、イベント効果(セール日だけの縦のスパイク)を別々に学習し、最後に単純に足し合わせることで最終的な予測曲線ができる。LSTMのように1つのネットワークが全部を同時に処理するのではなく、成分ごとに分けて学習・可視化できる点がProphetの最大の特徴。

🗂️ データスキーマ

列名説明
dsdatetime64Prophet専用の日付列名。2026-01-01から220日分の連続した日付
yfloatProphet専用の目的変数列名。売上 = 100 + トレンド(day×0.5) + 週次季節性(振幅15) + ノイズ(σ=5) + セール効果(+25、30日おき)
変数役割
TRAIN_LEN180先頭180日をtrain、残り40日をtest(未来予測の検証用)に分割する境界
sale_daysday % 30 == 2930日おきの「月末セール」日(例: day=29, 59, 89, ...)。holidaysとしてモデルに渡す

🎯 問題

Day121と同じ「架空店舗の日次売上データ」を220日分に拡張し、さらに30日おきの「月末セール」による売上の上乗せを加えたデータが与えられます。

import numpy as np
import pandas as pd

np.random.seed(42)
days = np.arange(220)
trend = days * 0.5
weekly_seasonality = 15 * np.sin(2 * np.pi * days / 7)
noise = np.random.normal(0, 5, size=220)
sales = 100 + trend + weekly_seasonality + noise

# 30日おき(day=29, 59, 89, ...)に「月末セール」で売上が+25される
sale_days = days[(days % 30) == 29]
sale_effect = np.where(np.isin(days, sale_days), 25, 0)
sales = sales + sale_effect

dates = pd.date_range(start="2026-01-01", periods=220, freq="D")
df = pd.DataFrame({"ds": dates, "y": sales})

TRAIN_LEN = 180  # 先頭180日をtrain、残り40日をtest(未来予測の検証用)
1
理論: 「加法モデル」の説明を踏まえ、このデータに対してProphetがLSTMより優れている可能性がある理由と、逆にLSTMの方が有利な可能性がある理由をそれぞれ1〜2文で説明する(Day121のLSTMとの比較)
2
コーディング(メイン): セール日をholidays形式に変換 → Prophetモデルをtrainで学習 → make_future_dataframeで40日先を予測 → test RMSEを算出 → 「最後の値を40回コピー」するナイーブベースライン(persistence forecast)と比較する

💡 ヒント

ヒント1方向性

Prophetはds(日付)とy(値)という決まった列名の2列DataFrameを要求します。holidaysはイベントの起きた日付の一覧を「いつ・何という名前のイベントか」という形で渡す仕組みです。今回の「セール日」は毎回同じ効果(+25相当)なので、Prophetの目線では「祝日」と全く同じ扱いができます。

ヒント2アプローチ
  • pip install prophet(旧fbprophet)が必要。from prophet import Prophetでインポートする
  • holidaysのDataFrameは最低限holiday列(イベント名の文字列)とds列(日付)が必要
  • model.fit()にはtrain部分(先頭180日)のds, y列だけを渡す。test部分の情報を学習に混ぜてはいけない(Day121と同じ「未来の情報を混ぜない」原則)
  • make_future_dataframe(periods=40)は「学習データの最終日から40日分の未来の日付」を自動生成する。学習に使った180日分の予測値も含めて返ってくるので、末尾40行がテスト期間の予測に対応する
  • forecast["yhat"]が予測値。persistence forecastはnp.full(40, train_df["y"].iloc[-1])のように、trainの最後の値を40回繰り返すだけでよい
ヒント3コード骨格
from prophet import Prophet

holidays_df = pd.DataFrame({
    "holiday": "monthly_sale",
    "ds": pd.to_datetime(dates[np.isin(days, sale_days)]),
})

train_df = df.iloc[:TRAIN_LEN].reset_index(drop=True)
test_df = df.iloc[TRAIN_LEN:].reset_index(drop=True)

model = Prophet(
    weekly_seasonality=True,
    yearly_seasonality=False,
    daily_seasonality=False,
    holidays=holidays_df,
)
model.fit(train_df[["ds", "y"]])

future = model.make_future_dataframe(periods=40)
forecast = model.predict(future)

pred = forecast["yhat"].values[-40:]
actual = test_df["y"].values
# ここからRMSEを計算する
...

模範解答

タスク1: ProphetとLSTMの得意・不得意(解答例)

💬
Prophetが有利な可能性: 今回のデータは「トレンド+週次季節性+30日おきの明確なイベント効果」という、Prophetの加法モデルの構造(トレンド・季節性・holidays)にそのまま一致する生成過程を持っている。Prophetは「セール日には+25される」という規則性をholidaysとして直接教えられるため、少ないデータ量でも安定してこのパターンを学習しやすい。
💬
LSTMが有利な可能性: もしデータの生成過程が「セール日の効果が売上の水準によって変わる」「複数の周期が複雑に絡み合う」といった非線形・非加法的な構造を持つ場合、Prophetの「足し算」の枠組みでは表現しきれない。LSTMは成分を明示的に分解しない代わりに、こうした複雑な相互作用を(データ量が十分にあれば)柔軟に学習できる可能性がある。

タスク2: Prophetでの実装と評価

import numpy as np
import pandas as pd
from prophet import Prophet

# --- サンプルデータ生成 ---
np.random.seed(42)
days = np.arange(220)
trend = days * 0.5
weekly_seasonality = 15 * np.sin(2 * np.pi * days / 7)
noise = np.random.normal(0, 5, size=220)
sales = 100 + trend + weekly_seasonality + noise

sale_days = days[(days % 30) == 29]
sale_effect = np.where(np.isin(days, sale_days), 25, 0)
sales = sales + sale_effect

dates = pd.date_range(start="2026-01-01", periods=220, freq="D")
df = pd.DataFrame({"ds": dates, "y": sales})

TRAIN_LEN = 180

# --- ステップ1: holidays(セールイベント)のDataFrameを作る ---
holidays_df = pd.DataFrame({
    "holiday": "monthly_sale",
    "ds": pd.to_datetime(dates[np.isin(days, sale_days)]),
})

# --- ステップ2: train/testに時系列順で分割し、Prophetモデルを学習する ---
train_df = df.iloc[:TRAIN_LEN].reset_index(drop=True)
test_df = df.iloc[TRAIN_LEN:].reset_index(drop=True)

model = Prophet(
    weekly_seasonality=True,
    yearly_seasonality=False,
    daily_seasonality=False,
    holidays=holidays_df,
    changepoint_prior_scale=0.05,  # デフォルト値。トレンドの折れやすさを控えめに設定
)
model.fit(train_df[["ds", "y"]])

# --- ステップ3: 40日先まで予測する ---
future = model.make_future_dataframe(periods=40)
forecast = model.predict(future)

# --- ステップ4: test RMSEを算出する ---
pred = forecast["yhat"].values[-40:]
actual = test_df["y"].values
prophet_rmse = np.sqrt(np.mean((pred - actual) ** 2))

# --- ステップ5: persistence forecast(最後の値を40回コピー)と比較する ---
last_train_value = train_df["y"].iloc[-1]
naive_pred = np.full(40, last_train_value)
naive_rmse = np.sqrt(np.mean((naive_pred - actual) ** 2))

print(f"Prophet test RMSE : {prophet_rmse:.3f}")
print(f"Naive   test RMSE : {naive_rmse:.3f}")
# 出力例:
# Prophet test RMSE : 8.2xx
# Naive   test RMSE : 38.7xx
# → naiveは「40日先まで同じ値を出し続ける」ため、右肩上がりのトレンドと
#   週次の波を完全に無視した予測になり誤差が急速に拡大する。
#   Prophetはトレンド・週次季節性・セール効果を分解して先まで延長できるため
#   長期の複数ステップ予測で圧倒的に有利になる。
🔍
タスク1の理論を裏付ける確認方法: model.plot_components(forecast)を実行すると、トレンド・週次季節性・holidaysの3つの成分がそれぞれ別々のグラフとして描画される。「セール日にだけ上乗せがある」という生成過程どおりの形がholidaysの成分に現れていれば、Prophetがデータの構造を正しく分解できている証拠になる。

📊 RMSE比較の可視化(test 40日先、値が小さいほど良い)

Naive(最後の値を40回コピー)
RMSE 38.7(例)
トレンド・季節性を無視
Prophet(トレンド+季節性+holidays)
RMSE 8.2(例)
3成分を分解して延長予測
参考: Day121 LSTM(1日先の逐次予測)
RMSE 6.8(例)
タスク条件が異なるため単純比較不可
🏁
naiveは「40日先まで同じ値を出し続ける」ため、右肩上がりのトレンドと週次の波を完全に無視した予測になり誤差が急速に拡大する。Prophetはトレンド・週次季節性・セール効果を成分として学習し、それを未来へ延長できるため、複数ステップ先までの長期予測で明確に有利になる。なお参考として載せたDay121のLSTMは「1日先だけを逐次予測」する条件だったため、今回の「40日先を一気に予測」するタスクとは前提が異なり、数値をそのまま比較することはできない点に注意。実際の数値は乱数シード・ハイパーパラメータで変動するため、必ず自分の環境で再現して確認すること。

🪜 Step-by-Step 解説

1holidaysはイベント名と日付の対応表として渡す

holidays_df = pd.DataFrame({
    "holiday": "monthly_sale",
    "ds": pd.to_datetime(dates[np.isin(days, sale_days)]),
})
🏷️
なぜこうするか: Prophetのholidays引数は「祝日」専用ではなく、「特定の日付にだけ効く一時的な補正項」を表現する汎用的な仕組み。holiday列に同じ名前("monthly_sale")を繰り返すことで、「このイベントは毎回同じ効果を持つ」と教えることができる。異なる種類のイベントを混在させたい場合は、holiday列に異なる文字列を入れればProphetが別々の効果として学習してくれる。

2model.fit()にはtrain部分だけを渡す

model.fit(train_df[["ds", "y"]])
🔒
なぜこうするか: Day121のLSTMと同じ「未来の情報を学習に混ぜない」原則。test期間(TRAIN_LEN以降)のデータをfitに含めてしまうと、モデルが本来知らないはずの未来の値を使ってトレンド・季節性を推定してしまい、実運用ではあり得ないほど良いスコアが出てしまう。

3make_future_dataframeは「学習データの続き」の日付を自動生成する

future = model.make_future_dataframe(periods=40)
forecast = model.predict(future)
🗓️
なぜこうするか: Prophetは「日付を入力として受け取り、その日の予測値を返す」という設計になっている(LSTMのように直近N日の実測値を毎回入力する必要がない)。periods=40と指定するだけで学習データの最終日の翌日から40日分の日付が自動生成され、その日付それぞれについてトレンド・季節性・holidays成分を計算して足し合わせる形で予測できる。

4ナイーブをpersistence forecast(最後の値を複製)にする理由

naive_pred = np.full(40, last_train_value)
🧭
なぜこうするか: Day121のナイーブベースラインは「1ステップ先」の予測だったためy[t-1]が使えたが、今回は40日先まで複数ステップ先を予測するタスク。複数ステップ先の予測では「1つ前の実測値」がそもそも存在しない(実測値は180日目までしかない)ため、「最後に分かっている値をそのまま複製し続ける」persistence forecastが素朴なベースラインとして使われる。トレンドがあるデータではこのベースラインの誤差は日が進むごとに大きくなりやすい。

🧮 数学・統計の補足(文系向け)

🎵
フーリエ級数を「波の重ね合わせ」として直感的に理解する: 音楽のシンセサイザーが「複数の異なる高さの音(サイン波)を重ねることで複雑な音色を作る」のと同じ発想です。Prophetの季節性は、周期の異なる複数のサイン・コサイン波を何本も足し合わせることで、単純な正弦波では表現できない「複雑な形の繰り返しパターン」(例: 週の前半は緩やかに増加し、週末に急に跳ね上がる、といった非対称な形)を表現します。重ねる波の本数(Fourier order)を増やすほど複雑な形を表現できますが、増やしすぎるとノイズにまで細かくフィットして過学習します。
📐
もし数式を見たら:
s(t) = Σ [a_n cos(2πnt/P) + b_n sin(2πnt/P)](n=1〜N)

→ これは季節性成分の計算式。「周期P(週次なら7日)の波をN本、少しずつ違う周波数で重ね合わせて、係数a_n, b_nは学習で決める」という意味。「波を何本重ねて複雑な形を作るか」というイメージで捉えれば十分。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☐ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☑ 時系列(Time Series)

EDA・診断用

🔬 ベースライン・診断ツールとしての利用

plot_componentsで成分構造を可視化

学習が速く(GPUも不要)、plot_componentsで「トレンドが急に折れていないか」「季節性が想定通りの形か」を可視化できるため、コンペ最初期に「データの成分構造」を把握する探索的な用途で重宝される。上位解法そのものへの採用は少ないが、EDA代わりに使われることは多い。

系列数が少ない場合

🌐 Web Traffic Forecasting系コンペ

明確な季節性・イベントがある系列で有効

系列数が少なく、明確な週次・年次季節性やイベント(記事のバズり、祝日アクセス増)がある系列では、Prophetのholidays機構が効果的に働くことがある。ただし数万系列を一括で扱う大規模コンペでは、系列ごとに個別学習が必要なProphetは計算コストの面で不利になりやすい。

使い分けの目安

⚖️ 解釈性・速さ重視ならProphet

大規模・非線形にはGBDT/LSTM

「成分ごとの解釈性」「学習の速さ」「明確なイベント日程が分かっている」場合はProphetが強い。「大量の系列を一括で学習したい」「非線形な相互作用が強い」場合はGBDT(Day098-100)やLSTM(Day121)に軍配が上がりやすい。

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
列名をds/y以外(例: date/sales)のままfitしようとしてエラーになるpandasの他の分析では自由に列名を付けられる習慣があるProphetは列名を固定で要求するAPI設計。rename(columns={"date": "ds", "sales": "y"})のように必ず変換してから渡す
make_future_dataframe(periods=40)がtrain全体を予測し直していることに気づかず、末尾40行だけを取り出し忘れる「未来だけの予測結果が返ってくる」と思い込んでしまう返ってくるforecastはtrain期間+未来期間の全行を含む。評価に使うのはforecast.tail(periods)(今回は末尾40行)だけ
holidaysに「効果の強さ」を数値で渡そうとする数値を渡す設計だと思い込んでしまうholidaysは「いつ・何という名前のイベントか」を渡すだけで、効果の大きさ自体はProphet内部の学習で推定される。事前に効果量を指定する引数ではない
Prophetは「万能で高精度なSOTAモデル」だと過大評価してしまうライブラリ名や知名度から「強いモデル」という印象を持ってしまうProphetは「解釈性の高さ」と「学習の速さ・簡便さ」に強みがあるモデルであり、精度そのものはLightGBMやLSTM/Transformer系に劣ることが多い。Kaggleでは「まず全体構造を把握するための道具」として位置づけるのが実践的

🚀 次のステップ

  • 発展: changepoint_prior_scaleを0.01(硬いトレンド)と0.5(柔らかいトレンド)に変えて再学習し、model.plot(forecast)で40日先の予測がどう変わるかを比較してみましょう。またholidays_dfを意図的に空にして学習し直し、セール効果を無視した場合にtest RMSEがどれだけ悪化するかを確認すると、「イベント情報をモデルに教えることの価値」が数値で実感できます
  • 次回予告: Day 123 — Temporal Fusion Transformer(TFT)入門。Day121のLSTM(時間を1ステップずつ処理)、Day122のProphet(成分に分解して予測)に続き、次回はAttention機構を使って「どの過去のタイムステップ・どの特徴量が予測に重要か」を学習しながら予測するTFTを扱います。時系列ブロック(Day121-123)の最終回として、3つのアプローチの使い分けを整理します

Phase 5 の学習マップ(全20テーマ予定)

1-5 DL基礎(PyTorch) 完了
6-10 NLP(後日)
11-15 CV 完了
16-18 時系列 ← 本日(②Prophet)
19-20 マルチモーダル

Day121でLSTM(ニューラルネットワークで時間の流れを直接扱うアプローチ)を学び、今日Day122ではProphet(成分に分解して予測するアプローチ)を学んだ。次回Day123のTemporal Fusion Transformerで、時系列ブロック(16-18)が完了する。

📝 自己評価(解いた後に記入)

自分の回答・気づき・メモ: