Библиотека ETNA в Python для прогнозирования временных рядов

Мне довелось работать с множеством инструментов для анализа временных рядов, и постоянно приходилось комбинировать различные библиотеки, чтобы получить полный цикл работы с временными рядами — от предобработки до бэктестинга, от подбора гиперпараметров до подготовки к деплою. Библиотека ETNA (Easy Time-series Analysis) от команды T-Bank AI Center решает эту проблему элегантно, предоставляя единый интерфейс для всех этапов прогнозирования временных рядов.

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

Что особенно привлекает в этой библиотеке — это ее ориентация на практические задачи и масштабируемость. Команда разработчиков использует ETNA в более чем 10 проектах внутри T-Банка, что говорит о ее надежности в продакшен среде.

Установка и начальная настройка

Процесс установки библиотеки ETNA довольно прост:

!pip install --upgrade pip
!pip install etna

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

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

  • prophet — добавляет модель Prophet;
  • torch — добавляет модели на основе нейронных сетей;
  • wandb — добавляет логгер wandb;
  • auto — добавляет функциональность AutoML;
  • classification — добавляет функциональность классификации временных рядов

Пример установки конкретного расширения:

pip install etna[prophet]
pip install etna[torch]

Установка всех расширений одновременно:

pip install etna[all]

Важный момент! Без соответствующего расширения вы получите ImportError при попытке импортировать модель, которая его требует. Например, etna.models.ProphetModel требует расширения prophet и не может использоваться без него. Этот подход может показаться неудобным, но на практике значительно уменьшает размер базовой установки и предотвращает конфликты зависимостей.

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

Обзор архитектурных возможностей

Одна из ключевых особенностей ETNA, которая отличает ее от других фреймворков — это последовательная реализация принципа единого интерфейса. За годы работы с различными библиотеками я понял, насколько важна унификация API, особенно когда нужно быстро сравнивать различные подходы к прогнозированию. В ETNA все модели, от простых статистических до сложных нейронных сетей, используют одинаковые методы fit(), predict() и forecast().

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

Встроенная поддержка мультисегментности

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

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

👉🏻  Лаговые переменные и их правильное использование. Избегаем data leakage в финансовых моделях

Ключевые функции и классы

TSDataset

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

import pandas as pd
import yfinance as yf
from etna.datasets import TSDataset
import numpy as np

# Загрузка данных криптовалют для демонстрации
crypto_symbols = ['SOL-USD', 'AVAX-USD', 'ETH-USD', 'DOT-USD']
start_date = '2024-09-01'
end_date = '2025-09-01'

# Создание датасета в формате ETNA
data_list = []
for symbol in crypto_symbols:
    ticker = yf.Ticker(symbol)
    hist = ticker.history(start=start_date, end=end_date)
    
    # Проверка на Multiindex и извлечение цен закрытия
    if isinstance(hist.columns, pd.MultiIndex):
        prices = hist['Close'].iloc[:, 0]
    else:
        prices = hist['Close']
    
    # Обработка NaN значений с помощью forward fill
    prices = prices.fillna(method='ffill')
    
    for date, price in prices.items():
        # Дополнительная проверка на NaN после ffill
        if not pd.isna(price):
            data_list.append({
                'timestamp': date.strftime('%Y-%m-%d'),
                'segment': symbol.replace('-USD', ''),
                'target': price
            })

df = pd.DataFrame(data_list)
df['timestamp'] = pd.to_datetime(df['timestamp'])

# Проверка на NaN в финальном DataFrame
print("Проверка на NaN значения:")
nan_counts = df.groupby('segment')['target'].apply(lambda x: x.isna().sum())
print(nan_counts)

if nan_counts.sum() > 0:
    print("Обнаружены NaN значения, применяем дополнительную обработку...")
    # Дополнительная обработка: заполнение по сегментам
    df['target'] = df.groupby('segment')['target'].fillna(method='ffill')
    df['target'] = df.groupby('segment')['target'].fillna(method='bfill')

# Преобразование в формат ETNA
df_wide = TSDataset.to_dataset(df)

# Дополнительная проверка и обработка NaN в wide формате
print(f"\nПроверка NaN в wide формате:")
for col in df_wide.columns:
    if col[1] == 'target':  # Проверяем только target колонки
        nan_count = df_wide[col].isna().sum()
        if nan_count > 0:
            print(f"Сегмент {col[0]}: {nan_count} NaN значений")
            # Заполняем NaN в wide формате
            df_wide[col] = df_wide[col].fillna(method='ffill').fillna(method='bfill')

ts = TSDataset(df_wide, freq='D')

# Выбор горизонта прогнозирования
HORIZON = 14

# Разделение на train/test
train_ts, test_ts = ts.train_test_split(test_size=HORIZON)

print(f"\nКоличество сегментов: {len(ts.segments)}")
print(f"Сегменты: {ts.segments}")
print("TSDataset успешно создан и готов к работе")

# Финальная проверка на NaN в test_ts
print(f"\nФинальная проверка test_ts на NaN:")
for segment in ts.segments:
    test_data = test_ts[:, segment, 'target']
    nan_count = test_data.isna().sum()
    if nan_count > 0:
        print(f"  {segment}: {nan_count} NaN значений")
    else:
        print(f"  {segment}: OK")

print(f"\nПервые 5 строк df_wide:")
print(df_wide.head())
Количество сегментов: 4
Сегменты: ['AVAX', 'DOT', 'MATIC', 'SOL']

Финальная проверка test_ts на NaN:
  AVAX: OK
  DOT: OK
  MATIC: OK
  SOL: OK

TSDataset успешно создан и готов к работе

Первые 5 строк df_wide:
segment          AVAX       DOT          ETH         SOL
feature        target    target       target      target
timestamp                                               
2024-09-01  21.448174  4.073628  2427.902344  128.690552
2024-09-02  22.368862  4.208609  2538.187256  135.002365
2024-09-03  21.383656  4.062910  2420.603760  127.577583
2024-09-04  21.899666  4.109952  2448.977051  133.603912
2024-09-05  21.366562  4.016058  2367.737549  129.328949

Так выглядит TSDataset для анализа криптовалют для ETNA. Главное — это правильное создание структуры данных, которая будет использоваться для обучения моделей. Обратите внимание на разделение train/test.

print("train", train_ts)
print("test", test_ts)
train segment          AVAX       DOT          ETH         SOL
feature        target    target       target      target
timestamp                                               
2024-09-01  21.448174  4.073628  2427.902344  128.690552
2024-09-02  22.368862  4.208609  2538.187256  135.002365
2024-09-03  21.383656  4.062910  2420.603760  127.577583
2024-09-04  21.899666  4.109952  2448.977051  133.603912
2024-09-05  21.366562  4.016058  2367.737549  129.328949
...               ...       ...          ...         ...
2025-08-13  25.531237  4.281788  4756.275879  201.590424
2025-08-14  23.670027  3.988957  4548.166504  192.585953
2025-08-15  23.756634  3.891083  4439.988770  185.741257
2025-08-16  24.313250  3.967054  4426.180176  189.766998
2025-08-17  25.075254  4.056698  4473.271484  191.164825

[351 rows x 4 columns]
test segment          AVAX       DOT          ETH         SOL
feature        target    target       target      target
timestamp                                               
2025-08-18  23.700531  3.914813  4312.504883  183.069870
2025-08-19  22.331470  3.715419  4073.464111  176.112274
2025-08-20  23.429131  3.880340  4334.500488  187.586304
2025-08-21  22.770454  3.793627  4223.209961  180.280624
2025-08-22  25.238823  4.175913  4831.348633  200.647446
2025-08-23  26.218695  4.226393  4776.090332  203.936737
2025-08-24  25.725376  4.109204  4779.647461  205.852371
2025-08-25  23.379616  3.735977  4372.987793  187.278442
2025-08-26  24.180391  3.888248  4600.426758  195.905090
2025-08-27  24.422859  3.834171  4503.393066  202.923370
2025-08-28  24.890280  3.980491  4507.177734  214.405640
2025-08-29  23.575598  3.776911  4360.152832  205.220001
2025-08-30  23.786201  3.810158  4374.153320  202.860138
2025-08-31  23.406010  3.742023  4390.019043  200.863541

Процесс создания TSDataset может показаться избыточным, но это инвестиция в долгосрочную производительность. Структура данных оптимизирована для быстрых операций индексирования и трансформаций, что становится заметно при работе с большими объемами данных. В моей практике переход от обычного DataFrame к TSDataset ускорил выполнение сложных pipeline в 3-4 раза.

👉🏻  Методы предиктивной аналитики и машинного обучения для оптимизации конверсии веб-сайтов

Acf_plot, Cross_corr_plot, Distribution_plot, Plot_correlation_matrix

Библиотека ETNA содержит встроенные функции визуализации:

  • acf_plot показывает, насколько значения временного ряда связаны с его прошлым (автокорреляция);
  • cross_corr_plot — как связаны между собой два разных временных ряда с учетом сдвигов во времени (кросс-корреляция);
  • distribution_plot визуализирует форму распределения данных;
  • plot_correlation_matrix показывает взаимосвязи между несколькими переменными в виде матрицы корреляций.

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

from etna.analysis import acf_plot
from etna.analysis import cross_corr_plot
from etna.analysis import distribution_plot
from etna.analysis import plot_correlation_matrix
ts.plot()

Отображение динамики криптовалют AVAX, DOR, ETH, SOL средствами ETNA

Рис. 1: Отображение динамики криптовалют AVAX, DOT, ETH, SOL средствами ETNA

acf_plot(ts, lags=21)

Автокорреляция динамики котировок криптовалют

Рис. 2: Автокорреляция динамики котировок криптовалют

acf_plot(ts, lags=21, partial=True)

Частичная автокорреляция (PACF)

Рис. 3: Частичная автокорреляция (PACF)

cross_corr_plot(ts, maxlags=100)

Кросс-кореляция криптовалют

Рис. 4: Кросс-кореляция криптовалют

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

distribution_plot(ts, freq="1Y")

График распределений динамики криптовалют в 2024 и 2025 гг

Рис. 5: График распределений динамики криптовалют в 2024 и 2025 гг

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

from etna.analysis import plot_trend
trends = [
    LinearTrendTransform(in_column="target", poly_degree=1),
    LinearTrendTransform(in_column="target", poly_degree=2),
    LinearTrendTransform(in_column="target", poly_degree=3),
]
plot_trend(ts, trend_transform=trends)

Графики криптовалют за последние 12 месяцев с наложенными линиями тренда со степенями полинома 1, 2 и 3

Рис. 6: Графики криптовалют за последние 12 месяцев с наложенными линиями тренда со степенями полинома 1, 2 и 3

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

Transform

Система трансформаций в ETNA представляет собой мощный инструмент для feature engineering. В отличие от ручного создания признаков, трансформации применяются автоматически ко всем сегментам и корректно обрабатывают train/test разделение.

from etna.transforms import (
    LagTransform, 
    DateFlagsTransform, 
    FourierTransform,
    TrendTransform,
    DensityOutliersTransform,
    TimeSeriesImputerTransform,
    SegmentEncoderTransform,
    LinearTrendTransform,
    MeanTransform
)

# Создание комплексного набора трансформаций
transforms = [
    # Обработка выбросов и пропущенных значений
    DensityOutliersTransform(in_column="target", distance_coef=3.0),
    TimeSeriesImputerTransform(in_column="target", strategy="forward_fill"),
    
    # Трендовые компоненты
    LinearTrendTransform(in_column="target"),
    TrendTransform(in_column="target", out_column="trend"),
    
    # Лаговые признаки с расширенным диапазоном для финансовых данных
    LagTransform(
        in_column="target", 
        lags=list(range(HORIZON, 122)),
        out_column="target_lag"
    ),
    
    # Временные признаки
    DateFlagsTransform(week_number_in_month=True, out_column="date_flag"),
    
    # Фурье признаки для годовой сезонности
    FourierTransform(period=360.25, order=6, out_column="fourier"),
    
    # Кодирование сегментов
    SegmentEncoderTransform(),
    
    # Скользящие средние для сглаживания
    MeanTransform(in_column=f"target_lag_{HORIZON}", window=12, seasonality=7),
    MeanTransform(in_column=f"target_lag_{HORIZON}", window=7),
]

print(f"Настроено {len(transforms)} трансформаций:")
for i, transform in enumerate(transforms, 1):
    print(f"  {i}. {type(transform).__name__}")
Настроено 10 трансформаций:
  1. DensityOutliersTransform
  2. TimeSeriesImputerTransform
  3. LinearTrendTransform
  4. TrendTransform
  5. LagTransform
  6. DateFlagsTransform
  7. FourierTransform
  8. SegmentEncoderTransform
  9. MeanTransform
  10. MeanTransform

Система трансформаций ETNA автоматически создает богатый набор признаков из исходного временного ряда. Каждая трансформация применяется консистентно ко всем сегментам с сохранением временной структуры данных.

Система трансформаций особенно ценна тем, что автоматически обрабатывает граничные случаи. Например, LagTransform корректно обрабатывает ситуации, когда лаговые значения выходят за границы доступных данных, а DateFlagsTransform автоматически адаптируется к различным частотам данных. В традиционных подходах эти детали часто становятся источником ошибок в production-системах.

Pipeline

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

from etna.models import CatBoostMultiSegmentModel
from etna.pipeline import Pipeline
from etna.metrics import SMAPE, MAE, MAPE
from etna.analysis import plot_forecast

# Создание модели
model = CatBoostMultiSegmentModel()

# Создание и обучение pipeline
pipeline = Pipeline(model=model, transforms=transforms, horizon=HORIZON)
pipeline.fit(train_ts)

# Выполнение прогнозирования
forecast_ts = pipeline.forecast()

# Визуализация результатов
plot_forecast(forecast_ts=forecast_ts, test_ts=test_ts, train_ts=train_ts, n_train_samples=50)

# Вычисление метрик
metric = SMAPE(mode="macro")
metric_value = metric(y_true=test_ts, y_pred=forecast_ts)

print("Результаты SMAPE по сегментам:")
for segment, value in metric_value.items():
    print(f"  {segment}: {value:.4f}")

# Дополнительные метрики для полной оценки
mae_metric = MAE(mode="macro")
mae_values = mae_metric(y_true=test_ts, y_pred=forecast_ts)

print("\nРезультаты MAE по сегментам:")
for segment, value in mae_values.items():
    print(f"  {segment}: {value:.2f}")
Результаты SMAPE по сегментам:
  AVAX: 10.5429
  DOT: 22.3159
  ETH: 17.9576
  SOL: 10.1806

Общий SMAPE: 15.2492

Результаты MAE по сегментам:
  AVAX: 2.42
  DOT: 1.00
  ETH: 735.12
  SOL: 19.29

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

Рис. 7: Результаты прогнозирования с использованием официального подхода ETNA показывают качество модели на тестовых данных. Pipeline автоматически обеспечивает корректное применение всех трансформаций и генерацию прогнозов.

Pipeline автоматически обеспечивает корректное выполнение временной валидации, что критично для финансовых временных рядов. Система гарантирует, что трансформации обучаются только на train данных и применяются к test данным без нарушения временной последовательности. Эта особенность устраняет одну из самых частых ошибок в прогнозировании временных рядов — утечку данных (data leakage).

👉🏻  Продвинутые способы кросс-валидации, разделения выборок рядов: Expanding Window Splitter, Blocked Time Series Split и другие

Ключевые параметры и их влияние

CatBoostMultiSegmentModel: оптимизация для временных рядов

CatBoost представляет собой одну из наиболее эффективных моделей в арсенале ETNA для задач прогнозирования. В отличие от традиционных подходов, где каждый сегмент моделируется отдельно, MultiSegment версия обучает единую модель на всех сегментах одновременно, используя метод энкодинга сегментов (segment encoding).

import warnings
warnings.filterwarnings('ignore')

from etna.models import CatBoostMultiSegmentModel
from etna.transforms import (
    SegmentEncoderTransform,
    TimeSeriesImputerTransform,
    LagTransform,
    DateFlagsTransform
)
from etna.pipeline import Pipeline
from etna.metrics import SMAPE
import numpy as np

def analyze_catboost_performance(train_data, test_data, horizon=14):
    """
    Анализ влияния гиперпараметров CatBoost на качество прогнозов
    """
    
    # Базовые трансформации для тестирования
    base_transforms = [
        TimeSeriesImputerTransform(in_column="target", strategy="forward_fill"),
        LagTransform(in_column="target", lags=list(range(horizon, horizon + 30))),
        DateFlagsTransform(week_number_in_month=True),
        SegmentEncoderTransform()
    ]
    
    # Различные конфигурации CatBoost для тестирования
    configurations = [
        {
            'name': 'Базовая модель',
            'params': {
                'random_seed': 42
            }
        },
        {
            'name': 'Оптимизированная для временных рядов',
            'params': {
                'iterations': 200,
                'depth': 6,
                'learning_rate': 0.1,
                'l2_leaf_reg': 3.0,
                'random_seed': 42
            }
        },
        {
            'name': 'Быстрая модель',
            'params': {
                'iterations': 100,
                'depth': 4,
                'learning_rate': 0.2,
                'random_seed': 42
            }
        },
        {
            'name': 'Глубокая модель',
            'params': {
                'iterations': 300,
                'depth': 8,
                'learning_rate': 0.05,
                'l2_leaf_reg': 5.0,
                'random_seed': 42
            }
        }
    ]
    
    results = {}
    
    for config in configurations:
        print(f"Тестирование: {config['name']}")
        
        # Создание модели с указанными параметрами
        model = CatBoostMultiSegmentModel(**config['params'])
        
        # Создание и обучение pipeline
        pipeline = Pipeline(
            model=model,
            transforms=base_transforms,
            horizon=horizon
        )
        
        try:
            pipeline.fit(train_data)
            forecast = pipeline.forecast()
            
            # Вычисление метрик
            smape_metric = SMAPE(mode="per-segment")
            smape_scores = smape_metric(y_true=test_data, y_pred=forecast)
            avg_smape = np.mean(list(smape_scores.values()))
            
            # Также вычисляем macro для общего результата
            smape_macro = SMAPE(mode="macro")
            macro_score = smape_macro(y_true=test_data, y_pred=forecast)
            
            results[config['name']] = {
                'avg_smape': avg_smape,
                'macro_smape': macro_score,
                'scores_by_segment': smape_scores,
                'params': config['params']
            }
            
            print(f"  Средний SMAPE: {avg_smape:.4f}")
            
        except Exception as e:
            print(f"  Ошибка: {e}")
            results[config['name']] = {'error': str(e)}
    
    return results

# Анализ различных конфигураций
performance_results = analyze_catboost_performance(train_ts, test_ts, HORIZON)

print("\nСводка результатов:")
successful_results = []
for name, result in performance_results.items():
    if 'avg_smape' in result:
        print(f"{name}: {result['avg_smape']:.4f}")
        successful_results.append((name, result))
    else:
        print(f"{name}: ОШИБКА - {result.get('error', 'Unknown error')}")

# Определение лучшей конфигурации
if successful_results:
    best_config = min(successful_results, key=lambda x: x[1]['avg_smape'])
    print(f"\nЛучшая конфигурация: {best_config[0]}")
    print(f"Параметры: {best_config[1]['params']}")
    print(f"SMAPE: {best_config[1]['avg_smape']:.4f}")
    
    print(f"\nДетальные результаты лучшей модели:")
    for segment, score in best_config[1]['scores_by_segment'].items():
        print(f"  {segment}: {score:.4f}")
else:
    print("\nВсе конфигурации завершились с ошибками.")
Тестирование: Базовая модель
  Средний SMAPE: 19.8979
Тестирование: Оптимизированная для временных рядов
  Средний SMAPE: 29.3781
Тестирование: Быстрая модель
  Средний SMAPE: 70.1958
Тестирование: Глубокая модель
  Средний SMAPE: 43.4293

Сводка результатов:
Базовая модель: 19.8979
Оптимизированная для временных рядов: 29.3781
Быстрая модель: 70.1958
Глубокая модель: 43.4293

Лучшая конфигурация: Базовая модель
Параметры: {'random_seed': 42}
SMAPE: 19.8979

Детальные результаты лучшей модели:
  AVAX: 11.4740
  DOT: 35.9648
  ETH: 21.5697
  SOL: 10.5831

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

👉🏻  Кто такие квант-аналитики (Quantitative Analysts) и чем они занимаются?

LagTransform: интеллектуальный выбор лагов

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

import warnings
warnings.filterwarnings('ignore')

from statsmodels.tsa.stattools import acf, pacf
import matplotlib.pyplot as plt
import numpy as np
from etna.transforms import LagTransform

def analyze_optimal_lags(ts_data, max_lags=60):
    """
    Анализ оптимальных лагов на основе автокорреляционной функции
    """
    
    fig, axes = plt.subplots(2, len(ts_data.segments), figsize=(20, 10))
    if len(ts_data.segments) == 1:
        axes = axes.reshape(-1, 1)
    
    significant_lags = {}
    
    for idx, segment in enumerate(ts_data.segments):
        segment_data = ts_data[:, segment, 'target'].dropna()
        
        # Применяем первые разности для удаления тренда
        diff_data = segment_data.diff().dropna()
        
        # ACF анализ
        acf_values, acf_confint = acf(diff_data, nlags=max_lags, alpha=0.05)
        axes[0][idx].plot(range(len(acf_values)), acf_values, 'b-', linewidth=2)
        axes[0][idx].fill_between(range(len(acf_values)), 
                                 acf_confint[:, 0] - acf_values, 
                                 acf_confint[:, 1] - acf_values, 
                                 color='gray', alpha=0.3)
        axes[0][idx].axhline(y=0, color='black', linestyle='-', alpha=0.3)
        axes[0][idx].set_title(f'ACF {segment} (diff)')
        axes[0][idx].set_xlabel('Лаг')
        axes[0][idx].set_ylabel('Автокорреляция')
        axes[0][idx].grid(True, alpha=0.3)
        
        # PACF анализ
        pacf_values, pacf_confint = pacf(diff_data, nlags=max_lags, alpha=0.05)
        axes[1][idx].plot(range(len(pacf_values)), pacf_values, 'r-', linewidth=2)
        axes[1][idx].fill_between(range(len(pacf_values)), 
                                 pacf_confint[:, 0] - pacf_values, 
                                 pacf_confint[:, 1] - pacf_values, 
                                 color='pink', alpha=0.3)
        axes[1][idx].axhline(y=0, color='black', linestyle='-', alpha=0.3)
        axes[1][idx].set_title(f'PACF {segment} (diff)')
        axes[1][idx].set_xlabel('Лаг')
        axes[1][idx].set_ylabel('Частичная автокорреляция')
        axes[1][idx].grid(True, alpha=0.3)
        
        # Определяем порог значимости
        threshold = 3 / np.sqrt(len(diff_data))
        significant = []
        
        # Проверяем только лаги, которые выходят за доверительный интервал
        for lag in range(1, len(acf_values)):
            acf_significant = (acf_values[lag] < acf_confint[lag, 0] or acf_values[lag] > acf_confint[lag, 1])
            pacf_significant = (pacf_values[lag] < pacf_confint[lag, 0] or pacf_values[lag] > pacf_confint[lag, 1])
            
            # Добавляем лаг только если он значим и в ACF, и в PACF
            if acf_significant and pacf_significant and abs(acf_values[lag]) > threshold:
                significant.append(lag)
            
            if len(significant) >= 10:  # Ограничиваем количество лагов
                break
        
        # Если не нашли значимых лагов, добавляем наиболее выраженные
        if len(significant) == 0:
            # Находим пики в ACF
            acf_abs = np.abs(acf_values[1:])  # Исключаем лаг 0
            top_lags = np.argsort(acf_abs)[::-1][:5] + 1  # Топ-5 лагов
            significant = top_lags.tolist()
        
        significant_lags[segment] = significant
        
        # Отметка значимых лагов на графике
        for lag in significant[:5]:  # Показываем только первые 5
            axes[0][idx].axvline(x=lag, color='red', linestyle='--', alpha=0.7)
            axes[1][idx].axvline(x=lag, color='red', linestyle='--', alpha=0.7)
    
    plt.tight_layout()
    plt.show()
    
    return significant_lags

# Анализ оптимальных лагов
optimal_lags = analyze_optimal_lags(ts)

print("Значимые лаги по сегментам (после дифференцирования):")
for segment, lags in optimal_lags.items():
    print(f"{segment}: {lags}")

def create_adaptive_lag_transform(optimal_lags, horizon=14, max_lag_distance=100):
    """
    Создание LagTransform на основе анализа автокорреляции
    """
    
    # Объединение значимых лагов всех сегментов
    all_lags = set()
    for segment_lags in optimal_lags.values():
        for lag in segment_lags:
            adjusted_lag = lag + horizon
            if adjusted_lag <= max_lag_distance: all_lags.add(adjusted_lag) # Добавление стандартных сезонных лагов seasonal_lags = [horizon + 7, horizon + 14, horizon + 30] # Недельная, двухнедельная, месячная # Объединение всех лагов final_lags = sorted(list(all_lags.union(seasonal_lags))) # Ограничение количества лагов if len(final_lags) > 20:
        final_lags = final_lags[:20]
    
    return LagTransform(
        in_column="target",
        lags=final_lags,
        out_column="target_lag"
    ), final_lags

adaptive_lag_transform, selected_lags = create_adaptive_lag_transform(optimal_lags, HORIZON)
print(f"\nВыбрано {len(selected_lags)} лагов для модели:")
print(f"Лаги: {selected_lags}")

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

Рис. 8: Автокорреляционные функции для различных криптовалют показывают различные паттерны зависимости. Пунктирные красные линии указывают на статистически значимые лаги, которые используются для настройки LagTransform

Значимые лаги по сегментам (после дифференцирования):
AVAX: [49, 10, 3, 15, 52]
DOT: [15, 13, 16, 1, 9]
ETH: [55, 9, 52, 12, 53]
SOL: [51, 55, 48, 58, 43]

Выбрано 19 лагов для модели:
Лаги: [15, 17, 21, 23, 24, 26, 27, 28, 29, 30, 44, 57, 62, 63, 65, 66, 67, 69, 72]

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

FourierTransform: захват сезонных паттернов

Преобразование Фурье в ETNA представляет собой один из наиболее элегантных способов моделирования периодических компонент временных рядов. Грамотное применение Фурье-анализа значительно превосходит примитивные подходы с dummy-переменными или жестко заданными сезонными индикаторами. Основное преимущество заключается в способности автоматически извлекать доминирующие частотные компоненты без предварительных предположений о структуре сезонности.

👉🏻  Автокорреляция (ACF) и частичная автокорреляция (PACF) в биржевом анализе

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

import warnings
warnings.filterwarnings('ignore')

from etna.transforms import FourierTransform
from etna.analysis import plot_periodogram
import matplotlib.pyplot as plt
import numpy as np
from scipy import signal

def analyze_seasonality_patterns(ts_data):
    """
    Анализ сезонных паттернов для оптимизации FourierTransform
    """
    
    fig, axes = plt.subplots(2, len(ts_data.segments), figsize=(20, 12))
    if len(ts_data.segments) == 1:
        axes = axes.reshape(-1, 1)
    
    optimal_periods = {}
    
    for idx, segment in enumerate(ts_data.segments):
        segment_data = ts_data[:, segment, 'target'].dropna()
        
        # Удаление тренда для выделения сезонных компонент
        detrended = signal.detrend(segment_data.values)
        
        # Вычисление периодограммы
        freqs, psd = signal.periodogram(detrended, fs=1.0)
        
        # Исключаем нулевую частоту (постоянную составляющую)
        freqs = freqs[1:]
        psd = psd[1:]
        
        # Конвертация частот в периоды
        periods = 1.0 / freqs
        
        # Ограничиваем анализ периодами от 2 до 365 дней
        valid_mask = (periods >= 2) & (periods <= 365) periods_filtered = periods[valid_mask] psd_filtered = psd[valid_mask] # График 1: Периодограмма axes[0][idx].semilogy(periods_filtered, psd_filtered, 'b-', linewidth=1) axes[0][idx].set_title(f'Периодограмма {segment}') axes[0][idx].set_xlabel('Период (дни)') axes[0][idx].set_ylabel('Спектральная плотность') axes[0][idx].grid(True, alpha=0.3) axes[0][idx].invert_xaxis() # Более короткие периоды слева # Поиск пиков в спектре peaks, properties = signal.find_peaks(psd_filtered, height=np.percentile(psd_filtered, 75), distance=5) significant_periods = periods_filtered[peaks] significant_power = psd_filtered[peaks] # Отметка значимых периодов axes[0][idx].scatter(significant_periods, significant_power, color='red', s=50, zorder=5) # График 2: Временной ряд с выделенными компонентами axes[1][idx].plot(segment_data.index, segment_data.values, 'b-', alpha=0.7, label='Исходный') axes[1][idx].plot(segment_data.index, detrended + segment_data.mean(), 'r-', alpha=0.8, label='Без тренда') axes[1][idx].set_title(f'Временной ряд {segment}') axes[1][idx].set_xlabel('Дата') axes[1][idx].set_ylabel('Цена') axes[1][idx].legend() axes[1][idx].grid(True, alpha=0.3) # Выбор топ-3 периодов для Фурье преобразования if len(significant_periods) > 0:
            top_periods = significant_periods[np.argsort(significant_power)[::-1][:3]]
            optimal_periods[segment] = top_periods.tolist()
        else:
            # Fallback к стандартным периодам
            optimal_periods[segment] = [7.0, 30.0, 365.25]
    
    plt.tight_layout()
    plt.show()
    
    return optimal_periods

# Анализ сезонности
seasonal_periods = analyze_seasonality_patterns(ts)

print("Оптимальные периоды для Фурье преобразования:")
for segment, periods in seasonal_periods.items():
    print(f"{segment}: {[f'{p:.1f}' for p in periods]}")

def create_optimized_fourier_transforms(periods_dict, max_order=6):
    """
    Создание оптимизированных Фурье преобразований
    """
    
    # Объединение всех значимых периодов
    all_periods = set()
    for segment_periods in periods_dict.values():
        all_periods.update(segment_periods)
    
    # Стандартные финансовые периоды
    standard_periods = [7.0, 30.44, 91.31, 365.25]  # неделя, месяц, квартал, год
    all_periods.update(standard_periods)
    
    transforms = []
    for period in sorted(all_periods):
        # Адаптивный выбор порядка в зависимости от периода
        if period <= 7:
            order = 2
        elif period <= 30:
            order = 3
        elif period <= 90:
            order = 4
        else:
            order = max_order
        
        transform = FourierTransform(
            period=period,
            order=order,
            out_column=f"fourier_{period:.0f}"
        )
        transforms.append(transform)
    
    return transforms

fourier_transforms = create_optimized_fourier_transforms(seasonal_periods)

print(f"\nСоздано {len(fourier_transforms)} Фурье преобразований:")
for transform in fourier_transforms:
    print(f"  Период: {transform.period:.1f}, Порядок: {transform.order}")

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

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

Оптимальные периоды для Фурье преобразования:
AVAX: ['182.5', '36.5', '9.9']
DOT: ['182.5', '52.1', '26.1']
ETH: ['36.5', '15.9', '21.5']
SOL: ['182.5', '30.4', '18.2']

Создано 13 Фурье преобразований:
  Период: 7.0, Порядок: 2
  Период: 9.9, Порядок: 3
  Период: 15.9, Порядок: 3
  Период: 18.2, Порядок: 3
  Период: 21.5, Порядок: 3
  Период: 26.1, Порядок: 3
  Период: 30.4, Порядок: 4
  Период: 30.4, Порядок: 4
  Период: 36.5, Порядок: 4
  Период: 52.1, Порядок: 4
  Период: 91.3, Порядок: 6
  Период: 182.5, Порядок: 6
  Период: 365.2, Порядок: 6

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

👉🏻  Расширенные методы тестирования стационарности рядов: тесты KPSS, Phillips-Perron и другие

Plot_backtest: комплексный бэктестинг и оценка устойчивости моделей

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

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

import warnings
warnings.filterwarnings("ignore")
from etna.analysis import plot_backtest
from etna.datasets.tsdataset import TSDataset
from etna.metrics import MAE
from etna.metrics import MSE
from etna.metrics import SMAPE
from etna.models import ProphetModel
from etna.pipeline import Pipeline

horizon = 31  
model = ProphetModel()  
transforms = []  

pipeline = Pipeline(model=model, transforms=transforms, horizon=horizon)

backtest_result = pipeline.backtest(ts=ts, metrics=[MAE(), MSE(), SMAPE()])

metrics_df = backtest_result["metrics"]
forecast_ts_list = backtest_result["forecasts"]
fold_info_df = backtest_result["fold_info"]
pipelines = backtest_result["pipelines"]

plot_backtest(forecast_ts_list, ts)

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

Рис. 10: Визуализация валидации бэктестинга модели прогнозирования рядов

Другие фишки библиотеки ETNA

  1. Автоматическое машинное обучение (AutoML). Одной из самых мощных особенностей ETNA является встроенная поддержка AutoML для временных рядов. Это особенно ценно в продакшен-среде, где нужно быстро получить baseline-модель или автоматизировать процесс выбора лучшей архитектуры для новых типов данных;
  2. Гиперпараметрическая оптимизация. Библиотека интегрирована с Optuna для автоматической оптимизации гиперпараметров. Система может автоматически подбирать не только параметры моделей, но и оптимальные наборы трансформаций, что особенно важно при работе с новыми типами данных, где оптимальная предобработка неизвестна заранее;
  3. Автоматический инжиниринг признаков. ETNA включает интеллектуальные алгоритмы для автоматического создания признаков. Система анализирует структуру временного ряда и автоматически генерирует релевантные лаговые переменные, скользящие средние и сезонные компоненты, основываясь на статистических характеристиках данных;
  4. Работа с экзогенными переменными. ETNA предоставляет гибкие механизмы для включения экзогенных переменных в процесс прогнозирования. Библиотека автоматически обрабатывает различные типы внешних факторов: непрерывные переменные (цены на нефть, процентные ставки), категориальные переменные (праздники, события) и временные паттерны (день недели, сезон);
  5. Декомпозиция прогнозов. ETNA автоматически разлагает прогнозы на составляющие компоненты (тренд, сезонность, остатки), что помогает понять драйверы прогнозируемых изменений и повышает интерпретируемость моделей для бизнес-пользователей;
  6. Автоматическая детекция аномалий. ETNA включает несколько алгоритмов для автоматического обнаружения аномалий в временных рядах. Система может выявлять как точечные выбросы, так и структурные изменения в данных, что немаловажно для поддержания качества прогнозов в условиях изменяющейся среды;
  7. Логирование и мониторинг. ETNA интегрирована с популярными платформами для отслеживания экспериментов (Weights & Biases, MLflow, TensorBoard). Система автоматически логирует метрики качества, гиперпараметры и артефакты моделей, что упрощает сравнение различных подходов и воспроизводимость результатов.

Заключение

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

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