404 Not Found

404 Not Found


nginx

【タイムシリーズ予測】Pandasプロジェクト:resample・rolling・shiftを使ったコーヒーショップ売上予測

タイムシリーズ予測はモデリングだけではありません。機械学習に手を伸ばす前に、トレンドと季節性を理解するだけでも、堅実なベースライン予測が得られます。本レッスンでは、アリスのコーヒーショップ売上予測をシナリオとして、前処理からresample + rolling + shiftを使った基本的な予測まで、パイプライン全体を一通り体験します。これはレッスン17で学んだツールの実践的な応用であり、今後の機械学習レッスンの前哨戦でもあります。

⚠️ 注意: 以下のコードはローカルのPython環境で実行してください。

1. 学習内容



2. プロジェクトの背景:コーヒーショップの売上予測

(1) 課題

アリスは来月のコーヒーショップの売上を予測したいと考えています。過去のデータにはトレンド(安定した前月比成長)と季節性(冬はホットドリンクの売上が増加)があります。

(2) 分析パイプライン

100%
graph TB
    A["1. 前処理<br>resample+fillna"] --> B["2. トレンド抽出<br>rolling+diff"]
    B --> C["3. 季節性分析<br>groupby month"]
    C --> D["4. 移動平均予測<br>rolling mean"]
    D --> E["5. 評価<br>MAE/MAPE"]
    E --> F["6. 来月の予測"]
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。


3. データの前処理

▶ サンプル:タイムシリーズの前処理

TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。

:タイムシリーズの前処理(難易度 ⭐⭐)

PYTHON
import pandas as pd
import numpy as np

# ============================================
# ステップ1:データの生成と前処理
# ============================================

np.random.seed(42)

# トレンド + 季節性 + ノイズを含む365日分の売上データを生成
dates = pd.date_range('2023-01-01', periods=365)
trend = np.linspace(300, 450, 365)  # 上昇トレンド
seasonality = 50 * np.sin(2 * np.pi * np.arange(365) / 365 * 2)  # 年2回のサイクル
winter_effect = np.where(dates.month.isin([11, 12, 1, 2]), 30, 0)  # 冬のブースト
noise = np.random.normal(0, 20, 365)

daily_sales = pd.DataFrame({
    'sales': np.round(trend + seasonality + winter_effect + noise, 0),
    'customers': np.round((trend + seasonality + winter_effect + noise) / 8, 0)
}, index=dates)

# 欠損日を注入
missing_idx = np.random.choice(365, 15, replace=False)
daily_sales.iloc[missing_idx] = np.nan

# 前処理:欠損の補完 → 日次頻度の確保
daily_sales = daily_sales.asfreq('D')  # 日付の欠損がないことを確認
daily_sales['sales'] = daily_sales['sales'].interpolate(method='time')
daily_sales['customers'] = daily_sales['customers'].interpolate(method='time')

print(f"日付範囲: {daily_sales.index[0].date()} から {daily_sales.index[-1].date()}")
print(f"補完後の欠損数: {daily_sales.isnull().sum().sum()}")
print(f"\n日次売上の統計:\n{daily_sales['sales'].describe()}")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。


4. トレンドの抽出

▶ サンプル:トレンドとデトレンド

TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。

:トレンドとデトレンド(難易度 ⭐⭐)

PYTHON
# ============================================
# ステップ2:トレンドの抽出
# ============================================

# 30日移動平均 = トレンド
daily_sales['trend'] = daily_sales['sales'].rolling(30, center=True).mean()

# デトレンド = 実績値 - トレンド(季節性 + ノイズを示す)
daily_sales['detrended'] = daily_sales['sales'] - daily_sales['trend']

# 前月比成長率
monthly = daily_sales['sales'].resample('M').sum()
monthly_growth = monthly.pct_change() * 100

print("=== 月次売上 ===")
print(monthly.tail(6))
print(f"\n平均月次成長率: {monthly_growth.mean():.1f}%")
print(f"年間総成長率: {((monthly.iloc[-1] / monthly.iloc[0]) - 1) * 100:.1f}%")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。


5. 季節性の分析

▶ サンプル:季節指数

TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。

:季節指数(難易度 ⭐⭐⭐)

PYTHON
# ============================================
# ステップ3:季節性の分析
# ============================================

# 月別平均(全期間)
daily_sales['month'] = daily_sales.index.month
monthly_avg = daily_sales.groupby('month')['sales'].mean()

# 季節指数 = 月別平均 / 全体平均
overall_avg = daily_sales['sales'].mean()
seasonal_index = (monthly_avg / overall_avg).round(3)

print("=== 季節指数 ===")
for m, idx in seasonal_index.items():
    label = "↑" if idx > 1.05 else ("↓" if idx < 0.95 else "→")
    print(f"  {m:2d}月: {idx:.3f} {label}")

# 曜日パターン
daily_sales['dayofweek'] = daily_sales.index.dayofweek
dow_avg = daily_sales.groupby('dayofweek')['sales'].mean()
dow_names = ['月', '火', '水', '木', '金', '土', '日']
print(f"\n=== 曜日別平均 ===")
for i, name in enumerate(dow_names):
    print(f"  {name}: ${dow_avg[i]:.0f}")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。


6. 移動平均による予測

▶ サンプル:ローリング予測

TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。

:ローリング予測(難易度 ⭐⭐⭐)

PYTHON
# ============================================
# ステップ4:移動平均による予測
# ============================================

# 訓練/テスト分割:最後の30日間をテストデータとする
train = daily_sales.iloc[:-30]
test = daily_sales.iloc[-30:]

# 単純移動平均予測(直近N日間の平均を使用)
def ma_forecast(train_series, window, horizon):
    """移動平均を使って次の'horizon'日分を予測する"""
    last_ma = train_series.rolling(window).mean().iloc[-1]
    return pd.Series([last_ma] * horizon,
                     index=pd.date_range(train.index[-1] + pd.Timedelta(days=1),
                                         periods=horizon))

# 異なるウィンドウを試す
for w in [7, 14, 30]:
    forecast = ma_forecast(train['sales'], w, 30)
    mae = (forecast - test['sales']).abs().mean()
    mape = ((forecast - test['sales']) / test['sales']).abs().mean() * 100
    print(f"MA({w:2d}): MAE=${mae:.0f}, MAPE={mape:.1f}%")

# 季節調整済み予測
# forecast = trend_forecast × seasonal_index
last_trend = train['sales'].rolling(30).mean().iloc[-1]
monthly_trend_growth = train['sales'].rolling(30).mean().diff(30).mean()
forecast_months = test.index.month
seasonal_forecast = pd.Series(
    [(last_trend + monthly_trend_growth * i) * seasonal_index[m]
     for i, m in enumerate(forecast_months)],
    index=test.index
)
mae_seasonal = (seasonal_forecast - test['sales']).abs().mean()
mape_seasonal = ((seasonal_forecast - test['sales']) / test['sales']).abs().mean() * 100
print(f"\n季節調整済み: MAE=${mae_seasonal:.0f}, MAPE={mape_seasonal:.1f}%")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。


7. 予測の評価

▶ サンプル:評価と来月の予測

TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。

:評価と来月の予測(難易度 ⭐⭐)

PYTHON
# ============================================
# ステップ5-6:評価と来月の予測
# ============================================

# 評価指標
def evaluate(actual, forecast, name="モデル"):
    mae = (forecast - actual).abs().mean()
    rmse = ((forecast - actual)  2).mean()  0.5
    mape = ((forecast - actual) / actual).abs().mean() * 100
    print(f"{name}: MAE=${mae:.0f}, RMSE=${rmse:.0f}, MAPE={mape:.1f}%")
    return mae, rmse, mape

# 最良モデルの評価
print("=== モデル評価 ===")
evaluate(test['sales'], ma_forecast(train['sales'], 14, 30), "MA(14)")
evaluate(test['sales'], seasonal_forecast, "季節調整済み")

# 来月の予測(2024年1月)
next_month_days = pd.date_range('2024-01-01', periods=31)
last_trend_full = daily_sales['sales'].rolling(30).mean().iloc[-1]
jan_index = seasonal_index[1]
next_month_forecast = last_trend_full * jan_index

print(f"\n=== 2024年1月の予測 ===")
print(f"  日次予測: ${next_month_forecast:.0f}")
print(f"  月次予測: ${next_month_forecast * 31:,.0f}")
print(f"  トレンド基準: ${last_trend_full:.0f} × 季節指数: {jan_index:.3f}")
TEXT
> 出力: ローカルのPython環境(pandas 2.x)で実行してください。Pistonサーバーにはpandasがプリインストールされていません。ローカルにインストール(`pip install pandas`)して一緒に進めてください。実際の値はpandasのバージョンによって若干異なる場合があります。

❓ よくある質問

Q 移動平均のウィンドウはどう選べばよいですか?
A データの自然なサイクルにウィンドウを合わせるのが最も安全です。日次データの場合、週次サイクルならwindow=7、月次サイクルならwindow=30が目安です。ウィンドウが小さすぎるとノイズを拾いすぎますし、大きすぎると深刻なラグが発生します。まずは自然な周期と同じウィンドウから始めて、その前後の値をいくつか試し、MAPEを比較してください。本レッスンでは、MA(14)がMA(7)やMA(30)よりもバランスの良い結果になることが多いです。
Q トレンドと季節性をどう分離しますか?
A トレンド = 長期移動平均(30日以上)、季節性 = 各月の平均値と全体平均値の比率です。デトレンドとは、元の値からトレンドを引くこと(加法モデル)、または元の値をトレンドで割ること(乗法モデル)を意味します。小売データは通常、乗法モデルが適しており(季節変動がレベルに比例する)、気温データは加法モデルが適しています。
Q MAPEはどの程度であれば良いとされますか?
A MAPE < 10%は高精度、10〜20%は良好、20〜50%は許容範囲、50%超は信頼性が低いとされます。ただし文脈が重要です。変動の大きい日次売上ではMAPE 20%でも十分優秀ですし、月次合計に集計した後ならMAPEは10%を下回るはずです。予測期間が先になるほど信頼性は低下します。7日先の予測は30日先の予測よりもはるかに信頼性が高いです。
Q どの程度先まで信頼性のある予測ができますか?
A 経験則として、信頼できる予測期間は過去データ長の約1/3〜1/2です。1年分の履歴データがあれば、信頼できる予測は最大4〜6ヶ月程度です。移動平均は「横ばい」の継続しか投影できず、転換点を予測することはできません。トレンドの変化や予期せぬ出来事があると予測は完全に無効になります。予測は意思決定の支援ツールであり、水晶玉ではありません。
Q 機械学習による予測とどう違いますか?
A 移動平均予測は「未来は過去の延長線上にある」と仮定しており、トレンドと季節性しか捉えられません。機械学習の手法(ARIMA/LSTM/Prophet)は、はるかに複雑なパターン(非線形トレンド、多変量相互作用、祝日効果)をモデル化できます。本レッスンは機械学習のベースラインとして位置づけられます。単純な移動平均でMAPE 15%を達成しているなら、機械学習モデルはそれ以上の複雑さに見合う価値があるために10%を下回るべきです。
Q 季節指数はどう計算しますか?
A 各月の平均値を全体平均値で割ります。例えば、1月の平均が420で全体平均が380の場合、季節指数は420/380 = 1.105(1月は平均より10.5%高い)となります。指数が1より大きければ繁忙期、1より小さければ閑散期を示します。信頼性のある季節指数を得るには、最低でも2年分のデータが必要です(1月は年によって異なる場合があります)。
Q 欠損値にはinterpolateとffillのどちらを使うべきですか?
A タイムシリーズにはinterpolateが推奨されます。隣接する値の間を補間することで、より滑らかな結果が得られ、トレンドも保持されます。ffillは最後の既知の値をそのまま前に持っていくため、欠損区間で階段状の効果(トレンドの不連続)が生じます。大きな欠損にはinterpolate(method='time')で時間的距離を考慮し、小さな欠損には線形補間で十分です。

📖 まとめ


📝 練習問題

  1. 基礎(難易度 ⭐):180日分の売上データ(トレンド付き)を生成し、resample('M')で月次合計に集計して、前月比成長率を計算してください。
  2. 応用(難易度 ⭐⭐):季節性を含む365日分のデータを生成し、30日のトレンド線を抽出して、12ヶ月の季節指数を計算し、繁忙期と閑散期を特定してください。
  3. チャレンジ(難易度 ⭐⭐⭐):完全な予測プロジェクトを完成させてください。前処理 → トレンド + 季節性 → MA(7/14/30)予測 → 季節調整済み予測 → MAPE評価 → 来月の予測出力。

← 前へ:プロジェクト - データ分析 · 次へ:プロジェクト - 総合分析 →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%