Меня зовут Андрей, я разработчик библиотеки 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: время начала и окончания ряда, длину ряда, количество пропущенных значений и другие важные параметры
Describe показывает основную информацию по временным рядам в Dataset: время начала и окончания ряда, длину ряда, количество пропущенных значений и другие важные параметры

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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