LESSON

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)を使用
ターゲットエンコーディングバリデーションデータのターゲットを使う学習データのみで計算

まとめ

項目ポイント
テーブル変換時系列 → ラグ/ローリング/カレンダーで特徴量化
主要特徴量ラグ、ローリング統計量、カレンダー、祝日、石油価格
LightGBMtweedie損失、ゼロ膨張データに強い
情報漏洩shift(1)、時系列分割で防止

チェックリスト

  • 時系列データをテーブルデータに変換できる
  • 5種類の特徴量エンジニアリングを実装できる
  • LightGBMで時系列予測モデルを構築できる
  • 情報漏洩のリスクを理解し防止策を実装できる

次のステップへ

LightGBMで強力な予測モデルを構築した。次はアンサンブル予測で、複数モデルの予測を組み合わせてさらなる精度向上を目指そう。

推定読了時間: 30分