вторник, 18 марта 2025 г.

ETNA : соревнование Data Fusion Contest 2025 4cast наивный прогноз

 Рассмотрим как наивный прогноз библиотеки 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

Полезные процедуры :

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])

Загружаем и смотрим данные:

  1. Целевые временные ряды (target_series.parquet) и добавка  (target_series_extended.parquet)
  2. Календарь (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_idweektarget
0inn100005104.221593e+05
1inn100005111.622887e+03
2inn100005121.120726e+06
3inn100005133.949485e+04
4inn100005144.302633e+05
5inn9998861132.081143e+07
6inn9998861142.964491e+07
7inn9998861151.488255e+07
8inn9998861162.593559e+07
9inn9998861177.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)

ColumnTypeUnique ValuesTotal Unique ValuesTotal Null Values
0inn_idobject[inn1000051, inn1000246, inn1000281, inn100036...519630
2targetfloat64[422159.328125, 1622.88745117188, 1120725.625,...56550480
1weekint32[0, 1, 2, 3, 4, 5, 6, 7, ...]1180


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)

dateweek
02022-07-250
12022-07-260
22022-07-270
32022-07-280
42022-07-290
52022-07-300
62022-07-310
72022-08-011
82025-01-12128
92025-01-13129
102025-01-14129
112025-01-15129
122025-01-16129
132025-01-17129
142025-01-18129
152025-01-19129

  • 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)

dateweekday
02022-07-2500
12022-07-2601
22022-07-2702
32022-07-2803
42022-07-2904
52022-07-3005
62022-07-3106

И оставим только понедельники, они будут обозначать у нас дату недели

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)

dateweek
02022-07-250
12022-08-011
22022-08-082
32022-08-153
42022-08-224
52024-12-16125
62024-12-23126
72024-12-30127
82025-01-06128
92025-01-13129

Теперь добавим в наши данные колонку с датами

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_idtargetdate
0inn10000514.221593e+052022-07-25
1inn10000511.622887e+032022-08-01
2inn10000511.120726e+062022-08-08
3inn10000513.949485e+042022-08-15
4inn10000514.302633e+052022-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()

segmenttargettimestamp
0inn10000514.221593e+052022-07-25
1inn10000511.622887e+032022-08-01
2inn10000511.120726e+062022-08-08
3inn10000513.949485e+042022-08-15
4inn10000514.302633e+052022-08-22

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

df_train_date.timestamp=df_train_date.timestamp - pd.offsets.Day(1)
df_train_date.head()

segmenttargettimestamp
0inn10000514.221593e+052022-07-24
1inn10000511.622887e+032022-07-31
2inn10000511.120726e+062022-08-07
3inn10000513.949485e+042022-08-14
4inn10000514.302633e+052022-08-21

df_ts=TSDataset(df_train_date,freq='w')
df_ts.head().T

timestamp2022-07-242022-07-312022-08-072022-08-142022-08-21
segmentfeature
inn1000051target4.221593e+051.622887e+031.120726e+063.949485e+044.302633e+05
inn1000246target1.100522e+068.728217e+056.190570e+053.056707e+057.085976e+04
inn1000281target0.000000e+000.000000e+003.515008e+041.617811e+052.082124e+05
inn1000367target3.026673e+050.000000e+003.252673e+034.074050e+056.170838e+05
inn1000409target2.896172e+051.531366e+056.355704e+045.182613e+041.088860e+05
.....................
inn999585target4.628829e+065.920269e+065.068046e+067.802438e+062.293972e+06
inn999624target3.055266e+052.796333e+051.957242e+052.474823e+056.455282e+04
inn999737target0.000000e+001.847396e+062.552035e+063.981410e+060.000000e+00
inn999759target8.971788e+066.644832e+068.973864e+066.876817e+065.698205e+06
inn999886target2.004297e+069.469168e+052.637690e+061.108871e+063.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))

df_ts[:,inn_id_msle_min,:].plot(subplots=True,layout=(5,2),figsize=(10,10))

Мягко говоря, непростые временные ряды и предсказать их с хорошей точностью очень сложно, если вообще возможно. Для оценки решений в соревновании используется оценка 
 RMSLE (Root Mean Squared Logarithmic Error), выведем ее для нашего решения


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_idweekpredict
0inn100005111882240.835938
1inn1000051119136050.637695
2inn100005112059850.707275
3inn100005112182240.835938
4inn1000051122136050.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-е. На этом
с наивной моделью заканчиваем и переходим к более сложным моделям.
































































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

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