時系列データの交差検証:なぜTimeSeriesSplitを使うべきなのか

はじめに

機械学習モデルの評価において、交差検証(Cross-Validation)は不可欠です。しかし、時系列データに対して通常のK-分割交差検証(K-Fold Cross-Validation)を適用すると、未来の情報を使って過去を予測してしまうという問題が生じ、モデルの性能を正しく評価できません。本記事では、時系列データに適した交差検証手法であるTimeSeriesSplitについて、その重要性とK-Fold Cross-Validationとの違いを、理論と実践の両面から解説します。

この記事を書いたひと

デジタルリアクタ合同会社 代表
機械学習・統計、数値計算などの領域を軸としたソフトウェアエンジニアリングを専門としています。スタートアップからグローバル企業まで、さまざまなスケールの企業にて、事業価値に直結する計算システムを設計・構築してきました。主に機械学習の応用分野において、出版・発表、特許取得等の実績があり、また、IT戦略やデータベース技術等の情報処理に関する専門領域の国家資格を複数保有しています。九州大学理学部卒業。日本ITストラテジスト協会正会員。

対象読者:

  • 機械学習エンジニア、データサイエンティスト
  • 時系列データの分析に携わる方
  • モデルの評価方法について深く理解したい方

記事のポイント:

  • 時系列データの特性と、通常の交差検証で生じる問題点を解説
  • 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計算などの高度な技術が求められる場面でも、最適なソリューションをご提案します。

初回相談は無料で受け付けております。困っていること、是非お聞かせください。