Правда в том, что тренда нет

Введение

Как правило, когда вы видите статьи или руководства по использованию таких моделей, как Xgboost или LightGBM, для решения проблемы временных рядов, автор обращается к неспособности древовидной модели улавливать тренд и прогнозировать за пределами данных. Это рассматривается как проблема, которую необходимо преодолеть. Но на самом деле это фича, а не баг. Функция, которую следует отключать только в том случае, если мы считаем, что наш прогноз должен выйти за пределы допустимого, а не потому, что в наших данных существует тенденция.

Подумайте о том, что вы заявляете, удаляя тренд ваших данных с помощью строки:

В какой-то момент в будущем данные будут иметь новое максимальное или минимальное значение.

Это довольно сильное заявление, и вы, вероятно, должны очень хорошо знать свои данные, чтобы сделать это. Особенно, когда детрендирование ваших данных также крадет информацию из дерева, даже если оно не может явно смоделировать ее.

Так что… признавая существование тренда, не полезно моделировать его…

Мотивирующий пример

Давайте взглянем на довольно известный временной ряд: набор данных Airline Passenger. Эти данные широко доступны, и одним из источников является Kaggle, который поставляется с лицензией Open Database.

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
import seaborn as sns
sns.set_style("darkgrid")#Airlines Data, if your csv is in a different filepath adjust this
df = pd.read_csv(r'AirPassengers.csv')
df.index = pd.to_datetime(df['Month'])
y = df['#Passengers']
plt.plot(y)
plt.show()

Ясно, что есть восходящая тенденция. Но сколько раз встречается новое значение «max»?

Это происходит примерно в 20% случаев, в основном в очень сезонные пики. А остальные 80%? Имеет ли смысл «позволять» модели выходить за пределы допустимого, особенно если мы уверены, что следующие несколько периодов не придутся на сезонный пик? Что еще более важно, не повлияет ли устранение тренда на точность нашего прогноза, если нам не нужно прогнозировать новое максимальное значение?

Моделирование

В этом примере мы будем использовать пакет, который я разработал для прогнозирования временных рядов с помощью LightGBM: LazyProphet. Просто выполните быструю установку pip:

pip install LazyProphet

Если вы хотите узнать больше об этом пакете, прочтите мою предыдущую статью:



Для нашего эксперимента мы будем удерживать данные за последние 12 месяцев, а затем прогнозировать с расширяющимся горизонтом как для стандартного прогноза, так и для прогноза без тренда с помощью LazyProphet. Затем мы взглянем на тестовый набор MSE для сравнения. Вот фрагмент кода, чтобы сделать именно это:

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.metrics import mean_squared_error
from LazyProphet import LazyProphet as lp
import seaborn as sns
sns.set_style("darkgrid")#Airlines Data, if your csv is in a different filepath adjust this
df = pd.read_csv(r'AirPassengers.csv')
df.index = pd.to_datetime(df['Month'])
y = df['#Passengers'][:-12]
results =[]
for i in range(1, 13):
    if (-12 + i) == 0:
        y_test = df['#Passengers'][-12:]
    else:
        y_test = df['#Passengers'][-12:-12 + i]
    lp_model = lp.LazyProphet(seasonal_period=12,
                              n_basis=10,
                              objective='regression',
                              fourier_order=5,
                              ar=list(range(1, 13)),
                              decay=.99,
                              linear_trend=False,
                              scale=True
                              )
    fitted = lp_model.fit(y)
    predicted = lp_model.predict(i)
    no_trend = mean_squared_error(y_test.values, predicted)
    lp_model = lp.LazyProphet(seasonal_period=12,
                              n_basis=10,
                              objective='regression',
                              fourier_order=5,
                              ar=list(range(1, 13)),
                              decay=.99,
                              linear_trend=True,
                              scale=True
                              )
    fitted = lp_model.fit(y)
    predicted = lp_model.predict(i)
    trend = mean_squared_error(y_test.values, predicted)
    results.append([no_trend, trend])

И давайте посмотрим на результаты:

plt.bar(x=range(1, 13), height=[i[0] for i in results], alpha=.5, label='No Trend')
plt.bar(x=range(1, 13), height=[i[1] for i in results], alpha=.5, label='De-Trended')
plt.legend()
plt.show()

Может показаться странным, что первая гистограмма не имеет значения «Нет тренда». Модель фактически предсказала это точно, поэтому значение MSE довольно мало. Даже игнорируя это, мы по-прежнему видим, что модель без тренда обычно имеет более высокий уровень ошибок для горизонта прогноза до горизонта в 7 периодов. Фактически, модель «Без тренда» имеет в среднем на 30% меньше ошибок для первых 6 горизонтов.

Можете ли вы догадаться, что происходит после горизонта в 6 периодов?

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

Это моя точка зрения. Тенденция явно существует в наших обучающих данных (и тестовых данных), но моделирование этой тенденции все время снижает нашу точность. Дело не только в существовании тренда, но и в том, верим ли мы, что наши данные в горизонте прогноза выходят за пределы нашей верхней границы. Это практический вопрос, а не статистический.

Некоторые средства правовой защиты

*Прежде чем мы рассмотрим некоторые «тесты», я просто хочу подчеркнуть, что это должны быть показатели, которые помогут вам принять решение. Или, возможно, пометить прогнозы для проверки. Линии тренда могут быть опасны!

Простым решением было бы использовать простой метод для «предсказания», если нам нужно расширить наши границы с помощью линейного тренда. Есть множество способов сделать это, но я обращусь к LazyProphet, чтобы выполнить задачу классификации. Нам нужно будет немного скорректировать модель, удалив компоненты масштабирования и ar-компоненты, а также скорректировав «цель».

Во-первых, давайте создадим наш новый набор данных, который будет просто 1, если значение является максимальным значением, или 0, если нет.

y_class = []
max_val = 0
for i in y:
    if i > max_val:
        max_val = i
        y_class.append(1)
    else:
        y_class.append(0)

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

lp_model = lp.LazyProphet(seasonal_period=12,
                          n_basis=10,
                          objective='classification',
                          fourier_order=5,
                           # ar=list(range(1, 13)),
                          decay=.99,
                          linear_trend=False,
                          scale=False,
                          return_proba=True
                          )
fitted = lp_model.fit(np.array(y_class))
predicted = lp_model.predict(12)
plt.bar(x=range(1,13), height=predicted.reshape(-1,))

Выглядит так, как ожидалось! Исходя из этого, нам нужно будет исключить тренд, если горизонт прогноза больше 6 (когда вероятность больше 0,5), что на 100% правильно. С помощью некоторой простой логики для интерпретации этого вывода мы могли бы передать True или False вместо linear_trend, что дало бы «оптимальную» производительность.

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

Другой способ, которым мы могли бы подойти к этому, — создать сегменты и проверить среднее значение этих сегментов. Если среднее значение всегда увеличивается, то у нас есть тренд. Вроде ничего нового здесь нет, но мы выберем размер корзины исходя из горизонта прогноза. Напишем функцию:

Размышляя над логикой, если ваш горизонт прогноза равен 1, то:

n_bins = int(len(y) / (forecast_horizon))

упрощается до длины ряда. Поэтому большой процент значений (больше заданного порогового процента) должен возрастать/уменьшаться. По мере увеличения горизонта прогноза мы сглаживаем данные и в итоге получаем что-то вроде этого для горизонта в 12 периодов:

Который всегда увеличивается в сглаженном представлении данных.

Далее, давайте фактически запустим этот тест:

trend = []
for i in range(1, 13):
    trend.append(tree_trend_test(y, i, threshold=.9))

Что дает нам:

[False, False, False, False, False, True, True, False, True, True, True, True]

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

Заключение

Вопрос детрендинга является важным. Этот вопрос требует нашего внимания. Я не сторонник полной автоматизации детрендинга, хотя понимаю, что иногда это требуется. Но вы всегда должны ошибаться в сторону не снижения тренда для деревьев.

Во введении я упомянул руководства по прогнозированию с помощью древовидных моделей, в какой-то момент они обычно приводят возможные «тесты» для линейного тренда. Затем продолжайте завершать раздел словами «Но ничто не сравнится с проверкой зрения», с чем я согласен. Мы могли бы показать эти данные всему сообществу Data Science, и все они сказали бы, что тренд есть — и они были бы правы.

С точки зрения традиционных временных рядов… тренд существует.

Но мы должны помнить, что мы не используем традиционный метод временных рядов. Вопрос не в существовании тренда, а скорее…

Нужна ли нам нужна тенденция?

Если вы нашли это интересным, то вы можете проверить некоторые из моих других статей: