Как стать автором
Обновить
46.3

FEDOT, да не тот

Время на прочтение6 мин
Количество просмотров738

Привет, Хабр!

Меня зовут Марина, я Head of Analytics and ML в SENSE, занимаюсь анализом данных уже более 5 лет. Сначала препарировала спектры в физике высоких энергий и сотрудничала с ЦЕРН-ом, а теперь строю рекомендательные системы и аналитику.

В статье расскажу про опыт работы с пакетом FEDOT для прогнозирования временных рядов. Статья пригодится тем, кто хочет вкатиться в тему временных рядов и потыкать свои первые модельки на примере отечественных библиотек. Объясняю на примере задачи прогнозирования выходов кандидатов.

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

Что такое временной ряд?

Если полезть в учебники по матстату, вам скажут что-то в духе:

Временной ряд

собранный в разные моменты времени статистический материал о значении каких-либо параметров (в простейшем случае одного) исследуемого процесса. Каждая единица статистического материала называется измерением или отсчётом, также допустимо называть его уровнем на указанный с ним момент времени. Во временном ряде для каждого отсчёта должно быть указано время измерения или номер измерения по порядку. Временной ряд существенно отличается от простой выборки данных, так как при анализе учитывается взаимосвязь измерений со временем, а не только статистическое разнообразие и статистические характеристики выборки (с) Википедия

Всё очень круто, но ничего не понятно

Давайте дадим более пацанское определение на пальцах:

Временной ряд — это последовательность точек, которая как-то меняется во времени. Кстати, на англоязычной википедии оно именно так в начале и даётся.

Из классических примеров временных рядов можно назвать среднесуточную температуру, которую замеряли каждый день в течение года, рост и колебания цен на нефть и даже график сна, который рисуют вам умные часы — тоже временной ряд. Главное, чтобы замеры происходили за равные промежутки времени (или усреднялись по равным промежуткам).

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

У меня стояла задача предсказать, сколько кандидатов выйдет в следующем месяце, и данные были аггрегированными выходами по неделям, которые я потом объединяла в месяцы. Среди всех пакетов для временных рядов я выбрала FEDOT – у него классные мануалы на сайте и у создателей есть чатик в Телеграмм, где любой желающий может задать вопрос.

FEDOT

Полную документацию можно посмотреть здесь. Или почитать вот эту статью.

В этом пакете меня привлекло, что решение из коробки можно было довольно легко подкрутить под себя. К тому же на их сайте отличные мануалы. У fbprophet-а, кстати, мануалы и близко не стояли (я их смотрела буквально в первый месяц после его выхода, возможно, сейчас что-то поменялось в лучшую сторону).

FEDOT юзает lagged transformation, то есть создаёт временные лаги, чтобы преобразовать исходный временной ряд. После этого к данным можно применить не только модели для временных рядов типа ARIMA, но и всякие регрессии, а в идеале — построить ансамбль из этих методов.

Прекрасная картинка с их сайта для пояснения лагов
Прекрасная картинка с их сайта для пояснения лагов

Предыдущий печальный опыт с ARIMA и SARIMAX научил меня сначала пытаться что-то сделать для стационарных рядов, и уже потом страдать с нестационарными.

Стационарный временной ряд — это ряд, у которого его средние и стандартные отклонения не меняются со временем. С ним часто проще работать. Поэтому часто нестационарные ряды стараются свести к стационарным путём дифференцирования.

Так что первое, что я сделала — проверила данные на стационарность и убедившись, что ряд таки нужно приводить к стационарности, пошла его дифференцировать. Здесь я использовала дефолтный тест Дики-Фуллера.

from statsmodels.tsa.stattools import adfuller

def test_stationarity(series, title=''):
    print(f"Results of ADF Test on {title}:")
    result = adfuller(series, autolag='AIC')
    print(f"ADF Statistic: {result[0]}")
    print(f"p-value: {result[1]}")
    if result[1] > 0.05:
        print("Non-stationary")
    else:
        print("stationary")
    for key, value in result[4].items():
        print(f"Critical Value ({key}): {value}")
    print("\n")

test_stationarity(df['count'], 'Count')

В целом, подойдёт любой другой тест на стационарность. Можно и тест KPSS, если очень хочется запихнуть, но мне хватало Дики-Фуллера.

Дальше я закодила функцию для отрисовки графиков

def plot_results(actual_time_series, predicted_values, len_train_data, y_name='Parameter'):
    plt.plot(np.arange(0, len(actual_time_series)), actual_time_series, label='Actual values', c='green')
    plt.plot(np.arange(len_train_data, len_train_data + len(predicted_values)), predicted_values, label='Predicted', c='blue')
    plt.plot([len_train_data, len_train_data], [min(actual_time_series), max(actual_time_series)], c='black', linewidth=1)
    plt.ylabel(y_name, fontsize=15)
    plt.xlabel('Time index', fontsize=15)
    plt.legend(fontsize=15)
    plt.grid()
    plt.show()

В FEDOT-е есть возможность создать свой союственный пайплайн. Визуализируется это примерно так:

Пример возможного пайплайна в FEDOT
Пример возможного пайплайна в FEDOT

Дальше я ручками конструировала вершины для своего пайплайна:

# Первый уровень
node_lagged_1 = PrimaryNode('lagged')
node_lagged_1.parameters = {'window_size': 3}
node_lagged_2 = PrimaryNode('lagged')
node_lagged_2.parameters = {'window_size': 450}

# Второй уровень
node_knnreg = SecondaryNode('rfe_non_lin_reg', nodes_from=[node_lagged_1])
node_ridge = SecondaryNode('ridge', nodes_from=[node_lagged_2])

# Третий уровень - финальный узел
node_final = SecondaryNode('ridge', nodes_from=[node_knnreg, node_ridge])
complex_pipeline = Pipeline(node_final)

Что здесь происходит?

  1. Первый уровень:

    • Два входных узла lagged:

      • window_size=3: тут юзается небольшое окно значений временного ряда.

      • window_size=365: здесь уже большое окно для анализа долгосрочных зависимостей.

  2. Второй уровень:

    • rfe_non_lin_reg (Recursive Feature Elimination) убирает незначимые признаки.

    • ridge выполняет сглаживание данных.

  3. Третий уровень:

    • Финальный узел ridge, который объединяет результаты предыдущих уровней.

Что здесь плохо — я не особо запаривалась с гиперпараметрами и тыкала их ручками, в идеале было бы ещё дописать перебор больших и малых окон.

Дальше делим на test и train, это уже классика. Единственный нюанс — у Fedota свой класс InputData для данных, придётся немного заморочиться с аккуратной инициализацией.

Ладно, покекали и хватит, погнали дальше:

# Определяем задачу прогнозирования временного ряда
task = Task(TaskTypesEnum.ts_forecasting,
            TsForecastingParams(forecast_length=len_f2))

# Подготовка данных для обучения
train_input = InputData(idx=np.arange(0, len(train_array)),
                        features=train_array,
                        target=train_array,
                        task=task,
                        data_type=DataTypesEnum.ts)

# Данные для предсказания
start_forecast = len(train_array)
end_forecast = start_forecast + len_f2

forecast_idx = np.arange(start_forecast, end_forecast)
predict_input = InputData(idx=forecast_idx,
                          features=train_array,
                          target=None,
                          task=task,
                          data_type=DataTypesEnum.ts)

Что здесь происходит?

  1. Создается Task для прогнозирования временного ряда (ts_forecasting).

  2. Разделяются данные:

    • train_input: обучающая выборка, где индексы идут по порядку.

    • predict_input: создаётся пустой объект InputData, в который позже упадут данные для предсказания с соответствующими индексами.

Обучение и предсказание пишут оч просто:

# Обучение пайплайна
complex_pipeline.fit(train_input)

# Предсказание
predicted_output = complex_pipeline.predict(predict_input)

# Преобразование предсказанных значений в одномерный массив
predicted_values = np.ravel(np.array(predicted_output.predict))

# Оценка качества модели
print(f'Mean absolute error: {mean_absolute_error(test_array, predicted_values):.3f}')

# Визуализация результатов
plot_results(actual_time_series=true_values,
             predicted_values=predicted_values,
             len_train_data=len(train_array),
             y_name='Count')

В целом подход с пайплайном получился, но результаты меня не устроили

Сравнение предсказанных и тестовых данных. К сожалению, не могу показать цифирьки из-за НДА
Сравнение предсказанных и тестовых данных. К сожалению, не могу показать цифирьки из-за НДА

Короче, в этом подходе FEDOT занижал. Дисклеймер: прямо скажем, я не сильно заморачивалась с файнтьюном.

AutoML

Другой вариант использования —  взять AutoML и целиком довериться FEDOT-у.

from fedot.api.main import Fedot
from fedot.core.data.data_split import train_test_data_setup
from fedot.core.data.multi_modal import MultiModalData

data = MultiModalData.from_csv(file_path='filename.csv', task='regression', target_columns='count_offers', index_col='date')
fit_data, predict_data = train_test_data_setup(data, shuffle_flag=True, split_ratio=0.7)
# Автоматический подбор модели с ограничением по времени в 10 минут
automl_model = Fedot(problem='regression', metric=['mae', 'mse', 'mape', 'rmse'], timeout=10, seed=42)

# Обучение
automl_model.fit(features=fit_data,
                 target=fit_data.target)

# Предсказание
prediction = automl_model.predict(predict_data)
metrics = automl_model.get_metrics()

В случае AutoML ситуация чуть хуже, что иронично. Возможно, стоило дать ей больше времени на поиск себя)

Заключение

В целом, пакет FEDOT мне лично зашёл. Мне, как новичку, было интересно в нём покопаться и изучить базовые настройки. Для продвинутых юзеров, разработчики FEDOT предлагают много точечных настроек. Кстати, с Optuna он отлично сочетается.

Пишите ваши замечания и комментарии, буду рада обратной связи)

Ермак Марина

SENSE

Теги:
Хабы:
+1
Комментарии2

Публикации

Информация

Сайт
sense-group.ru
Дата регистрации
Дата основания
Численность
201–500 человек