Изучение различных методов разделения данных для анализа временных рядов

Оценка производительности является важным аспектом проектов машинного обучения, цель которых состоит в том, чтобы предсказать ошибку, с которой может столкнуться прогностическая модель при представлении новых, невидимых данных. Однако в случае задач прогнозирования временных рядов возникают уникальные проблемы, возникающие из-за зависимости между наблюдениями во времени.

Основной проблемой при оценке эффективности моделей прогнозирования временных рядов является зависимость наблюдений во времени. Традиционные методы, такие как перекрестная проверка, которые включают случайное разбиение данных на наборы для обучения и проверки, предполагают, что наблюдения независимы и одинаково распределены (IID). Это предположение не выполняется для данных временных рядов, где наблюдения зависят друг от друга.

Однако в данных временных рядов существуют определенные сценарии, в которых могут применяться модифицированные версии перекрестной проверки. Например, при работе со стационарными или ограниченными наборами данных перекрестная проверка может быть полезным способом эффективного использования всех доступных данных.

Техники

В этом сообщении блога мы рассмотрим распространенные методы оценки эффективности прогнозирования временных рядов. Основная цель методов оценки производительности при прогнозировании временных рядов - обрабатывать корреляцию между наблюдениями. Для этого модели тестируются на будущих наблюдениях после обучения. Вневыборочное (OOS) тестирование и различные варианты перекрестной проверки (CVAL) относятся к наиболее часто используемым методам.

  • вне выборки
  • предварительный
  • Перекрестная проверка

OOS вне выборки

Методы вне выборки (OOS) обычно используются для оценки эффективности прогностических моделей в данных, зависящих от времени. Эти методы включают выделение части временного ряда для тестирования при сохранении временного порядка наблюдений.

предварительный

Еще один — превентивные техники. Концепция этого метода заключается в том, что каждое наблюдение используется сначала для проверки модели, а затем для обучения модели. Этот метод может применяться в последовательных блоках экземпляров. Во время начальной итерации используются только первые два блока, причем первый блок используется для обучения, а второй — для тестирования. В последующей итерации второй блок объединяется с первым, а третий блок используется для тестирования. Этот процесс повторяется до тех пор, пока не будут проверены все блоки.

Перекрестная проверка

Как упоминалось выше, было бы неправильно применять перекрестную проверку к задачам временных рядов из-за ее структуры. Однако некоторые варианты перекрестной проверки можно использовать для задач временных рядов. Многие исследования показали, что эти варианты дают даже лучшие результаты, чем другие методы в стационарных временных рядах.

Реализации Python

Теперь давайте напишем код Python для этих методов.

  • Генерация данных
  • OOS: Удержание
  • OOS: повторная задержка
  • Предварительные блоки
  • Предварительные скользящие блоки
  • Предварительные блоки с пробелом
  • Предварительное скользящее окно
  • Предварительное растущее окно
  • Перекрестная проверка
  • Модифицированная перекрестная проверка
  • Заблокированная перекрестная проверка
  • Перекрестная проверка с блокировкой Hv

Генерация данных

Давайте создадим фиктивные данные временного ряда для работы.

# imports

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose
from sklearn.model_selection import KFold
def generate_data(size):
    """generate stationary time series data"""
    np.random.seed(1)
    # A stationary time series
    data = np.random.normal(loc=0, scale=1, size=size)
    print(f"Length: {len(data)}")
    print(f"Mean: {np.mean(data)} and Variance: {np.var(data)}")
    return data

data = generate_data(20)
"""
Length: 20
Mean: -0.1333646364607294 and Variance: 1.209572049428708
"""
# Plot the time series
plt.plot(data)
plt.title('Dummy Time Series')
plt.xlabel('Time')
plt.ylabel('Values')
plt.show()

Мы можем использовать сезонную декомпозицию, чтобы проверить тренд, сезонные и остаточные компоненты ряда. Здесь это не имеет особого смысла, так как я создал набор данных с 20 данными для контекста и простоты отображения. Во-первых, давайте преобразуем данные в серию pandas.

date_range = pd.date_range(start='1/1/2020', end='1/20/2020', freq='D')
data_series = pd.Series(data, index=date_range)
print(data_series)

"""
2020-01-01    1.624345
2020-01-02   -0.611756
2020-01-03   -0.528172
2020-01-04   -1.072969
2020-01-05    0.865408
2020-01-06   -2.301539
2020-01-07    1.744812
2020-01-08   -0.761207
2020-01-09    0.319039
2020-01-10   -0.249370
2020-01-11    1.462108
2020-01-12   -2.060141
2020-01-13   -0.322417
2020-01-14   -0.384054
2020-01-15    1.133769
2020-01-16   -1.099891
2020-01-17   -0.172428
2020-01-18   -0.877858
2020-01-19    0.042214
2020-01-20    0.582815
Freq: D, dtype: float64
"""

Построение разложения:

def check_decomposition(data_series):
    """decompose it into a trend, seasonal, and residual components"""
    decomposition = seasonal_decompose(data_series)
    # plot
    fig, axes = plt.subplots(4, 1, figsize=(10,8))
    axes[0].plot(data_series)
    axes[0].set_ylabel('Observed')
    axes[0].set_title('Decomposition')
    axes[1].plot(decomposition.trend)
    axes[1].set_ylabel('Trend')
    axes[2].plot(decomposition.seasonal)
    axes[2].set_ylabel('Seasonal')
    axes[3].plot(decomposition.resid)
    axes[3].set_ylabel('Residual')
    plt.tight_layout()
    plt.show()
    return

check_decomposition(data_series)

Эта вспомогательная функция будет отображать расколы.

def plot_split(train_idx_list, test_idx_list,title):
    # Plot the folds
    fig, ax = plt.subplots()
    for i, (train_idx, test_idx) in enumerate(zip(train_idx_list, test_idx_list)):
        ax.scatter(train_idx, [i+1]*len(train_idx), color='blue', label='Train' , s=50)
        ax.scatter(test_idx, [i+1]*len(test_idx), color='red', label='Test', s=50)
    ax.set_xlabel('Index')
    ax.set_ylabel('Fold')
    ax.set_title(title)
    ax.legend(loc='best')
    plt.show()
    return

Держись

Мы делим доступные данные временных рядов на два набора: обучающий набор и набор для тестирования. Например, первые 80% данных будут использоваться для обучения, а остальные 20% — для тестирования.

def hold_out(data, cut):
    """returns the indexes for hold out split"""
    train_idx = list(range(0, int(len(data) * cut)))
    test_idx = list(range(int(len(data)*cut), len(data)))
    print(f"train: {train_idx} \nTest: {test_idx}")
    return train_idx, test_idx

train_idx, test_idx = hold_out(data, 0.8)

"""
train: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 
Test: [16, 17, 18, 19]
"""

Повторное удержание

Повторное удержание включает в себя выбор блока из набора данных (например, 50%). Затем часть этого блока используется для обучения модели, а оставшаяся часть используется для тестирования. Этот процесс повторяется несколько раз (например, 5), при этом каждый раз выбираются разные блоки. В отличие от стандартного метода удержания, не все наблюдения используются в одной оценке. Такой подход позволяет более полно оценить эффективность модели.

def repeated_holdout(data, n_repeats, cut, block_cut):
    """first obtains the block series, then splits it"""
    train_list = []
    test_list = []
    for r in range(n_repeats):
        block_size = int(len(data) * block_cut)
        start_idx = np.random.randint(len(data) - block_size)
        block = list(range(start_idx, start_idx + block_size))
        train_idx = block[:int(len(block)*cut)]
        test_idx = block[int(len(block)*cut):]
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {r} \ntrain: {train_idx} \nTest: {test_idx}")
        
    return train_list, test_list

train_list, test_list = repeated_holdout(data, 5, 0.8, 0.5)
"""
Fold: 0 
train: [8, 9, 10, 11, 12, 13, 14, 15] 
Test: [16, 17]
Fold: 1 
train: [7, 8, 9, 10, 11, 12, 13, 14] 
Test: [15, 16]
Fold: 2 
train: [7, 8, 9, 10, 11, 12, 13, 14] 
Test: [15, 16]
Fold: 3 
train: [1, 2, 3, 4, 5, 6, 7, 8] 
Test: [9, 10]
Fold: 4 
train: [1, 2, 3, 4, 5, 6, 7, 8] 
Test: [9, 10]
"""

Предварительные блоки

Подход с предварительными блоками включает создание складок, состоящих из смежных точек данных. При обучении модели на сгибе используются только те точки данных, которые предшествуют этому сгибу. В результате первая складка не тестируется, так как нет предшествующих данных для обучения модели.

def preq_bls(data, n_folds):
    """divide data into blocks. Incrementally slide over blocks.
    A test block is added to the training block.
    """
    train_list = []
    test_list = []
    indices = np.arange(len(data))
    block_size = len(data) // n_folds
    block_starts = np.arange(0, len(data) + 1, block_size)
    for i in range(n_folds-1):
        train_start = block_starts[0]
        train_end = block_starts[i+1]
        test_start = block_starts[i+1]
        test_end = block_starts[i+2]
        train_idx = indices[train_start:train_end]
        test_idx = indices[test_start:test_end]
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = preq_bls(data, 5)

plot_split(train_list, test_list, "Preq Bls")

"""
Fold: 0 
train: [0 1 2 3] 
Test: [4 5 6 7]
Fold: 1 
train: [0 1 2 3 4 5 6 7] 
Test: [ 8  9 10 11]
Fold: 2 
train: [ 0  1  2  3  4  5  6  7  8  9 10 11] 
Test: [12 13 14 15]
Fold: 3 
train: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15] 
Test: [16 17 18 19]
"""

Предварительные скользящие блоки

В этом подходе процесс обучения и тестирования модели выполняется путем скольжения по блокам данных, а не добавления тестовых данных к обучающим данным после каждой итерации. Это означает, что данные тестирования не объединяются с данными обучения во время итеративного процесса.

def preq_sld_bls(data, n_folds):
    """divide data into blocks. Incrementally slide over blocks.
    """
    train_list = []
    test_list = []
    indices = np.arange(len(data))
    n_folds = 5
    block_size = len(data) // n_folds
    block_starts = np.arange(0, len(data) + 1, block_size)
    for i in range(n_folds-1):
        train_start = block_starts[i]
        train_end = block_starts[i+1]
        test_start = block_starts[i+1]
        test_end = block_starts[i+2]
        train_idx = indices[train_start:train_end]
        test_idx = indices[test_start:test_end]
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = preq_sld_bls(data, 5)

plot_split(train_list, test_list, "Preq Sld Bls")

"""
Fold: 0 
train: [0 1 2 3] 
Test: [4 5 6 7]
Fold: 1 
train: [4 5 6 7] 
Test: [ 8  9 10 11]
Fold: 2 
train: [ 8  9 10 11] 
Test: [12 13 14 15]
Fold: 3 
train: [12 13 14 15] 
Test: [16 17 18 19]
"""

Предварительные блоки с пробелом

В этом подходе между набором обучающих данных и набором тестовых данных намеренно оставлен разрыв. По мере продолжения итеративного процесса предыдущий блок пробелов включается в обучающий набор данных для следующего сгиба, в то время как тестовый набор сдвигается в одну сторону. Это позволяет обучать модель на большем количестве данных без ущерба для независимости набора тестов.

def preq_bls_gap(data, n_folds):
    train_list = []
    test_list = []
    indices = np.arange(len(data))
    block_size = len(data) // n_folds
    block_starts = np.arange(0, len(data) + 1, block_size)
    for i in range(n_folds-2):
        train_start = block_starts[0]
        train_end = block_starts[i+1]
        test_start = block_starts[i+2]
        test_end = block_starts[i+3]
        train_idx = indices[train_start:train_end]
        test_idx = indices[test_start:test_end]
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = preq_bls_gap(data, 5)

plot_split(train_list, test_list, "Preq Bls Gap")

"""
Fold: 0 
train: [0 1 2 3] 
Test: [ 8  9 10 11]
Fold: 1 
train: [0 1 2 3 4 5 6 7] 
Test: [12 13 14 15]
Fold: 2 
train: [ 0  1  2  3  4  5  6  7  8  9 10 11] 
Test: [16 17 18 19]
"""

Предварительное скользящее окно

Во время первой итерации тестовый набор создается путем выбора последней части наблюдений (например, последних 20%), а первая часть используется для обучения. По мере продолжения итераций размер тестовой выборки постепенно уменьшается, а обучающая часть смещается вперед, но длина остается прежней.

def preq_slide(data, cut, n=1):
    """n is the number of observations to shift for each iteration"""
    train_list = []
    test_list = []
    n_fold = int((len(data) * (1-cut))/n)
    for i in range(n_fold+1):
        train_idx = list(range(0+(n*i), int(len(data)*cut)+(n*i)))
        test_idx = list(range(len(train_idx)+(n*i), len(data)))
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = preq_slide(data, 0.8, 1)

plot_split(train_list, test_list, "Preq Slide")
"""
Fold: 0 
train: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 
Test: [16, 17, 18, 19]
Fold: 1 
train: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] 
Test: [17, 18, 19]
Fold: 2 
train: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] 
Test: [18, 19]
Fold: 3 
train: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18] 
Test: [19]
"""

Предварительное растущее окно

При каждой итерации сгибов размер обучающих данных продолжает расти.

def preq_grow(data, cut, n=1):
    train_list = []
    test_list = []
    n_fold = int((len(data) * (1-cut))/n)
    for i in range(n_fold):
        train_idx = list(range(0, int(len(data)*cut)+(n*i)))
        test_idx = list(range(len(train_idx), len(data)))
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = preq_grow(data, 0.8, 1)

plot_split(train_list, test_list, "Preq Grow")

"""
Fold: 0 
train: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] 
Test: [16, 17, 18, 19]
Fold: 1 
train: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] 
Test: [17, 18, 19]
Fold: 2 
train: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17] 
Test: [18, 19]
"""

Перекрестная проверка

Мы случайным образом назначаем данные для обучающих и тестовых наборов.

def cross_valid(data, n_folds):
    train_list = []
    test_list = []    
    kf = KFold(n_splits=n_folds, shuffle=True, random_state=42)
    for i, (train_idx, test_idx) in enumerate(kf.split(data)):
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = cross_valid(data, 5)

plot_split(train_list, test_list, "Cross Validation")

"""
Fold: 0 
train: [ 2  3  4  5  6  7  8  9 10 11 12 13 14 16 18 19] 
Test: [ 0  1 15 17]
Fold: 1 
train: [ 0  1  2  4  6  7  9 10 12 13 14 15 16 17 18 19] 
Test: [ 3  5  8 11]
Fold: 2 
train: [ 0  1  3  4  5  6  7  8  9 10 11 12 14 15 17 19] 
Test: [ 2 13 16 18]
Fold: 3 
train: [ 0  1  2  3  5  6  7  8 10 11 13 14 15 16 17 18] 
Test: [ 4  9 12 19]
Fold: 4 
train: [ 0  1  2  3  4  5  8  9 11 12 13 15 16 17 18 19] 
Test: [ 6  7 10 14]
"""

Модифицированная перекрестная проверка

При анализе временных рядов применяется модифицированная версия перекрестной проверки, которая похожа на традиционную перекрестную проверку, но исключает p точек до и q точек после каждой точки тестирования из обучающего набора.

def cv_mod(data, n_folds, p, q):
    train_list= []
    test_list = []
    kf = KFold(n_splits=n_folds, shuffle=True, random_state=42)

    for i, (train_idx, test_idx) in enumerate(kf.split(data)):
    # Exclude points from the training set based on p and q values
        for idx in test_idx:
            excluded_points = np.arange(max(0, idx - p), min(len(data), idx + q + 1))
            train_idx = np.setdiff1d(train_idx, excluded_points)
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {i} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = cv_mod(data, 5,1,1)
plot_split(train_list, test_list, "Modified Cross Validation")

Заблокированная перекрестная проверка

Она аналогична стандартной форме, за исключением того факта, что в ней нет начального случайного перемешивания наблюдений. Вместо этого создаются K блоков смежных наблюдений, сохраняя естественный порядок наблюдений внутри каждого блока, но разбивая его между ними.

def cv_bl(data, n_folds):
    train_list= []
    test_list = []
    
    block_size = len(data) // n_folds
    indices = np.arange(len(data))
    # Generate the split indexes for each fold
    for fold in range(n_folds):
        # Get the test indexes for this fold
        test_start = fold * block_size
        test_end = (fold + 1) * block_size
        test_idx = indices[test_start:test_end]

        # Get the train indexes for this fold
        train_idx = np.concatenate((indices[:test_start], indices[test_end:]))
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {fold} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = cv_bl(data, 5)
plot_split(train_list, test_list, "Blocked Cross Validation")
"""
Fold: 0 
train: [ 4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19] 
Test: [0 1 2 3]
Fold: 1 
train: [ 0  1  2  3  8  9 10 11 12 13 14 15 16 17 18 19] 
Test: [4 5 6 7]
Fold: 2 
train: [ 0  1  2  3  4  5  6  7 12 13 14 15 16 17 18 19] 
Test: [ 8  9 10 11]
Fold: 3 
train: [ 0  1  2  3  4  5  6  7  8  9 10 11 16 17 18 19] 
Test: [12 13 14 15]
Fold: 4 
train: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15] 
Test: [16 17 18 19]
"""

Перекрестная проверка с блокировкой Hv

Этот метод выходит за рамки стандартной блокированной перекрестной проверки. Смежные наблюдения удаляются между обучающим и тестовым наборами, создавая разрыв между обоими наборами для дальнейшего повышения независимости наблюдений.

def cv_hvbl(data, n_folds, p, q):
    train_list= []
    test_list = []
    block_size = len(data) // n_folds
    indices = np.arange(len(data))
    
    # Generate the split indexes for each fold
    for fold in range(n_folds):
    # Get the test indexes for this fold
        test_start = fold * block_size
        test_end = (fold + 1) * block_size
        test_idx = indices[test_start:test_end]

        # Get the train indexes for this fold
        train_idx = np.concatenate((indices[:test_start], indices[test_end:]))
        for idx in test_idx:
            exclude_range = np.arange(max(0, idx - p), min(len(data), idx + q + 1))
            train_idx = np.setdiff1d(train_idx, exclude_range)
        train_list.append(train_idx)
        test_list.append(test_idx)
        print(f"Fold: {fold} \ntrain: {train_idx} \nTest: {test_idx}")
    return train_list, test_list

train_list, test_list = cv_hvbl(data, 5, 1, 1)
plot_split(train_list, test_list, "Hv-Blocked Cross Validation")

"""
Fold: 0 
train: [ 5  6  7  8  9 10 11 12 13 14 15 16 17 18 19] 
Test: [0 1 2 3]
Fold: 1 
train: [ 0  1  2  9 10 11 12 13 14 15 16 17 18 19] 
Test: [4 5 6 7]
Fold: 2 
train: [ 0  1  2  3  4  5  6 13 14 15 16 17 18 19] 
Test: [ 8  9 10 11]
Fold: 3 
train: [ 0  1  2  3  4  5  6  7  8  9 10 17 18 19] 
Test: [12 13 14 15]
Fold: 4 
train: [ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14] 
Test: [16 17 18 19]
"""

Заключение

В заключение, понимание различных способов разделения и оценки данных временных рядов имеет решающее значение для разработки точных и надежных моделей прогнозирования. Методы, обсуждаемые в этом исследовании, включая подходы удержания, перекрестной проверки и предварительной проверки, предлагают различные компромиссы между предвзятостью, дисперсией и вычислительной сложностью. Исследователи и практики должны тщательно учитывать свои характеристики данных и цели моделирования при выборе подходящего метода. В целом исследование подчеркивает важность тщательной оценки эффективности прогнозирования временных рядов и дает ценную информацию для повышения надежности моделей прогнозирования.

Читать далее















Источники



Дополнительные материалы на PlainEnglish.io.

Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord .

Заинтересованы в масштабировании запуска вашего программного обеспечения? Ознакомьтесь с разделом Схема.