GroupTimeSeriesSplit: scikit-learn 兼容的带分组的时间序列验证版本

一种 scikit-learn 兼容的时间序列交叉验证器,支持非重叠分组。

from mlxtend.evaluate import GroupTimeSeriesSplit

概述

机器学习中的时间序列任务需要特殊类型的验证,因为对象的时间顺序对于更公平地评估机器学习模型的质量非常重要。
此外,针对不同的任务,数据拆分可以使用不同的时间单位——小时、天、月等。

在这里,我们使用支持分组的时间序列验证,分组可以与其他参数一起灵活配置

  • 测试集大小
  • 训练集大小
  • 拆分次数
  • 间隔大小
  • 移动大小
  • 窗口类型

GroupTimeSeriesSplit 实现灵感来自 scikit-learn 的 TimeSeriesSplit,但它具有一些优点

  • GroupTimeSeriesSplit 允许你在拆分前按需分组数据,而 TimeSeriesSplit 只考虑记录级别。
  • 它可以用于留出验证 (n_splits=1) 和交叉验证 (n_splits>=2),而 TimeSeriesSplit 只能用于后一种情况。
  • TimeSeriesSplit 只使用扩展窗口,而对于此实现,你可以选择滚动或扩展窗口类型。
  • GroupTimeSeriesSplit 通过额外的 shift size 参数提供了额外的拆分控制。

有一些需要考虑的特性

  • GroupTimeSeriesSplit 与 sklearn-learn API 兼容。
  • 数字或自定义非数值可以作为分组使用
  • 但是,分组应该是连续的
  • 通过 a) 训练集大小 或 b) 拆分次数 来指定测试集大小是拆分的必需参数
  • 如果无法使用特定参数处理全部数据,则会考虑使用最近的数据进行拆分
  • 如果使用指定参数无法进行拆分(例如,数据不足以进行拆分),则会引发异常

在下面的示例中说明 GroupTimeSeriesSplit 的用法之前,让我们先设置一个将在后续部分中重用的 DummyClassifier。此外,我们还将导入在后续示例中将使用的库

import numpy as np
import pandas as pd
from sklearn.dummy import DummyClassifier
from sklearn.model_selection import cross_val_score

from mlxtend.evaluate.time_series import (
    GroupTimeSeriesSplit,
    plot_splits,
    print_cv_info,
    print_split_info,
)

准备样本数据

对于以下示例,我们将创建一个包含 16 个训练数据点及相应目标的样本数据集。

特征和目标

假设我们有一个数值特征和一个用于二元分类任务的目标变量。

data = [[0], [7], [6], [4], [4], [8], [0], [6], [2], [0], [5], [9], [7], [7], [7], [7]]
target = [1, 0, 1, 0, 1, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 0]

X = pd.DataFrame(data, columns=["num_feature"])
y = pd.Series(target, name="target")

分组编号

我们创建 6 个不同的分组,以便第一个训练样本属于分组 0,接下来的 4 个属于分组 1,以此类推。
这些分组不必按升序排列(如本数据集所示),但必须是连续的。

groups = np.array([0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 5, 5])
groups
array([0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 5, 5, 5, 5])

请注意,以下是另一个正确的分组顺序示例(未排序但连续)

np.array([5, 5, 5, 5, 1, 1, 1, 1, 3, 3, 2, 2, 2, 4, 4, 0])

但是,下面的示例显示了一个不正确的分组顺序(不连续),它与 GroupTimeSeriesSplit 不兼容

np.array([0, 1, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 2, 2, 2, 2])

分组名称(月份)

我们将根据指定的分组添加月份作为索引,以便提供更具说明性的示例。

months_map = {i: f"2021-0{i+1}" for i in range(6)}
months = np.array([months_map[group] for group in groups])
months
array(['2021-01', '2021-02', '2021-02', '2021-02', '2021-02', '2021-03',
       '2021-03', '2021-03', '2021-04', '2021-04', '2021-05', '2021-05',
       '2021-06', '2021-06', '2021-06', '2021-06'], dtype='<U7')
X = X.set_index(months)

示例 1 -- 多个训练分组(指定训练集大小)

让我们创建一个时间序列拆分,其中训练集包含 3 个分组。我们将使用 1 个分组作为测试集。在这种情况下,由于指定了训练集和测试集的大小,拆分次数将自动计算。

可视化中的前 3 行描述了每个拆分如何在分组之间分布。
分组在最后一行可视化显示,每种颜色代表不同的分组。

cv_args = {"test_size": 1, "train_size": 3}

plot_splits(X, y, groups, **cv_args)
print_split_info(X, y, groups, **cv_args)

png

Train indices: [0 1 2 3 4 5 6 7]
Test indices: [8 9]
Train length: 8
Test length: 2
Train groups: [0 1 1 1 1 2 2 2]
Test groups: [3 3]
Train group size: 3
Test group size: 1
Train group months: ['2021-01' '2021-02' '2021-02' '2021-02' '2021-02' '2021-03' '2021-03'
 '2021-03']
Test group months: ['2021-04' '2021-04']

Train indices: [1 2 3 4 5 6 7 8 9]
Test indices: [10 11]
Train length: 9
Test length: 2
Train groups: [1 1 1 1 2 2 2 3 3]
Test groups: [4 4]
Train group size: 3
Test group size: 1
Train group months: ['2021-02' '2021-02' '2021-02' '2021-02' '2021-03' '2021-03' '2021-03'
 '2021-04' '2021-04']
Test group months: ['2021-05' '2021-05']

Train indices: [ 5  6  7  8  9 10 11]
Test indices: [12 13 14 15]
Train length: 7
Test length: 4
Train groups: [2 2 2 3 3 4 4]
Test groups: [5 5 5 5]
Train group size: 3
Test group size: 1
Train group months: ['2021-03' '2021-03' '2021-03' '2021-04' '2021-04' '2021-05' '2021-05']
Test group months: ['2021-06' '2021-06' '2021-06' '2021-06']

请注意,如果为训练集和测试集都指定了分组数量,则拆分大小将自动确定,并且拆分次数会随着分组大小而自然改变。例如,增加训练分组的数量将自然导致拆分次数减少,如下所示。

cv_args = {"test_size": 1, "train_size": 4}

plot_splits(X, y, groups, **cv_args)

png

在交叉验证中的使用

下面的示例说明了如何在 scikit-learn 中使用时间序列拆分器,即使用 cross_val_score

cv = GroupTimeSeriesSplit(**cv_args)
clf = DummyClassifier(strategy="most_frequent")

scores = cross_val_score(clf, X, y, groups=groups, scoring="accuracy", cv=cv)
print_cv_info(cv, X, y, groups, clf, scores)
Split number: 1
Train true target: [1 0 1 0 1 0 0 1]
Train predicted target: [0 0 0 0 0 0 0 0]
Test true target: [1 1]
Test predicted target: [0 0]
Accuracy: 0.0

Split number: 2
Train true target: [0 1 0 1 0 0 1 1 1]
Train predicted target: [1 1 1 1 1 1 1 1 1]
Test true target: [0 1]
Test predicted target: [1 1]
Accuracy: 0.5

Split number: 3
Train true target: [0 0 1 1 1 0 1]
Train predicted target: [1 1 1 1 1 1 1]
Test true target: [1 0 0 0]
Test predicted target: [1 1 1 1]
Accuracy: 0.25

示例 2 -- 多个训练分组(指定拆分次数)

现在来看一个不指定训练分组数量的示例。在这里,我们将使用测试集大小(2 个分组)和指定的拆分次数(3 次)来拆分数据集,这足以自动计算训练集大小。

cv_args = {"test_size": 2, "n_splits": 3}

plot_splits(X, y, groups, **cv_args)
print_split_info(X, y, groups, **cv_args)

png

Train indices: [0 1 2 3 4]
Test indices: [5 6 7 8 9]
Train length: 5
Test length: 5
Train groups: [0 1 1 1 1]
Test groups: [2 2 2 3 3]
Train group size: 2
Test group size: 2
Train group months: ['2021-01' '2021-02' '2021-02' '2021-02' '2021-02']
Test group months: ['2021-03' '2021-03' '2021-03' '2021-04' '2021-04']

Train indices: [1 2 3 4 5 6 7]
Test indices: [ 8  9 10 11]
Train length: 7
Test length: 4
Train groups: [1 1 1 1 2 2 2]
Test groups: [3 3 4 4]
Train group size: 2
Test group size: 2
Train group months: ['2021-02' '2021-02' '2021-02' '2021-02' '2021-03' '2021-03' '2021-03']
Test group months: ['2021-04' '2021-04' '2021-05' '2021-05']

Train indices: [5 6 7 8 9]
Test indices: [10 11 12 13 14 15]
Train length: 5
Test length: 6
Train groups: [2 2 2 3 3]
Test groups: [4 4 5 5 5 5]
Train group size: 2
Test group size: 2
Train group months: ['2021-03' '2021-03' '2021-03' '2021-04' '2021-04']
Test group months: ['2021-05' '2021-05' '2021-06' '2021-06' '2021-06' '2021-06']

在交叉验证中的使用

再次,让我们看看这在 scikit-learn 环境中,使用 cross_val_score 是什么样子

cv = GroupTimeSeriesSplit(**cv_args)
clf = DummyClassifier(strategy="most_frequent")

scores = cross_val_score(clf, X, y, groups=groups, scoring="accuracy", cv=cv)
print_cv_info(cv, X, y, groups, clf, scores)
Split number: 1
Train true target: [1 0 1 0 1]
Train predicted target: [1 1 1 1 1]
Test true target: [0 0 1 1 1]
Test predicted target: [1 1 1 1 1]
Accuracy: 0.6

Split number: 2
Train true target: [0 1 0 1 0 0 1]
Train predicted target: [0 0 0 0 0 0 0]
Test true target: [1 1 0 1]
Test predicted target: [0 0 0 0]
Accuracy: 0.25

Split number: 3
Train true target: [0 0 1 1 1]
Train predicted target: [1 1 1 1 1]
Test true target: [0 1 1 0 0 0]
Test predicted target: [1 1 1 1 1 1]
Accuracy: 0.33

示例 3 -- 定义训练集和测试集之间的时间间隔大小

GroupTimeSeriesSplit 允许你指定大于 1 的间隔大小,以便在训练集和测试集折叠之间跳过指定数量的分组(默认间隔大小为 0)。在下面的示例中,我们使用 1 个分组的间隔来演示这一点。

cv_args = {"test_size": 1, "n_splits": 3, "gap_size": 1}

plot_splits(X, y, groups, **cv_args)
print_split_info(X, y, groups, **cv_args)

png

Train indices: [0 1 2 3 4]
Test indices: [8 9]
Train length: 5
Test length: 2
Train groups: [0 1 1 1 1]
Test groups: [3 3]
Train group size: 2
Test group size: 1
Train group months: ['2021-01' '2021-02' '2021-02' '2021-02' '2021-02']
Test group months: ['2021-04' '2021-04']

Train indices: [1 2 3 4 5 6 7]
Test indices: [10 11]
Train length: 7
Test length: 2
Train groups: [1 1 1 1 2 2 2]
Test groups: [4 4]
Train group size: 2
Test group size: 1
Train group months: ['2021-02' '2021-02' '2021-02' '2021-02' '2021-03' '2021-03' '2021-03']
Test group months: ['2021-05' '2021-05']

Train indices: [5 6 7 8 9]
Test indices: [12 13 14 15]
Train length: 5
Test length: 4
Train groups: [2 2 2 3 3]
Test groups: [5 5 5 5]
Train group size: 2
Test group size: 1
Train group months: ['2021-03' '2021-03' '2021-03' '2021-04' '2021-04']
Test group months: ['2021-06' '2021-06' '2021-06' '2021-06']

在交叉验证中的使用

下面的示例展示了这在 scikit-learn 环境中,使用 cross_val_score 是什么样子

cv = GroupTimeSeriesSplit(**cv_args)
clf = DummyClassifier(strategy="most_frequent")

scores = cross_val_score(clf, X, y, groups=groups, scoring="accuracy", cv=cv)
print_cv_info(cv, X, y, groups, clf, scores)
Split number: 1
Train true target: [1 0 1 0 1]
Train predicted target: [1 1 1 1 1]
Test true target: [1 1]
Test predicted target: [1 1]
Accuracy: 1.0

Split number: 2
Train true target: [0 1 0 1 0 0 1]
Train predicted target: [0 0 0 0 0 0 0]
Test true target: [0 1]
Test predicted target: [0 0]
Accuracy: 0.5

Split number: 3
Train true target: [0 0 1 1 1]
Train predicted target: [1 1 1 1 1]
Test true target: [1 0 0 0]
Test predicted target: [1 1 1 1]
Accuracy: 0.25

API

GroupTimeSeriesSplit(test_size, train_size=None, n_splits=None, gap_size=0, shift_size=1, window_type='rolling')

分组时间序列交叉验证器。

参数

  • test_size : int

    测试集大小。

  • train_size : int (默认值=None)

    训练集大小。

  • n_splits : int (默认值=None)

    拆分次数。

  • gap_size : int (默认值=0)

    训练集和测试集之间的间隔大小。

  • shift_size : int (默认值=1)

    下一次折叠的移动步长。

  • window_type : str (默认值="rolling")

    窗口类型。可能的值:"rolling"(滚动)、"expanding"(扩展)。

示例

有关使用示例,请参阅 https://mlxtend.cn/mlxtend/user_guide/evaluate/GroupTimeSeriesSplit/

方法


get_n_splits(X=None, y=None, groups=None)

返回交叉验证器中的拆分迭代次数。

参数

  • X : object

    始终被忽略,仅为兼容性而存在。

  • y : object

    始终被忽略,仅为兼容性而存在。

  • groups : object

    始终被忽略,仅为兼容性而存在。

返回值

  • n_splits : int

    返回交叉验证器中的拆分迭代次数。


split(X, y=None, groups=None)

生成索引以将数据拆分为训练集和测试集。

参数

  • X : 类似数组的对象

    训练数据。

  • y : 类似数组的对象 (默认值=None)

    始终被忽略,仅为兼容性而存在。

  • groups : 类似数组的对象 (默认值=None)

    包含分组名称或序列号的数组。

生成

  • train : ndarray

    该拆分的训练集索引。

  • test : ndarray

    该拆分的测试集索引。