![](https://habrastorage.org/getpro/habr/upload_files/da8/432/445/da8432445220d2cd523ad3af67de482f.png)
Меня зовут Андрей, я разработчик библиотеки 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()
. Используем его и посмотрим, как изменились данные, а точнее их формат:
![В нашей практике такой формат удобнее для работы с несколькими временными рядами В нашей практике такой формат удобнее для работы с несколькими временными рядами](https://habrastorage.org/getpro/habr/upload_files/5db/069/815/5db069815da73842e1646fb7fece8998.png)
Теперь можно создать TSDataset с данными.
Зачем тратить столько сил на конвертацию данных из одного формата в другой и пользоваться специальным форматом данных?
Плюсы использования TSDataset:
удобно индексирует данные по времени, сегменту и имени колонки;
проводит валидацию данных;
взаимодействует с другими частями пайплайна прогнозирования;
помогает проводить базовую аналитику данных;
генерирует будущие значения ряда для прогнозирования;
удобно индексироваться по рядам: первый индекс отвечает за временное измерение, второй за сегмент, а третий за отдельную колонку.
Покажу еще несколько важных этапов работы с временными рядами, где функции TSDataset оказываются незаменимыми.
![Так можно отобразить отдельный сегмент Так можно отобразить отдельный сегмент](https://habrastorage.org/getpro/habr/upload_files/665/39c/91c/66539c91c83fd19ae6ffb8d26318a28c.png)
![Так можно отобразить несколько сегментов и выбрать временной промежуток для отображения Так можно отобразить несколько сегментов и выбрать временной промежуток для отображения](https://habrastorage.org/getpro/habr/upload_files/949/1dd/5eb/9491dd5eb77c08c32286946006dce44c.png)
Анализ рядов
Посмотреть данные после загрузки и проанализировать можно с помощью метода describe. Видно, что с данными все хорошо: пропущенных значений нет и они заканчиваются в одну дату.
![Describe показывает основную информацию по временным рядам в Dataset: время начала и окончания ряда, длину ряда, количество пропущенных значений и другие важные параметры Describe показывает основную информацию по временным рядам в Dataset: время начала и окончания ряда, длину ряда, количество пропущенных значений и другие важные параметры](https://habrastorage.org/getpro/habr/upload_files/3bb/54d/4d0/3bb54d4d017a471b7c713b9fdc490d32.png)
У TSDataset есть встроенный метод plot, с помощью которого можно посмотреть на временные ряды. Ряды имеют годовую сезонность, пики распределены не случайным образом — это праздники. Можно предположить, что в рядах присутствует тренд.
![](https://habrastorage.org/getpro/habr/upload_files/3de/ff4/70c/3deff470c4390d1fe8091bf51f46ce84.png)
![Если увеличить график, то видно снижение спроса к середине недели и увеличение — к концу. Видим, что есть недельная сезонность. Если увеличить график, то видно снижение спроса к середине недели и увеличение — к концу. Видим, что есть недельная сезонность.](https://habrastorage.org/getpro/habr/upload_files/565/358/a71/565358a716131018a8831060c60203e7.png)
Генерация признаков
Сгенерирую различные признаки с помощью ETNA и постараюсь объяснить, что они значат.
Лаги — это некоторое предыдущее значение временного ряда. Например, первый лаг — это вчерашнее значение. А пятый лаг — значение пять дней назад. Такие признаки необходимы для регрессионных моделей, например линейной регрессии или бустинга, чтобы получить информацию о прошлом ряда.
![При прогнозировании модель смотрит на значения лагов и учится предсказывать значения самого временного ряда. Информацию о прошлом модель получает из лагов напрямую При прогнозировании модель смотрит на значения лагов и учится предсказывать значения самого временного ряда. Информацию о прошлом модель получает из лагов напрямую](https://habrastorage.org/getpro/habr/upload_files/586/361/56c/58636156cb303bb23ecc26afcaf2bd49.png)
![Как проходит модель по таблице при обучении. Красным выделено то, что модель пытается спрогнозировать, а серым — обучающая выборка модели Как проходит модель по таблице при обучении. Красным выделено то, что модель пытается спрогнозировать, а серым — обучающая выборка модели](https://habrastorage.org/getpro/habr/upload_files/b20/81c/361/b2081c361bbf03b670701b0b0db23a0a.gif)
Попробуем применить лаг с шагом 1 и посмотреть, что получится.
Укажем список нужных нам лагов. В этом случае lags=[1]
. Видим, что появилась новая колонка и в ней лежит наш лаг. Причем этот лаг мы сразу получили для всех сегментов благодаря тому, что ETNA способна работать с несколькими рядами одновременно.
![](https://habrastorage.org/getpro/habr/upload_files/d1a/d64/fd7/d1ad64fd7211989fa54d98194e29d824.png)
Но что, если нам нужно сгенерировать несколько лагов? Это тоже легко сделать с помощью ETNA. Нужно указать в списке все лаги, которые нам нужны.
![](https://habrastorage.org/getpro/habr/upload_files/bdf/ce8/7fa/bdfce87fa34c3abc4a93a3512e31c33b.png)
![Можно использовать более сложные конструкции для задания лагов, например range или list comprehension Можно использовать более сложные конструкции для задания лагов, например range или list comprehension](https://habrastorage.org/getpro/habr/upload_files/a03/4ab/d78/a034abd7824196a7bfa0d79a460903b1.png)
Статистики — еще один важный признак. Он хорошо себя показывает в регрессионных моделях, так как дает возможность передать моделям информацию о прошлом, но в отличном от лагов формате. Статистики бывают такие: среднее, медиана, среднеквадратичное отклонение, минимум или максимум на отрезке.
Покажу, как этот признак работает на примере среднего. Запустить MeanTransform не сложнее, чем лаги.
![Указали усреднение с окном 5 Указали усреднение с окном 5](https://habrastorage.org/getpro/habr/upload_files/c43/216/dc1/c43216dc18fa1f95b308e90c132c9d24.png)
![Window — это сколько предыдущих значений мы хотим усреднить, чтобы получить значение в точке, — то самое окно. На первом шаге мы пытаемся усреднить пять значений, которые шли до первого значения включительно. Но до него данных не было, и поэтому заполняем самим числом 18. До значения 26 была только одна точка — 18. Поэтому усредняем 26 и 18. И так далее. Когда добираемся до числа, для которого есть все пять значений, усредняем их. И для всех следующих точек усредняем только пять значений — ведь именно такую ширину окна мы выбрали Window — это сколько предыдущих значений мы хотим усреднить, чтобы получить значение в точке, — то самое окно. На первом шаге мы пытаемся усреднить пять значений, которые шли до первого значения включительно. Но до него данных не было, и поэтому заполняем самим числом 18. До значения 26 была только одна точка — 18. Поэтому усредняем 26 и 18. И так далее. Когда добираемся до числа, для которого есть все пять значений, усредняем их. И для всех следующих точек усредняем только пять значений — ведь именно такую ширину окна мы выбрали](https://habrastorage.org/getpro/habr/upload_files/461/dd1/87f/461dd187f139e64e21cc17f1ed6e9591.gif)
С помощью такой фичи можно передавать модели информацию о среднем значении за последний месяц и неделю. А можно получать информацию о среднем значении за конкретные дни недели.
![Усреднение двух точек, которые идут с шагом 2 Усреднение двух точек, которые идут с шагом 2](https://habrastorage.org/getpro/habr/upload_files/5f9/d1c/fef/5f9d1cfef4caa69061f76e8fc217e4c8.png)
![Расчет статистики с шагом 2 Расчет статистики с шагом 2](https://habrastorage.org/getpro/habr/upload_files/432/b53/0c1/432b530c1189cdf50f58a9f7dbd345c6.gif)
Даты — это временные метки, и их можно использовать. Например, для того, чтобы указывать дни недели, месяца, номер недели в месяце и в году или информацию о том, выходной сегодня или нет.
Попробуем это сделать для нашего Dataset:
![В DateFlagsTransform не нужно указывать, к какой колонке применить эту трансформацию, достаточно указать, какие именно данные мы хотим достать, как отдельные признаки. Я выбрал четыре признака, но в самом трансформе их девять, так что есть из чего выбрать В DateFlagsTransform не нужно указывать, к какой колонке применить эту трансформацию, достаточно указать, какие именно данные мы хотим достать, как отдельные признаки. Я выбрал четыре признака, но в самом трансформе их девять, так что есть из чего выбрать](https://habrastorage.org/getpro/habr/upload_files/3ec/2a1/e90/3ec2a1e90ecb6e01c791ddfdfbe5f0d5.png)
Праздники. C временными метками работает и HolidayTransform. Для работы он использует библиотеку holidays, в которой уже записаны основные праздники для большинства стран. Нам нужно указать только ISO-код страны, и готово:
![Кажется, на Новый год в Финляндии отдыхают только 1 и 6 января =( Кажется, на Новый год в Финляндии отдыхают только 1 и 6 января =(](https://habrastorage.org/getpro/habr/upload_files/361/fd2/545/361fd25451f392f13050af4eae9dc030.png)
Логарифмирование. Я уже рассказал про трансформы, которые для генерации новых признаков используют сам ряд и которые используют его временную метку. Расскажу еще про один тип трансформов — те, что меняют сам ряд, или inplace-трансформы. Среди них самый простой — LogTransform. Он логарифмирует значения временного ряда.
![Запускается LogTransform так же, как все предыдущие трансформы Запускается LogTransform так же, как все предыдущие трансформы](https://habrastorage.org/getpro/habr/upload_files/703/f7d/565/703f7d5651cf3e29023089182f409695.png)
Если мы хотим вернуть ряду его исходный вид, нужно воспользоваться методом inverse_transform:
![Получается как в sklearn Получается как в sklearn](https://habrastorage.org/getpro/habr/upload_files/057/f4f/154/057f4f1541e3e57e80feaecf9da70db6.png)
Если мы хотим получить логарифмированные значения ряда, но не хотим «затирать» исходное, это можно сделать с помощью параметра inplace=False
.
![Получили логарифмированное значение и сохранили исходное, чтобы оставить возможность считать и другие признаки от исходного значения Получили логарифмированное значение и сохранили исходное, чтобы оставить возможность считать и другие признаки от исходного значения](https://habrastorage.org/getpro/habr/upload_files/0e1/e5c/234/0e1e5c234ba1b16fbaebe65023268f7b.png)
Прогнозирование
Мы получили значения, с помощью которых можно спрогнозировать ряды. Для прогноза я использовал обычную линейную модель. Это регрессионная модель, поэтому для нее нужно собственноручно сгенерировать фичи.
О разнице между регрессионными и авторегрессионными моделями и о разных стратегиях прогнозирования мы расскажем в одном из следующих туториалов.
Прогнозируем продажи на год вперед, поэтому горизонт прогноза — 365 дней. Предварительный анализ показал, что у рядов есть недельная сезонность, поэтому попробую использовать семь лагов: с 365 до 371 включительно.
Используем сдвиг в год, а не лаги с первого по седьмой, потому что модель не сможет ничего спрогнозировать. В таком случае почти на всех точках мы не сможем получить значение признака для прогнозирования. Горизонт прогноза определяет минимальный лаг, которым модель может воспользоваться.
![В качестве более простого примера: горизонт прогноза — 3. Модель учится на значениях лагов и пытается предсказать значения ряда. Но первый лаг дает сдвиг только на один шаг, уже на горизонте 2 модель не сможет им воспользоваться В качестве более простого примера: горизонт прогноза — 3. Модель учится на значениях лагов и пытается предсказать значения ряда. Но первый лаг дает сдвиг только на один шаг, уже на горизонте 2 модель не сможет им воспользоваться](https://habrastorage.org/getpro/habr/upload_files/739/210/091/73921009115e1841245db3e26555f54b.png)
![Получается, что минимально подходящий лаг — третий. Такая же логика работает и для расчета статистик, поэтому мы будем их считать от лага 365. Причем в статистиках я тоже хочу учесть недельную сезонность, поэтому укажу параметры seasonality=7 и window=104. Это значит, что я хочу усреднить значения каждого дня недели за последние два года. То есть для понедельников это среднее значение 104 предыдущих понедельников, для вторников — 104 вторников и так далее Получается, что минимально подходящий лаг — третий. Такая же логика работает и для расчета статистик, поэтому мы будем их считать от лага 365. Причем в статистиках я тоже хочу учесть недельную сезонность, поэтому укажу параметры seasonality=7 и window=104. Это значит, что я хочу усреднить значения каждого дня недели за последние два года. То есть для понедельников это среднее значение 104 предыдущих понедельников, для вторников — 104 вторников и так далее](https://habrastorage.org/getpro/habr/upload_files/bc1/a60/5e4/bc1a605e4489d797b117b70e506c90d0.png)
Для дат синтаксис, кажется, понятен без слов, так что посмотрим на праздники. Магазины в соревновании находятся в трех разных странах: Швеции, Норвегии и Финляндии. Задаем праздники этих стран и учитываем, что перед праздниками люди могут вести себя иначе. С помощью лагов передадим эту информацию модели.
Переходим к коду обучения. LinearPerSegmentModel — это модель. PerSegment значит, что для каждого временного ряда — сегмента — будет обучена своя линейная регрессия. Также есть LinearMultiSegmentModel, которая учится сразу на всех сегментах.
Pipeline — это класс, который объединяет модель и трансформации временного ряда и позволяет делать backtest над временным рядом. Это снимает довольно много головной боли с исследователя.
SMAPE — это метрика, относительная ошибка прогноза. SMAPE можно интерпретировать как процент, на который мы ошиблись при прогнозе. Pipeline знает, как применять метрики к временным рядам, и выдает подробный отчет с их значениями под каждый ряд.
Важно упомянуть, что я запускаю не прогнозирование, а backtest, чтобы посчитать метрики, сравнить результаты модели с последним известным годом и оценить качество модели. Для этого в ETNA есть несколько специальных функций. Я воспользуюсь plot_backtest.
![Функция выдает графики спрогнозированного и реального значения ряда по всем сегментам Функция выдает графики спрогнозированного и реального значения ряда по всем сегментам](https://habrastorage.org/getpro/habr/upload_files/8ac/06f/eb2/8ac06feb25886229b59c47b46017dd98.png)
Подробнее про backtest мы расскажем в будущих туториалах, а пока можно посмотреть jupyter notebook с примером.
Кажется, модель показывает себя хорошо. Попробуем спрогнозировать будущее.
![Строим график прогноза Строим график прогноза](https://habrastorage.org/getpro/habr/upload_files/e47/5bc/5c8/e475bc5c8645eac64a3af1b26f91ca9a.png)
Упакуем наши прогнозы в формат для submission и загрузим в Kaggle.
Заключение
Для этого соревнования мы подготовили более сложный, но робастный Kaggle-ноутбук.
В этой статье я:
показал, как работать с TSDataset;
рассказал, как работают лаги, статистики, флаги дат, праздники и логарифмирование;
познакомил с интерфейсами моделей и пайплайнов;
написал, как запускать backtest, строить графики и запускать прогноз.
В следующих туториалах мы расскажем про более сложные, но интересные признаки, которые можно найти в ETNA, а также про другие модели и инструменты для анализа рядов. Stay tuned =)
Если вы хотите предложить новую фичу, задать вопрос или предложить тему для статьи, залетайте в наш GitHub — там все контакты.