Loading [MathJax]/extensions/tex2jax.js

понедельник, 24 февраля 2025 г.

ETNA : модель наивного прогноза

 Начинаю серию статей о сравнительно молодой библиотеки ETNA для прогнозирования временных рядов от команды Тинькофф Банк. В работе над статьями использую очень полезную книгу Груздев А. В. "Прогнозирование временных рядов с помощью Facebook Prophet, ETNA,sktime и LinkedIn Greykite".Начинаю с самой простой модели :  модель наивного прогноза

Итак, поехали и начинаем с того, что импортируем библиотеки pandas, NumPy, модули re и datetime

import pandas as pd
import numpy as np
import re
import datetime

Модуль re (регулярные выражения) в Python предоставляет мощные инструменты для работы с текстом. Регулярные выражения позволяют искать, заменять и делить текст на основе паттернов или шаблонов. Модуль datetime - работа с временными метками.
Далее по порядку импортируем функции и классы библиотеки ETNA.

# импортируем модели наивного прогноза

from etna.models import NaiveModel

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

from etna.metrics import MAE, MSE, SMAPE, MAPE

# импортируем функцию plot_forecast() для визуализации прогнозов

from etna.analysis import plot_forecast

Удобство библиотеки ETNA заключается в том, что мы можем применить выбранную модель прогноза  сразу к нескольким временным рядам и получить прогнозы для каждого из них. Будем работать с набором данных, представляющим месячные выручки десяти розничных магазинов одежды в период январь 2015 декабрь 2019.

Загружаем данные из excel файла :

df = pd.read_excel('data/shops_10_rev_month.xlsx')
df.head()

monthshoprev
02015-01-01Sh03040820
12015-01-01Sh13990770
22015-01-01Sh24356330
32015-01-01Sh34630860
42015-01-01Sh42450160

ETNA требует определенного формата данных: столбец, который мы хотим
спрогнозировать, должен называться target, столбец с временными метками
должен называться timestamp. Поскольку библиотека может работать с несколькими
временными рядами, столбец segment  является меткой ряда.

# задаем обязательные столбцы target, timestamp и segment

df=df.rename(columns={"month":"timestamp", "shop":"segment", "rev":"target"})
df['timestamp'] = pd.to_datetime(df['timestamp'])
df.head()

timestampsegmenttarget
02015-01-01Sh03040820
12015-01-01Sh13990770
22015-01-01Sh24356330
32015-01-01Sh34630860
42015-01-01Sh42450160

Библиотека работает со специальной структурой данных TSDataset. Класс
TSDataset – основной класс библиотеки ETNA для работы с временными рядами.
Его главные методы:

 .to_dataset() – превращает датафрейм c плоским индексом в датафрейм
c мультииндексом;

 .to_pandas() – превращает объект TSDataset в датафрейм c плоским индексом
(flatten=True) или датафрейм c мультииндексом (flatten=False);

 .fit_transform() – вычисляет и применяет преобразования и конструирование
признаков (например, логарифмирует зависимую переменную,
прогнозирует тренд и удаляет тренд из зависимой переменной, вычисляет
скользящее среднее с шириной окна 20 дней и создает столбец с
ним), требует списка экземпляров классов-трансформеров;

 .inverse_transform() – применяет обратные преобразования (например,
выполняет экспоненцирование прологарифмированной зависимой переменной,
добавляет тренд к зависимой переменной c удаленным трендом),
требует списка экземпляров классов-трансформеров;

 .make_future() – создает тестовый набор / набор новых данных, увеличив
обучающий/исторический набор данных на длину горизонта прогнозирования
и применив преобразования и конструирование признаков,
требует списка экземпляров классов-трансформеров;

 .train_test_split() – разбивает на обучающую и тестовую выборку c
учетом временной структуры;

 .describe() – вывод описательных статистик;

 .plot() – визуализирует временной ряд в переменной target (в зависимости
от количества сегментов может быть визуализирован один
или несколько временных рядов).


А сейчас с помощью класса TSDataset превращаем наш датафрейм в объект
TSDataset, указав частоту временного ряда.


ts = TSDataset(df, freq='MS')
ts.head()

segmentSh0Sh1Sh2Sh3Sh4Sh5Sh6Sh7Sh8Sh9
featuretargettargettargettargettargettargettargettargettargettarget
timestamp
2015-01-013040820399077043563304630860245016038581604566790462627064689004189940
2015-02-012409980338406036833603310420193143029599903766150381475047616403838840
2015-03-012604970385644044192004434280232520034572904352330430878054339604684460
2015-04-013001640409582051343305254790257439038614704510820492424066584105339020
2015-05-013065120395492052484505156630238039038481304216470494978067247904774140

С помощью метода .describe() можем посмотреть базовые характеристики
объекта TSDataset:
 start_timestamp – стартовая дата временного ряда;
 end_timestamp – конечная дата временного ряда;
 length – длина временного ряда;
 num_missing – количество пропусков;
 num_segments – количество сегментов (рядов);
 num_exogs – количество экзогенных переменных;
 num_regressors – количество регрессоров;
 num_known_future – количество регрессоров, добавленных из датафрейма
df_exog;
 freq – частота временного ряда.

ts.describe().head().T


segmentsSh0Sh1Sh2Sh3Sh4
start_timestamp2015-01-01 00:00:002015-01-01 00:00:002015-01-01 00:00:002015-01-01 00:00:002015-01-01 00:00:00
end_timestamp2019-12-01 00:00:002019-12-01 00:00:002019-12-01 00:00:002019-12-01 00:00:002019-12-01 00:00:00
length6060606060
num_missing00000
num_segments1010101010
num_exogs00000
num_regressors00000
num_known_future00000
freqMSMSMSMSMS

#Давайте визуализируем временной ряд.
# визуализируем временной ряд
ts.plot()
Как видим, имеем дело с непростыми временными рядами с явно выраженной сезонностью и убывающими и возрастающими трендами.
Прогнозирование временных рядов нужно начинать с самых простых моделей.
Прогнозирование начнем с самой простой модели - модели наивного прогноза, т. е. в качестве прогноза берем последнее известное значение. Для этого воспользуемся классом NaiveModel –моделью наивного прогноза и обучим ее.
Горизонт прогнозирования зададим равным 12 месяцам.

# задаем горизонт прогнозирования
HORIZON = 12

Разбиваем набор
(наш объект TSDataset) на обучающую и тестовую выборки с учетом временной
структуры, так чтобы последние 12 месяцев ушли в тестовую выборку.

train_ts, test_ts = ts.train_test_split(
train_start='2015-01-01',
train_end='2018-12-01',
test_start='2019-01-01',
test_end='2019-12-01')


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


# создаем экземпляр класса NaiveModel, в котором
# реализована модель наивного прогноза,
# задав 12 лагов

model = NaiveModel(lag=12)

# обучаем модель наивного прогноза
model.fit(train_ts)

С помощью метода .make_future() формируем набор, для которого нужно
получить прогнозы (по сути, тестовую выборку). Параметр future_steps задает
количество моментов времени, которые мы хотим предсказать в будущем
(горизонт прогнозирования). Параметр tail_steps задает количество моментов
времени, предшествующих горизонту прогнозирования, которые мы хотим
использовать для прогнозов (контекст модели). Этот параметр требуется
для простых моделей типа наивного прогноза, сезонного наивного прогноза,
скользящего среднего, сезонного скользящего среднего.

future_ts = train_ts.make_future(future_steps=HORIZON,
tail_steps=model.context_size)

С помощью метода .forecast() получаем наши прогнозы. В метод .forecast()
мы, помимо датафрейма, передаем параметр prediction_size, потому что
прогнозы определяются контекстом модели (количеством моментов времени,
предшествующих горизонту прогнозирования, которые мы хотим использовать
для прогнозов).

# получаем прогнозы

forecast_ts = model.forecast(future_ts,
prediction_size=HORIZON)
forecast_ts

segmentSh0Sh1Sh2Sh3Sh4Sh5Sh6Sh7Sh8Sh9
featuretargettargettargettargettargettargettargettargettargettarget
timestamp
2019-01-013390640.02388210.05470110.03944450.03501260.04130470.03674330.03485820.06132930.03601470.0
2019-02-012669090.02027720.04399290.02872760.02747370.03117610.02978520.02791070.04419260.03253810.0
2019-03-012865420.02308770.05043260.03898760.03285700.03581490.03377230.03065430.04946960.03913890.0
2019-04-013269360.02453530.05564900.04667040.03600160.03915510.03419200.03495010.05908400.04485810.0
2019-05-013305000.02373230.05422920.04632980.03300240.03826670.03127490.03504710.05820050.04034560.0
2019-06-012982130.02216290.04795610.04308310.02992590.03523600.02742860.03099660.05032720.03483950.0
2019-07-012820750.01885370.03799020.03630070.02806560.02936160.02334890.02714280.04224710.03352260.0
2019-08-013138740.01915560.03954220.03991890.03020100.03173640.02612630.03022230.04493300.03752480.0
2019-09-013868920.02427400.05586130.05218370.03916480.04296840.03525440.04044120.06217680.05196860.0
2019-10-014880810.02878200.06980350.05968900.05265910.05155930.04458310.05258060.07904450.06638270.0
2019-11-015525890.03280960.07617220.07008110.06389720.05806650.05154010.06053910.08480420.06654180.0
2019-12-014830620.03216540.06905310.06946570.05821860.05487170.04596640.05446120.07458580.05474160.0

# вычисляем метрику MAPE

mape = MAPE()
mape_=mape(y_true=test_ts, y_pred=forecast_ts)
mape_ = sorted(mape_.items(), key=lambda item: item[1], reverse=True)
mape_

[('Sh8', 28.17434156973828),
 ('Sh6', 19.517295260293512),
 ('Sh2', 13.288054471502697),
 ('Sh7', 13.25201785225388),
 ('Sh9', 10.819537264180356),
 ('Sh5', 10.509905475866542),
 ('Sh3', 8.591273575294613),
 ('Sh4', 8.255704643405032),
 ('Sh0', 5.609373332921791),
 ('Sh1', 2.419442811335598)]

Выведем графики

plot_forecast(forecast_ts, test_ts,
train_ts, n_train_samples=12)



Таким образом в качестве прогноза мы получили прошлогодние месячные выручки, а в качестве теста - фактические значения выручки в прогнозируемом году. А точность прогноза по каждому магазину показывает изменение годовой выручки в этом году относительно прошлого и этот показатель можно использовать в работе. Так как это абсолютная ошибка, нельзя понять лучше стал работать магазин или хуже. Чтобы ответить на этот вопрос лучше вместо MAPE использовать показатель MPE (

mean forecast error). Для того, чтобы его рассчитать проведем некоторые преобразования : помощью значения параметра flatten=True метода .to_pandas()

превращаем объект TSDataset в датафрейм с плоским индексом


test_df = test_ts.to_pandas(flatten=True)
test_df.head()

imestampsegmenttarget
02019-01-01Sh03337850
12019-02-01Sh02603750
22019-03-01Sh02774420
32019-04-01Sh03146650
42019-05-01Sh03164340

forecast_df = forecast_ts.to_pandas(flatten=True)
forecast_df.head()

timestampsegmenttarget
02019-01-01Sh03390640.0
12019-02-01Sh02669090.0
22019-03-01Sh02865420.0
32019-04-01Sh03269360.0
42019-05-01Sh03305000.0

Объединим два датафрейма и переименуем колонки для ясности

df_test_fc=pd.merge(test_df, forecast_df, how='inner', on=['timestamp','segment'])
df_test_fc=df_test_fc.rename(columns={"timestamp":"month", "segment":"shop", "target_x":"rev_test", "target_y":"rev_fc"})
df_test_fc.head()

monthshoprev_testrev_fc
02019-01-01Sh033378503390640.0
12019-02-01Sh026037502669090.0
22019-03-01Sh027744202865420.0
32019-04-01Sh031466503269360.0
42019-05-01Sh031643403305000.0

А теперь рассчитаем обе метрики

df_test_fc['MPE']=(df_test_fc['rev_test']-df_test_fc['rev_fc'])/df_test_fc['rev_test']
df_test_fc['MAPE']=abs(df_test_fc['rev_test']-df_test_fc['rev_fc'])/df_test_fc['rev_test']
df_test_fc

monthshoprev_testrev_fcMPEMAPE
02019-01-01Sh033378503390640.0-0.0158160.015816
12019-02-01Sh026037502669090.0-0.0250950.025095
22019-03-01Sh027744202865420.0-0.0328000.032800
32019-04-01Sh031466503269360.0-0.0389970.038997
42019-05-01Sh031643403305000.0-0.0444520.044452
.....................
1152019-08-01Sh934144803752480.0-0.0989900.098990
1162019-09-01Sh945587705196860.0-0.1399700.139970
1172019-10-01Sh956333606638270.0-0.1783860.178386
1182019-11-01Sh954569706654180.0-0.2193910.219391
1192019-12-01Sh943387105474160.0-0.2617020.261702

120 rows × 6 columns


Таким образом мы видим как соотносятся по магазинам текущие выручки м прошлогодние. Если сгруппируем, то получим среднегодовой показатель

df_er=df_test_fc.groupby('shop').agg(
avg_mpe=('MPE', 'mean'),
avg_mape=('MAPE', 'mean')).reset_index()
df_er

shopavg_mpeavg_mape
0Sh0-0.0560940.056094
1Sh10.0241940.024194
2Sh2-0.1328810.132881
3Sh3-0.0483430.085913
4Sh40.0825570.082557
5Sh5-0.1050990.105099
6Sh6-0.1951730.195173
7Sh70.1325200.132520
8Sh8-0.2817430.281743
9Sh9-0.0550030.108195
 
В заключении можно сказать что и наивный прогноз можно с пользой использовать.










Комментариев нет:

Отправить комментарий