Библиотека sktime для анализа временных рядов

Библиотека для Python sktime предлагает унифицированный интерфейс для различных задач обработки временных рядов, включая классификацию, регрессию, кластеризацию, прогнозирование и многое другое. Что делает ее по-настоящему уникальной — это интеграция с экосистемой scikit-learn, что позволяет использовать привычные паттерны и методологии в контексте временных рядов.

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

Почему временные ряды требуют специализированных инструментов

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

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

Наличие временной составляющей

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

Временные ряды часто демонстрируют сложные паттерны

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

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

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

Временные ряды часто требуют специфической предобработки

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

Именно здесь на сцену выходит sktime — библиотека, созданная специально для решения всех этих проблем в рамках единого, последовательного API, совместимого с экосистемой scikit-learn.

Архитектура и философия sktime

Библиотека sktime была разработана с четкой философией, которая отличает ее от других инструментов для анализа временных рядов. Ключевая идея заключается в создании унифицированного интерфейса для различных задач, связанных с временными рядами, при сохранении совместимости с экосистемой scikit-learn.

Модульная структура и композиция алгоритмов

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

  • Прогнозирование (forecasting) — для предсказания будущих значений временных рядов;
  • Классификация (classification) — для отнесения временных рядов к предопределенным классам;
  • Регрессия (regression) — для предсказания непрерывных целевых переменных на основе временных рядов;
  • Трансформация (transformation) — для предобработки и преобразования временных рядов;
  • Композиция (composition) — для создания сложных пайплайнов обработки данных

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

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

Интеграция с экосистемой scikit-learn

Другим ключевым принципом sktime является тесная интеграция с scikit-learn. Это проявляется в нескольких аспектах:

  • Совместимый API — интерфейсы классов в sktime следуют той же парадигме, что и в scikit-learn, с методами fit(), predict(), transform() и т.д;
  • Поддержка пайплайнов — модели sktime можно включать в пайплайны scikit-learn, что позволяет создавать комплексные рабочие потоки;
  • Совместимые оценки качества — метрики оценки качества моделей в sktime совместимы с аналогичными метриками в scikit-learn;
  • Адаптеры — sktime предоставляет адаптеры для использования алгоритмов scikit-learn в контексте временных рядов.

Такая интеграция позволяет специалистам, уже знакомым с scikit-learn, быстро освоить sktime и эффективно использовать его возможности.

Расширяемость и поддержка сторонних библиотек

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

Кроме того, sktime включает адаптеры для интеграции с другими популярными библиотеками для анализа временных рядов, такими как:

  • Prophet;
  • statsmodels;
  • tbats;
  • pmdarima (для моделей ARIMA);
  • PyTorch (для нейронных сетей).

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

Установка и начало работы с sktime

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

Установка и зависимости

Установка sktime стандартна для большинства библиотек Python и может быть выполнена через pip:

pip install sktime

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

Например, для использования алгоритмов глубокого обучения необходимо установить PyTorch:

pip install sktime[dl]

Для работы с моделями Prophet:

pip install sktime[prophet]

Для доступа ко всем возможностям библиотеки:

pip install sktime[all_extras]

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

Структура данных в sktime

Sktime работает с временными рядами, представленными в различных форматах. Основные форматы данных, поддерживаемые библиотекой:

  • Одномерные временные ряды — обычно представлены как pandas.Series с временным индексом;
  • Многомерные (многовариантные) временные ряды — представлены как pandas.DataFrame с временным индексом;
  • Панельные данные — коллекции временных рядов, обычно представленные как вложенные pandas.DataFrame

Рассмотрим пример загрузки и подготовки данных для работы с sktime:

import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import matplotlib.pyplot as plt
import yfinance as yf
pd.set_option('display.expand_frame_repr', False)

# Загрузка данных для VIX (индекс волатильности)
vix_ticker = yf.Ticker("^VIX")
vix_data = vix_ticker.history(period="3y")
# Преобразуем в формат, удобный для sktime
data = vix_data['Close'].rename('VIX')

# Оставляем только последние 3 года данных
data = data[-756:]  # ~756 торговых дней за 3 года

plt.figure(figsize=(12, 6))
plt.plot(data.index, data.values, color='black')
plt.title('Индекс волатильности VIX за последние 3 года')
plt.xlabel('Дата')
plt.ylabel('Значение индекса')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

print(f"Форма данных: {data.shape}")
print(f"Тип данных: {type(data)}")
print(f"Пример данных:\n{data.head()}")

Динамика индекса волатильности VIX за последние 3 года

Рис. 1: Динамика индекса волатильности VIX за последние 3 года

Форма данных: (752,)
Тип данных: <class 'pandas.core.series.Series'>
Пример данных:
Date
2022-07-07 00:00:00-05:00    26.080000
2022-07-08 00:00:00-05:00    24.639999
2022-07-11 00:00:00-05:00    26.170000
2022-07-12 00:00:00-05:00    27.290001
2022-07-13 00:00:00-05:00    26.820000
Name: VIX, dtype: float64

В этом примере мы загружаем данные индекса волатильности VIX с помощью Yahoo Finance API. Это одномерный временной ряд, представленный как pandas.Series с датами в качестве индекса. Такой формат данных идеально подходит для работы с sktime.

👉🏻  Расчет показателей доходности и риска биржевой торговли на Python

Базовые концепции и терминология

Для эффективной работы с sktime необходимо понимать несколько ключевых концепций:

  • Задачи (Tasks) — определяют тип проблемы, которую мы пытаемся решить. Основные типы задач: Прогнозирование (Forecasting) — предсказание будущих значений временного ряда, Классификация (Classification) — отнесение временных рядов к категориям, Регрессия (Regression) — предсказание непрерывных значений на основе временных рядов.
  • Алгоритмы (Algorithms) — методы решения конкретных задач. Например, ARIMA для прогнозирования или DTW для классификации.
  • Трансформеры (Transformers) — преобразуют временные ряды в другие временные ряды или признаки.
  • Стратегии разделения данных (Splitters) — определяют, как временные ряды разделяются на обучающие и тестовые выборки с учетом их временной структуры.
  • Метрики (Metrics) — функции для оценки качества моделей.

Пример простого рабочего процесса с использованием sktime:

from sktime.forecasting.model_selection import temporal_train_test_split
from sktime.forecasting.naive import NaiveForecaster
from sktime.forecasting.base import ForecastingHorizon
from sktime.performance_metrics.forecasting import mean_absolute_percentage_error

# Переиндексируем данные на регулярную сетку биржевых дней
business_days = pd.bdate_range(start=data.index[0], end=data.index[-1])
data = data.reindex(business_days, method='ffill').dropna()
data.index.freq = 'B'

# Разделение на обучающую и тестовую выборки
y_train, y_test = temporal_train_test_split(data, test_size=0.2)

# Создание горизонта прогнозирования
fh = ForecastingHorizon(y_test.index, is_relative=False)

# Инициализация модели
forecaster = NaiveForecaster(strategy="last")

# Обучение модели
forecaster.fit(y_train)

# Прогнозирование
y_pred = forecaster.predict(fh)

# Оценка качества
mape = mean_absolute_percentage_error(y_test, y_pred)
print(f"MAPE: {mape:.4f}")

# Визуализация результатов
plt.figure(figsize=(12, 6))
plt.plot(y_train.index, y_train.values, label='Тренировочные данные', color='black')
plt.plot(y_test.index, y_test.values, label='Тестовые данные', color='darkgray')
plt.plot(y_pred.index, y_pred.values, label='Прогноз', color='red', linestyle='--')
plt.title('Наивный прогноз индекса VIX')
plt.xlabel('Дата')
plt.ylabel('Значение индекса')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

MAPE: 0.2567

Наивный прогноз индекса VIX с помощью sktime

Рис. 2: Наивный прогноз индекса VIX с помощью sktime

В этом примере мы:

  1. Разделили временной ряд на обучающую и тестовую выборки с учетом временной структуры;
  2. Создали горизонт прогнозирования, соответствующий тестовой выборке;
  3. Инициализировали и обучили простую наивную модель прогнозирования;
  4. Выполнили прогноз и оценили его качество с помощью метрики MAPE (показывающую % ошибок модели);
  5. Визуализировали результаты.

Обращаю ваше внимание на использование pd.bdate_range() для создания регулярной сетки биржевых дней и переиндексация данных на эту сетку — это стандартный подход при работе с финансовыми временными рядами в sktime.

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

Предобработка и трансформация временных рядов

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

Обработка пропущенных значений

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

import pandas as pd
import numpy as np
from sktime.transformations.series.impute import Imputer

# Создадим временной ряд с пропущенными значениями
dates = pd.date_range(start='2023-01-01', periods=100, freq='D')
values = np.sin(np.linspace(0, 10, 100)) + np.random.normal(0, 0.1, 100)
ts = pd.Series(values, index=dates)

# Внесем пропуски случайным образом
random_indices = np.random.choice(range(len(ts)), size=15, replace=False)
ts.iloc[random_indices] = np.nan

# Импутация с использованием различных стратегий
imputer_mean = Imputer(method='mean')
imputer_median = Imputer(method='median')
imputer_linear = Imputer(method='linear')
imputer_nearest = Imputer(method='nearest')
imputer_forward = Imputer(method='ffill')

# Применение импутеров
ts_mean = imputer_mean.fit_transform(ts)
ts_median = imputer_median.fit_transform(ts)
ts_linear = imputer_linear.fit_transform(ts)
ts_nearest = imputer_nearest.fit_transform(ts)
ts_forward = imputer_forward.fit_transform(ts)

# Визуализация результатов
plt.figure(figsize=(12, 8))

# Исходные данные с пропусками
plt.subplot(3, 2, 1)
plt.plot(ts.index, ts.values, 'o-', color='black')
plt.title('Исходный ряд с пропусками')
plt.grid(True, alpha=0.3)

# Заполнение средним
plt.subplot(3, 2, 2)
plt.plot(ts_mean.index, ts_mean.values, 'o-', color='darkgray')
plt.title('Импутация средним')
plt.grid(True, alpha=0.3)

# Заполнение медианой
plt.subplot(3, 2, 3)
plt.plot(ts_median.index, ts_median.values, 'o-', color='darkgray')
plt.title('Импутация медианой')
plt.grid(True, alpha=0.3)

# Линейная интерполяция
plt.subplot(3, 2, 4)
plt.plot(ts_linear.index, ts_linear.values, 'o-', color='darkgray')
plt.title('Линейная интерполяция')
plt.grid(True, alpha=0.3)

# Метод ближайшего соседа
plt.subplot(3, 2, 5)
plt.plot(ts_nearest.index, ts_nearest.values, 'o-', color='darkgray')
plt.title('Метод ближайшего соседа')
plt.grid(True, alpha=0.3)

# Forward fill
plt.subplot(3, 2, 6)
plt.plot(ts_forward.index, ts_forward.values, 'o-', color='darkgray')
plt.title('Forward Fill')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Сравнение методов импутации пропущенных значений во временных рядах (заменой средним, медианой, линейная интерполяция, метод ближайшего соседа, forward fill)

Рис. 3: Сравнение методов импутации пропущенных значений во временных рядах (заменой средним, медианой, линейная интерполяция, метод ближайшего соседа, forward fill)

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

  • Метод среднего/медианы прост, но не учитывает временную структуру;
  • Линейная интерполяция предполагает линейное изменение между соседними точками;
  • Метод ближайшего соседа копирует ближайшее известное значение.

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

Ресемплинг и агрегация

Часто временные ряды необходимо преобразовать к другой частоте — либо уменьшить (downsampling), либо увеличить (upsampling). Sktime предоставляет инструменты для этой задачи:

import yfinance as yf
import pandas as pd

# Загрузим дневные данные с Yahoo Finance
unh_ticker = yf.Ticker("UNH")  # UnitedHealth Group
unh_data = unh_ticker.history(period="1y")
# Преобразуем в формат, удобный для анализа
daily_data = unh_data['Close'].rename('UNH')

# Запускаем ресемплинг
weekly_data = daily_data.resample('W').mean()
monthly_data = daily_data.resample('M').mean()

# Визуализация
plt.figure(figsize=(12, 8))

plt.subplot(3, 1, 1)
plt.plot(daily_data.index, daily_data.values, color='black')
plt.title('Акции UnitedHealth Group - Дневные данные')
plt.grid(True, alpha=0.3)

plt.subplot(3, 1, 2)
plt.plot(weekly_data.index, weekly_data.values, color='darkgray')
plt.title('Акции UnitedHealth Group - Недельные данные (среднее)')
plt.grid(True, alpha=0.3)

plt.subplot(3, 1, 3)
plt.plot(monthly_data.index, monthly_data.values, color='darkgray')
plt.title('Акции UnitedHealth Group - Месячные данные (среднее)')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# Демонстрация различных методов агрегации
aggregation_methods = ['mean', 'median', 'min', 'max', 'first', 'last']
plt.figure(figsize=(14, 10))

for i, method in enumerate(aggregation_methods):
    if method == 'mean':
        resampled_data = daily_data.resample('W').mean()
    elif method == 'median':
        resampled_data = daily_data.resample('W').median()
    elif method == 'min':
        resampled_data = daily_data.resample('W').min()
    elif method == 'max':
        resampled_data = daily_data.resample('W').max()
    elif method == 'first':
        resampled_data = daily_data.resample('W').first()
    elif method == 'last':
        resampled_data = daily_data.resample('W').last()
    
    plt.subplot(3, 2, i+1)
    plt.plot(resampled_data.index, resampled_data.values, color='darkgray')
    plt.title(f'Недельная агрегация ({method})')
    plt.grid(True, alpha=0.3)

plt.suptitle('Сравнение методов агрегации при ресемплинге временных рядов', 
             fontsize=14, fontweight='bold', y=0.98)
plt.tight_layout()
plt.show()

Сравнение различных временных масштабов для акций UnitedHealth Group (UNH). Показано преобразование дневных данных в недельные и месячные с использованием среднего значения

Рис. 4: Сравнение различных временных масштабов для акций UnitedHealth Group (UNH). Показано преобразование дневных данных в недельные и месячные с использованием среднего значения

Влияние различных методов агрегации на недельный ресемплинг дневных данных акций UNH

Рис. 5: Влияние различных методов агрегации на недельный ресемплинг дневных данных акций UNH

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

  1. Уменьшения шума в данных;
  2. Выявления долгосрочных трендов;
  3. Снижения вычислительной сложности при работе с длинными временными рядами;
  4. Согласования временных рядов с разной частотой для совместного анализа.

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

Выделение и инжиниринг признаков

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

import yfinance as yf
import pandas as pd
import numpy as np
from sktime.transformations.series.detrend import Detrender
from sktime.transformations.series.difference import Differencer
from sktime.transformations.series.exponent import ExponentTransformer

# Подготовим данные с Yahoo Finance
gs_ticker = yf.Ticker("GS")  # Акции Goldman Sachs
gs_data = gs_ticker.history(period="1y")
ts_data = gs_data['Close'].rename('GS')

# Устанавливаем частоту для корректной работы с sktime
business_days = pd.bdate_range(start=ts_data.index[0], end=ts_data.index[-1])
ts_data = ts_data.reindex(business_days, method='ffill').dropna()
ts_data.index.freq = 'B'

# 1. Извлечение статистических признаков
print("Статистические признаки:")
stats = {
    'mean': ts_data.mean(),
    'std': ts_data.std(),
    'min': ts_data.min(),
    'max': ts_data.max(),
    'median': ts_data.median(),
    'skew': ts_data.skew(),
    'kurtosis': ts_data.kurtosis()
}
for key, value in stats.items():
    print(f"{key}: {value:.4f}")

# 2. Удаление тренда
from sktime.forecasting.trend import PolynomialTrendForecaster
detrend_transformer = Detrender(forecaster=PolynomialTrendForecaster(degree=1))
detrended = detrend_transformer.fit_transform(ts_data)

# 3. Дифференцирование
diff_transformer = Differencer(lags=1)
differenced = diff_transformer.fit_transform(ts_data)

# 4. Логарифмирование
log_ts = pd.Series(np.log(ts_data), index=ts_data.index, name='log_GS')

# 5. Создание лаговых признаков
def create_lag_features(ts, lags=[1, 5, 10, 21]):
    """Создание лаговых признаков"""
    result = pd.DataFrame(index=ts.index)
    result['original'] = ts
    
    for lag in lags:
        result[f'{ts.name}_lag_{lag}'] = ts.shift(lag)
    
    return result

lagged_features = create_lag_features(ts_data, lags=[1, 5, 10, 21])

# Визуализация
plt.figure(figsize=(14, 15))

plt.subplot(5, 1, 1)
plt.plot(ts_data.index, ts_data.values, color='black')
plt.title('Исходный временной ряд')
plt.grid(True, alpha=0.3)

plt.subplot(5, 1, 2)
plt.plot(detrended.index, detrended.values, color='darkgray')
plt.title('Временной ряд после удаления линейного тренда')
plt.grid(True, alpha=0.3)

plt.subplot(5, 1, 3)
plt.plot(differenced.index, differenced.values, color='darkgray')
plt.title('Первая разность (дифференцирование)')
plt.grid(True, alpha=0.3)

plt.subplot(5, 1, 4)
plt.plot(log_ts.index, log_ts.values, color='darkgray')
plt.title('Логарифмированный ряд')
plt.grid(True, alpha=0.3)

plt.subplot(5, 1, 5)
# Выберем несколько лаговых признаков для визуализации
lag_cols = ['GS_lag_1', 'GS_lag_5', 'GS_lag_21']
for col in lag_cols:
    if col in lagged_features.columns:
        plt.plot(lagged_features.index, lagged_features[col], label=col)
plt.title('Лаговые признаки')
plt.grid(True, alpha=0.3)
plt.legend()

plt.tight_layout()
plt.show()

# Создание признаков скользящих средних
def add_moving_averages(ts, windows=[5, 10, 20, 50]):
    result = pd.DataFrame(index=ts.index)
    # Добавляем исходный ряд
    result['original'] = ts
    
    # Добавляем скользящие средние
    for window in windows:
        result[f'ma_{window}'] = ts.rolling(window=window).mean()
    
    return result

# Применение к нашим данным
ma_features = add_moving_averages(ts_data)

# Визуализация скользящих средних
plt.figure(figsize=(12, 6))
plt.plot(ma_features.index, ma_features['original'], color='black', label='Исходный ряд', linewidth=1)
colors = ['blue', 'green', 'red', 'orange']
for i, col in enumerate(ma_features.columns[1:]):
    plt.plot(ma_features.index, ma_features[col], label=col, color=colors[i], alpha=0.8)
plt.title('Скользящие средние разных периодов')
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.show()

# Дополнительные технические индикаторы
def create_technical_features(ts):

    result = pd.DataFrame(index=ts.index)
    result['price'] = ts
    
    # Простые скользящие средние
    result['sma_5'] = ts.rolling(5).mean()
    result['sma_20'] = ts.rolling(20).mean()
    
    # Экспоненциальные скользящие средние
    result['ema_5'] = ts.ewm(span=5).mean()
    result['ema_20'] = ts.ewm(span=20).mean()
    
    # Волатильность (стандартное отклонение)
    result['volatility_10'] = ts.rolling(10).std()
    result['volatility_20'] = ts.rolling(20).std()
    
    # Относительные изменения
    result['returns_1d'] = ts.pct_change(1)
    result['returns_5d'] = ts.pct_change(5)
    
    # RSI (упрощенная версия)
    delta = ts.diff()
    gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
    loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() rs = gain / loss result['rsi'] = 100 - (100 / (1 + rs)) return result # Создание технических признаков tech_features = create_technical_features(ts_data) print("\nПример технических признаков (последние 5 значений):") print(tech_features[['price', 'sma_20', 'ema_20', 'volatility_20', 'returns_1d', 'rsi']].tail()) # Корреляционная матрица признаков correlation_features = ['price', 'sma_5', 'sma_20', 'ema_5', 'ema_20', 'volatility_20', 'rsi'] corr_matrix = tech_features[correlation_features].corr() plt.figure(figsize=(10, 8)) import matplotlib.pyplot as plt plt.imshow(corr_matrix, cmap='coolwarm', aspect='auto') plt.colorbar() plt.xticks(range(len(correlation_features)), correlation_features, rotation=45) plt.yticks(range(len(correlation_features)), correlation_features) plt.title('Корреляционная матрица технических индикаторов') # Добавляем значения корреляции в ячейки for i in range(len(correlation_features)): for j in range(len(correlation_features)): plt.text(j, i, f'{corr_matrix.iloc[i, j]:.2f}', ha='center', va='center', color='white' if abs(corr_matrix.iloc[i, j]) > 0.5 else 'black')

plt.tight_layout()
plt.show()
Статистические признаки:
mean: 554.5408
std: 61.6577
min: 449.5894
max: 723.6800
median: 560.0982
skew: 0.2546
kurtosis: -0.7999

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

Рис. 6: Различные виды преобразований временного ряда акций Goldman Sachs. Показаны исходные данные, детрендированный ряд, первая разность, логарифмированные данные и лаговые признаки

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

Рис. 7: Сравнение скользящих средних различных периодов для акций Goldman Sachs. Более длинные периоды дают более сглаженные кривые, отражающие долгосрочные тренды, однако это достигается за счет большего запаздывания

Пример технических признаков (последние 5 значений):
                                price      sma_20      ema_20  volatility_20  returns_1d        rsi
2025-06-27 00:00:00-04:00  690.809998  631.672495  638.564775      27.432336    0.005312  86.167543
2025-06-30 00:00:00-04:00  707.750000  637.123996  645.153844      31.124258    0.024522  87.928802
2025-07-01 00:00:00-04:00  706.460022  642.255496  650.992528      33.699929   -0.001823  85.956518
2025-07-02 00:00:00-04:00  715.890015  648.089496  657.173241      35.884637    0.013348  86.926492
2025-07-03 00:00:00-04:00  723.679993  653.979495  663.507217      38.185749    0.010882  96.222979

Корреляционная матрица технических индикаторов. Заметна сильная корреляция между ценой и скользящими среднии (SMA, EMA) и низкая с индикаторами волатильности и RSI

Рис. 8: Корреляционная матрица технических индикаторов. Заметна сильная корреляция между ценой и скользящими среднии (SMA, EMA) и низкая с индикаторами волатильности и RSI

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

  1. Извлечение статистических признаков — полезно для сжатия информации о ряде в набор скалярных значений;
  2. Удаление тренда (детрендирование) — помогает сделать ряд стационарным;
  3. Дифференцирование — также способствует стационарности;
  4. Логарифмирование — стабилизирует дисперсию и линеаризует экспоненциальный рост;
  5. Создание лаговых признаков — позволяет модели учитывать исторические значения на разных временных горизонтах;
  6. Скользящие средние (SMA, EMA) — сглаживают краткосрочные колебания и выявляют долгосрочные тренды;
  7. Технические индикаторы (волатильность, доходность, RSI) — предоставляют дополнительную информацию о рыночной динамике;
  8. Корреляционный анализ — помогает выявить взаимосвязи между различными признаками и избежать мультиколлинеарности
👉🏻  MSE, RMSE, MAE, MAPE для оценки качества прогнозов временных рядов

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

Прогнозирование временных рядов с использованием sktime

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

Базовые модели прогнозирования

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

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
from sktime.forecasting.naive import NaiveForecaster
from sktime.forecasting.trend import PolynomialTrendForecaster
from sktime.forecasting.theta import ThetaForecaster
from sktime.forecasting.model_selection import temporal_train_test_split
from sktime.forecasting.base import ForecastingHorizon
from sktime.performance_metrics.forecasting import mean_absolute_percentage_error, mean_squared_error

# Загрузка данных
voo_ticker = yf.Ticker("VOO")  # Vanguard S&P 500 ETF
voo_data = voo_ticker.history(period="2y")
ts_data = voo_data['Close'].rename('VOO')

# Устанавливаем частоту для корректной работы с sktime
daily_range = pd.date_range(start=ts_data.index[0], end=ts_data.index[-1], freq='D')
ts_data = ts_data.reindex(daily_range, method='ffill').dropna()
ts_data.index.freq = 'D'

# Разделение на обучающую и тестовую выборки
y_train, y_test = temporal_train_test_split(ts_data, test_size=63)  # Примерно 3 месяца на тест
fh = ForecastingHorizon(y_test.index, is_relative=False)

# Инициализация моделей
naive_last = NaiveForecaster(strategy="last")  # Прогноз равен последнему известному значению
naive_mean = NaiveForecaster(strategy="mean")  # Прогноз равен среднему всех значений
naive_drift = NaiveForecaster(strategy="drift")  # Линейная экстраполяция последних двух точек
poly_trend = PolynomialTrendForecaster(degree=2)  # Полиномиальный тренд степени 2
theta = ThetaForecaster()  # Модель Theta, комбинирующая наивный прогноз с детрендированием

# Обучение моделей и получение прогнозов
models = {
    "Последнее значение": naive_last,
    "Среднее": naive_mean,
    "Дрейф": naive_drift,
    "Полиномиальный тренд": poly_trend,
    "Theta": theta
}

forecasts = {}
metrics = {}

for name, model in models.items():
    model.fit(y_train)
    forecasts[name] = model.predict(fh)
    metrics[name] = {
        "MAPE": mean_absolute_percentage_error(y_test, forecasts[name]),
        "MSE": mean_squared_error(y_test, forecasts[name])
    }

# Вывод метрик качества
metrics_df = pd.DataFrame(metrics).T
print("Метрики качества прогнозов:")
print(metrics_df)

# Визуализация результатов
plt.figure(figsize=(14, 8))
plt.plot(y_train.index, y_train.values, color='black', label='Обучающие данные')
plt.plot(y_test.index, y_test.values, color='darkgray', label='Тестовые данные')

for name, forecast in forecasts.items():
    plt.plot(forecast.index, forecast.values, linestyle='--', label=f'Прогноз: {name}')

plt.title('Сравнение базовых моделей прогнозирования')
plt.xlabel('Дата')
plt.ylabel('Цена закрытия VOO')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Метрики качества прогнозов:
                          MAPE          MSE
Последнее значение    0.056251  1175.512502
Среднее               0.129063  5175.530137
Дрейф                 0.046184   788.784198
Полиномиальный тренд  0.024264   251.738120
Theta                 0.048729   878.893244

Сравнение качества прогнозирования различных базовых моделей для ETF VOO на 3-месячном горизонте. Черная линия - обучающая выборка, фактические тестовые данные (серая линия), показаны прогнозы пяти различных методов. Модели включают наивные подходы (последнее значение, среднее, дрейф), полиномиальный тренд и метод Theta

Рис. 9: Сравнение качества прогнозирования различных базовых моделей для ETF VOO на 3-месячном горизонте. Черная линия — обучающая выборка, фактические тестовые данные (серая линия), показаны прогнозы пяти различных методов. Модели включают наивные подходы (последнее значение, среднее, дрейф), полиномиальный тренд и метод Theta

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

  1. Наивные модели (NaiveForecaster) с различными стратегиями:
    • «last» — прогноз равен последнему известному значению;
    • «mean» — прогноз равен среднему всех значений в ряду;
    • «drift» — линейная экстраполяция на основе последних двух точек.
  2. Полиномиальная трендовая модель (PolynomialTrendForecaster) — аппроксимирует временной ряд полиномиальной кривой;
  3. Модель Theta (ThetaForecaster) — комбинирует наивный прогноз с корректировкой на долгосрочные тренды
👉🏻  Алгоритмы программирования. Что важно знать трейдеру и инвестору?

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

Продвинутые статистические модели

Перейдем к более сложным статистическим моделям, которые учитывают автокорреляцию и другие характеристики временных рядов:

from sktime.forecasting.ets import AutoETS
from sktime.forecasting.theta import ThetaForecaster
from sktime.forecasting.trend import STLForecaster
from sktime.forecasting.trend import PolynomialTrendForecaster

# Загрузка данных
cof_ticker = yf.Ticker("COF")  # Capital One Financial
cof_data = cof_ticker.history(period="2y")
ts_data = cof_data['Close'].rename('COF')

# Устанавливаем частоту для корректной работы с sktime
daily_range = pd.date_range(start=ts_data.index[0], end=ts_data.index[-1], freq='D')
ts_data = ts_data.reindex(daily_range, method='ffill').dropna()
ts_data.index.freq = 'D'

# Разделение на обучающую и тестовую выборки
y_train, y_test = temporal_train_test_split(ts_data, test_size=63)
fh = ForecastingHorizon(y_test.index, is_relative=False)

# Инициализация моделей
auto_ets = AutoETS(auto=True)

theta_model = ThetaForecaster(
    deseasonalize=True,
    sp=5
)

stl_model = STLForecaster(
    sp=5
)

poly_trend = PolynomialTrendForecaster(degree=3)

# Обучение моделей и получение прогнозов
models = {
    "AutoETS": auto_ets,
    "Theta": theta_model,
    "STL": stl_model,
    "Polynomial Trend": poly_trend
}

forecasts = {}
metrics = {}

for name, model in models.items():
    print(f"Обучение модели {name}...")
    model.fit(y_train)
    forecasts[name] = model.predict(fh)
    metrics[name] = {
        "MAPE": mean_absolute_percentage_error(y_test, forecasts[name]),
        "MSE": mean_squared_error(y_test, forecasts[name])
    }

# Вывод метрик качества
metrics_df = pd.DataFrame(metrics).T
print("\nМетрики качества прогнозов:")
print(metrics_df)

# Визуализация результатов
plt.figure(figsize=(14, 8))
plt.plot(y_train.index, y_train.values, color='black', label='Обучающие данные')
plt.plot(y_test.index, y_test.values, color='darkgray', label='Тестовые данные')

for name, forecast in forecasts.items():
    plt.plot(forecast.index, forecast.values, linestyle='--', label=f'Прогноз: {name}')

plt.title('Capital One Financial - Сравнение прогнозов продвинутых статистических моделей')
plt.xlabel('Дата')
plt.ylabel('Цена закрытия COF')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Анализ компонентов
import statsmodels.api as sm

print("Параметры лучших моделей:")
for name, model in models.items():
    if hasattr(model, 'get_fitted_params'):
        try:
            params = model.get_fitted_params()
            print(f"{name}: {params}")
        except:
            print(f"{name}: параметры недоступны")
    else:
        print(f"{name}: параметры недоступны")

# Декомпозиция временного ряда
decomposition = sm.tsa.seasonal_decompose(y_train, model='additive', period=5)

# Визуализация компонентов
plt.figure(figsize=(12, 10))

plt.subplot(4, 1, 1)
plt.plot(decomposition.observed, color='black')
plt.title('Исходный ряд')
plt.grid(True, alpha=0.3)

plt.subplot(4, 1, 2)
plt.plot(decomposition.trend, color='darkgray')
plt.title('Тренд')
plt.grid(True, alpha=0.3)

plt.subplot(4, 1, 3)
plt.plot(decomposition.seasonal, color='darkgray')
plt.title('Сезонность')
plt.grid(True, alpha=0.3)

plt.subplot(4, 1, 4)
plt.plot(decomposition.resid, color='darkgray')
plt.title('Остатки')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()
Метрики качества прогнозов:
                      MAPE         MSE
AutoETS           0.082031  346.442045
Theta             0.069361  254.087403
STL               0.056735  182.422551
Polynomial Trend  0.027368   46.618322

Сравнение качества прогнозирования продвинутых статистических моделей для акций Capital One Financial (COF) на 3-месячном горизонте. Показаны обучающие данные (черная линия), фактические тестовые данные (серая линия) и прогнозы 4-х различных методов: AutoETS , Theta, STL, и полиномиальный тренд третьей степени

Рис. 10: Сравнение качества прогнозирования продвинутых статистических моделей для акций Capital One Financial (COF) на 3-месячном горизонте. Показаны обучающие данные (черная линия), фактические тестовые данные (серая линия) и прогнозы 4-х различных методов: AutoETS, Theta, STL, и полиномиальный тренд третьей степени

В этом примере мы рассматриваем несколько продвинутых статистических моделей:

  • AutoETS — модель экспоненциального сглаживания с учетом тренда и сезонности, которая автоматически выбирает оптимальные параметры для ошибки, тренда и сезонной компоненты на основе информационных критериев;
  • Theta — классическая и эффективная модель прогнозирования, которая комбинирует локальные и глобальные характеристики временного ряда через параметр theta. Включает десезонализацию для лучшей обработки сезонных паттернов;
  • STL (Seasonal and Trend decomposition using Loess) — продвинутый метод декомпозиции временного ряда на тренд, сезонность и остатки с использованием локально взвешенной регрессии, что обеспечивает высокую адаптивность к изменениям в структуре данных;
  • Полиномиальный тренд — модель третьей степени для моделирования нелинейных трендов в данных, особенно эффективная при наличии выраженных изгибов в долгосрочной динамике.

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

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

Ансамблевые методы и комбинирование прогнозов

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

from sktime.forecasting.compose import EnsembleForecaster, TransformedTargetForecaster
from sktime.transformations.series.detrend import Detrender
from sktime.forecasting.compose import DirectTabularRegressionForecaster
from sklearn.ensemble import RandomForestRegressor

# Загрузка данных
xlf_ticker = yf.Ticker("XLF")  # Financial Select Sector SPDR Fund
xlf_data = xlf_ticker.history(period="2y")
ts_data = xlf_data['Close'].rename('XLF')

# Устанавливаем частоту для корректной работы с sktime
daily_range = pd.date_range(start=ts_data.index[0], end=ts_data.index[-1], freq='D')
ts_data = ts_data.reindex(daily_range, method='ffill').dropna()
ts_data.index.freq = 'D'

# Разделение на обучающую и тестовую выборки
y_train, y_test = temporal_train_test_split(ts_data, test_size=63)
fh = ForecastingHorizon(y_test.index, is_relative=False)

# Создание базовых моделей
naive_last = NaiveForecaster(strategy="last")
naive_drift = NaiveForecaster(strategy="drift")
theta = ThetaForecaster()
auto_ets = AutoETS(auto=True)

# Создание ансамбля с равными весами
ensemble_equal = EnsembleForecaster(
    forecasters=[
        ("naive_last", naive_last),
        ("naive_drift", naive_drift),
        ("theta", theta),
        ("auto_ets", auto_ets)
    ]
)

# Создание трансформированной модели с предварительной обработкой
transformed_forecaster = TransformedTargetForecaster(
    steps=[
        ("detrend", Detrender(forecaster=PolynomialTrendForecaster(degree=1))),
        ("forecast", auto_ets)
    ]
)

# Создание модели на основе машинного обучения с прямой стратегией
regressor = RandomForestRegressor(n_estimators=100, random_state=42)
direct_forecaster = DirectTabularRegressionForecaster(
    estimator=regressor,
    window_length=30  # Использовать 30 последних наблюдений для прогнозирования
)

# Обучение моделей и получение прогнозов
models = {
    "Наивная (last)": naive_last,
    "Наивная (drift)": naive_drift,
    "Theta": theta,
    "AutoETS": auto_ets,
    "Ансамбль (равные веса)": ensemble_equal,
    "Трансформированная ETS": transformed_forecaster,
    "Прямой RandomForest": direct_forecaster
}

forecasts = {}
metrics = {}

for name, model in models.items():
    print(f"Обучение модели {name}...")
    if name == "Прямой RandomForest":
        # DirectTabularRegressionForecaster требует fh в fit
        model.fit(y_train, fh=fh)
    else:
        model.fit(y_train)
    forecasts[name] = model.predict(fh)
    metrics[name] = {
        "MAPE": mean_absolute_percentage_error(y_test, forecasts[name]),
        "MSE": mean_squared_error(y_test, forecasts[name])
    }

# Вывод метрик качества
metrics_df = pd.DataFrame(metrics).T.sort_values(by="MAPE")
print("\nМетрики качества прогнозов (отсортированные по MAPE):")
print(metrics_df)

# Визуализация результатов лучших моделей
plt.figure(figsize=(14, 8))
plt.plot(y_train.index, y_train.values, color='black', label='Обучающие данные')
plt.plot(y_test.index, y_test.values, color='darkgray', label='Тестовые данные')

# Выберем топ-3 модели по MAPE
top_models = metrics_df.index[:3]
for name in top_models:
    plt.plot(forecasts[name].index, forecasts[name].values, linestyle='--', label=f'Прогноз: {name}')

plt.title('Сравнение лучших моделей прогнозирования')
plt.xlabel('Дата')
plt.ylabel('Цена закрытия XLF')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

# Создание пользовательского ансамбля с весами, обратно пропорциональными MAPE на валидационной выборке
from sktime.forecasting.model_selection import temporal_train_test_split

# Разделим обучающую выборку на подвыборки для обучения и валидации
y_train_train, y_train_val = temporal_train_test_split(y_train, test_size=21)  # 1 месяц на валидацию
fh_val = ForecastingHorizon(y_train_val.index, is_relative=False)

# Базовые модели
base_models = {
    "naive_last": naive_last,
    "naive_drift": naive_drift,
    "theta": theta,
    "auto_ets": auto_ets
}

# Обучаем модели на подвыборке и оцениваем на валидации
val_forecasts = {}
val_mape = {}

for name, model in base_models.items():
    model.fit(y_train_train)
    val_forecasts[name] = model.predict(fh_val)
    val_mape[name] = mean_absolute_percentage_error(y_train_val, val_forecasts[name])

# Вычисляем веса, обратно пропорциональные MAPE
total_inverse_mape = sum(1/mape for mape in val_mape.values())
weights = {name: (1/mape)/total_inverse_mape for name, mape in val_mape.items()}

print("\nВеса моделей в ансамбле:")
for name, weight in weights.items():
    print(f"{name}: {weight:.3f}")

# Создаем взвешенный прогноз
weighted_forecast = pd.Series(0, index=y_test.index)
for name, model in base_models.items():
    # Переобучаем модель на всей обучающей выборке
    model.fit(y_train)
    forecast = model.predict(fh)
    weighted_forecast += weights[name] * forecast

# Оцениваем качество взвешенного прогноза
weighted_mape = mean_absolute_percentage_error(y_test, weighted_forecast)
weighted_mse = mean_squared_error(y_test, weighted_forecast)

print(f"\nВзвешенный ансамбль - MAPE: {weighted_mape:.4f}, MSE: {weighted_mse:.4f}")

# Сравниваем с лучшей отдельной моделью и равновзвешенным ансамблем
best_model_name = metrics_df.index[0]
best_model_mape = metrics_df.loc[best_model_name, "MAPE"]
ensemble_equal_mape = metrics_df.loc["Ансамбль (равные веса)", "MAPE"]

print(f"Лучшая отдельная модель ({best_model_name}) - MAPE: {best_model_mape:.4f}")
print(f"Ансамбль с равными весами - MAPE: {ensemble_equal_mape:.4f}")

# Визуализация сравнения ансамблей
plt.figure(figsize=(14, 8))
plt.plot(y_train.index, y_train.values, color='black', label='Обучающие данные')
plt.plot(y_test.index, y_test.values, color='darkgray', label='Тестовые данные')
plt.plot(forecasts[best_model_name].index, forecasts[best_model_name].values, 
         linestyle='--', label=f'Лучшая модель: {best_model_name}')
plt.plot(forecasts["Ансамбль (равные веса)"].index, forecasts["Ансамбль (равные веса)"].values, 
         linestyle='--', label='Ансамбль (равные веса)')
plt.plot(weighted_forecast.index, weighted_forecast.values, 
         linestyle='--', label='Взвешенный ансамбль')

plt.title('Сравнение ансамблевых методов прогнозирования')
plt.xlabel('Дата')
plt.ylabel('Цена закрытия XLF')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
Обучение модели Наивная (last)...
Обучение модели Наивная (drift)...
Обучение модели Theta...
Обучение модели AutoETS...
Обучение модели Ансамбль (равные веса)...
Обучение модели Трансформированная ETS...
Обучение модели Прямой RandomForest...

Метрики качества прогнозов (отсортированные по MAPE):
                            MAPE       MSE
Трансформированная ETS  0.021979  1.739221
Наивная (drift)         0.025751  2.244370
Прямой RandomForest     0.029125  4.209711
Theta                   0.030909  3.061274
Ансамбль (равные веса)  0.034501  3.731715
AutoETS                 0.040671  5.094034
Наивная (last)          0.040671  5.094038

Сравнение качества прогнозирования трех лучших моделей для ETF XLF (Financial Select Sector SPDR Fund) на 3-месячном горизонте. Показаны обучающие данные (черная линия), фактические тестовые данные (серая линия) и прогнозы 3-х наиболее точных методов, отобранных по критерию MAPE

Рис. 11: Сравнение качества прогнозирования трех лучших моделей для ETF XLF (Financial Select Sector SPDR Fund) на 3-месячном горизонте. Показаны обучающие данные (черная линия), фактические тестовые данные (серая линия) и прогнозы 3-х наиболее точных методов, отобранных по критерию MAPE

Веса моделей в ансамбле:
naive_last: 0.233
naive_drift: 0.270
theta: 0.264
auto_ets: 0.233

Взвешенный ансамбль - MAPE: 0.0341, MSE: 3.6469
Лучшая отдельная модель (Трансформированная ETS) - MAPE: 0.0220
Ансамбль с равными весами - MAPE: 0.0345

Сравнение различных ансамблевых подходов к прогнозированию ETF XLF. Демонстрируется эффективность комбинирования прогнозов: лучшая индивидуальная модель, ансамбль с равными весами (простое усреднение) и взвешенный ансамбль с весами, обратно пропорциональными MAPE на валидационной выборке. Взвешенный подход позволяет автоматически определить оптимальный вклад каждой базовой модели в итоговый прогноз

Рис. 12: Сравнение различных ансамблевых подходов к прогнозированию ETF XLF (лучшая индивидуальная модель, ансамбль с равными весами (простое усреднение) и взвешенный ансамбль с весами, обратно пропорциональными MAPE на валидационной выборке). Взвешенный подход позволяет автоматически определить оптимальный вклад каждой базовой модели в итоговый прогноз

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

  • Ансамбль с равными весами (EnsembleForecaster) — комбинирует прогнозы нескольких моделей с одинаковыми весами, включая наивные методы, Theta и AutoETS;
  • Трансформированный прогноз (TransformedTargetForecaster) — применяет предварительную обработку (удаление линейного тренда) перед применением AutoETS модели для улучшения качества прогнозирования;
  • Прямой прогноз на основе машинного обучения (DirectTabularRegressionForecaster) — использует алгоритм RandomForest для прогнозирования, преобразуя временной ряд в табличные данные с использованием скользящего окна;
  • Пользовательский взвешенный ансамбль — присваивает моделям веса, обратно пропорциональные их ошибкам MAPE на валидационной выборке, что позволяет автоматически определить оптимальный вклад каждой модели.
👉🏻  Методы разделения деревьев решений: Gini, Энтропия, Gain Ratio, Хи-квадрат, Variance Reduction, Classification Error

Ансамблевые методы часто обеспечивают более стабильные и точные прогнозы, поскольку объединяют сильные стороны различных подходов: статистических моделей (ETS, Theta), наивных методов и машинного обучения. Особенно эффективен взвешенный ансамбль, где веса моделей определяются на основе их исторической производительности.

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

Классификация и кластеризация рядов с помощью sktime

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

Классификация временных рядов

Классификация временных рядов — это задача отнесения временного ряда к одному из предопределенных классов. Sktime предлагает множество алгоритмов для решения этой задачи:

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, classification_report, precision_score, recall_score, f1_score, accuracy_score
import seaborn as sns
from sktime.classification.distance_based import KNeighborsTimeSeriesClassifier
from sktime.classification.interval_based import TimeSeriesForestClassifier
from sktime.classification.dictionary_based import BOSSEnsemble
from sktime.datasets import load_arrow_head
# Загрузка датасета
print("Загрузка датасета Arrow Head...")
X_train, y_train = load_arrow_head(split="train", return_X_y=True)
X_test, y_test = load_arrow_head(split="test", return_X_y=True)
print(f"Форма обучающих данных: {X_train.shape}")
print(f"Форма тестовых данных: {X_test.shape}")
print(f"Количество классов: {len(np.unique(y_train))}")
print(f"Классы: {np.unique(y_train)}")
print(f"Длина временных рядов: {len(X_train.iloc[0, 0])}")
# Анализ распределения классов
train_counts = pd.Series(y_train).value_counts().sort_index()
test_counts = pd.Series(y_test).value_counts().sort_index()
print(f"\nРаспределение классов в обучающей выборке:")
for cls, count in train_counts.items():
print(f"Класс {cls}: {count} примеров ({count/len(y_train)*100:.1f}%)")
print(f"\nРаспределение классов в тестовой выборке:")
for cls, count in test_counts.items():
print(f"Класс {cls}: {count} примеров ({count/len(y_test)*100:.1f}%)")
# Визуализация примеров временных рядов для каждого класса
plt.figure(figsize=(15, 8))
classes = np.unique(y_train)
colors = ['black', 'darkgray', 'gray']
for i, cls in enumerate(classes):
# Найдем индексы для каждого класса
class_indices = np.where(y_train == cls)[0]
# Покажем первые 5 примеров каждого класса
examples_to_show = min(5, len(class_indices))
plt.subplot(len(classes), 1, i+1)
for j in range(examples_to_show):
idx = class_indices[j]
time_series = X_train.iloc[idx, 0]  # Берем первый (и единственный) столбец
plt.plot(time_series.values, color=colors[i % len(colors)], alpha=0.7, linewidth=1)
plt.title(f'Класс {cls} - {examples_to_show} примеров (форма наконечника стрелы)')
plt.grid(True, alpha=0.3)
plt.ylabel('Значение')
if i == len(classes) - 1:  # Только для последнего графика
plt.xlabel('Время')
plt.tight_layout()
plt.show()
# Определение моделей классификации
models = {
"k-NN (Euclidean)": KNeighborsTimeSeriesClassifier(
n_neighbors=5,
distance="euclidean"
),
"k-NN (DTW)": KNeighborsTimeSeriesClassifier(
n_neighbors=5,
distance="dtw"
),
"TimeSeriesForest": TimeSeriesForestClassifier(
n_estimators=100,
random_state=42
),
"BOSS Ensemble": BOSSEnsemble(
max_ensemble_size=10,
random_state=42
)
}
# Обучение моделей и получение прогнозов
results = {}
predictions = {}
print("\n" + "="*50)
print("ОБУЧЕНИЕ И ОЦЕНКА МОДЕЛЕЙ")
print("="*50)
for name, model in models.items():
print(f"\nОбучение модели {name}...")
try:
# Обучение
model.fit(X_train, y_train)
# Предсказание
y_pred = model.predict(X_test)
predictions[name] = y_pred
# Оценка точности
accuracy = accuracy_score(y_test, y_pred)
results[name] = accuracy
print(f"✓ {name} - Точность: {accuracy:.4f} ({accuracy*100:.1f}%)")
except Exception as e:
print(f"✗ Ошибка при обучении {name}: {str(e)}")
results[name] = 0
predictions[name] = np.array([classes[0]] * len(y_test))
# Визуализация результатов точности
plt.figure(figsize=(12, 6))
models_list = list(results.keys())
accuracies = list(results.values())
bars = plt.bar(models_list, accuracies, color=['darkgray', 'gray', 'lightgray', 'silver'])
plt.title('Сравнение точности классификаторов временных рядов')
plt.xlabel('Модель')
plt.ylabel('Точность')
plt.ylim(0, 1)
# Добавляем значения над столбцами
for i, (bar, acc) in enumerate(zip(bars, accuracies)):
plt.text(bar.get_x() + bar.get_width()/2, acc + 0.01, f'{acc:.3f}', 
ha='center', va='bottom', fontweight='bold')
plt.xticks(rotation=45, ha='right')
plt.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.show()
# Найдем лучшую модель
best_model_name = max(results.keys(), key=lambda k: results[k])
best_predictions = predictions[best_model_name]
print(f"\n" + "="*50)
print(f"ДЕТАЛЬНЫЙ АНАЛИЗ ЛУЧШЕЙ МОДЕЛИ: {best_model_name}")
print("="*50)
# Матрица ошибок для лучшей модели
cm = confusion_matrix(y_test, best_predictions, labels=classes)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', 
xticklabels=[f'Класс {c}' for c in classes], 
yticklabels=[f'Класс {c}' for c in classes])
plt.title(f'Матрица ошибок для {best_model_name}')
plt.xlabel('Предсказанный класс')
plt.ylabel('Истинный класс')
plt.tight_layout()
plt.show()
# Детальные метрики для всех моделей
print(f"\n" + "="*50)
print("ПОДРОБНЫЕ МЕТРИКИ ДЛЯ ВСЕХ МОДЕЛЕЙ")
print("="*50)
metrics_detailed = []
for name, y_pred in predictions.items():
if name in results and results[name] > 0:
accuracy = accuracy_score(y_test, y_pred)
precision = precision_score(y_test, y_pred, average='weighted', zero_division=0)
recall = recall_score(y_test, y_pred, average='weighted', zero_division=0)
f1 = f1_score(y_test, y_pred, average='weighted', zero_division=0)
metrics_detailed.append({
'Model': name,
'Accuracy': accuracy,
'Precision': precision,
'Recall': recall,
'F1-Score': f1
})
print(f"\n{name}:")
print(f"  Accuracy:  {accuracy:.4f}")
print(f"  Precision: {precision:.4f}")
print(f"  Recall:    {recall:.4f}")
print(f"  F1-Score:  {f1:.4f}")
# Создание сводной таблицы метрик
metrics_df = pd.DataFrame(metrics_detailed)
print(f"\n{'='*50}")
print("СВОДНАЯ ТАБЛИЦА МЕТРИК")
print("="*50)
print(metrics_df.round(4).to_string(index=False))
# Детальный отчет о классификации для лучшей модели
print(f"\n{'='*50}")
print(f"ОТЧЕТ О КЛАССИФИКАЦИИ - {best_model_name}")
print("="*50)
report = classification_report(y_test, best_predictions, target_names=[f'Класс {c}' for c in classes])
print(report)
# Анализ ошибок классификации
print(f"\n{'='*50}")
print("АНАЛИЗ ОШИБОК КЛАССИФИКАЦИИ")
print("="*50)
# Найдем примеры правильной и неправильной классификации
correct_predictions = y_test == best_predictions
incorrect_predictions = ~correct_predictions
print(f"Правильно классифицировано: {correct_predictions.sum()}/{len(y_test)} ({correct_predictions.mean()*100:.1f}%)")
print(f"Неправильно классифицировано: {incorrect_predictions.sum()}/{len(y_test)} ({incorrect_predictions.mean()*100:.1f}%)")
if incorrect_predictions.sum() > 0:
print(f"\nПримеры ошибок:")
error_indices = np.where(incorrect_predictions)[0]
for i, idx in enumerate(error_indices[:5]):  # Показываем первые 5 ошибок
true_class = y_test[idx]
pred_class = best_predictions[idx]
print(f"  Образец {idx}: Истинный класс = {true_class}, Предсказанный класс = {pred_class}")
# Визуализация примеров правильной и неправильной классификации
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
# Правильные классификации
correct_indices = np.where(correct_predictions)[0]
if len(correct_indices) >= 3:
for i in range(3):
idx = correct_indices[i]
time_series = X_test.iloc[idx, 0]
axes[0, i].plot(time_series.values, color='green', linewidth=2)
axes[0, i].set_title(f'Правильно: Класс {y_test[idx]}')
axes[0, i].grid(True, alpha=0.3)
# Неправильные классификации
incorrect_indices = np.where(incorrect_predictions)[0]
if len(incorrect_indices) >= 3:
for i in range(min(3, len(incorrect_indices))):
idx = incorrect_indices[i]
time_series = X_test.iloc[idx, 0]
axes[1, i].plot(time_series.values, color='red', linewidth=2)
axes[1, i].set_title(f'Ошибка: {y_test[idx]}→{best_predictions[idx]}')
axes[1, i].grid(True, alpha=0.3)
# Убираем пустые подграфики
for i in range(3):
if len(correct_indices) < 3:
axes[0, 2].set_visible(False)
if len(incorrect_indices) < 3: axes[1, 2].set_visible(False) plt.tight_layout() plt.show() # Анализ важности признаков (для TimeSeriesForest, если это лучшая модель) if best_model_name == "TimeSeriesForest" and hasattr(models[best_model_name], 'feature_importances_'): print(f"\n{'='*50}") print("АНАЛИЗ ВАЖНОСТИ ВРЕМЕННЫХ ТОЧЕК") print("="*50) # Получаем важности признаков feature_importances = models[best_model_name].feature_importances_ time_points = range(len(feature_importances)) plt.figure(figsize=(12, 6)) plt.plot(time_points, feature_importances, color='darkgray', linewidth=2) plt.fill_between(time_points, feature_importances, alpha=0.3, color='lightgray') plt.title('Важность временных точек в TimeSeriesForest') plt.xlabel('Временная точка') plt.ylabel('Важность признака') plt.grid(True, alpha=0.3) plt.tight_layout() plt.show() # Найдем наиболее важные временные точки top_indices = np.argsort(feature_importances)[-5:][::-1] print("Топ-5 наиболее важных временных точек:") for i, idx in enumerate(top_indices): print(f" {i+1}. Временная точка {idx}: важность = {feature_importances[idx]:.4f}") print(f"\n{'='*50}") print("ИТОГОВЫЕ РЕЗУЛЬТАТЫ") print("="*50) print(f"Датасет: Arrow Head (классификация форм наконечников стрел)") print(f"Размер обучающей выборки: {len(X_train)} временных рядов") print(f"Размер тестовой выборки: {len(X_test)} временных рядов") print(f"Количество классов: {len(classes)}") print(f"Длина временных рядов: {len(X_train.iloc[0, 0])} точек") print(f"\nЛучшая модель: {best_model_name}") print(f"Точность лучшей модели: {results[best_model_name]:.4f} ({results[best_model_name]*100:.1f}%)") # Сравнение с базовым классификатором (most frequent) baseline_accuracy = max(test_counts) / len(y_test) print(f"Точность базового классификатора (most frequent): {baseline_accuracy:.4f} ({baseline_accuracy*100:.1f}%)") if results[best_model_name] > baseline_accuracy:
improvement = results[best_model_name] - baseline_accuracy
print(f"✓ Улучшение относительно базовой модели: +{improvement:.4f} ({improvement*100:.1f} п.п.)")
else:
print("⚠ Модель не превзошла базовый классификатор")
Загрузка датасета Arrow Head...
Форма обучающих данных: (36, 1)
Форма тестовых данных: (175, 1)
Количество классов: 3
Классы: ['0' '1' '2']
Длина временных рядов: 251
Распределение классов в обучающей выборке:
Класс 0: 12 примеров (33.3%)
Класс 1: 12 примеров (33.3%)
Класс 2: 12 примеров (33.3%)
Распределение классов в тестовой выборке:
Класс 0: 69 примеров (39.4%)
Класс 1: 53 примеров (30.3%)
Класс 2: 53 примеров (30.3%)

Примеры временных рядов для каждого класса в датасете Arrow Head. Каждый класс представляет различную форму наконечника стрелы. Показано по 5 примеров для каждого класса с характерными паттернами формы

Рис. 13: Примеры временных рядов для каждого класса в датасете Arrow Head. Каждый класс представляет различную форму наконечника стрелы. Показано по 5 примеров для каждого класса с характерными паттернами формы

==================================================
ОБУЧЕНИЕ И ОЦЕНКА МОДЕЛЕЙ
==================================================
Обучение модели k-NN (Euclidean)...
✓ k-NN (Euclidean) - Точность: 0.6686 (66.9%)
Обучение модели k-NN (DTW)...
✓ k-NN (DTW) - Точность: 0.6857 (68.6%)
Обучение модели TimeSeriesForest...
✓ TimeSeriesForest - Точность: 0.7257 (72.6%)
Обучение модели BOSS Ensemble...
✓ BOSS Ensemble - Точность: 0.8343 (83.4%)

Сравнение точности четырех различных алгоритмов классификации временных рядов. k-NN с DTW использует динамическое программирование для сравнения последовательностей, TimeSeriesForest - ансамбль деревьев решений, BOSS Ensemble - словарный подход

Рис. 14: Сравнение точности четырех различных алгоритмов классификации временных рядов. k-NN с DTW использует динамическое программирование для сравнения последовательностей, TimeSeriesForest — ансамбль деревьев решений, BOSS Ensemble — словарный подход

==================================================
ДЕТАЛЬНЫЙ АНАЛИЗ ЛУЧШЕЙ МОДЕЛИ: BOSS Ensemble
==================================================

Матрица ошибок для лучшей модели - Boss Ensemble. Диагональные элементы показывают правильные классификации

Рис. 15: Матрица ошибок для лучшей модели — Boss Ensemble. Диагональные элементы показывают правильные классификации

==================================================
ПОДРОБНЫЕ МЕТРИКИ ДЛЯ ВСЕХ МОДЕЛЕЙ
==================================================
k-NN (Euclidean):
Accuracy:  0.6686
Precision: 0.7285
Recall:    0.6686
F1-Score:  0.6654
k-NN (DTW):
Accuracy:  0.6857
Precision: 0.7111
Recall:    0.6857
F1-Score:  0.6906
TimeSeriesForest:
Accuracy:  0.7257
Precision: 0.7290
Recall:    0.7257
F1-Score:  0.7238
BOSS Ensemble:
Accuracy:  0.8343
Precision: 0.8391
Recall:    0.8343
F1-Score:  0.8348
==================================================
СВОДНАЯ ТАБЛИЦА МЕТРИК
==================================================
Model  Accuracy  Precision  Recall  F1-Score
k-NN (Euclidean)    0.6686     0.7285  0.6686    0.6654
k-NN (DTW)    0.6857     0.7111  0.6857    0.6906
TimeSeriesForest    0.7257     0.7290  0.7257    0.7238
BOSS Ensemble    0.8343     0.8391  0.8343    0.8348
==================================================
ОТЧЕТ О КЛАССИФИКАЦИИ - BOSS Ensemble
==================================================
precision    recall  f1-score   support
Класс 0       0.85      0.84      0.85        69
Класс 1       0.77      0.87      0.81        53
Класс 2       0.89      0.79      0.84        53
accuracy                           0.83       175
macro avg       0.84      0.83      0.83       175
weighted avg       0.84      0.83      0.83       175
==================================================
АНАЛИЗ ОШИБОК КЛАССИФИКАЦИИ
==================================================
Правильно классифицировано: 146/175 (83.4%)
Неправильно классифицировано: 29/175 (16.6%)
Примеры ошибок:
Образец 1: Истинный класс = 0, Предсказанный класс = 2
Образец 7: Истинный класс = 0, Предсказанный класс = 1
Образец 13: Истинный класс = 0, Предсказанный класс = 1
Образец 15: Истинный класс = 0, Предсказанный класс = 1
Образец 17: Истинный класс = 0, Предсказанный класс = 2

Примеры правильной (зеленые ряды) и неправильной (красные ряды) классификации для Boss Ensemble

Рис. 16: Примеры правильной (зеленые ряды) и неправильной (красные ряды) классификации для Boss Ensemble

==================================================
ИТОГОВЫЕ РЕЗУЛЬТАТЫ
==================================================
Датасет: Arrow Head (классификация форм наконечников стрел)
Размер обучающей выборки: 36 временных рядов
Размер тестовой выборки: 175 временных рядов
Количество классов: 3
Длина временных рядов: 251 точек
Лучшая модель: BOSS Ensemble
Точность лучшей модели: 0.8343 (83.4%)
Точность базового классификатора (most frequent): 0.3943 (39.4%)
✓ Улучшение относительно базовой модели: +0.4400 (44.0 п.п.)

Данный пример демонстрирует применение различных алгоритмов машинного обучения для классификации временных рядов на реальном датасете из UCI/UEA архива. Давайте рассмотрим подробно что делает этот код:

  1. Загружает готовый датасет Arrow Head из sktime (формы наконечников стрел из археологии);
  2. Анализирует данные — показывает распределение классов, длину временных рядов;
  3. Визуализирует примеры временных рядов для каждого класса;
  4. Обучает 4 модели: k-NN с Евклидовым расстоянием, k-NN с DTW (Dynamic Time Warping), TimeSeriesForest, BOSS Ensemble;
  5. Оценивает качество: Accuracy, Precision, Recall, F1-Score, матрица ошибок лучшей модели, детальный отчет классификации;
  6. Визуализирует результаты.
👉🏻  Эконометрика в биржевой аналитике: современные подходы и методы

Результаты демонстрируют высокую эффективность машинного обучения для классификации временных рядов: лучшая модель BOSS Ensemble достигла точности 83.4%, что более чем в 2 раза превышает точность базового классификатора (39.4%). Словарный подход BOSS оказался оптимальным для данной задачи с малой обучающей выборкой (36 примеров) и длинными временными рядами (251 точка), показав улучшение на 44 процентных пункта. Это подтверждает, что временные паттерны содержат информативные признаки для автоматической классификации и могут успешно применяться в практических задачах.

Кластеризация временных рядов

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

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

!pip install tslearn
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import yfinance as yf
from tslearn.clustering import TimeSeriesKMeans, KShape
from tslearn.preprocessing import TimeSeriesScalerMeanVariance
from sklearn.preprocessing import MinMaxScaler
# Для демонстрации кластеризации временных рядов соберем данные по нескольким ETF
# Список ETF из разных секторов
etfs = ['XLF', 'XLE', 'XLK', 'XLV', 'XLU', 'XLI', 'XLB', 'XLP', 'XLY', 'XLRE']
etf_names = {
'XLF': 'Финансы',
'XLE': 'Энергетика',
'XLK': 'Технологии',
'XLV': 'Здравоохранение',
'XLU': 'Коммунальные услуги',
'XLI': 'Промышленность',
'XLB': 'Материалы',
'XLP': 'Товары первой необходимости',
'XLY': 'Потребительские товары',
'XLRE': 'Недвижимость'
}
# Загрузка данных
time_series_data = {}
normalized_data = {}
for etf in etfs:
ticker = yf.Ticker(etf)
data = ticker.history(period="1y")
# Преобразование в недельную частоту (берем закрытие пятницы)
weekly_data = data['Close'].resample('W').last()
ts_data = weekly_data.rename(etf)
# Нормализация для сравнимости
scaler = MinMaxScaler()
normalized = scaler.fit_transform(ts_data.values.reshape(-1, 1)).flatten()
time_series_data[etf] = ts_data
normalized_data[etf] = normalized
print(f"Количество недельных наблюдений: {len(normalized_data[etfs[0]])}")
# Визуализация исходных данных
plt.figure(figsize=(14, 8))
for etf, ts_data in time_series_data.items():
plt.plot(ts_data.index, normalized_data[etf], label=f"{etf} - {etf_names[etf]}", marker='o', markersize=3)
plt.title('Нормализованные недельные цены ETF за последний год')
plt.xlabel('Дата')
plt.ylabel('Нормализованная цена')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Подготовка данных для кластеризации
X = np.array([normalized_data[etf] for etf in etfs])
X = X.reshape(X.shape[0], X.shape[1], 1)  # Формат [n_samples, n_timesteps, n_features]
# Нормализация временных рядов (центрирование и масштабирование)
scaler = TimeSeriesScalerMeanVariance()
X_scaled = scaler.fit_transform(X)
# Применение алгоритмов кластеризации
# 1. K-Means с DTW метрикой
km_dtw = TimeSeriesKMeans(
n_clusters=3,
metric="dtw",
random_state=42
)
# 2. K-Means с евклидовой метрикой
km_euclidean = TimeSeriesKMeans(
n_clusters=3,
metric="euclidean",
random_state=42
)
# 3. KShape алгоритм (инвариантный к сдвигу и масштабу)
kshape = KShape(
n_clusters=3,
random_state=42
)
# Обучение моделей
print("Обучение K-Means с DTW...")
km_dtw_labels = km_dtw.fit_predict(X_scaled)
print("Обучение K-Means с евклидовой метрикой...")
km_euclidean_labels = km_euclidean.fit_predict(X_scaled)
print("Обучение KShape...")
kshape_labels = kshape.fit_predict(X_scaled)
# Визуализация результатов кластеризации
methods = {
"K-Means (DTW)": km_dtw_labels,
"K-Means (Евклидово)": km_euclidean_labels,
"KShape": kshape_labels
}
plt.figure(figsize=(16, 12))
count = 1
for name, labels in methods.items():
plt.subplot(3, 1, count)
# Создаем словарь для отображения ETF по кластерам
clusters = {}
for i, label in enumerate(labels):
if label not in clusters:
clusters[label] = []
clusters[label].append(etfs[i])
# Визуализация временных рядов по кластерам
for cluster_id, cluster_etfs in clusters.items():
for etf in cluster_etfs:
plt.plot(normalized_data[etf], alpha=0.7, label=f"{etf} - {etf_names[etf]}", marker='o', markersize=2)
# Вычисляем и отображаем центроид кластера
if name == "K-Means (DTW)":
centroid = km_dtw.cluster_centers_[cluster_id].flatten()
elif name == "K-Means (Евклидово)":
centroid = km_euclidean.cluster_centers_[cluster_id].flatten()
else:  # KShape
centroid = kshape.cluster_centers_[cluster_id].flatten()
plt.plot(centroid, color='black', linewidth=3, 
label=f"Центроид кластера {cluster_id}")
plt.title(f'Результаты кластеризации: {name}')
plt.xlabel('Недели')
plt.ylabel('Нормализованное значение')
plt.legend(loc='upper right')
plt.grid(True, alpha=0.3)
# Выводим информацию о составе кластеров
cluster_info = "\n".join([f"Кластер {cid}: {', '.join(cetfs)}" for cid, cetfs in clusters.items()])
plt.figtext(0.02, 0.85 - 0.28 * (count - 1), cluster_info, wrap=True, fontsize=9)
count += 1
plt.tight_layout()
plt.show()
# Оценка качества кластеризации
from sklearn.metrics import silhouette_score, calinski_harabasz_score, davies_bouldin_score
# Функция для оценки качества кластеризации
def evaluate_clustering(X, labels, method_name):
# Преобразуем 3D данные в 2D для sklearn метрик
X_reshaped = X.reshape(X.shape[0], -1)
silhouette = silhouette_score(X_reshaped, labels)
calinski = calinski_harabasz_score(X_reshaped, labels)
davies = davies_bouldin_score(X_reshaped, labels)
return {
"Метод": method_name,
"Силуэтный коэффициент": silhouette,
"Индекс Калински-Харабаша": calinski,
"Индекс Дэвиса-Болдина": davies
}
# Оценка для каждого метода
evaluation_results = []
evaluation_results.append(evaluate_clustering(X, km_dtw_labels, "K-Means (DTW)"))
evaluation_results.append(evaluate_clustering(X, km_euclidean_labels, "K-Means (Евклидово)"))
evaluation_results.append(evaluate_clustering(X, kshape_labels, "KShape"))
# Вывод результатов
evaluation_df = pd.DataFrame(evaluation_results)
print("Оценка качества кластеризации:")
print(evaluation_df)
# Анализ состава кластеров и их интерпретация
for name, labels in methods.items():
print(f"\nАнализ кластеров для метода {name}:")
clusters = {}
for i, label in enumerate(labels):
if label not in clusters:
clusters[label] = []
clusters[label].append(etfs[i])
for cluster_id, cluster_etfs in clusters.items():
sector_names = [etf_names[etf] for etf in cluster_etfs]
print(f"Кластер {cluster_id}: {', '.join(cluster_etfs)} ({', '.join(sector_names)})")
Количество недельных наблюдений: 53
Обучение K-Means с DTW...
Обучение K-Means с евклидовой метрикой...
Обучение KShape...

Нормализованные недельные цены 10 секторальных ETF за последний год. Каждая линия представляет отдельный сектор экономики США. Нормализация позволяет сравнивать относительную динамику различных секторов независимо от абсолютных уровней цен

Рис. 17: Нормализованные недельные цены 10 секторальных ETF за последний год. Каждая линия представляет отдельный сектор экономики США. Нормализация позволяет сравнивать относительную динамику различных секторов независимо от абсолютных уровней цен

Результаты кластеризации секторальных ETF 3-мя различными алгоритмами. K-Means с DTW учитывает временные искажения, K-Means с евклидовой метрикой фокусируется на абсолютных различиях, KShape инвариантен к сдвигам и масштабированию. Черные линии показывают центроиды кластеров

Рис. 18: Результаты кластеризации секторальных ETF 3-мя различными алгоритмами. K-Means с DTW учитывает временные искажения, K-Means с евклидовой метрикой фокусируется на абсолютных различиях, KShape инвариантен к сдвигам и масштабированию. Черные линии показывают центроиды кластеров

Оценка качества кластеризации:
Метод  Силуэтный коэффициент  Индекс Калински-Харабаша  Индекс Дэвиса-Болдина
0        K-Means (DTW)               0.091764                  2.469186               1.133940
1  K-Means (Евклидово)               0.148615                  3.180583               0.783840
2               KShape               0.060468                  1.931877               2.061288
Анализ кластеров для метода K-Means (DTW):
Кластер 1: XLF, XLU, XLI, XLP, XLY, XLRE (Финансы, Коммунальные услуги, Промышленность, Товары первой необходимости, Потребительские товары, Недвижимость)
Кластер 2: XLE (Энергетика)
Кластер 0: XLK, XLV, XLB (Технологии, Здравоохранение, Материалы)
Анализ кластеров для метода K-Means (Евклидово):
Кластер 1: XLF, XLK, XLU, XLI, XLP, XLY, XLRE (Финансы, Технологии, Коммунальные услуги, Промышленность, Товары первой необходимости, Потребительские товары, Недвижимость)
Кластер 2: XLE (Энергетика)
Кластер 0: XLV, XLB (Здравоохранение, Материалы)
Анализ кластеров для метода KShape:
Кластер 2: XLF, XLU, XLI, XLP (Финансы, Коммунальные услуги, Промышленность, Товары первой необходимости)
Кластер 0: XLE, XLB, XLRE (Энергетика, Материалы, Недвижимость)
Кластер 1: XLK, XLV, XLY (Технологии, Здравоохранение, Потребительские товары)

В этом примере мы применили три алгоритма кластеризации к временным рядам ETF из разных секторов:

  • K-Means с DTW метрикой — классический алгоритм кластеризации, адаптированный для временных рядов с использованием Dynamic Time Warping в качестве меры расстояния. DTW позволяет находить сходства между временными рядами даже при наличии временных искажений, растяжений или сжатий;
  • K-Means с евклидовой метрикой — стандартный K-Means, использующий евклидово расстояние между временными рядами. Этот подход более чувствителен к точному временному выравниванию, но вычислительно эффективнее;
  • KShape — специализированный алгоритм для кластеризации временных рядов, инвариантный к масштабированию и сдвигам во времени. Особенно эффективен для выявления сходных форм временных рядов, даже если они имеют разные амплитуды.
👉🏻  Foundation-модели для временных рядов

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

  • Силуэтный коэффициент — измеряет, насколько объект похож на свой кластер по сравнению с другими кластерами (выше — лучше);
  • Индекс Калински-Харабаша — отношение дисперсии между кластерами к дисперсии внутри кластеров (выше — лучше);
  • Индекс Дэвиса-Болдина — измеряет среднее сходство между кластерами (ниже — лучше).

Интерпретация результатов кластеризации:

  1. Качество кластеризации: K-Means с евклидовой метрикой показал лучшие результаты (силуэтный коэффициент 0.149, наименьший индекс Дэвиса-Болдина 0.784), что указывает на более четкое разделение кластеров по сравнению с DTW и KShape методами;
  2. Секторальные паттерны: Все три метода выделяют энергетический сектор (XLE) как отдельный кластер, что отражает его уникальную динамику, связанную с волатильностью цен на нефть. Защитные секторы (коммунальные услуги, товары первой необходимости) последовательно группируются вместе, демонстрируя схожую стабильную динамику. Технологический и здравоохранительный секторы также показывают тенденцию к совместной кластеризации, что может отражать их инновационную природу и схожие циклы роста;
  3. Методологические выводы: DTW метод сгруппировал больше секторов в один крупный кластер, что может указывать на фокус на общих трендах, в то время как евклидова метрика и KShape обеспечили более сбалансированное распределение, лучше выявляя тонкие различия между секторами.

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

Заключение

Библиотека sktime представляет собой мощный и гибкий инструмент для анализа временных рядов, который объединяет лучшие практики и алгоритмы в рамках единого, последовательного API. Ее интеграция с экосистемой scikit-learn делает ее особенно привлекательной для специалистов по данным, уже знакомых с этой популярной библиотекой машинного обучения.

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

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

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