如何按年创建时间序列数据的训练/测试拆分?

3
我希望对我的时间序列数据进行交叉验证,并按时间戳的年份进行拆分。
以下是pandas数据框中的数据:
mock_data

timestamp             counts
'2015-01-01 03:45:14' 4
     .
     .
     .
'2016-01-01 13:02:14' 12
     .
     .
     .
'2017-01-01 09:56:54' 6
     .
     .
     .
'2018-01-01 13:02:14' 8
     .
     .
     .
'2019-01-01 11:39:40' 24
     .
     .
     .
'2020-01-01 04:02:03' 30

mock_data.dtypes
timestamp object
counts    int64

查看scikit-learn的TimeSeriesSplit()函数,似乎无法按年份指定n_split部分。是否有其他方法可以创建连续的训练集,从而得到以下的训练测试拆分?

tscv = newTimeSeriesSplit(n_splits=5, by='year')
>>> print(tscv)  
newTimeSeriesSplit(max_train_size=None, n_splits=5, by='year')
>>> for train_index, test_index in tscv.split(mock_data):
...    print("TRAIN:", train_index, "TEST:", test_index)
...    X_train, X_test = X[train_index], X[test_index]
...    y_train, y_test = y[train_index], y[test_index]
TRAIN: [2015] TEST: [2016]
TRAIN: [2015 2016] TEST: [2017]
TRAIN: [2015 2016 2017] TEST: [2018]
TRAIN: [2015 2016 2017 2018] TEST: [2019]
TRAIN: [2015 2016 2017 2018 2019] TEST: [2020]

感谢您的浏览!

1个回答

7

更新回复

针对每年数据点数量任意的通用方法。

首先,根据示例,我们有一些数据,其中每年的数据点数量不同。这种方法类似于原始答案。

import numpy as np
import pandas as pd

ts_2015 = pd.date_range('2015-01-01', '2015-12-31', periods=4).to_series()
ts_2016 = pd.date_range('2016-01-01', '2016-12-31', periods=12).to_series()
ts_2017 = pd.date_range('2017-01-01', '2017-12-31', periods=6).to_series()
ts_2018 = pd.date_range('2018-01-01', '2018-12-31', periods=8).to_series()
ts_2019 = pd.date_range('2019-01-01', '2019-12-31', periods=24).to_series()
ts_2020 = pd.date_range('2020-01-01', '2020-12-31', periods=30).to_series()
ts_all = pd.concat([ts_2015, ts_2016, ts_2017, ts_2018, ts_2019, ts_2020])

df = pd.DataFrame({'X': np.random.randint(0, 100, size=ts_all.shape), 
                   'Y': np.random.randint(100, 200, size=ts_all.shape)},
                 index=ts_all)
df['year'] = df.index.year
df = df.reset_index()

现在我们创建一个唯一年份的列表进行迭代,并创建一个字典来存储各个拆分后的数据框。
year_list = df['year'].unique().tolist()
splits = {'train': [], 'test': []}

for idx, yr in enumerate(year_list[:-1]):
    train_yr = year_list[:idx+1]
    test_yr = [year_list[idx+1]]
    print('TRAIN: ', train_yr, 'TEST: ',test_yr)
    
    splits['train'].append(df.loc[df.year.isin(train_yr), :])
    splits['test'].append(df.loc[df.year.isin(test_yr), :])

结果:

TRAIN:  [2015] TEST:  [2016]
TRAIN:  [2015, 2016] TEST:  [2017]
TRAIN:  [2015, 2016, 2017] TEST:  [2018]
TRAIN:  [2015, 2016, 2017, 2018] TEST:  [2019]
TRAIN:  [2015, 2016, 2017, 2018, 2019] TEST:  [2020]

分割后的数据框大致如下:
>>> splits['train'][0]

                index   X    Y  year
0 2015-01-01 00:00:00  20  127  2015
1 2015-05-02 08:00:00  25  197  2015
2 2015-08-31 16:00:00  61  185  2015
3 2015-12-31 00:00:00  75  144  2015

原始回答

有人指出这种方法行不通,因为它假设每年包含相同数量的记录。

你的意图有点不清楚,但我认为你想要做的是将一个具有时间戳索引的数据框传递到新版本的TimeSeriesSplit类中,该类将基于数据中的年数生成n_split = n_years - 1TimeSeriesSplit类使您可以灵活地执行此操作,但您需要先从时间戳索引中提取年份。结果看起来与您提出的不太相似,但我相信结果是符合您预期的。

首先是一些虚拟数据:

import numpy as np
import pandas as pd
from sklearn.model_selection import TimeSeriesSplit
    
ts_index = pd.date_range('2015-01-01','2020-12-31',freq='M')
df = pd.DataFrame({'X': np.random.randint(0, 100, size=ts_index.shape), 
                   'Y': np.random.randint(100, 200, size=ts_index.shape)},
                 index=ts_index)


现在是TimeSeriesSplit工作的一年。因为我们必须按行号索引到这个东西,并且pd.ix已经被弃用,所以我将索引从时间戳重置为数字。
df['year'] = df.index.year
df = df.reset_index()

然后使用正确数量的拆分次数(n_years - 1)创建一个TimeSeriesSplit实例:
tscv = TimeSeriesSplit(n_splits=len(df['year'].unique()) - 1)

现在我们可以生成索引。不要打印索引,而是打印对应的年份列,并且只打印唯一的年份:
for train_idx, test_idx in tscv.split(df['year']):
    print('TRAIN: ', df.loc[df.index.isin(train_idx), 'year'].unique(), 
          'TEST: ', df.loc[df.index.isin(test_idx), 'year'].unique())


TRAIN:  [2015] TEST:  [2016]
TRAIN:  [2015 2016] TEST:  [2017]
TRAIN:  [2015 2016 2017] TEST:  [2018]
TRAIN:  [2015 2016 2017 2018] TEST:  [2019]
TRAIN:  [2015 2016 2017 2018 2019] TEST:  [2020]

当然,您可以类似地访问您的训练/测试数据集。如果您真的想把它整理得好一些,可以扩展TimeSeriesSplit类并自定义初始化或添加一些新方法。

谢谢@mayosten,我认为这非常接近。但是,当我尝试对自己的数据集运行此代码时,测试集中的年份会重叠。这是因为每年的数据量不均匀。例如,运行numpy的random.randint总是生成一个具有相等数据量的数据框架,而“真实”的数据每年的数据量都不同。我的输出最终看起来像: 训练集:[2015] 测试集:[2016] 训练集:[2016] 测试集:[2016 2017] 训练集:[2016 2017] 测试集:[2017 2018] 训练集:[2016 2017 2018] 测试集:[2018 2019 2020] - Sepa
1
@Sepa 我现在明白了,也明白为什么之前提供的答案不起作用。我认为在这种情况下你可能需要一个定制函数,但它应该与已有的函数相差不大。您可以使用从日期时间中提取的年份构建一组唯一的年份,然后遍历它们直到年份n-1来收集测试和训练集,在df中使用 df.loc[df.years < test_year,:] 索引您的训练数据集。这样讲清楚了吗?还是需要修改我先前的回答? - mayosten
这很有道理,你能否修改之前的回复以反映你所指的内容?你的答案将比下面的评论更加永久。 - Sepa
@Sepa现在已经修改为更通用的处理跨年记录数量不同的情况。我想这就是你想要的吧? - mayosten
不错!我在我的“真实环境”上尝试了一下,输出结果与预期相符。我相信我使用的是较旧版本的pandas(0.20.3),它没有生成随机数据,而是出现了“ValueError:必须指定开始、结束或周期中的两个”。我现在将尝试实际进行训练/测试,但这解决了我提出的问题。非常感谢! - Sepa
太好了,很高兴能帮到你。 - mayosten

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接