MLによる時系列予測
「統計モデルは基本に忠実だが、外部変数の扱いに限界がある。MLなら石油価格もプロモーションも祝日も、すべてを特徴量として投入できる。」
田中VPoEが特徴量のリストを表示する。
「LightGBMで勝負する。鍵は特徴量エンジニアリングだ。時系列データをテーブルデータに変換する技術が、このアプローチの成否を分ける。」
時系列データのテーブル変換
時系列データをMLモデルで扱うには、各時点を「行」、過去の情報を「特徴量(列)」に変換する。
変換前(時系列):
日付 売上
2017-08-01 100
2017-08-02 120
2017-08-03 110
変換後(テーブル):
日付 売上 lag_1 lag_7 rolling_7_mean dayofweek month ...
2017-08-03 110 120 ??? ??? 3 8 ...
特徴量エンジニアリング
1. ラグ特徴量
import pandas as pd
import numpy as np
def create_lag_features(df, target='sales', lags=[1, 2, 3, 7, 14, 28]):
"""ラグ特徴量の作成"""
for lag in lags:
df[f'lag_{lag}'] = df.groupby(['store_nbr', 'family'])[target].shift(lag)
return df
# 注意: テストデータで使えるのは予測開始日より前のデータのみ
# lag_1はt-1のデータが必要 → 逐次予測が必要な場合あり
2. ローリング統計量
def create_rolling_features(df, target='sales', windows=[7, 14, 28]):
"""ローリング統計量の作成"""
for window in windows:
# shift(1)で情報漏洩を防ぐ
rolled = df.groupby(['store_nbr', 'family'])[target].shift(1).rolling(window)
df[f'rolling_mean_{window}'] = rolled.mean().values
df[f'rolling_std_{window}'] = rolled.std().values
df[f'rolling_min_{window}'] = rolled.min().values
df[f'rolling_max_{window}'] = rolled.max().values
return df
3. カレンダー特徴量
def create_calendar_features(df):
"""カレンダー特徴量の作成"""
df['dayofweek'] = df['date'].dt.dayofweek
df['dayofmonth'] = df['date'].dt.day
df['month'] = df['date'].dt.month
df['year'] = df['date'].dt.year
df['weekofyear'] = df['date'].dt.isocalendar().week.astype(int)
df['is_weekend'] = df['dayofweek'].isin([5, 6]).astype(int)
df['is_month_start'] = df['date'].dt.is_month_start.astype(int)
df['is_month_end'] = df['date'].dt.is_month_end.astype(int)
df['quarter'] = df['date'].dt.quarter
# 給料日フラグ(15日と月末を給料日と仮定)
df['is_payday'] = ((df['dayofmonth'] == 15) | df['is_month_end']).astype(int)
return df
4. 祝日特徴量
def create_holiday_features(df, holidays):
"""祝日関連特徴量の作成"""
national = holidays[holidays['locale'] == 'National']['date'].unique()
df['is_holiday'] = df['date'].isin(national).astype(int)
df['is_holiday_eve'] = df['date'].isin(national - pd.Timedelta(days=1)).astype(int)
df['is_holiday_after'] = df['date'].isin(national + pd.Timedelta(days=1)).astype(int)
# 次の祝日までの日数
def days_to_next_holiday(date):
future = national[national > date]
if len(future) > 0:
return (future.min() - date).days
return 999
df['days_to_holiday'] = df['date'].apply(days_to_next_holiday)
return df
5. 石油価格特徴量
def create_oil_features(df, oil):
"""石油価格特徴量の作成"""
oil_filled = oil.set_index('date')['dcoilwtico'].interpolate().reset_index()
oil_filled.columns = ['date', 'oil_price']
df = df.merge(oil_filled, on='date', how='left')
df['oil_price'] = df['oil_price'].interpolate()
df['oil_ma_7'] = df['oil_price'].rolling(7).mean()
df['oil_ma_30'] = df['oil_price'].rolling(30).mean()
df['oil_diff_7'] = df['oil_price'].diff(7)
df['oil_pct_change_7'] = df['oil_price'].pct_change(7)
return df
LightGBMモデルの構築
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
# 特徴量の統合
feature_cols = [
# ラグ特徴量
'lag_1', 'lag_7', 'lag_14', 'lag_28',
# ローリング統計量
'rolling_mean_7', 'rolling_mean_28', 'rolling_std_7',
# カレンダー
'dayofweek', 'month', 'dayofmonth', 'weekofyear',
'is_weekend', 'is_month_start', 'is_month_end',
# 祝日
'is_holiday', 'is_holiday_eve', 'days_to_holiday',
# 石油
'oil_price', 'oil_ma_7', 'oil_diff_7',
# プロモーション
'onpromotion',
# 店舗情報
'store_type', 'cluster',
]
# 時系列分割(未来のデータで学習しない)
train_mask = df['date'] < '2017-08-01'
val_mask = (df['date'] >= '2017-08-01') & (df['date'] <= '2017-08-15')
X_train = df.loc[train_mask, feature_cols]
y_train = df.loc[train_mask, 'sales']
X_val = df.loc[val_mask, feature_cols]
y_val = df.loc[val_mask, 'sales']
# LightGBMパラメータ
params = {
'objective': 'tweedie', # ゼロ膨張データに適した損失関数
'tweedie_variance_power': 1.5,
'metric': 'rmse',
'learning_rate': 0.05,
'num_leaves': 127,
'max_depth': -1,
'min_child_samples': 20,
'feature_fraction': 0.8,
'bagging_fraction': 0.8,
'bagging_freq': 1,
'lambda_l1': 0.1,
'lambda_l2': 0.1,
'verbose': -1,
}
# 学習
train_set = lgb.Dataset(X_train, y_train)
val_set = lgb.Dataset(X_val, y_val, reference=train_set)
model = lgb.train(
params,
train_set,
num_boost_round=1000,
valid_sets=[train_set, val_set],
callbacks=[
lgb.early_stopping(50),
lgb.log_evaluation(100)
],
)
特徴量重要度の分析
# 特徴量重要度
importance = pd.DataFrame({
'feature': feature_cols,
'importance': model.feature_importance(importance_type='gain')
}).sort_values('importance', ascending=False)
print("特徴量重要度 Top 10:")
print(importance.head(10))
# 可視化
lgb.plot_importance(model, max_num_features=15, importance_type='gain', figsize=(10, 8))
plt.title('特徴量重要度(Gain)')
plt.tight_layout()
plt.show()
情報漏洩の防止
時系列MLでは情報漏洩(Data Leakage)に特に注意が必要。
| 漏洩パターン | 説明 | 対策 |
|---|---|---|
| 未来のラグ | shift(0)やshift(-1)を使う | shift(1)以上のみ使用 |
| ローリングの漏洩 | ローリング計算に当日を含む | shift(1)してからrolling |
| バリデーション漏洩 | ランダム分割でバリデーション | 時系列分割(TimeSeriesSplit)を使用 |
| ターゲットエンコーディング | バリデーションデータのターゲットを使う | 学習データのみで計算 |
まとめ
| 項目 | ポイント |
|---|---|
| テーブル変換 | 時系列 → ラグ/ローリング/カレンダーで特徴量化 |
| 主要特徴量 | ラグ、ローリング統計量、カレンダー、祝日、石油価格 |
| LightGBM | tweedie損失、ゼロ膨張データに強い |
| 情報漏洩 | shift(1)、時系列分割で防止 |
チェックリスト
- 時系列データをテーブルデータに変換できる
- 5種類の特徴量エンジニアリングを実装できる
- LightGBMで時系列予測モデルを構築できる
- 情報漏洩のリスクを理解し防止策を実装できる
次のステップへ
LightGBMで強力な予測モデルを構築した。次はアンサンブル予測で、複数モデルの予測を組み合わせてさらなる精度向上を目指そう。
推定読了時間: 30分