Стъпка 1: Ограничете безумната тенденция

На този етап не е тайна, че Prophet страда от проблем с точността на прогнозата. Отново и отново той дава ужасни резултати в множество сравнителни тестове и състезания за прогнозиране. Все пак това е един от най-използваните алгоритми за прогнозиране там...

И така… време е да се справим с проблемите, които го измъчват, като използваме някои дребни корекции и (да се надяваме) да подобрим точността на прогнозата.

„Проект TSUtilities“

Проблем с тенденциите на Пророка

Колкото и да е странно, една от основните черти на Пророка е и една от основните му слабости. Той предоставя завладяваща изглеждаща тенденция, пълна с точки на промяна и линейни сегменти, които са много лесни за анализиране. Но понякога подходът за монтиране на тази тренд част е едновременно прекомерно и недостатъчно монтиране - просто изместване на ниво близо до края на вашата серия може да изпрати тенденцията до безкрайност над вашия прогнозен хоризонт, като същевременно остави тонполезен сигнал в остатъците.

Тази статия има за цел да разгледа този първи проблем: неограничена-безумна тенденция.

За да илюстрираме този проблем, ще използваме наборите от данни M4. Всички набори от данни са с отворен код и са активни в M-състезанията github.

За нашия първи пример нека да разгледаме седмичния набор от данни, по-специално 52-ия времеви ред в този набор от данни.

Тези графики трябва да изглеждат познати, ако сте работили с Prophet преди.

Тук прогнозираме за хоризонт от 100 - много повече от това, което беше направено за тази серия в състезанието M4. Но това показва колко извън релсите може да бъде Prophet чрез рязък спад в тенденцията от 7000 до около 5500. Това предполага пълен срив на времевата поредица в продължение на няколко години. Визуално виждам какво взема моделът и в близко бъдеще на прогнозния хоризонт вероятно е добре да го следвам. В дългосрочен план обаче тази тенденция може да доведе до катастрофални резултати. Тази идея е важна, така че ще я пренапиша с получер текст:

Следвайте тенденцията в краткосрочен план, контролирайте тенденцията в дългосрочен план.

Преди да стигнем до най-важното, нека изясним малко терминология. Виждал съм думите: овлажняване, овлажняване, овлажняване и затихване, използвани взаимозаменяемо в различни източници и епохи. Знам, че има „правилен“, който веднъж потърсих в Google, но ще ги използвам, както чувствам. Просто знайте, че те ще означават същото в тази статия и ако имате предпочитания, определено ме уведомете!

Сега към корекцията.

Фиксиране на тенденцията

Обикновено в контекста на времевия ред се появява „затихнала“ тенденция, когато се обсъжда двойно експоненциално изглаждане. За нас ще използваме нещо много по-просто, по-в съответствие с „правилото на палеца“, а не със строг статистически метод. Нещо като - експоненциално разпадане.

С експоненциално затихване ще започнем много близо до първоначалната тенденция, но ще се отдалечаваме от нея колкото по-навътре в хоризонта навлизаме. През цялото време се доближаваме до „дестинация“, която ще бъде предоставена като съгласуван параметър.

Нека да разгледаме прост пример, първо с хубав ред:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

sns.set_style('darkgrid')
y = np.linspace(0, 100, 100)
plt.plot(y)
plt.show()

Много хубава линия!

След това нека разделим това на различните части, които очакваме да имаме, когато работим с времеви серии. Тук тенденцията е равна на действителния сигнал без шум или сезонност.

y_train = y[:80]
future_y = y[80:]
future_trend = future_y

Сега ще използваме „future_trend“ и ще намалим този сигнал. За целта ще инсталираме пакет „TSUtilities“, който е комбинация от различни помощни програми, които използвам в другите си пакети „ThymeBoost“, „LazyProphet“ и „TimeMurmur“, които исках да централизирам.

pip install TSUtilities

Все още се работи върху организацията и структурата на всичко това, но TrendDampen живее тук и може да бъде импортиран като:

from TSUtilities.TSTrend.trend_dampen import TrendDampen

Двата аргумента, предадени при създаването на класа, са:

damp_factor: Поплавък между 0 и 1, където 0 означава „Пълно овлажняване“, а 1 означава „Без овлажняване“.

damp_style: Как да „заглушите“ тенденцията, преминаването на гладко заглушава тенденцията „плавно“, използвайки експоненциално затихване.

dampener = TrendDampen(damp_factor=.7,
                       damp_style='smooth')
dampened_trend = dampener.dampen(future_trend)

Тук използваме damp_factor от .7, което означава, че новата тенденция постига приблизително 70% от постигнатото със старата тенденция. Единственото нещо, което трябва да предадем на метода dampen тук, е компонентът на прогнозираната тенденция.

Както споменахме по-рано, започваме близо до първоначалната тенденция, но се отдалечаваме все повече и повече.

Това всъщност не ни помага много, тъй като предаваме твърдо кодирана стойност за damp_factor, но можем да се възползваме от допълнителна логика, вградена в този метод. Ако преминем към монтираните компоненти:

  1. реални тренировки
  2. втален тренд компонент
  3. вграден сезонен компонент

(Забележка: Всичко това е предоставено от Prophet)

и променете damp_factor на „auto“ — методът ще избере подходящ параметър за вас. Той прави това, като оценява силата на монтираната тенденция - колкото по-силна е тази тенденция, толкова повече искаме да й се доверим в дългосрочен план.

*Забележка: Силата тук се определя чрез мярката за сила на тренда, както е дефинирана от Wang, Smith, & Hyndman¹.

Това „основно правило“, което използваме, ще се разпадне в зависимост от алгоритъма. Ако приложим логиката към нещо като ETS метод, тогава тази тенденция ще бъде неоправдано силна според тези определения и ние ще й се доверим твърде много. Но за метод като Prophet или всеки друг метод, който има някакво подобие на детерминистична тенденция, той работи добре на практика.

Нека да видим как работи този метод с нашата проста линия. Първо, имаме нужда от някаква мярка за монтираната тенденция и сезонността.

trend = y_train
seasonality = np.zeros(len(y_train))

След това ще предадем тези стойности на метода dampen.

dampener = TrendDampen(damp_factor='auto',
                       damp_style='smooth')
dampened_trend = dampener.dampen(future_trend,
                                 y=y_train,
                                 trend_component=trend,
                                 seasonality_component=seasonality)
plt.plot(future_trend, color='black', alpha=.5,label='Actual Trend', linestyle='dotted')
plt.plot(dampened_trend, label='Damped Trend', alpha=.7)
plt.legend()
plt.show()

Както можете да видите, „авто“ реши да не потушава тенденцията изобщо!

Доста добра проверка на разума - въпреки че съм сигурен, че можете да нарушите тази логика доста лесно.

Нека да видим как изглеждат различните damp_factors за тази серия:

for damp_factor in [.1, .3, .5, .7, .9, 'auto']:
    dampener = TrendDampen(damp_factor=damp_factor,
                           damp_style='smooth')
    dampened_trend = dampener.dampen(future_trend,
                                     y=y_train,
                                     trend_component=trend,
                                     seasonality_component=seasonality)
    plt.plot(dampened_trend, label=damp_factor, alpha=.7)

plt.plot(future_trend, color='black', alpha=.5,label='Actual Trend', linestyle='dotted')
plt.legend()
plt.show()

Готино!

Още веднъж имаме хубав и лесно смилаем параметър с някаква „автоматична“ логика, която изглежда е добре.

А сега към основната атракция: Fixing Prophet’s Trend.

За този пример използвам 52-ия времеви ред в седмичния набор от данни от M4 и ще прогнозирам за хоризонт от 100 периода. Този прогнозен хоризонт е дълъг, но ще покаже проблема добре. Освен това ще използвам функция, която приема резултатите от Prophet и намалява тенденцията да връща тези коригирани прогнози. Тази функция изглежда така:

def dampen_prophet(y, fit_df, forecast_df):
    """
    A function that takes in the forecasted dataframe output of Prophet and
    constrains the trend based on it's percieved strength'

    Parameters
    ----------
    y : pd.Series
        The single time series of actuals that are fitted with Prophet.
    fit_df : pd.DataFrame
        The Fitted DataFrame from Prophet.
    forecast_df : pd.DataFrame
        The future forecast dataframe from prophet which includes the predicted trend.

    Returns
    -------
    forecasts_damped : np.array
        The damped trend forecast.

    """
    predictions = forecast_df.tail(len(forecast_df) - len(fit_df))
    predicted_trend = predictions['trend'].values
    trend_component = fit_df['trend'].values
    if 'multiplicative_terms' in forecast_df.columns:
        seasonality_component = fit_df['trend'].values * \
                                fit_df['multiplicative_terms'].values
        dampener = TrendDampen(damp_factor='auto',
                                damp_style='smooth')
        dampened_trend = dampener.dampen(predicted_trend,
                                         y=y,
                                         trend_component=trend_component,
                                         seasonality_component=seasonality_component)
        forecasts_damped = predictions['additive_terms'].values + \
                           dampened_trend + \
                           (dampened_trend * \
                           predictions['multiplicative_terms'].values)
    else:
        seasonality_component = fit_df['additive_terms'].values
        dampener = TrendDampen(damp_factor='auto',
                                damp_style='smooth')
        dampened_trend = dampener.dampen(predicted_trend,
                                         y=y,
                                         trend_component=trend_component,
                                         seasonality_component=seasonality_component)
        forecasts_damped = predictions['additive_terms'].values + dampened_trend
    return forecasts_damped

Но тази функция също съществува в TSUtilities и можем да я импортираме директно с помощта на:

from TSUtilities.functions import dampen_prophet

Сега нека да разгледаме какво прави нормалният изход на Prophet спрямо заглушения изход:

Можем да видим, че Prophet приема скорошна точка на промяна на тенденцията далеч под всички предишни стойности в прогнозния хоризонт, но затихналата тенденция е много по-резервирана.

Като погледнем как изглежда това от гледна точка на тенденцията, можем да видим какво точно се случва и какво е променено от метода.

Забележете как нашият метод изобщо не променя зададените стойности. Ние се стремим единствено да управляваме компонента на бъдещата тенденция.

И накрая, виждаме SMAPE и на двата подхода — доста значително увеличение на точността при използване на Damped Trend:

Бенчмаркинг с M4

Досега разгледахме един-единствен пример, но сега нека да разгледаме 2 набора от данни от M4 -Седмичнии Дневнинабори от данни. Само напомняне, че всички тези набори от данни са с отворен код и служат като добре показатели за бърникане в света на времевите редове.

Нека да импортираме седмичния набор от данни:

import matplotlib.pyplot as plt
import numpy as np
from tqdm import tqdm
import pandas as pd
from prophet import Prophet
import seaborn as sns
sns.set_style('darkgrid')

train_df = pd.read_csv(r'm4-weekly-train.csv')
test_df = pd.read_csv(r'm4-weekly-test.csv')
train_df.index = train_df['V1']
train_df = train_df.drop('V1', axis = 1)
test_df.index = test_df['V1']
test_df = test_df.drop('V1', axis = 1)

След това нека дефинираме функцията SMAPE, която ще използваме за оценка на прогнозите:

def smape(A, F):
    return 100/len(A) * np.sum(2 * np.abs(F - A) / (np.abs(A) + np.abs(F)))

Сега, след като имаме дефинирани данни и нашата метрика, нека да прегледаме набора от данни и да генерираме прогноза с Prophet – една стандартна прогноза и една със затихнала тенденция.

seasonality = 52
no_damp_smapes = []
damp_smapes = []
naive_smape = []
j = tqdm(range(len(train_df)))
for row in j:
    y = train_df.iloc[row, :].dropna()
    y = y.iloc[-(3*seasonality):]
    y_test = test_df.iloc[row, :].dropna()
    #create a random datetime index to pass to Prophet
    ds = pd.date_range(start='01-01-2000',
                       periods=len(y) + len(y_test),
                       freq='W')
    ts = y.to_frame()
    ts.columns = ['y']
    ts['ds'] = ds[:len(y)]
    j.set_description(f'{np.mean(no_damp_smapes)}, {np.mean(damp_smapes)}')
    prophet = Prophet()
    prophet.fit(ts)
    fitted = prophet.predict()

    # create a future data frame
    future = prophet.make_future_dataframe(freq='W',periods=len(y_test))
    forecast = prophet.predict(future)

    #get predictions and required data inputs for auto-damping
    predictions = forecast.tail(len(y_test))
    predicted_trend = predictions['trend'].values
    trend_component = fitted['trend'].values
    seasonality_component = fitted['additive_terms'].values
    forecasts_no_dampen = predictions['yhat'].values
    forecasts_damped = dampen_prophet(y=y.values,
                                      fit_df=fitted,
                                      forecast_df=forecast)

    #append smape for each method
    no_damp_smapes.append(smape(y_test.values, forecasts_no_dampen))
    damp_smapes.append(smape(y_test.values, forecasts_damped))
    naive_smape.append(smape(y_test.values, np.tile(y.iloc[-1], len(y_test))))
print(f'Standard Prophet {np.mean(no_damp_smapes)}')
print(f'Damped Prophet {np.mean(damp_smapes)}')
print(f'Naive {np.mean(naive_smape)}')

А резултатите?

Амортизираният метод проработи! Но само с около 2,5% намаление на SMAPE. Няма какво да пиша за вкъщи, но все пак е хубав удар. Също така си струва да припомним, че всъщност не загубихме нищо от Prophet, просто променихме тенденцията му. Ако Prophet е неразделна част от вашия процес на прогнозиране, тогава това може да се смята просто за черешката на върха.

Резултатите обаче стават по-добри за ежедневния набор от данни. Повторното изпълнение на процеса дава:

Ето, намаление на SMAPE със 7,8%!

Когато сравняваме тези резултати с други модели, сравнени с M4, откриваме, че дори стези подобрения Prophet дава лоши резултати независимо от всичко. Предполагам, че един аргумент е, че не сме настроили хиперпараметри. Така или иначе - не бих използвал Prophet.

Не бих използвал Prophet в ансамбъл.

Не бих използвал Prophet за многовариантно прогнозиране.

Просто никога не бих написал: import Prophet

Но смисълът на поредицата е да опитаме и да подобримПророка. Следващият път - ако има следващ път - имаме друг трик в ръкава си, за да постигнем тази цел.

Заключение

Показахме, че базирана на практическо правило процедура за намаляване на тренда може да бъде ефективна за повишаване на точността на прогнозата на Prophet. Процедурата е доста гъвкава и може да се използва с всякакви изходи (въпреки че самата функция dampen_prophet не е гъвкава). Показахме намаление на SMAPE с ~2,4% и ~7,8% на два набора от данни в серията M4.

Ако ви е харесала тази статия, може да се насладите на някои от другите ми!







*Предупреждение за спойлер*

Крайната точка на тази поредица ще се отърве изцяло от Prophet — подобно по идея на това, което Nixtla прави с техния StatForecast Package. За разлика от Nixtla обаче, ние ще запазим същото разлагане на вашите времеви редове и смилаема тенденция на линейна точка на промяна. Всичко това ще бъде постигнато чрез ThymeBoost.

И, разбира се, можете да се регистрирате в Medium чрез моята реферална връзка:



Референции

  1. Wang, X., Smith, K. A. & Hyndman, R. J. (2006). Групиране на базата на характеристики за данни от времеви редове. Извличане на данни и откриване на знания, 13(3), 335–364.