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

Использую ETNA для решения соревнования Tabular Playground Series — Jan 2022. В соревновании нужен прогноз продаж мерча: кружек, стикеров и шапок. Прогнозы строятся для шести воображаемых магазинов, по два в Финляндии, Норвегии и Швеции. Задача: спрогнозировать продажи на год вперед и оценить качество прогноза по SMAPE.

Если вы еще не знакомы с ETNA, о ней рассказала моя коллега Юля:

Как загружать данные в ETNA

Данные собраны в таблицу: row_id — номер строки, date — временная метка, country — страна продажи, store — название магазина, product — вид товара, num_sold — количество проданного товара. Нужно спрогнозировать комбинации country-store-item.

Чтобы подсчитать общее количество комбинаций country-store-item посмотрим на уникальные значения в колонках country, store и product.

Чтобы привести данные в формат, с которым работает ETNA, нужно выделить отдельные временные ряды — сегменты. Для этого добавим колонку segment — именно такое имя будет ждать ETNA — и положим в нее комбинацию country-store-item. Это позволит фреймворку в дальнейшем отделять разные временные ряды друг от друга. Получается 18 временных рядов: нужно спрогнозировать три различных товара в двух магазинах, каждый из которых находится в трех странах.

Пример, как получить колонку segment:

Посмотрим на то, что получилось:

Теперь каждый временной ряд имеет свою метку, которая лежит в колонке segment. Но Dataset все еще не в формате, который сможет «переварить» ETNA. Добавим пару штрихов:

Это минимально необходимый Dataset для ETNA. Target — специальное зарезервированное имя для обозначения колонки, которую мы хотим спрогнозировать. А timestamp — для обозначения временной метки, так как ETNA умеет работать с разной частотностью данных. Timestamp, segment и target — именно из таких колонок должен состоять Dataset для ETNA. Остальные колонки я удалил, но в следующих туториалах мы покажем, как ими можно было бы воспользоваться.

Подготовительный этап закончен, и можно импортировать в ETNA. В первую очередь нам понадобится TSDataset, в котором будут храниться данные. Внутри TSDataset они лежат в особом формате. Для конвертации уже существует специальный метод TSDataset.to_dataset(). Используем его и посмотрим, как изменились данные, а точнее их формат:

В нашей практике такой формат удобнее для работы с несколькими временными рядами

Теперь можно создать TSDataset с данными.

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

  • удобно индексирует данные по времени, сегменту и имени колонки; 

  • проводит валидацию данных; 

  • взаимодействует с другими частями пайплайна прогнозирования;

  • помогает проводить базовую аналитику данных; 

  • генерирует будущие значения ряда для прогнозирования; 

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

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

Так можно отобразить отдельный сегмент
Так можно отобразить несколько сегментов и выбрать временной промежуток для отображения

Анализ рядов

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

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

У TSDataset есть встроенный метод plot, с помощью которого можно посмотреть на временные ряды. Ряды имеют годовую сезонность, пики распределены не случайным образом — это праздники. Можно предположить, что в рядах присутствует тренд.

Если увеличить график, то видно снижение спроса к середине недели и увеличение — к концу. Видим, что есть недельная сезонность.

Генерация признаков

Сгенерирую различные признаки с помощью ETNA и постараюсь объяснить, что они значат.

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

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

Попробуем применить лаг с шагом 1 и посмотреть, что получится.
Укажем список нужных нам лагов. В этом случае lags=[1]. Видим, что появилась новая колонка и в ней лежит наш лаг. Причем этот лаг мы сразу получили для всех сегментов благодаря тому, что ETNA способна работать с несколькими рядами одновременно.

Но что, если нам нужно сгенерировать несколько лагов? Это тоже легко сделать с помощью ETNA. Нужно указать в списке все лаги, которые нам нужны.

Можно использовать более сложные конструкции для задания лагов, например range или list comprehension

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

Покажу, как этот признак работает на примере среднего. Запустить MeanTransform не сложнее, чем лаги.

Указали усреднение с окном 5
Window — это сколько предыдущих значений мы хотим усреднить, чтобы получить значение в точке, — то самое окно. На первом шаге мы пытаемся усреднить пять значений, которые шли до первого значения включительно. Но до него данных не было, и поэтому заполняем самим числом 18. До значения 26 была только одна точка — 18. Поэтому усредняем 26 и 18. И так далее. Когда добираемся до числа, для которого есть все пять значений, усредняем их. И для всех следующих точек усредняем только пять значений — ведь именно такую ширину окна мы выбрали

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

Усреднение двух точек, которые идут с шагом 2
Расчет статистики с шагом 2

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

Попробуем это сделать для нашего Dataset:

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

Праздники. C временными метками работает и HolidayTransform. Для работы он использует библиотеку holidays, в которой уже записаны основные праздники для большинства стран. Нам нужно указать только ISO-код страны, и готово:

Кажется, на Новый год в Финляндии отдыхают только 1 и 6 января =(

Логарифмирование. Я уже рассказал про трансформы, которые для генерации новых признаков используют сам ряд и которые используют его временную метку. Расскажу еще про один тип трансформов — те, что меняют сам ряд, или inplace-трансформы. Среди них самый простой — LogTransform. Он логарифмирует значения временного ряда.

Запускается LogTransform так же, как все предыдущие трансформы

Если мы хотим вернуть ряду его исходный вид, нужно воспользоваться методом inverse_transform:

Получается как в sklearn

Если мы хотим получить логарифмированные значения ряда, но не хотим «затирать» исходное, это можно сделать с помощью параметра inplace=False.

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

Прогнозирование

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

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

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

В качестве более простого примера: горизонт прогноза — 3. Модель учится на значениях лагов и пытается предсказать значения ряда. Но первый лаг дает сдвиг только на один шаг, уже на горизонте 2 модель не сможет им воспользоваться
Получается, что минимально подходящий лаг — третий. Такая же логика работает и для расчета статистик, поэтому мы будем их считать от лага 365. Причем в статистиках я тоже хочу учесть недельную сезонность, поэтому укажу параметры seasonality=7 и window=104. Это значит, что я хочу усреднить значения каждого дня недели за последние два года. То есть для понедельников это среднее значение 104 предыдущих понедельников, для вторников — 104 вторников и так далее

Для дат синтаксис, кажется, понятен без слов, так что посмотрим на праздники. Магазины в соревновании находятся в трех разных странах: Швеции, Норвегии и Финляндии. Задаем праздники этих стран и учитываем, что перед праздниками люди могут вести себя иначе. С помощью лагов передадим эту информацию модели. 

Переходим к коду обучения. LinearPerSegmentModel — это модель. PerSegment значит, что для каждого временного ряда — сегмента — будет обучена своя линейная регрессия. Также есть LinearMultiSegmentModel, которая учится сразу на всех сегментах.

Pipeline — это класс, который объединяет модель и трансформации временного ряда и позволяет делать backtest над временным рядом. Это снимает довольно много головной боли с исследователя. 

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

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

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

Подробнее про backtest мы расскажем в будущих туториалах, а пока можно посмотреть jupyter notebook с примером.

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

Строим график прогноза

Упакуем наши прогнозы в формат для submission и загрузим в Kaggle.

Заключение

Для этого соревнования мы подготовили более сложный, но робастный Kaggle-ноутбук.

В этой статье я:

  • показал, как работать с TSDataset;

  • рассказал, как работают лаги, статистики, флаги дат, праздники и логарифмирование;

  • познакомил с интерфейсами моделей и пайплайнов;

  • написал, как запускать backtest, строить графики и запускать прогноз.

В следующих туториалах мы расскажем про более сложные, но интересные признаки, которые можно найти в ETNA, а также про другие модели и инструменты для анализа рядов. Stay tuned =)

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