時系列パート(基礎編:特徴量エンジニアリング)#

ここでは,カフェの顧客データ(cafe_customers.csv)を使用して,機械学習の精度を向上させるための特徴量の作成(特徴量エンジニアリング)について学習します.

必要なライブラリのインポート#

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.feature_selection import mutual_info_regression

# 警告メッセージを非表示にするライブラリ・設定
import warnings
warnings.filterwarnings('ignore')

データの読み込みと前処理#

データセットについて#

時系列予測では,これらの特性を特徴量として抽出することで,モデルの予測精度を大幅に向上させることができます.

# データの読み込み
df = pd.read_csv('../data/raw/cafe_customers.csv')

# Timestampをdatetime型に変換
df['Timestamp'] = pd.to_datetime(df['Timestamp'])
print(df.head())

基本的な時系列特徴量の作成#

時間特徴量の重要性#

時系列データにおいて,時間そのものが強力な予測因子となります.人間の行動には規則的なパターンがあり,それを特徴量として抽出することで予測精度を向上させることができます.

主要な時間特徴量の種類:

  1. 基本時間情報

    • hour: 時間帯による顧客数の変化(朝・昼・夕方・夜の違い)

    • day_of_week: 曜日による違い(平日 vs 週末)

    • month: 季節性(夏休み,年末年始など)

  2. カテゴリカル特徴量

    • is_weekend: 週末フラグ(0: 平日, 1: 週末)

    • is_business_hour: 営業時間フラグ

    • is_lunch_time: ランチタイムフラグ

これらの特徴量により,「金曜日の夕方は顧客数が多い」「月曜日の朝は少ない」といったパターンを機械学習モデルが学習できるようになります.

def create_basic_time_features(df):
    """基本的な時間特徴量を作成する関数"""
    # コピーを作成
    df = df.copy()

    # 時間関連の特徴量
    df['hour']         = df['Timestamp'].dt.hour               # 0-23
    df['day_of_week']  = df['Timestamp'].dt.dayofweek          # 0-6 (月曜日=0, 日曜日=6)
    df['day_of_month'] = df['Timestamp'].dt.day                # 1-31
    df['day_of_year']  = df['Timestamp'].dt.dayofyear          # 1-365 (閏年は366)
    df['week_of_year'] = df['Timestamp'].dt.isocalendar().week # 1-52
    df['month']        = df['Timestamp'].dt.month              # 1-12
    df['quarter']      = df['Timestamp'].dt.quarter            # 1-4
    
    # カテゴリカル特徴量
    df['is_weekend']       = (df['day_of_week'] >= 5).astype(int)
    df['is_business_hour'] = ((df['hour'] >= 7) & (df['hour'] <= 21)).astype(int)
    df['is_lunch_time']    = ((df['hour'] >= 11) & (df['hour'] <= 14)).astype(int)
    df['is_dinner_time']   = ((df['hour'] >= 17) & (df['hour'] <= 20)).astype(int)
    
    return df
# 基本的な時間特徴量の作成
feature_df = create_basic_time_features(df)
feature_df.head()

ラグ特徴量の作成#

ラグ特徴量とは?#

ラグ特徴量(Lag Features)は,過去の値を現在の予測に使用する重要な時系列特徴量です.

基本概念:

  • lag_1h: 1時間前の顧客数

  • lag_24h: 24時間前(1日前)の顧客数

  • lag_168h: 168時間前(1週間前)の顧客数

なぜラグ特徴量が重要なのか?

  1. 時系列の自己相関: 過去の値と現在の値には相関がある

  2. 短期トレンドの捕捉: 直前の値から短期的な変化を予測

  3. 周期性の活用: 1日前,1週間前の同じ時間帯の値は参考になる

# ラグ特徴量を作成
def create_lag_features(df, target_col='Customers', lags=[1, 2, 3, 24, 48, 168]):
    """ラグ特徴量を作成する関数"""
    # コピーを作成
    df = df.copy()
    
    # ラグ特徴量の作成(1h前,2h前,...,168h前)
    for lag in lags:
        df[f'{target_col}_lag_{lag}h'] = df[target_col].shift(lag)
    
    return df
# ラグ特徴量の作成
feature_df = create_lag_features(feature_df)
feature_df.head()

移動平均・統計特徴量の作成#

移動平均・統計特徴量の意義#

移動平均(Moving Average)は時系列データのノイズを除去し,トレンドを明確化する重要な手法です.

主要な統計特徴量:

  1. 移動平均 (MA)

    • 過去N時間の平均値

    • 短期的な変動を平滑化

    • トレンドの方向性を把握

  2. 移動標準偏差 (STD)

    • 過去N時間のばらつき

    • データの変動性・不安定性を測定

    • 異常値検出にも活用

  3. 移動最大値・最小値

    • 過去N時間の極値

    • レンジ(変動幅)の把握

  4. 比率特徴量

    • 現在値 / 移動平均: 現在が平均より高いか低いか

    • 1.0より大きい場合は平均以上,小さい場合は平均以下

ウィンドウサイズの選択:

  • 短期(3〜6時間): 直近のトレンド変化を捕捉

  • 中期(12〜24時間): 日内パターンの平滑化

  • 長期(168時間): 週次トレンドの把握

これらの特徴量により,「現在の顧客数が過去24時間の平均と比較してどの程度なのか」といった相対的な情報をモデルが学習できます.

# 移動平均・統計特徴量を作成
def create_rolling_features(df, target_col='Customers', windows=[3, 6, 12, 24, 168]):
    """移動平均・統計特徴量を作成する関数"""
    # コピーを作成
    df = df.copy()
    
    # 各ウィンドウサイズに対して特徴量を作成(3h, 6h, 12h, 24h, 168h)
    for window in windows:
        # 移動平均
        df[f'{target_col}_ma_{window}h'] = df[target_col].rolling(window=window).mean()
        
        # 移動標準偏差
        df[f'{target_col}_std_{window}h'] = df[target_col].rolling(window=window).std()
        
        # 移動最大値・最小値
        df[f'{target_col}_max_{window}h'] = df[target_col].rolling(window=window).max()
        df[f'{target_col}_min_{window}h'] = df[target_col].rolling(window=window).min()
        
        # 現在値と移動平均の比率
        df[f'{target_col}_ratio_ma_{window}h'] = df[target_col] / (df[f'{target_col}_ma_{window}h'] + 1e-8)
    
    return df
# 移動統計特徴量の作成
feature_df = create_rolling_features(feature_df)
feature_df.head()

周期性特徴量(sin/cos変換)#

なぜsin/cos変換が必要なのか?#

問題:カテゴリカル特徴量の課題

  • 時刻「23時」と「0時」は隣接しているが,数値的には23と0で大きく離れている

  • 12月と1月も同様の問題が発生

解決:三角関数による周期性の表現

周期的な特徴量を以下の式で変換します:

  • \(\sin\left(\frac{2\pi \times \text{value}}{\text{period}}\right)\)

  • \(\cos\left(\frac{2\pi \times \text{value}}{\text{period}}\right)\)

具体例(時刻の場合):

  • \(\text{hour\_sin} = \sin\left(\frac{2\pi \times \text{hour}}{24}\right)\)

  • \(\text{hour\_cos} = \cos\left(\frac{2\pi \times \text{hour}}{24}\right)\)

メリット:

  1. 連続性の確保: 23時と0時が数学的に近い値を持つ

  2. 周期性の保持: 24時間,7日間,12ヶ月の周期を正確に表現

  3. 機械学習アルゴリズムとの親和性: 線形関係として扱える

# 周期性特徴量を作成
def create_cyclical_features(df):
    """周期性特徴量を作成する関数"""
    # コピーを作成
    df = df.copy()
    
    # 時間の周期性 (0-23)
    df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
    df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
    
    # 曜日の周期性 (0-6)
    df['dow_sin'] = np.sin(2 * np.pi * df['day_of_week'] / 7)
    df['dow_cos'] = np.cos(2 * np.pi * df['day_of_week'] / 7)
    
    # 月の周期性 (1-12)
    df['month_sin'] = np.sin(2 * np.pi * (df['month'] - 1) / 12)
    df['month_cos'] = np.cos(2 * np.pi * (df['month'] - 1) / 12)
    
    # 日の周期性 (1-31)
    df['day_sin'] = np.sin(2 * np.pi * (df['day_of_month'] - 1) / 31)
    df['day_cos'] = np.cos(2 * np.pi * (df['day_of_month'] - 1) / 31)
    
    return df
# 周期性特徴量の作成
feature_df = create_cyclical_features(feature_df)
feature_df.head()

下のグラフを見ると,sin-cos変換により時刻が円形に配置され,隣接する時刻が近い位置に来ることが分かります.

# 可視化(散布図)
plt.figure(figsize=(15, 5))

# Hour vs Hour_sin
plt.subplot(1, 3, 1)
plt.scatter(feature_df['hour'], feature_df['hour_sin'], alpha=0.6, s=20)
plt.title('Hour vs Hour_sin')
plt.xlabel('Hour')
plt.ylabel('Hour_sin')

# Hour vs Hour_cos
plt.subplot(1, 3, 2)
plt.scatter(feature_df['hour'], feature_df['hour_cos'], alpha=0.6, s=20)
plt.title('Hour vs Hour_cos')
plt.xlabel('Hour')
plt.ylabel('Hour_cos')

# Hour_sin vs Hour_cos (circular)
plt.subplot(1, 3, 3)
plt.scatter(feature_df['hour_sin'], feature_df['hour_cos'], alpha=0.6, s=20)
plt.title('Hour_sin vs Hour_cos (circular)')
plt.xlabel('Hour_sin')
plt.ylabel('Hour_cos')

plt.tight_layout()
plt.show()

差分・トレンド特徴量の作成#

差分特徴量とトレンド除去#

差分特徴量は時系列データの変化量に着目した重要な特徴量です.

主要な差分特徴量:

  1. 1次差分 (diff)

    • \(\text{diff}_t = \text{value}_t - \text{value}_{t-1}\)

    • 直前との変化量(増加・減少の大きさ)

  2. 変化率 (pct_change)

    • \(\text{pct\_change}_t = \frac{\text{value}_t - \text{value}_{t-1}}{\text{value}_{t-1}}\)

    • 変化の割合(相対的な変化)

  3. 期間別差分

    • diff_24h: 24時間前との差(同じ時間帯での日次変化)

    • diff_168h: 168時間前との差(同じ曜日・時間帯での週次変化)

なぜ差分が重要なのか?

  1. トレンド除去: 長期的な増減傾向を取り除く

  2. 定常性の向上: 統計的性質が安定化(平均・分散が一定)

  3. 変化の検出: 急激な増減を敏感に捉える

  4. 異常検知: 通常とは異なる変化パターンを発見

トレンド特徴量は移動平均の変化率を表し,データの方向性(上昇・下降・横ばい)を定量化します.

# 差分・トレンド特徴量を作成
def create_diff_trend_features(df, target_col='Customers'):
    """差分・トレンド特徴量を作成する関数"""
    # コピーを作成
    df = df.copy()
    
    # 1次差分(前の時点との差)
    df[f'{target_col}_diff_1h'] = df[target_col].diff(1)
    df[f'{target_col}_diff_24h'] = df[target_col].diff(24)    # 1日前との差
    df[f'{target_col}_diff_168h'] = df[target_col].diff(168)  # 1週間前との差
    
    # 変化率
    df[f'{target_col}_pct_change_1h'] = df[target_col].pct_change(1)
    df[f'{target_col}_pct_change_24h'] = df[target_col].pct_change(24)
    
    # トレンド特徴量(移動平均の勾配)
    for window in [6, 12, 24]:
        ma_col = f'{target_col}_ma_{window}h'
        if ma_col in df.columns:
            df[f'{target_col}_trend_{window}h'] = df[ma_col].diff(1)
    
    return df
# 差分・トレンド特徴量の作成
feature_df = create_diff_trend_features(feature_df)
feature_df.head()

下のグラフでは,元データの変動が差分によってどのように変化量として表現されるかを確認できます.

# 可視化(時系列)
plt.figure(figsize=(15, 8))

# Original
plt.subplot(2, 2, 1)
plt.plot(feature_df['Customers'].iloc[24:168], alpha=0.7, label='Original')
plt.title('Original Customers Values')
plt.legend()

# 1-hour Difference
plt.subplot(2, 2, 2)
plt.plot(feature_df['Customers_diff_1h'].iloc[24:168], alpha=0.7, label='1h Diff', color='red')
plt.title('1-hour Difference')
plt.legend()

# 24-hour Difference
plt.subplot(2, 2, 3)
plt.plot(feature_df['Customers_diff_24h'].iloc[24:168], alpha=0.7, label='24h Diff', color='green')
plt.title('24-hour Difference')
plt.legend()

# 24-hour Percent Change
plt.subplot(2, 2, 4)
plt.plot(feature_df['Customers_pct_change_24h'].iloc[24:168], alpha=0.7, label='24h % Change', color='purple')
plt.title('24-hour Percent Change')
plt.legend()

plt.tight_layout()
plt.show()

カレンダー特徴量の作成#

カレンダー特徴量の実用価値#

カレンダー特徴量ビジネス上の意味を持つ特徴量で,実際の顧客行動パターンを反映します.

基本パターン特徴量:

  1. 曜日パターン

    • is_monday: 月曜日の憂鬱効果

    • is_friday: 週末前の解放感

    • is_weekend: 週末の行動パターン

  2. 時間帯パターン

    • is_morning: 朝の通勤・通学時間帯

    • is_lunch_time: 昼食時間帯の混雑

    • is_evening: 夕方の帰宅時間帯

  3. 月内パターン

    • is_month_start: 給料日後の消費活発期

    • is_month_end: 月末の節約モード

これらの特徴量により,人間の生活リズムに基づいた予測が可能になります.

# カレンダー特徴量を作成
def create_calendar_features(df):
    """カレンダー特徴量を作成する関数"""
    # コピーを作成
    df = df.copy()
    
    # 曜日パターン特徴量
    df['is_monday']   = (df['day_of_week'] == 0).astype(int) # 月曜日
    df['is_friday']   = (df['day_of_week'] == 4).astype(int) # 金曜日
    df['is_saturday'] = (df['day_of_week'] == 5).astype(int) # 土曜日
    df['is_sunday']   = (df['day_of_week'] == 6).astype(int) # 日曜日
    
    # 月初・月末
    df['is_month_start'] = (df['day_of_month'] <= 3).astype(int)  # 月初(1-3日)
    df['is_month_end']   = (df['day_of_month'] >= 28).astype(int) # 月末(28-31日)
    
    # 時間帯パターン
    df['is_morning']   = ((df['hour'] >= 6) & (df['hour'] <= 11)).astype(int)  # 朝(6-11時)
    df['is_afternoon'] = ((df['hour'] >= 12) & (df['hour'] <= 17)).astype(int) # 昼(12-17時)
    df['is_evening']   = ((df['hour'] >= 18) & (df['hour'] <= 22)).astype(int) # 夕方(18-22時)
    df['is_night']     = ((df['hour'] >= 23) | (df['hour'] <= 5)).astype(int)  # 夜(23-5時)
    
    # 複合特徴量
    df['weekend_evening'] = (df['is_weekend'] * df['is_evening']).astype(int)          # 週末の夕方
    df['weekday_lunch']   = ((1 - df['is_weekend']) * df['is_lunch_time']).astype(int) # 平日のランチタイム
    
    return df
# カレンダー特徴量を追加
feature_df = create_calendar_features(feature_df)
feature_df.head()

特徴量の重要度評価#

特徴量重要度評価の意義#

作成した多数の特徴量の中から最も予測に有効な特徴量を特定することは,機械学習の成功に不可欠です.

特徴量選択の重要性:

  1. 計算効率の向上: 不要な特徴量を除去

  2. 過学習の防止: 特徴量が多すぎると汎化性能が低下

  3. 解釈性の向上: 重要な特徴量に焦点を当てた分析

  4. メモリ使用量の削減: 大規模データでの実用性向上

前処理の必要性:

  • 無限大値の処理: 計算エラーを防ぐためNaNに変換

  • 欠損値の除去: 正確な重要度計算のため

  • 標準化: 異なるスケールの特徴量を公平に比較

# 特徴量の重要度評価
eval_df = feature_df.copy()

# データの前処理
print("前処理前: " + str(eval_df.shape))

eval_df = eval_df.replace([np.inf, -np.inf], np.nan) # 無限大値をNaNに置換
eval_df = eval_df.dropna()                           # NaNを含む行を削除

print("前処理後: " + str(eval_df.shape))
# 特徴量(説明変数)とターゲット(目的変数)を分離
feature_cols = [col for col in eval_df.columns if col not in ['Timestamp', 'Customers']]
X = eval_df[feature_cols] # 説明変数
y = eval_df['Customers']  # 目的変数
# 特徴量を標準化
X_scaled = StandardScaler().fit_transform(X)

相互情報量(Mutual Information)とは?

相互情報量は,ある特徴量が目的変数(顧客数)の不確実性をどの程度減少させるかを測定する指標です.

  • 高い値: その特徴量は予測に非常に有用

  • 低い値: その特徴量の予測貢献度は低い

  • 0: その特徴量は予測に全く寄与しない

数式: $\(MI(X;Y) = \sum_{x,y} p(x,y) \log\frac{p(x,y)}{p(x)p(y)}\)$

# 相互情報量による特徴量重要度計算
mi_scores = mutual_info_regression(X_scaled, y, random_state=42)

# 結果をDataFrameに整理(重要度の高い順にソート)
feature_importance = pd.DataFrame({'feature': feature_cols, 'importance': mi_scores}).sort_values('importance', ascending=False)
feature_importance.head()

下のグラフから,どの特徴量が予測に最も寄与するかを確認し,今後のモデル構築に活用しましょう.

# 可視化(棒グラフ)
plt.figure(figsize=(15, 6))
plt.barh(range(len(feature_importance)), feature_importance['importance'])
plt.yticks(range(len(feature_importance)), feature_importance['feature'])
plt.xlabel('Mutual Information Score')
plt.title('Feature Importance (Mutual Information)')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()
# 可視化(棒グラフ)- Top 15
top_features = feature_importance.head(15)
plt.figure(figsize=(15, 6))
plt.barh(range(len(top_features)), top_features['importance'])
plt.yticks(range(len(top_features)), top_features['feature'])
plt.xlabel('Mutual Information Score')
plt.title('Feature Importance (Mutual Information)')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.show()

CSVファイルの保存#

eval_df.to_csv('../data/processed/feature_engineered_data.csv', index=False)