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

Конструирование признаков для временных рядов: создание идеального рецепта данных

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

В этом разделе мы рассмотрим некоторые из наиболее эффективных методов.

1. Лаговые признаки: Использование силы прошлого

Одним из самых простых и мощных методов в конструировании признаков для временных рядов являются лаговые признаки (lag features).

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

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

df['lag_1'] = df['value'].shift(1)  # Создание лагового признака из предыдущего временного шага
df['lag_7'] = df['value'].shift(7)  # Создание лагового признака давностью в 7 месяцев

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

Изображение 4: Пример лаговых признаков | Изображение автора
Изображение 4: Пример лаговых признаков | Изображение автора

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

2. Скользящая статистика: устранение шума

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

Рассчитывая скользящие средние или другие показатели (например, скользящее стандартное отклонение), мы можем сгладить краткосрочные колебания и выделить тренды.

Например, чтобы сгладить данные и выявить более долгосрочные тренды, можно рассчитать скользящее среднее значение за определенный период, например за 12 месяцев:

df['rolling_mean_12'] = df['value'].rolling(window=12).mean()  # Скользящее среднее за 12 месяцев 
df['rolling_std_24'] = df['value'].rolling(window=12).std()  # Стандартное отклонение за 12 месяцев
Изображение 5: Пример скользящего среднего и стандартного отклонения | Изображение автора
Изображение 5: Пример скользящего среднего и стандартного отклонения | Изображение автора

rolling(window=12) создает скользящее окно из 12 временных шагов (в данном случае месяцев) для расчета скользящего среднего значения. То же самое мы делаем и со стандартным отклонением.

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

3. Темпоральные признаки: извлечение временной информации

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

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

Вы можете легко извлечь такие признаки с помощью Pandas:

df['hour'] = df.index.hour
df['day_of_week'] = df.index.dayofweek
df['month'] = df.index.month
df['year'] = df.index.year  

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

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

4. Разность: учет трендов и сезонности

В некоторых случаях ваши данные временных рядов могут демонстрировать сильные тренды или сезонность, что делает их нестационарными.

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

df['diff_1'] = df['value'].diff(1)  # Разница между последовательными наблюдениями
df['diff_7'] = df['value'].diff(7)  # Разница между 7-месячными наблюдениями

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

5. Циклические признаки: учет периодичности

При работе с сезонными данными простое числовое представление времени (например, месяца или дня) может дать неожиданные результаты.

Например, разница между декабрем (12-й месяц) и январем (1-й месяц) на самом деле составляет всего один месяц, но численно она выглядит намного большей. Чтобы преодолеть эту проблему, вы можете создавать циклические признаки путем преобразования темпоральных признаков в синусоидальные и косинусоидальные формы:

df['sin_month'] = np.sin(2  np.pi  df['month'] / 12)
df['cos_month'] = np.cos(2  np.pi  df['month'] / 12)

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

6. Внешние признаки: добавление контекста к вашим данным

Наконец, не стоит забывать, что ваши данные временных рядов существуют не в вакууме!

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

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

df['avg_temperature'] = external_weather_data['avg_temp']
df['max_temperature'] = external_weather_data['max_temp']
df['sunlight_hours'] = external_weather_data['sunlight_hours']

Эти внешние признаки предоставляют контекст, который помогает вашей модели понять, какие внешние факторы влияют на данные временных рядов.

Разделение данных временных рядов: как избежать утечки данных в машинном обучении

Разделение данных (data splitting) является одним из первых шагов для любого проекта машинного обучения, и при анализе временных рядов этот процесс становится особенно важным.

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

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

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

При работе с данными временных рядов необходимо соблюдать хронологический порядок наблюдений.

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

Такой подход гарантирует, что модель никогда не получит доступа о будущей информации во время обучения.

Как разделить ваши данные последовательно

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

Для разбиения данных временных рядов на обучающую и тестовую выборки многие специалисты используют TimeSeriesSplit из библиотеки Scikit-learn. Вот как это выглядит:

import matplotlib.pyplot as plt
from sklearn.model_selection import TimeSeriesSplit

# Отображение обучающих выборок с их диапазонами на графике
plt.figure(figsize=(10, 6))

for i, (train_index, test_index) in enumerate(tscv.split(df)):
    train_data, test_data = df.iloc[train_index], df.iloc[test_index]

    # Отображение на графике обучающих выборок
    plt.plot(train_data.index, train_data['value'], label=f"Train Split {i+1}", alpha=0.5)

    # Получаем диапазон обучающих выборок
    train_start, train_end = train_data.index[0], train_data.index[-1]

    # Вывод диапазонов обучающих выборок
    print(f"Train Split {i+1}: {train_start.date()} to {train_end.date()} ({len(train_data)} samples)")

plt.title("TimeSeriesSplit - Train Splits")
plt.xlabel("Date")
plt.ylabel("Electric Production Value")
plt.legend()
plt.xticks(rotation=45)
plt.grid(True)
plt.show()
Изображение 6: Визуализация TimeSeriesSplit, показывающая разделение на обучающие выборки для данных о выработке электроэнергии с течением времени | Изображение автора
Изображение 6: Визуализация TimeSeriesSplit, показывающая разделение на обучающие выборки для данных о выработке электроэнергии с течением времени | Изображение автора

(Значения частично перекрываются, поэтому цвета не совпадают с указанными на подписи).

Это выведет:

Train Split 1: 1985-01-01 to 1993-04-01 (100 samples)
Train Split 2: 1985-01-01 to 2001-07-01 (199 samples)
Train Split 3: 1985-01-01 to 2009-10-01 (298 samples)

# Отображение обучающих выборок с их диапазонами на графике
plt.figure(figsize=(10, 6))

for i, (train_index, test_index) in enumerate(tscv.split(df)):
    test_data = df.iloc[test_index]

    # Отображение на графике обучающих выборок
    plt.plot(test_data.index, test_data['value'], label=f"Test Split {i+1}", linestyle='--', alpha=0.7)

    # Получаем диапазон обучающих выборок
    test_start, test_end = test_data.index[0], test_data.index[-1]

    # Вывод диапазонов обучающих выборок
    print(f"Test Split {i+1}: {test_start.date()} to {test_end.date()} ({len(test_data)} samples)")

plt.title("TimeSeriesSplit - Test Splits")
plt.xlabel("Date")
plt.ylabel("Electric Production Value")
plt.legend()
plt.xticks(rotation=45)
plt.grid(True)
plt.show
Изображение 7: Визуализация TimeSeriesSplit, показывающая разделение на обучающие выборки для данных о выработке электроэнергии с течением времени | Изображение автора
Изображение 7: Визуализация TimeSeriesSplit, показывающая разделение на обучающие выборки для данных о выработке электроэнергии с течением времени | Изображение автора

И это выведет:

Test Split 1: 1993-05-01 to 2001-07-01 (99 samples)
Test Split 2: 2001-08-01 to 2009-10-01 (99 samples)
Test Split 3: 2009-11-01 to 2018-01-01 (99 samples)

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

Мы инициализируем разделитель с заданным количеством разделений (splits) и выполняем итерации по каждому разделению для получения обучающих и тестовых индексов. Такой подход позволяет оценивать производительность модели за различные периоды времени без нарушения последовательности данных.

Как настроить параметр n_splits?

  • Размер данных: Для больших наборов требуется больше разделений, в то время как для меньших может потребоваться меньшее количество, чтобы обеспечить достаточное количество данных для каждого из них.

  • Потребности в оценке модели: Большее количество разбиений может обеспечить более надежную оценку за счет использования разных подмножеств для тестирования, но может увеличить вычислительные затраты.

Временные ряды в машинном обучении: когда ваша перекрестная проверка требует доработки

Давайте поговорим о том, почему ваша k‑кратная перекрестная проверка (k‑fold cross‑validation) может давать искаженную картину при работе с данными временных рядов.

Почему традиционная перекрестная проверка не работает

Помните, как мы обсуждали последовательный характер данных временных рядов?

Именно здесь возникает проблема с перекрестной проверкой.

Ваша стандартная k‑кратная перекрестная проверка основана на предположении, что значения независимы и распределены идентично (i.i.d.). Но во временных рядах завтра зависит от сегодня, которое, в свою очередь, зависит от вчера…

Использование традиционных методов перекрестной проверки может привести к:

  • Утечке данных (ваша модель только что увидела будущее ?).

  • Чрезмерно оптимистичным оценкам производительности.

  • Моделям, которые отлично работают на тренировочных данных, но терпят неудачу в реальном мире.

Перекрестная проверка временных рядов

Перекрестная проверка временных рядов — это метод, который учитывает временной порядок ваших данных.

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

Вот что делает TimeSeriesSplit (пример):

  • Разбиение 1: Обучение на [1, 2, 3], тестирование на [4]

  • Разбиение 2: Обучение на [1, 2, 3, 4], тестирование на [5]

  • Разбиение 3: Обучение на [1, 2, 3, 4, 5], тестирование на [6]

Это гарантирует реалистичную оценку моделей (обучение на прошлых данных и тестирование на будущих).

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

Чтобы помочь модели выявить временные закономерности и взаимосвязи, мы можем сконструировать дополнительные признаки, такие как лаговые значения (предыдущие наблюдения) или скользящая статистика (средние значения или тренды во времени).

Давайте рассмотрим, как это можно реализовать на практике:

import pandas as pd
import numpy as np
from sklearn.model_selection import TimeSeriesSplit
import matplotlib.pyplot as plt

# Загружаем набор данных (убедитесь, что столбец 'DATE' является индексом)
df = pd.read_csv('Electric_Production.csv', index_col='DATE', parse_dates=True)

# Конструируем лаговые признаки, которые будут служить нашей матрицей признаков X
df['lag_1'] = df['value'].shift(1)
df['lag_2'] = df['value'].shift(2)
df['lag_3'] = df['value'].shift(3)

# Исключаем лаговые значения, которые получились NaN 
df = df.dropna()

# Определяем X (признаки) и y (целевая переменная)
X = df[['lag_1', 'lag_2', 'lag_3']]  # Feature matrix: lagged values
y = df['value']  # Target variable

# Применяем TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)

# Визуализация перекрестной проверки временных рядов
fig, ax = plt.subplots(figsize=(10, 5))

for i, (train_index, test_index) in enumerate(tscv.split(X)):
    ax.plot(train_index, [i] * len(train_index), 'b-', label='Train' if i == 0 else '')
    ax.plot(test_index, [i] * len(test_index), 'r-', label='Test' if i == 0 else '')

ax.set_title('Time Series Cross-Validation')
ax.set_xlabel('Sample Index')
ax.set_ylabel('CV Iteration')
ax.legend()
plt.tight_layout()
plt.show()
Изображение 8: Перекрестная проверка временных рядов с использованием 5 разбиений | Изображение автора
Изображение 8: Перекрестная проверка временных рядов с использованием 5 разбиений | Изображение автора

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

Полезные советы по перекрестной проверке временных рядов:

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

tscv = TimeSeriesSplit(n_splits=5, gap=2)

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

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

2. Соображения сезонности: Если ваши данные сильно подвержены сезонности, убедитесь, что ваши учебные и тестовые выборки охватывают полные сезонные циклы.

Пример

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

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

Пример решения:

  • Предположим, у вас есть данные за три года, начиная с января 2018 года по декабрь 2020 года, с помесячными показателями. Если вы разделите эти данные для обучения и тестирования в рамках одного года (например, обучение в январе‑мае 2019 года и тестирование в июне‑декабре того же года), это не позволит вашей модели уловить всю годовую динамику — как летние, так и зимние тренды.

  • Вместо этого вы можете обучить модель на данных за полные годы (например, за 2018 и 2019) и протестировать ее в течение всего сезона или года (например, по 2020 году). Таким образом, ваша модель сможет охватить всю сезонную закономерность.

3. Прямая цепочка: В качестве еще более надежного подхода попробуйте перекрестную проверку с прямой цепочкой:

def forward_chaining(X, n_splits):
    for i in range(n_splits):
        train_end = int(len(X) * (i + 1) / (n_splits + 1))
        yield np.arange(train_end), np.arange(train_end, len(X))

# Использование
for train_index, test_index in forward_chaining(X, n_splits=5):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
Изображение 8: График перекрестной проверки с прямой цепочкой, показывающий разделение обучающих и тестовых данных для прогнозирования временных рядов | Изображение автора
Изображение 8: График перекрестной проверки с прямой цепочкой, показывающий разделение обучающих и тестовых данных для прогнозирования временных рядов | Изображение автора

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

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

Выбросы во временных рядах: когда ваши данные начинают выделяться

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

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

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

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

Идентификация выбросов: отличие сигнала от шума

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

Некоторые распространенные методы обнаружения выбросов во временных рядах включают:

  • Скользящие средние и скользящие окна: Внезапные отклонения от скользящего среднего могут сигнализировать о выбросах.

  • Статистические тесты: Можно использовать такие методы, как тест Граббса или z‑оценки, но при этом необходимо учитывать последовательный характер данных.

  • Визуализация: Построение временных рядов данных часто является лучшим первым шагом для визуальной проверки на наличие аномальных выбросов или провалов.

Я написала серию статей касающихся выбросов в данных временных рядов.

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

В третьей статье рассматриваются различные стратегии управления выбросами с учетом особых факторов для данных временных рядов:

Я надеюсь, что эти статьи станут для вас ценными инструментами в ваших исследованиях!

Заключение

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

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

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

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

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

Методы перекрестной проверки, такие как TimeSeriesSplit и прямая цепочка, гарантируют соблюдение временных зависимостей.

Конструирование признаков, включая создание лаговых и статистических признаков, позволяют нам фиксировать временные закономерности в данных.

Наконец, обнаружение и устранение выбросов делает прогнозы вашей модели надежными.

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

Оставайтесь любознательными и удачи в прогнозах!



Хотите научиться работать с временными рядами не только в теории, но и на практике? Тогда приходите на открытый урок «Методы уменьшения размерности: „Я беру камень и отсекаю всё лишнее“» — расскажем, как применять PCA, t‑SNE UMAP методы для сжатия данных, когда использовать линейные, а когда — нелинейные подходы.

А чтобы сразу понять, насколько вам подходит программа курса по ML уровня Pro — предлагаем пройти бесплатное тестирование.

Комментарии (3)


  1. hitlocker
    16.06.2025 13:20

    а часть 1, есть ссылка на нее? поиск по названию подсовывает что угодно но не часть 1. (спасибо)


  1. digtatordigtatorov
    16.06.2025 13:20

    Смахивает на базу, а не супер крутые техники, которые должен знать каждый


  1. mephastopheles
    16.06.2025 13:20

    "Темпоральные" признаки..вы серьезно?