【问题标题】:How to create a train/test split of time-series data by year?如何按年份创建时间序列数据的训练/测试拆分?
【发布时间】:2020-01-23 22:50:32
【问题描述】:

我想交叉验证我的时间序列数据并按时间戳的年份进行拆分。

这是 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]

感谢观看!

【问题讨论】:

    标签: python pandas scikit-learn time-series cross-validation


    【解决方案1】:

    更新回复

    每年具有任意点数的数据的通用方法。

    首先,根据示例,一些数据包含几年的数据,每个数据中的点数不同。这与原始答案的方法相似。

    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
    

    原始回复

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

    您的意图有点不清楚,但我相信您想要做的是将带有 时间戳索引dataframe 传递到 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()
    

    然后是具有正确拆分数的 TimeSeriesSplit 实例 (n_years - 1):

    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 总是会在每年生成一个数据量相等的数据框,而“真实”数据每年都有不同的数量。我的输出最终看起来像: TRAIN: [2015] TEST: [2016] TRAIN: [2016] TEST: [2016 2017] TRAIN: [2016 2017] TEST: [2017 2018] TRAIN: [2016 2017 2018] TEST: [ 2018 2019 2020]
    • @Sepa 我现在看到了,我明白为什么之前提供的答案不起作用。我认为在这种情况下你可能需要一个自定义函数,但它不应该离已经存在的函数太远。您将使用从日期时间中提取的年份来构建唯一年份列表,然后遍历它们直到年份 n-1 以收集您的测试和训练集,使用 df.loc[df.years < test_year,:] 为您的训练集索引到 df。这有意义吗?或者修改我之前的回复是否有用?
    • 这是有道理的,您是否可以修改较早的响应以反映您所指的内容?您的答案将比下面这些 cmets 更持久。
    • @Sepa 现在修改为更通用地处理不同年份的不同数量的记录。我想这就是你想要的?
    • 不错!我在我的“真实”上试过这个,它按预期打印。我相信我有一个旧版本的熊猫(0.20.3),它没有创建随机数据并留下“ValueError:必须指定两个开始、结束或句点”。我现在将尝试实际执行训练/测试,但这解决了我提出的问题。非常感谢!
    猜你喜欢
    • 1970-01-01
    • 2019-05-10
    • 2019-08-04
    • 2021-01-14
    • 2020-02-06
    • 2022-01-21
    • 2019-09-11
    • 1970-01-01
    • 2020-05-05
    相关资源
    最近更新 更多