はじめに
機械学習モデルの評価において、交差検証(Cross-Validation)は不可欠です。しかし、時系列データに対して通常のK-分割交差検証(K-Fold Cross-Validation)を適用すると、未来の情報を使って過去を予測してしまうという問題が生じ、モデルの性能を正しく評価できません。本記事では、時系列データに適した交差検証手法であるTimeSeriesSplitについて、その重要性とK-Fold Cross-Validationとの違いを、理論と実践の両面から解説します。
対象読者:
- 機械学習エンジニア、データサイエンティスト
- 時系列データの分析に携わる方
- モデルの評価方法について深く理解したい方
記事のポイント:
- 時系列データの特性と、通常の交差検証で生じる問題点を解説
- TimeSeriesSplitの仕組みと利点を説明
- Python (scikit-learn) での実装方法を紹介
- 可視化を通じて、TimeSeriesSplitとK-Fold Cross-Validationの違いを明確化
時系列データの特殊性
時系列データは、以下の特徴を持つため、一般的なデータセットとは異なる取り扱いが必要です。
- 時間的な依存関係 (Temporal Dependency): ある時点のデータが、過去のデータに影響を受ける。
- データの順序性 (Sequential Nature): データの順序に意味があり、入れ替えができない。
- 非定常性 (Non-stationarity): 時間の経過とともに、データの平均や分散などの統計的特性が変化する可能性がある。
これらの特性を持つ時系列データに対し、通常のK-分割交差検証を適用すると、以下のような問題が発生します。
- データリーケージ: 未来のデータを使って過去を予測してしまう。
- 時間的依存関係の無視: データの時間的な依存関係が考慮されず、モデルの性能が過大評価される。
- 非現実的な評価: 現実の状況(過去のデータのみを用いて未来を予測する)と異なる評価となる。
交差検証手法の比較
通常のK-分割交差検証
通常のK-分割交差検証は、データをランダムにK個のフォールド(部分集合)に分割し、そのうち1つを検証データ、残りを訓練データとしてモデルの評価を行います。
しかし、時系列データにおいては、このランダムな分割が問題を引き起こします。
\text{時刻 } t \text{ のデータが } t-1 \text{ のデータに依存する場合:}\\
P(X_t|X_{t-1}) \neq P(X_t)
時系列データは時間的な依存関係があるため、上記のように条件付き確率と周辺確率が等しくなりません。
ランダムに分割すると、モデルは未来のデータを使って過去を予測することが可能となり、結果としてモデルの性能を過大評価してしまいます。
TimeSeriesSplit
TimeSeriesSplitは、時系列データの特性を考慮した交差検証手法であり、以下の特徴を持ちます。
- 時間順序の保持: データを時間順に分割し、常に過去のデータで未来を予測する。
- データリークの防止: 未来のデータが訓練データに含まれることを防ぐ。
- 現実的な評価: 実際の運用状況に近い形でモデルの性能を評価できる。
TimeSeriesSplitでは、データを以下のように分割します。
\text{分割 } i \text{ において:}\\
\text{訓練データ:} [X_1, X_2, ..., X_{t_i}]\\
\text{検証データ:} [X_{t_i+1}, X_{t_i+2}, ..., X_{t_i+k}]
訓練データは常に検証データよりも前の時点のデータで構成されます。これにより、モデルは常に過去の情報のみを用いて未来を予測するという、現実的な状況をシミュレーションできます。
実践的な検証
データ分割の可視化
K-Fold Cross ValidationとTimeSeriesSplitの分割方法の違いを視覚的に確認しましょう。紫が学習用データ。黄色が、検証用データです。
- K-Fold: データがランダムに分割され、時間的な順序が無視されていることがわかります。
- TimeSeriesSplit: 時間的な順序が保たれ、常に過去のデータで未来を予測するように分割されていることがわかります。
予測性能の比較
平均二乗誤差(MSE)を用いて予測性能を比較すると、以下のことがわかります。
- K-Fold: 楽観的な評価結果(実際よりも良い性能)を示す傾向があります。これは、未来のデータを使って過去を予測できるためです。
- TimeSeriesSplit: より現実的な評価結果を示します。
実装のポイント
scikit-learnライブラリを使用すると、TimeSeriesSplitを簡単に実装できます。
from sklearn.model_selection import TimeSeriesSplit
# 5分割の場合
tscv = TimeSeriesSplit(n_splits=5)
# 交差検証の実行
for train_index, test_index in tscv.split(X):
X_train, X_test = X[train_index], X[test_index]
y_train, y_test = y[train_index], y[test_index]
# モデルの学習と評価
まとめ
時系列データの交差検証において、TimeSeriesSplitは以下の点で重要です。
- データの時間的な順序を保持する。
- 未来のデータによるリークを防ぎ、モデルの性能を過大評価しない。
- より現実的な予測性能の評価を可能にする。
特に、金融データ、需要予測、センサーデータなど、時間的依存関係が強いデータを扱う際には、TimeSeriesSplitの使用が強く推奨されます。適切な交差検証手法を選択することは、モデルの信頼性を確保し、実世界での応用につなげるために不可欠です。
コード
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib
from sklearn.model_selection import KFold, TimeSeriesSplit
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
# シード固定
np.random.seed(42)
def generate_time_series_data(n_samples=100):
"""時系列データの生成"""
t = np.linspace(0, 10, n_samples)
# トレンド + 季節性 + ノイズ
trend = 0.5 * t
seasonal = 2 * np.sin(2 * np.pi * t)
noise = np.random.normal(0, 0.5, n_samples)
y = trend + seasonal + noise
return t, y
def create_features(t, y, lookback=3):
"""特徴量とターゲットの作成"""
X, Y = [], []
for i in range(lookback, len(y)):
X.append(y[i-lookback:i])
Y.append(y[i])
return np.array(X), np.array(Y)
def plot_cv_indices(cv, X, ax, n_splits, lw=10):
"""交差検証の分割を可視化"""
for i, (train_idx, val_idx) in enumerate(cv.split(X)):
train = np.zeros(len(X))
train[train_idx] = 1
val = np.zeros(len(X))
val[val_idx] = 2
used_indices = np.concatenate([train_idx, val_idx])
train_plot = train[used_indices]
val_plot = val[used_indices]
ax.scatter(used_indices, [i + .5] * len(used_indices),
c=train_plot, marker='_', lw=lw, label='Training Set')
ax.scatter(used_indices, [i + .5] * len(used_indices),
c=val_plot, marker='_', lw=lw, label='Validation Set')
ax.set_xlabel('Sample Index')
ax.set_ylabel('CV Iteration')
ax.set_title('Data Split Comparison')
ax.set_yticks(np.arange(n_splits) + .5)
ax.set_yticklabels([f'Split {i+1}' for i in range(n_splits)])
# データ生成
t, y = generate_time_series_data()
X, Y = create_features(t, y)
# 交差検証の設定
n_splits = 5
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
tscv = TimeSeriesSplit(n_splits=n_splits)
# 可視化
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10))
# K-Fold
plot_cv_indices(kf, X, ax1, n_splits)
ax1.set_title('K-Fold Cross Validation')
# TimeSeriesSplit
plot_cv_indices(tscv, X, ax2, n_splits)
ax2.set_title('Time Series Split')
plt.tight_layout()
plt.savefig('cv_comparison.png')
plt.close()
# 予測性能の比較
def evaluate_cv(cv, X, y):
mse_scores = []
for train_idx, val_idx in cv.split(X):
X_train, X_val = X[train_idx], X[val_idx]
y_train, y_val = y[train_idx], y[val_idx]
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_val)
mse = mean_squared_error(y_val, y_pred)
mse_scores.append(mse)
return np.mean(mse_scores)
kf_score = evaluate_cv(kf, X, Y)
ts_score = evaluate_cv(tscv, X, Y)
# 結果の可視化
results = pd.DataFrame({
'Method': ['K-Fold', 'TimeSeriesSplit'],
'MSE': [kf_score, ts_score]
})
plt.figure(figsize=(10, 6))
plt.bar(results['Method'], results['MSE'])
plt.title('Prediction Error Comparison')
plt.ylabel('Mean Squared Error')
plt.savefig('error_comparison.png')
plt.close()
お気軽にご相談ください!
弊社のデジタル技術を活用し、貴社のDXをサポートします。
基本的な設計や実装支援はもちろん、機械学習・データ分析・3D計算などの高度な技術が求められる場面でも、最適なソリューションをご提案します。
初回相談は無料で受け付けております。困っていること、是非お聞かせください。