Фаза 02 · урок 15

Основы временных рядов

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

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Основы машинного обучения
Чтение
20 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Чем временные ряды отличаются
  5. Компоненты временного ряда
  6. Стационарность
  7. Автокорреляция
  8. Лаговые признаки: превращаем временной ряд в обучение с учителем
  9. Walk-forward-валидация
  10. Интуиция ARIMA
  11. Когда что использовать
  12. Горизонты и стратегии прогнозирования
  13. Распространённые ошибки во временных рядах
  14. Соберите это
  15. Создатель лаговых признаков
  16. Walk-forward-кросс-валидация
  17. Простая авторегрессионная модель
  18. Проверка стационарности
  19. Автокорреляция
  20. Используйте это
  21. sklearn TimeSeriesSplit
  22. Метрики оценки
  23. Скользящие признаки
  24. Выпустите это
  25. Базовые решения, которые нужно превзойти
  26. Практические советы
  27. Упражнения
  28. Ключевые термины
  29. Дополнительное чтение

Прошлые результаты всё же предсказывают будущие — если сначала проверить стационарность.

Тип: Сборка Язык: Python Предварительные требования: Фаза 2, уроки 01–09 Время: ~90 минут

Цели обучения

  • Разложить временной ряд на компоненты тренда, сезонности и остатка, а также проверить стационарность
  • Реализовать признаки лагов и скользящую статистику, чтобы превратить временной ряд в задачу обучения с учителем
  • Построить схему walk-forward-валидации, не допускающую утечки будущих данных в обучение
  • Объяснить, почему случайные разбиения на обучение и тест неприменимы к временным рядам, и показать разрыв в качестве по сравнению с корректными временными разбиениями

Проблема

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

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

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

Модель с точностью 95% при случайной кросс-валидации может показать 55% при корректной оценке по времени. Это не техническая мелочь. Это разница между моделью, работающей на бумаге, и моделью, работающей в production.

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

Концепция

Чем временные ряды отличаются

Стандартное ML предполагает i.i.d. — независимость и одинаковое распределение. Каждое наблюдение извлекается из того же распределения независимо от остальных. Временные ряды нарушают оба условия:

  • Не независимы. Сегодняшняя цена акции зависит от вчерашней. Продажи этой недели коррелируют с продажами прошлой.
  • Не одинаково распределены. Распределение меняется со временем. Продажи в декабре отличаются от продаж в марте.

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

Диаграмма к уроку «Основы временных рядов»

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

Компоненты временного ряда

Каждый временной ряд является сочетанием:

Диаграмма к уроку «Основы временных рядов»

  • Тренд: долгосрочное направление. Выручка растёт на 10% в год. Глобальная температура повышается.
  • Сезонность: повторяющиеся паттерны с фиксированными интервалами. Розничные продажи взлетают в декабре. Использование кондиционеров достигает пика в июле.
  • Остаток: всё, что остаётся после удаления тренда и сезонности. Если остаток выглядит как белый шум, разложение захватило сигнал.

Стационарность

Временной ряд стационарен, если его статистические свойства (среднее, дисперсия, автокорреляция) не меняются со временем. Большинство методов прогнозирования предполагает стационарность.

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

Как проверить: вычислите скользящее среднее и скользящее стандартное отклонение по окнам. Если они дрейфуют, ряд нестационарен.

Как исправить: использовать разности. Вместо моделирования исходных значений моделируйте изменение между соседними значениями:

diff[t] = value[t] - value[t-1]

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

Пример:

Исходный ряд: [100, 102, 106, 112, 120] Первая разность: [2, 4, 6, 8] (всё ещё возрастает) Вторая разность: [2, 2, 2] (постоянна — стационарна)

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

Формальный тест: расширенный тест Дики—Фуллера (Augmented Dickey-Fuller, ADF) — стандартный статистический тест стационарности. Нулевая гипотеза: «ряд нестационарен». p-value ниже 0,05 означает, что можно отвергнуть нулевую гипотезу и заключить, что ряд стационарен. Мы не реализуем ADF с нуля (для него требуются таблицы асимптотических распределений), но подход со скользящей статистикой в нашем коде даёт практическую визуальную проверку.

Автокорреляция

Автокорреляция измеряет, насколько значение во время t коррелирует со значением во время t-k (k шагов в прошлом). Функция автокорреляции (ACF) отображает эту корреляцию для каждого лага k.

ACF сообщает вам:

  • Насколько далеко ряд помнит прошлое. Если ACF падает до нуля после лага 5, значения, лежащие более чем на 5 шагов в прошлом, несущественны.
  • Есть ли сезонность. Если ACF имеет всплеск на лаге 12 (месячные данные), присутствует годовая сезонность.
  • Сколько лаговых признаков создавать. Используйте лаги до момента, когда ACF становится пренебрежимо малой.

PACF (функция частичной автокорреляции) удаляет косвенные корреляции. Если сегодняшний день коррелирует с днём трёхдневной давности лишь потому, что оба коррелируют со вчерашним днём, PACF на лаге 3 будет равна нулю, а ACF на лаге 3 — нет.

Лаговые признаки: превращаем временной ряд в обучение с учителем

Стандартным ML-моделям нужны матрица признаков X и целевая переменная y. Временной ряд даёт вам один столбец значений. Мост между ними — лаговые признаки.

Возьмём ряд [10, 12, 14, 13, 15] и создадим признаки lag-1 и lag-2:

lag_2 lag_1 target
10 12 14
12 14 13
14 13 15

Теперь у вас стандартная задача регрессии. Любая ML-модель (линейная регрессия, случайный лес, градиентный бустинг) может предсказать target по лагам.

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

  • Скользящая статистика: mean, std, min, max по последним k значениям
  • Календарные признаки: день недели, месяц, is_holiday, is_weekend
  • Значения разностей: изменение относительно предыдущего шага
  • Расширяющаяся статистика: накопленное среднее, накопленная сумма
  • Отношения: текущее значение / скользящее среднее (насколько оно далеко от недавнего среднего)
  • Признаки взаимодействия: lag_1 * day_of_week (влияние буднего дня на импульс)

Сколько лагов? Используйте функцию автокорреляции. Если ACF значима до лага 10, используйте как минимум 10 лагов. При недельной сезонности включите лаг 7 (а возможно, и 14). Большее число лагов даёт модели больше истории, но также больше признаков для подгонки, увеличивая риск переобучения.

Ловушка выравнивания целевой переменной. При создании лаговых признаков целью должно быть значение в момент t, а все признаки должны использовать значения из t-1 или более раннего времени. Если вы случайно включите значение в t как признак, получите идеальный предиктор и совершенно бесполезную модель. Это самая распространённая ошибка при проектировании признаков для временных рядов.

Walk-forward-валидация

Это важнейшее понятие урока. Стандартная k-fold кросс-валидация случайным образом распределяет наблюдения по обучению и тесту. Для временных рядов это приводит к утечке информации из будущего.

Диаграмма к уроку «Основы временных рядов»

Walk-forward-валидация:

  1. Обучите модель на данных до момента t
  2. Предскажите значение в t+1 (или от t+1 до t+k при многошаговом прогнозе)
  3. Сдвиньте окно вперёд
  4. Повторите

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

Расширяющееся окно использует для обучения все исторические данные (окно растёт). Скользящее окно использует окно обучения фиксированного размера (окно сдвигается). Используйте расширяющееся окно, если считаете старые данные всё ещё релевантными. Используйте скользящее, если мир меняется и старые данные ухудшают результат.

Интуиция ARIMA

ARIMA — классическая модель временных рядов. Она состоит из трёх компонентов:

  • AR (авторегрессия): предсказывает по прошлым значениям. AR(p) использует последние p значений.
  • I (интегрирование): разности для достижения стационарности. I(d) применяет d проходов разностей.
  • MA (скользящее среднее): предсказывает по прошлым ошибкам прогноза. MA(q) использует последние q ошибок.

ARIMA(p, d, q) объединяет все три. Вы выбираете p, d, q на основе анализа ACF/PACF или автоматического поиска (auto-ARIMA).

Мы не будем реализовывать ARIMA с нуля: для неё требуется численная оптимизация, выходящая за рамки урока. Ключевая идея — понять, что делает каждый компонент, чтобы интерпретировать результаты ARIMA и знать, когда её применять.

Когда что использовать

Подход Лучше всего подходит для Обрабатывает сезонность Обрабатывает внешние признаки
Лаговые признаки + ML Табличные данные со множеством внешних признаков С календарными признаками Да
ARIMA Один одномерный ряд, краткосрочный прогноз Вариант SARIMA Нет (ограниченно — ARIMAX)
Экспоненциальное сглаживание Простой тренд + сезонность Да (Holt-Winters) Нет
Prophet Бизнес-прогнозирование, праздники Да (ряды Фурье) Ограниченно
Нейросети (LSTM, Transformer) Длинные последовательности, много рядов Обучается Да

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

Горизонты и стратегии прогнозирования

Одношаговое прогнозирование предсказывает на один временной шаг вперёд. Многошаговое — на несколько. Существуют три стратегии:

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

Прямая: обучите отдельную модель для каждого горизонта. Модель-1 предсказывает t+1, модель-5 — t+5. Ошибки не накапливаются, но у каждой модели меньше обучающих примеров, и они не обмениваются информацией.

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

Для большинства практических задач начинайте с рекурсивной стратегии для коротких горизонтов (1–5 шагов) и прямой — для длинных.

Распространённые ошибки во временных рядах

Ошибка Почему возникает Как исправить
Случайное разбиение на обучение и тест Привычка из стандартного ML Использовать walk-forward или временное разбиение
Использование будущих признаков Признак в момент t включён по ошибке Проверить временное выравнивание каждого признака
Переобучение на сезонности Модель запоминает календарные паттерны Отложить для теста полный сезонный цикл
Игнорирование изменения масштаба Выручка удваивается, но паттерны сохраняются Моделировать процентное изменение, а не абсолютное
Слишком много лаговых признаков «Чем больше истории, тем лучше» Использовать ACF для определения важных лагов
Отсутствие разностей «Модель сама разберётся» Деревья обрабатывают тренды; линейным моделям нужна стационарность

Соберите это

Код в code/time_series.py реализует основные строительные блоки с нуля.

Создатель лаговых признаков

def make_lag_features(series, n_lags):
    n = len(series)
    X = np.full((n, n_lags), np.nan)
    for lag in range(1, n_lags + 1):
        X[lag:, lag - 1] = series[:-lag]
    valid = ~np.isnan(X).any(axis=1)
    return X[valid], series[valid]

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

Walk-forward-кросс-валидация

def walk_forward_split(n_samples, n_splits=5, min_train=50):
    assert min_train < n_samples, "min_train must be less than n_samples"
    step = max(1, (n_samples - min_train) // n_splits)
    for i in range(n_splits):
        train_end = min_train + i * step
        test_end = min(train_end + step, n_samples)
        if train_end >= n_samples:
            break
        yield slice(0, train_end), slice(train_end, test_end)

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

Простая авторегрессионная модель

Чистая AR-модель — это просто линейная регрессия на лаговых признаках:

class SimpleAR:
    def __init__(self, n_lags=5):
        self.n_lags = n_lags
        self.weights = None
        self.bias = None

    def fit(self, series):
        X, y = make_lag_features(series, self.n_lags)
        # Solve via normal equations
        X_b = np.column_stack([np.ones(len(X)), X])
        theta = np.linalg.lstsq(X_b, y, rcond=None)[0]
        self.bias = theta[0]
        self.weights = theta[1:]
        return self

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

Проверка стационарности

Код вычисляет скользящую статистику для визуальной и численной оценки стационарности:

def check_stationarity(series, window=50):
    rolling_mean = np.array([
        series[max(0, i - window):i].mean()
        for i in range(1, len(series) + 1)
    ])
    rolling_std = np.array([
        series[max(0, i - window):i].std()
        for i in range(1, len(series) + 1)
    ])
    return rolling_mean, rolling_std

Если скользящее среднее дрейфует или скользящее стандартное отклонение меняется, ряд нестационарен. Примените разности и проверьте снова.

Код также проверяет стационарность, сравнивая первую и вторую половины ряда. Если средние отличаются более чем на половину стандартного отклонения или отношение дисперсий превышает 2x, ряд помечается как нестационарный.

Автокорреляция

def autocorrelation(series, max_lag=20):
    n = len(series)
    mean = series.mean()
    var = series.var()
    acf = np.zeros(max_lag + 1)
    for k in range(max_lag + 1):
        cov = np.mean((series[:n-k] - mean) * (series[k:] - mean))
        acf[k] = cov / var if var > 0 else 0
    return acf

Используйте это

С sklearn вы используете лаговые признаки напрямую с любым регрессором:

from sklearn.linear_model import Ridge
from sklearn.ensemble import GradientBoostingRegressor

X, y = make_lag_features(series, n_lags=10)

for train_idx, test_idx in walk_forward_split(len(X)):
    model = Ridge(alpha=1.0)
    model.fit(X[train_idx], y[train_idx])
    predictions = model.predict(X[test_idx])

Для ARIMA используйте statsmodels:

from statsmodels.tsa.arima.model import ARIMA

model = ARIMA(train_series, order=(5, 1, 2))
fitted = model.fit()
forecast = fitted.forecast(steps=30)

Код в time_series.py демонстрирует оба подхода и сравнивает их с помощью walk-forward-валидации.

sklearn TimeSeriesSplit

sklearn предоставляет TimeSeriesSplit, реализующий walk-forward-валидацию:

from sklearn.model_selection import TimeSeriesSplit

tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X):
    X_train, X_test = X[train_index], X[test_index]
    y_train, y_test = y[train_index], y[test_index]
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)

Это эквивалент нашей реализации walk_forward_split с нуля, но встроенный в инфраструктуру кросс-валидации sklearn. Его можно использовать с cross_val_score:

from sklearn.model_selection import cross_val_score

scores = cross_val_score(model, X, y, cv=TimeSeriesSplit(n_splits=5))
print(f"Mean score: {scores.mean():.4f} +/- {scores.std():.4f}")

Метрики оценки

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

  • MAE (средняя абсолютная ошибка): среднее |y_true - y_pred|. Легко интерпретировать в исходных единицах: «в среднем предсказания отклоняются на 3,2 градуса».
  • RMSE (корень из средней квадратичной ошибки): квадратный корень из средней квадратичной ошибки. Штрафует большие ошибки сильнее MAE. Используйте, когда крупные ошибки хуже множества маленьких.
  • MAPE (средняя абсолютная процентная ошибка): среднее |error / true_value| * 100. Не зависит от масштаба, полезна для сравнения разных рядов. Но не определена, когда истинные значения равны нулю.
  • Сравнение с наивным baseline: всегда сравнивайте с простыми базовыми решениями. Сезонный наивный baseline предсказывает значение одного периода назад (вчера, на прошлой неделе). Если ваша модель не превосходит наивную, что-то не так.

Скользящие признаки

Код демонстрирует добавление скользящей статистики (mean, std, min, max по окнам в 7 и 14 дней) к лаговым признакам. Она даёт модели информацию о недавних трендах и волатильности, которую одни лаговые признаки не захватывают.

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

Выпустите это

Этот урок создаёт:

  • outputs/prompt-time-series-advisor.md — промпт для постановки задач временных рядов
  • code/time_series.py — лаговые признаки, walk-forward-валидацию, AR-модель, проверки стационарности

Базовые решения, которые нужно превзойти

Перед построением любой модели определите baselines:

  1. Последнее значение (persistence). Предскажите, что завтра будет таким же, как сегодня. Для многих рядов это удивительно трудно превзойти.
  2. Сезонное наивное решение. Предскажите, что сегодня будет таким же, как тот же день на прошлой неделе (или в прошлом году). Если модель не превосходит его, она не выучила никакого полезного паттерна сверх сезонности.
  3. Скользящее среднее. Предскажите среднее последних k значений. Оно сглаживает шум, но не может уловить резкие изменения.

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

Практические советы

  1. Начинайте с графика. До любого моделирования постройте исходный ряд. Ищите тренды, сезонность, выбросы, структурные разрывы (резкие изменения поведения). Визуальная проверка за 30 секунд часто сообщает больше, чем час автоматического анализа.

  2. Сначала разности, потом модель. Если у ряда есть явный тренд, возьмите разности до создания лаговых признаков. Древовидные модели могут обрабатывать тренды, но линейные — нет, а разности никогда не вредят.

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

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

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

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

Упражнения

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

  2. Выбор лагов. Вычислите ACF для сезонного ряда (period=7). У каких лагов самая высокая автокорреляция? Создайте лаговые признаки, используя только эти лаги (а не последовательные лаги). Улучшается ли точность по сравнению с использованием лагов от 1 до 7?

  3. Walk-forward против случайного разбиения. Обучите Ridge-регрессию на лаговых признаках. Оцените её со случайным разбиением 80/20 и с walk-forward-валидацией. Насколько случайное разбиение завышает качество?

  4. Проектирование признаков. Добавьте к лаговым признакам скользящее среднее (window=7), скользящее std (window=7) и признаки дня недели. Сравните точность с этими дополнениями и без них, используя walk-forward-валидацию.

  5. Многошаговое прогнозирование. Измените AR-модель так, чтобы она предсказывала на 5 шагов вперёд вместо 1. Сравните две стратегии: (a) предсказывать один шаг и использовать предсказание как вход для следующего (рекурсивно) и (b) обучать отдельные модели для каждого горизонта (прямо). Какая точнее?

Ключевые термины

Термин Как обычно говорят Что это означает на самом деле
Стационарность «Статистики не меняются со временем» Ряд, у которого среднее, дисперсия и структура автокорреляции постоянны во времени
Разности «Вычесть последовательные значения» Вычисление y[t] - y[t-1] для удаления трендов и достижения стационарности
Автокорреляция (ACF) «Как ряд коррелирует сам с собой» Корреляция между временным рядом и его копией, сдвинутой по времени, как функция лага
Частичная автокорреляция (PACF) «Только прямая корреляция» Автокорреляция на лаге k после удаления эффекта всех более коротких лагов
Лаговые признаки «Прошлые значения как входы» Использование y[t-1], y[t-2], …, y[t-k] как признаков для предсказания y[t]
Walk-forward-валидация «Кросс-валидация с учётом времени» Оценка, где обучающие данные всегда хронологически предшествуют тестовым
ARIMA «Классическая модель временных рядов» AutoRegressive Integrated Moving Average: сочетает прошлые значения (AR), разности (I) и прошлые ошибки (MA)
Сезонность «Повторяющиеся календарные паттерны» Регулярные предсказуемые циклы временного ряда, привязанные к календарным периодам (день, неделя, год)
Тренд «Долгосрочное направление» Устойчивый рост или снижение уровня ряда
Расширяющееся окно «Использовать всю историю» Walk-forward-валидация, где обучающий набор растёт с каждым фолдом
Скользящее окно «История фиксированного размера» Walk-forward-валидация, где обучающий набор — окно фиксированной длины, сдвигающееся вперёд

Дополнительное чтение


Источник: Time Series Fundamentals 02.14 — Наивный Байес · Фаза 2 — Основы машинного обучения · Полный каталог · 02.16 — Обнаружение аномалий