Рассмотрим как наивный прогноз библиотеки ETNA справится с задачей соревнования Data Fusion Contest 2025 4cast
Необходимо решить задачу прогнозирования динамики денежных переводов клиентов банка. В роли временного ряда, который требуется прогнозировать, выступают переводы со счетов юридических лиц клиентов банка — другим юридическим лицам, агрегированные суммарно по неделям. Для обучения прогнозных моделей предоставляются транзакции за 2 года (118 недель, с 0 по 117 включительно), а горизонт прогнозирования — следующие 12 недель (с 118 по 129 включительно).
Загружаем необходимые библиотеки
from etna.datasets import TSDataset
from etna.transforms import LagTransform
from etna.metrics import MAE, MSE, SMAPE, MAPE,MSLE
from sklearn.metrics import root_mean_squared_log_error
from etna.transforms import MeanTransform
from etna.transforms import LagTransform
from etna.pipeline import Pipeline
from etna.models import LinearPerSegmentModel
from etna.transforms import DateFlagsTransform
from etna.transforms import LogTransform
from etna.analysis import plot_backtest
from etna.models import (NaiveModel,
MovingAverageModel,
SeasonalMovingAverageModel,
SARIMAXModel,
HoltWintersModel)
import warnings
warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
import random
import pyarrow.parquet as pa
from etna.transforms import LagTransform
from etna.metrics import MAE, MSE, SMAPE, MAPE,MSLE
from sklearn.metrics import root_mean_squared_log_error
from etna.transforms import MeanTransform
from etna.transforms import LagTransform
from etna.pipeline import Pipeline
from etna.models import LinearPerSegmentModel
from etna.transforms import DateFlagsTransform
from etna.transforms import LogTransform
from etna.analysis import plot_backtest
from etna.models import (NaiveModel,
MovingAverageModel,
SeasonalMovingAverageModel,
SARIMAXModel,
HoltWintersModel)
import warnings
warnings.filterwarnings("ignore")
import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
import random
import pyarrow.parquet as pa
Полезные процедуры :
def df_info(df):
#Возвращает количество строк и количество столбцов data_frame.
print("Kоличество строк и количество столбцов : ",df.shape)
#Возвращает количество измерений в кадре данных.
print("Kоличество измерений ",df.ndim)
#Возвращает целое число, указывающее количество элементов в
#data_frame.
print('Kоличество элементов : ',df.size)
#Возвращает имена столбцов data_frame.
print('Имена столбцов : ',df.columns)
#Возвращает метки строк data_frame в виде диапазона.
print('Метки строк : ',df.index)
def unique_info(df):
columns = []
coltypes=[]
unique_values = []
unique_counts = []
isnull_counts = []
MAX_LIST_LENGTH = 8
for col in df.columns:
columns.append(col)
coltypes.append(df[col].dtypes)
unique_vals = df[col].unique()
null_counts = df[col].isnull().sum()
unique_values.append(list(unique_vals))
unique_counts.append(len(unique_vals))
isnull_counts.append(null_counts)
def truncate_list(lst, max_length=MAX_LIST_LENGTH):
if len(lst) > max_length:
return lst[:max_length] + ['...']
return lst
unique_info_df = pd.DataFrame({
'Column': columns,
'Type': coltypes,
'Unique Values': unique_values,
'Total Unique Values': unique_counts,
'Total Null Values': isnull_counts
})
unique_info_df['Unique Values'] = unique_info_df['Unique Values'].apply(lambda x: truncate_list(x))
return unique_info_df.sort_values(['Type','Column'], ascending=[False, True])
Загружаем и смотрим данные:
- Целевые временные ряды (target_series.parquet) и добавка (target_series_extended.parquet)
- Календарь (calendar.csv)
df1 = pa.read_table('target_series.parquet').to_pandas()
df2 = pa.read_table('target_series_extended.parquet').to_pandas()
df_train=pd.concat([df1,df2], ignore_index=True)
pd.concat([df_train.head(),df_train.tail()], ignore_index=True)
inn_id | week | target | |
---|---|---|---|
0 | inn1000051 | 0 | 4.221593e+05 |
1 | inn1000051 | 1 | 1.622887e+03 |
2 | inn1000051 | 2 | 1.120726e+06 |
3 | inn1000051 | 3 | 3.949485e+04 |
4 | inn1000051 | 4 | 4.302633e+05 |
5 | inn999886 | 113 | 2.081143e+07 |
6 | inn999886 | 114 | 2.964491e+07 |
7 | inn999886 | 115 | 1.488255e+07 |
8 | inn999886 | 116 | 2.593559e+07 |
9 | inn999886 | 117 | 7.668105e+06 |
inn_id
– ИНН клиента банкаweek
— глобальный номер неделиtarget
– значения временного ряда
df_info(df_train)
Kоличество строк и количество столбцов : (6131634, 3) Kоличество измерений 2 Kоличество элементов : 18394902 Имена столбцов : Index(['inn_id', 'week', 'target'], dtype='object') Метки строк : RangeIndex(start=0, stop=6131634, step=1)
unique_info(df_train)
Column | Type | Unique Values | Total Unique Values | Total Null Values | |
---|---|---|---|---|---|
0 | inn_id | object | [inn1000051, inn1000246, inn1000281, inn100036... | 51963 | 0 |
2 | target | float64 | [422159.328125, 1622.88745117188, 1120725.625,... | 5655048 | 0 |
1 | week | int32 | [0, 1, 2, 3, 4, 5, 6, 7, ...] | 118 | 0 |
df_date_week = pd.read_csv("calendar_extended.csv", usecols=["date", "week"])
pd.concat([df_date_week.head(8),df_date_week.tail(8)], ignore_index=True)
date | week | |
---|---|---|
0 | 2022-07-25 | 0 |
1 | 2022-07-26 | 0 |
2 | 2022-07-27 | 0 |
3 | 2022-07-28 | 0 |
4 | 2022-07-29 | 0 |
5 | 2022-07-30 | 0 |
6 | 2022-07-31 | 0 |
7 | 2022-08-01 | 1 |
8 | 2025-01-12 | 128 |
9 | 2025-01-13 | 129 |
10 | 2025-01-14 | 129 |
11 | 2025-01-15 | 129 |
12 | 2025-01-16 | 129 |
13 | 2025-01-17 | 129 |
14 | 2025-01-18 | 129 |
15 | 2025-01-19 | 129 |
date
– дата (с “2022-07-25” по "2025-01-19)week
— глобальный номер недели (с 0 по 129)
Как видим, календарь по дневной, а нам нужен по недельный, поэтому добавим колонку с номером дня недели, где 0 - понедельник, 6 - воскресенье
df_date_week['date'] = pd.to_datetime(df_date_week['date'])
df_date_week['day'] = df_date_week['date'].dt.dayofweek
df_date_week.head(7)
date | week | day | |
---|---|---|---|
0 | 2022-07-25 | 0 | 0 |
1 | 2022-07-26 | 0 | 1 |
2 | 2022-07-27 | 0 | 2 |
3 | 2022-07-28 | 0 | 3 |
4 | 2022-07-29 | 0 | 4 |
5 | 2022-07-30 | 0 | 5 |
6 | 2022-07-31 | 0 | 6 |
И оставим только понедельники, они будут обозначать у нас дату недели
df_date_week0=df_date_week[df_date_week.day==0]
df_week0=df_date_week0.loc[:,['date','week']]
pd.concat([df_week0.head(),df_week0.tail()], ignore_index=True)
date | week | |
---|---|---|
0 | 2022-07-25 | 0 |
1 | 2022-08-01 | 1 |
2 | 2022-08-08 | 2 |
3 | 2022-08-15 | 3 |
4 | 2022-08-22 | 4 |
5 | 2024-12-16 | 125 |
6 | 2024-12-23 | 126 |
7 | 2024-12-30 | 127 |
8 | 2025-01-06 | 128 |
9 | 2025-01-13 | 129 |
Теперь добавим в наши данные колонку с датами
df_train_date=df_train.merge(df_week0, how='left', on='week')
df_train_date=df_train_date.drop(columns="week")
df_train_date.head()
inn_id | target | date | |
---|---|---|---|
0 | inn1000051 | 4.221593e+05 | 2022-07-25 |
1 | inn1000051 | 1.622887e+03 | 2022-08-01 |
2 | inn1000051 | 1.120726e+06 | 2022-08-08 |
3 | inn1000051 | 3.949485e+04 | 2022-08-15 |
4 | inn1000051 | 4.302633e+05 | 2022-08-22 |
ETNA требует определенного формата данных: столбец, который мы хотим
спрогнозировать, должен называться target, столбец с временными метками
должен называться timestamp. Поскольку библиотека может работать с несколькими временными рядами, столбец segment является меткой ряда.
# задаем обязательные столбцы target, timestamp и segment
df_train_date=df_train_date.rename(columns={"date":"timestamp", "inn_id":"segment"})
df_train_date['timestamp'] = pd.to_datetime(df_train_date['timestamp'])
df_train_date.head()
segment | target | timestamp | |
---|---|---|---|
0 | inn1000051 | 4.221593e+05 | 2022-07-25 |
1 | inn1000051 | 1.622887e+03 | 2022-08-01 |
2 | inn1000051 | 1.120726e+06 | 2022-08-08 |
3 | inn1000051 | 3.949485e+04 | 2022-08-15 |
4 | inn1000051 | 4.302633e+05 | 2022-08-22 |
А сейчас с помощью класса TSDataset превращаем наш датафрейм в объект
TSDataset, указав частоту временного ряда по неделям (w). Но перед этим сдвинем дату начала недели на один день назад
df_train_date.timestamp=df_train_date.timestamp - pd.offsets.Day(1)
df_train_date.head()
segment | target | timestamp | |
---|---|---|---|
0 | inn1000051 | 4.221593e+05 | 2022-07-24 |
1 | inn1000051 | 1.622887e+03 | 2022-07-31 |
2 | inn1000051 | 1.120726e+06 | 2022-08-07 |
3 | inn1000051 | 3.949485e+04 | 2022-08-14 |
4 | inn1000051 | 4.302633e+05 | 2022-08-21 |
df_ts=TSDataset(df_train_date,freq='w')
df_ts.head().T
timestamp | 2022-07-24 | 2022-07-31 | 2022-08-07 | 2022-08-14 | 2022-08-21 | |
---|---|---|---|---|---|---|
segment | feature | |||||
inn1000051 | target | 4.221593e+05 | 1.622887e+03 | 1.120726e+06 | 3.949485e+04 | 4.302633e+05 |
inn1000246 | target | 1.100522e+06 | 8.728217e+05 | 6.190570e+05 | 3.056707e+05 | 7.085976e+04 |
inn1000281 | target | 0.000000e+00 | 0.000000e+00 | 3.515008e+04 | 1.617811e+05 | 2.082124e+05 |
inn1000367 | target | 3.026673e+05 | 0.000000e+00 | 3.252673e+03 | 4.074050e+05 | 6.170838e+05 |
inn1000409 | target | 2.896172e+05 | 1.531366e+05 | 6.355704e+04 | 5.182613e+04 | 1.088860e+05 |
... | ... | ... | ... | ... | ... | ... |
inn999585 | target | 4.628829e+06 | 5.920269e+06 | 5.068046e+06 | 7.802438e+06 | 2.293972e+06 |
inn999624 | target | 3.055266e+05 | 2.796333e+05 | 1.957242e+05 | 2.474823e+05 | 6.455282e+04 |
inn999737 | target | 0.000000e+00 | 1.847396e+06 | 2.552035e+06 | 3.981410e+06 | 0.000000e+00 |
inn999759 | target | 8.971788e+06 | 6.644832e+06 | 8.973864e+06 | 6.876817e+06 | 5.698205e+06 |
inn999886 | target | 2.004297e+06 | 9.469168e+05 | 2.637690e+06 | 1.108871e+06 | 3.463403e+06 |
51963 rows × 5 columns
Для прогноза используем модель наивного прогноза, т. е. в качестве прогноза берем последнее известное значение. Для этого воспользуемся классом NaiveModel –моделью наивного прогноза и обучим ее.
Горизонт прогнозирования зададим равным 12 месяцам.
# задаем горизонт прогнозирования
HORIZON = 12
Разбиваем набор
(наш объект TSDataset) на обучающую и тестовую выборки с учетом временной
структуры, так чтобы последние 12 месяцев ушли в тестовую выборку.
train_ts, test_ts = df_ts.train_test_split(
train_start='2022-07-24',
train_end='2024-07-28',
test_start='2024-08-04',
test_end='2024-10-20')
Количество лагов в модели надо задавать равным горизонту прогнозирования и выше его, чтобы избежать повторения одних и тех же прогнозов.
Создаем модель наивного прогноза, задав 12 лагов, т. е. по длине горизонта.
Теперь прогнозами будут последние 12 известных значений.
# создаем экземпляр класса NaiveModel, в котором
# реализована модель наивного прогноза,
# задав 12 лагов
model = NaiveModel(lag=12)
# обучаем модель наивного прогноза
model.fit(train_ts)
С помощью метода .make_future() формируем набор, для которого нужно
получить прогнозы (по сути, тестовую выборку). Параметр future_steps задает
количество моментов времени, которые мы хотим предсказать в будущем
(горизонт прогнозирования). Параметр tail_steps задает количество моментов
времени, предшествующих горизонту прогнозирования, которые мы хотим
использовать для прогнозов (контекст модели). Этот параметр требуется
для простых моделей типа наивного прогноза, сезонного наивного прогноза,
скользящего среднего, сезонного скользящего среднего.
HORIZON=12
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)
Точность прогноза измеряем с помощью метрики MSLE
msle = MSLE()
msle_=msle(y_true=test_ts, y_pred=forecast_ts)
Отсортируем ошибки от большего к меньшему
msle_ = sorted(msle_.items(), key=lambda item: item[1], reverse=True)
И выведем десять inn_id с наибольшими и десять с наименьшими ошибками
inn_id_sort=list(map(lambda x: x[0], msle_))
inn_id_msle_max=inn_id_sort[:10]
inn_id_msle_min=inn_id_sort[-10:]
И посмотрим на графике как соотносятся факт и прогноз
plot_forecast(TSDataset(forecast_ts[:,inn_id_msle_max,:],freq='w'), TSDataset(test_ts[:,inn_id_msle_max,:],freq='w'),
TSDataset(train_ts[:,inn_id_msle_max,:],freq='w'), n_train_samples=12)
plot_forecast(TSDataset(forecast_ts[:,inn_id_msle_min,:],freq='w'),
TSDataset(test_ts[:,inn_id_msle_min,:],freq='w'),
TSDataset(train_ts[:,inn_id_msle_min,:],freq='w'), n_train_samples=12)
Чтобы было видно более наглядно выведем графики за весь период
df_ts[:,inn_id_msle_max,:].plot(subplots=True,layout=(5,2),figsize=(10,10))
round(np.mean(np.sqrt(list(map(lambda x: x[1], msle_)))),9)
2.266491006
Сделаем прогноз на всех данных и оправим свое решение
model = NaiveModel(lag=12)
model.fit(df_ts)
future_ts = df_ts.make_future(future_steps=HORIZON,
tail_steps=model.context_size)
forecast_ts = model.forecast(future_ts,prediction_size=HORIZON)
forecast_df = forecast_ts.to_pandas(flatten=True)
forecast_df['week'] = forecast_df['timestamp'].dt.week
forecast_df['week']=forecast_df['week'].apply(lambda x: x+75 if x>2 else x+127)
forecast_df=forecast_df.rename(columns={"segment":"inn_id","target":"predict"})
df_subm=forecast_df.loc[:,['inn_id','week','predict']]
df_subm.head()
inn_id | week | predict | |
---|---|---|---|
0 | inn1000051 | 118 | 82240.835938 |
1 | inn1000051 | 119 | 136050.637695 |
2 | inn1000051 | 120 | 59850.707275 |
3 | inn1000051 | 121 | 82240.835938 |
4 | inn1000051 | 122 | 136050.637695 |
df_subm.to_csv('fcts180325_1.csv', index=False)
Получаем оценку 2,176951937 и 26-е место из 27-ми значений в таблице лидеров из 436
зарегистрированных участников.
Глядя на графики данных можно предположить, что наивный прогноз с меньшим лагом
может оказаться лучше. Берем лаг 4 и на тесте получаем 2.136 вместо 2.266,
а при лаге 3 результат становится еще лучше 2.125. Отправляем решение с лагом 3
и получаем оценку 1,957001039 и переходим с 26-го места на 21-е. На этом
с наивной моделью заканчиваем и переходим к более сложным моделям.
Комментариев нет:
Отправить комментарий