時系列予測の精度が頭打ちになったとき、皆さんはどうアプローチしますか? 複雑な最新アルゴリズムに飛びつく前に、まずはデータそのものに含まれる情報量を増やすことを検討してみてください。AIモデルが学習する源泉は、結局のところデータに他ならないからです。
時系列データには特有の「文脈」が存在します。昨日の売上が明日にどう影響するか、週末の傾向がどう繰り返されるか、季節の変わり目がどう作用するか。これらの文脈を、生のタイムスタンプや単なる数値の羅列からモデルが自力で学習するのは困難です。
今回はPandasを使い、データに眠る「時間の文脈」を掘り起こし、モデルが理解できる形に翻訳する時系列特徴量エンジニアリングについて解説します。理論だけでなく「実際にどう動くか」を重視し、統計的・数理的な背景(Why)を理解した上で、実務ですぐに試せるコード(How)へと落とし込んでいきましょう。仮説を即座に形にして検証するプロセスを一緒に体験してみてください。
なぜ「アルゴリズム」より「特徴量」が重要なのか
AI開発の現場において「Garbage In, Garbage Out(ゴミを入れればゴミが出てくる)」は絶対的な原則ですが、時系列解析ではその重みがさらに増します。ビジネスの現場で直面する予測精度のボトルネックは、最新のアルゴリズムの欠如ではなく、入力データの表現力不足にあることが大半です。モデルは、データとして与えられていない情報を勝手に学習してはくれません。
時系列データにおける「情報の文脈」とは
一般的な表形式データ(Tabular Data)は各行が独立している(独立同分布)前提で処理されますが、時系列データには「順序」という不可逆な構造が存在し、過去から未来へ流れる因果の連鎖が含まれます。
例えば商品の需要予測で「気温」や「価格」という瞬間の状態だけを入力しても、モデルは背後にある「流れ」を理解できません。「気温が上がり続けている局面での25度」と「下がり続けている局面での25度」では、消費者の購買行動は全く異なる可能性があります。
この「文脈」を明示的な数値としてデータフレームに追加する作業が、時系列における特徴量エンジニアリングの本質です。ディープラーニング分野では、RNN(リカレントニューラルネットワーク)からLSTMやGRUへ発展し、現在は並列処理に優れたTransformerアーキテクチャが主流です。Hugging FaceのTransformersライブラリもPyTorch中心に最適化が進み、エコシステム全体で進化を遂げています。
しかし、最新アーキテクチャが文脈を自動獲得できる現代でも、明示的な特徴量(Hand-crafted Features)としてドメイン知識を与えた方が、学習の収束が早く予測が安定し、結果の解釈性が高まるケースは珍しくありません。データ量が限られる実務現場では、特徴量による的確なサポートが不可欠です。
ディープラーニング全盛時代でも手動特徴量が生きる理由
高度なディープラーニングモデルを導入しても、必ず予測精度が飛躍的に向上するわけではありません。モデル構造を複雑にするより、シンプルな勾配ブースティング木(GBDT)などのアルゴリズムに適切に設計された特徴量を追加する方が効果的な場合があります。
例えば、時系列データに「過去3日間の移動平均」や「前年同月のトレンド乖離率」といった特徴量を加えるだけで、予測精度(RMSEなどの指標)が大幅に改善することは多くのプロジェクトで報告されています。これはドメイン知識を特徴量としてモデルに「注入」するプロセスです。モデルに「ゼロからすべてのパターンを見つけ出せ」と強要せず、「ここに着目すべき」という明確なヒントを与えることが、ビジネスへの最短距離を描く実用的なAI開発では極めて重要です。
Pandasが可能にする「試行錯誤の高速化」
特徴量エンジニアリングは、一度の設計で完璧な正解が出るものではありません。「まず動くものを作る」というアジャイルな思考で仮説を立て、新しい特徴量を実装し、その有効性を即座に検証する。この反復サイクルのスピードこそが、プロジェクトを成功へ導く鍵となります。
Pandasはこの試行錯誤を高速に回す強力なツールです。時系列データ操作において直感的かつ高速なメソッド群を豊富に提供しています。SQLの複雑なウィンドウ関数やPythonの標準ループよりも、Pandasのベクトル化演算や組み込みの時系列関数を活用することで、効率的に「時間の文脈」を数値化できます。
ここからは、Pandasの機能を最大限に活用し、予測モデルの性能を引き上げる具体的なアプローチを深掘りしていきましょう。
ラグ特徴量(Lag Features):過去と現在の因果を結ぶ
時系列予測で最も基本的かつ強力な特徴量が「ラグ(Lag)」です。これは「過去の値を現在の行に持ってくる」操作であり、「昨日の売上は、今日の売上の予測因子になる」という考え方を数値化します。
自己相関(Autocorrelation)のメカニズムと視覚化
闇雲に「1日前」「2日前」「3日前」と全ての特徴量を作るとデータ量が爆発し、次元の呪い(Curse of Dimensionality)に陥ります。ここで重要なのが「自己相関(Autocorrelation)」の概念です。
自己相関とは、時系列データが自分自身の過去の値とどれくらい相関しているかを示す指標です。
- ACF(自己相関関数): 直接的な相関も含めた全体的な相関。
- PACF(偏自己相関関数): 中間のラグの影響を取り除いた、純粋な2点間の相関。
例えば「今日の売上」が「昨日の売上」と相関し、「昨日の売上」が「一昨日の売上」と相関する場合、ACFでは「一昨日の売上」も「今日の売上」と強く相関して見えます。しかしPACFを見れば、その相関が「昨日」を経由した間接的なものか分かります。
まずはPandasとStatsmodelsを使って可視化し、データの性質を掴むことから始めましょう。
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
# サンプルデータの読み込み(例: 日次売上データ)
# df = pd.read_csv('sales_data.csv', parse_dates=['date'], index_col='date')
# 自己相関と偏自己相関のプロット
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
plot_acf(df['sales'], lags=40, ax=ax1)
plot_pacf(df['sales'], lags=40, ax=ax2)
plt.show()
プロットを見て、信頼区間から飛び出しているラグこそがモデルに教えるべき「意味のある過去」です。例えば7日前のラグで強い相関が出れば、週次(Weekly)の周期性が示唆されます。
単なるshiftでは不十分?適切なラグ幅の選定戦略
Pandasでの実装は shift() 関数を使います。
# 基本的なラグ生成
df['lag_1'] = df['sales'].shift(1)
df['lag_7'] = df['sales'].shift(7)
ここで注意すべきは、予測のホライズン(期間)です。
「明日」を予測するなら shift(1) は有効ですが、「1週間後」を予測する場合、推論時に「明日の実績値」は存在しません。この場合、使える最新情報は「今日」の値であり、1週間後にとっては shift(7) に相当します。
これをDirect Strategy(各時点ごとにモデルを作る)で行うか、Recursive Strategy(予測値を次の入力に使う)で行うかで設計は変わりますが、「推論時に手に入らないデータを使って学習させてはいけない(リーク)」という鉄則を忘れないでください。
Pandasによる効率的なラグ生成とメモリ管理
実務では数十個のラグ特徴量を生成することもあります。ループよりも辞書内包表記と pd.concat を使う方が高速で可読性も高くなります。
# 複数のラグをまとめて生成
lags = [1, 7, 14, 28, 365]
new_features = {f'lag_{lag}': df['sales'].shift(lag) for lag in lags}
# 元のデータフレームに結合
df = pd.concat([df, pd.DataFrame(new_features, index=df.index)], axis=1)
また、データフレームの肥大化を防ぐためデータ型(dtype)に注意しましょう。デフォルトの float64 はメモリを大量に消費しますが、多くのビジネスデータでは float32 で十分な精度が保てます。
# メモリ最適化
for col in df.columns:
if 'lag_' in col:
df[col] = df[col].astype('float32')
周期性特徴量(Cyclical Features):時間の「円環」を表現する
次は時間そのものの扱い方です。多くのデータセットには「月」「日」「曜日」「時間」といったタイムスタンプが含まれています。
DatetimeIndexからの基本情報抽出(曜日、月、時間)
Pandasの dt アクセサを使えば、これらの情報は簡単に抽出できます。
df['month'] = df.index.month
df['hour'] = df.index.hour
しかし、この数値をそのままモデル(特に決定木ベース以外のモデルや、距離ベースのモデル)に入れる際は注意が必要です。皆さんは、この数値がモデルにどう解釈されるか想像できますか?
「12月の次は1月」をモデルに教える:三角関数変換(Sin/Cos)
数値として「12」と「1」は離れていますが、時間的な意味での「12月」と「1月」は隣り合っています。同様に「23時」と「0時」も隣同士です。数値をそのまま使うと、モデルは「23時と0時は非常に遠い関係だ」と誤解して学習する可能性があります。
これを解決するのが、時間を円上の座標として表現するサイクリックエンコーディング(Cyclical Encoding)です。三角関数(Sin/Cos)を使い、時間を2次元の座標に変換します。
import numpy as np
# 時間(0-23)の周期性変換
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
# 月(1-12)の周期性変換
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
この変換により、23時(sin≈-0.26, cos≈0.96)と0時(sin=0, cos=1)の距離が近くなり、時間の連続性を保ったまま特徴量化できます。これはニューラルネットワークやk-近傍法などを用いる場合に特に効果を発揮します。
祝日フラグとイベント効果の取り込み方
周期性だけでなく「特異日」の扱いも重要です。特に日本市場を対象とする場合、ゴールデンウィークや年末年始、不定期な祝日の影響は大きいです。
よく利用されるのは jpholiday ライブラリとPandasの連携です。
import jpholiday
# 日付インデックスを用いて祝日フラグを作成
df['is_holiday'] = df.index.to_series().map(jpholiday.is_holiday).astype(int)
# 祝日の前日フラグ(駆け込み需要などの予測に有効)
df['is_day_before_holiday'] = df['is_holiday'].shift(-1).fillna(0).astype(int)
このように、カレンダー上の事実だけでなく「祝日の前日」のような人間の行動パターンに紐づく特徴量を作成することが重要です。
ローリング・ウィンドウ(Rolling Window):局所的なトレンドを捉える
ラグ特徴量が「点」の情報だとすれば、ローリングウィンドウ集計は「面(期間)」の情報です。過去一定期間の要約統計量を計算し、データのトレンドやボラティリティ(変動の激しさ)を捉えます。
移動平均と移動分散が語る「安定性」と「変化」
Pandasの rolling() メソッドは非常に強力です。
- 移動平均(Rolling Mean): 平滑化されたトレンドを示します。ノイズを除去し、ベースラインの動きをモデルに伝えます。
- 移動標準偏差(Rolling Std): データのばらつき具合を示します。予測の不確実性が高まっている時期の特定に役立ちます。
# 7日間の移動平均
df['rolling_mean_7'] = df['sales'].shift(1).rolling(window=7).mean()
# 30日間の移動標準偏差
df['rolling_std_30'] = df['sales'].shift(1).rolling(window=30).std()
Leakage(リーク)の罠:未来の情報を混ぜないための鉄則
ここで最も重要なポイントを強調します。上記のコードでは意図的に .shift(1) を挟んでいます。
# 悪い例(リーク発生!)
# 今日の売上を含んで平均を計算し、それを今日の特徴量にしてしまう
df['rolling_mean_7_leak'] = df['sales'].rolling(window=7).mean()
もし shift(1) を忘れて rolling() を適用すると、計算される窓の中に「予測対象である当日の値(target)」が含まれてしまいます。これを学習データとして使うと、モデルは「答え(当日の値)」の一部をカンニングして学習することになります。
結果として学習時の精度は高くなる可能性がありますが、実運用では使い物にならないモデルが出来上がります。これがData Leakage(データリーク)です。
必ず「過去のデータのみ」を使って現在の特徴量を作るよう注意してください。
過去の情報を要約する:Min/Max/Meanの活用
平均や分散だけでなく、最小値(Min)や最大値(Max)も有効です。例えばサーバーの負荷予測などで「過去24時間でのピーク負荷」は、次のピークを予測する上で重要な情報になります。
# 過去ウィンドウ内の最大値と最小値の差(レンジ)
window = 24
rolled = df['sales'].shift(1).rolling(window=window)
df['rolling_max'] = rolled.max()
df['rolling_min'] = rolled.min()
df['rolling_range'] = df['rolling_max'] - df['rolling_min']
実装戦略:Pandasパイプラインによる特徴量生成の自動化
ここまで紹介した手法を実際のプロジェクトでどのようにコード管理すべきでしょうか。
Jupyter Notebook上でセルを継ぎ足すコードは実験の再現性を損なう可能性があります。
Pandasの pipe() メソッドを活用し、処理を関数チェーンとして記述するスタイルを推奨します。
カスタム変換器としての関数設計
各処理を独立した関数として定義し、データフレームを受け取ってデータフレームを返すように設計します。
def add_lag_features(df, lags, col_name='sales'):
df_out = df.copy()
for lag in lags:
df_out[f'lag_{lag}'] = df_out[col_name].shift(lag)
return df_out
def add_cyclical_features(df):
df_out = df.copy()
df_out['month_sin'] = np.sin(2 * np.pi * df_out.index.month / 12)
df_out['month_cos'] = np.cos(2 * np.pi * df_out.index.month / 12)
return df_out
def add_rolling_features(df, windows, col_name='sales'):
df_out = df.copy()
for window in windows:
# shift(1)でリーク防止
rolled = df_out[col_name].shift(1).rolling(window=window)
df_out[f'rolling_mean_{window}'] = rolled.mean()
df_out[f'rolling_std_{window}'] = rolled.std()
return df_out
実務コード例:生データから学習用データセットまで
これらをパイプラインとして繋ぎ合わせることで、前処理の流れが一目瞭然になります。
# パイプラインの実行
df_features = (
raw_data
.pipe(add_lag_features, lags=[1, 7, 14])
.pipe(add_cyclical_features)
.pipe(add_rolling_features, windows=[7, 28])
.dropna() # ラグ生成で発生したNaNを除去
)
このアプローチの利点は、機能の追加・削除が容易でありテストがしやすいことです。各関数に単体テストを書くことで、複雑になりがちな特徴量生成ロジックの品質を担保できます。
まとめ
時系列予測における「精度」は、アルゴリズムだけでなくデータの理解とそれを表現する特徴量エンジニアリングによってもたらされます。
今回解説した3つのポイントを振り返りましょう。
- ラグ特徴量: 過去と現在の直接的な因果関係を捉える。
- 周期性特徴量: 時間の円環構造を数学的に表現する。
- ローリングウィンドウ: 局所的なトレンドやボラティリティを捉える。
これら全てにおいて共通する最重要事項は、「未来の情報をリークさせない」という規律です。
Pandasはこれらの概念をコードに落とし込むツールキットです。データサイエンティストがビジネス現場で何が起きているか、データにどのような周期や因果が潜んでいるかを仮説立てし、Pandasで検証する反復プロセスが重要です。
もしチームが予測精度の向上に苦戦しているなら、あるいは高度なAI導入でビジネスインパクトを創出したいと考えているなら、多くの成功事例やベストプラクティスに目を向けてみるのも良いでしょう。同じ課題をどうデータ活用で解決したか、そのヒントがブレイクスルーにつながる可能性があります。
コメント