Prophet/ SARIMA Model Ensembling using scipy.signal.butter | by Mario Camargo | Towards Data Science

Have you seen 2 great musicians harmoniously playing their own instruments at the same time? The outcome is much better than seeing them playing individually. That was our approach when forecasting daily revenue for our company. While there are many models that can solve this challenge, in our case, ensembling different models produced the most accurate results.
What is the Challenge?
We were comparing two different models: Prophet by Facebook and SARIMA.
Prophet is a forecasting method created by Facebook, it is capable of creating daily sales predictions. Prophet’s main benefit is that it allows you to input and analyze data with long cycles (365-day cycles, it needs at least 2 years of data). The con for us was that Prophet predictions presented low accuracy for the 10 months that we have monitored its performance.
SARIMA is another forecasting method that has been very accurate for monthly predictions in the last 10 months. However, computing a SARIMA model using input data with 365-day cycles requires a lot of resources and it would probably fail in most machines.
What is FB Prophet?
Prophet is an open Python/R library for time series forecasting based on an additive model where non-linear trends are fit with yearly, weekly, and daily seasonality. It works best with time series that have strong seasonal effects and several seasons of historical data.
For this case study, we will feed the models with 2.5 years of daily online sales data for a random set of products from my company’s database (for Data anonymization purposes).
Let’s load the data and libraries:
import pandas as pdfrom fbprophet import Prophetdf = pd.read_csv('Random_Products_SALES.csv')df.head()
We can run the model now and create predictions for the next 84 days (or 12 weeks):
m = Prophet()m.fit(df)future = m.make_future_dataframe(periods=84)forecast = m.predict(future)forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail()
Visualization of the output:
fig1 = m.plot(forecast)
What is SARIMA?
SARIMA adds Seasonality to the hyperparameters: Autoregression (AR), Integrated or Differencing (I) and Moving Average (MA) to create one of the most popular forecasting methods for univariate time series data.
Let’s try SARIMA for a 30-day cycle calculation using our data, we can start by loading the libraries:
import warningsimport itertoolsimport numpy as npimport matplotlib.pyplot as pltimport statsmodels.api as smimport matplotlib
We have to adjust the dataset to set ds as index:
y = df.set_index([‘ds’])
SARIMA requires configuration for Trend Elements:
p: Trend autoregression order.d: Trend difference order.q: Trend moving average order.
And Seasonal Elements:
P: Seasonal autoregressive order.D: Seasonal difference order.Q: Seasonal moving average order.m: The number of time steps for a single seasonal period.
Lest’s find the best AIC for any combination of p, d, q, P, D, Q:
p = d = q = range(0, 2)pdq = list(itertools.product(p, d, q))seasonal_pdq = [(x[0], x[1], x[2], 30) for x in list(itertools.product(p, d, q))]for param in pdq:    for param_seasonal in seasonal_pdq:        try:            mod = sm.tsa.statespace.SARIMAX(y,order=param,seasonal_order=param_seasonal,enforce_stationarity=False,enforce_invertibility=False)            results = mod.fit()            print('ARIMA{}x{}30 - AIC:{}'.format(param,param_seasonal,results.aic))        except:             continue
ARIMA(0, 0, 0)x(0, 0, 0, 30)30 — AIC:23755.302023704375ARIMA(0, 0, 0)x(0, 0, 1, 30)30 — AIC:22717.295494658476ARIMA(0, 0, 0)x(0, 1, 0, 30)30 — AIC:22483.193953066246ARIMA(0, 0, 0)x(0, 1, 1, 30)30 — AIC:21435.565748542045ARIMA(0, 0, 0)x(1, 0, 0, 30)30 — AIC:22383.511004484564ARIMA(0, 0, 0)x(1, 0, 1, 30)30 — AIC:22164.97784280904ARIMA(0, 0, 0)x(1, 1, 0, 30)30 — AIC:21533.45831970808ARIMA(0, 0, 0)x(1, 1, 1, 30)30 — AIC:21388.544533619006ARIMA(0, 0, 1)x(0, 0, 0, 30)30 — AIC:23131.646912989316ARIMA(0, 0, 1)x(0, 0, 1, 30)30 — AIC:22281.513637258548ARIMA(0, 0, 1)x(0, 1, 0, 30)30 — AIC:21950.950544055235ARIMA(0, 0, 1)x(0, 1, 1, 30)30 — AIC:21027.22720284167ARIMA(0, 0, 1)x(1, 0, 0, 30)30 — AIC:22291.20146896413ARIMA(0, 0, 1)x(1, 0, 1, 30)30 — AIC:22196.625728289ARIMA(0, 0, 1)x(1, 1, 0, 30)30 — AIC:21140.70479338173ARIMA(0, 0, 1)x(1, 1, 1, 30)30 — AIC:21029.135369934822ARIMA(0, 1, 0)x(0, 0, 0, 30)30 — AIC:21406.101487605534ARIMA(0, 1, 0)x(0, 0, 1, 30)30 — AIC:20701.342066955454ARIMA(0, 1, 0)x(0, 1, 0, 30)30 — AIC:21322.37708503959ARIMA(0, 1, 0)x(0, 1, 1, 30)30 — AIC:20086.59997264333ARIMA(0, 1, 0)x(1, 0, 0, 30)30 — AIC:20723.00369864943ARIMA(0, 1, 0)x(1, 0, 1, 30)30 — AIC:20702.784158890106ARIMA(0, 1, 0)x(1, 1, 0, 30)30 — AIC:20378.827165586816ARIMA(0, 1, 0)x(1, 1, 1, 30)30 — AIC:20069.301851563276ARIMA(0, 1, 1)x(0, 0, 0, 30)30 — AIC:21263.940221960394ARIMA(0, 1, 1)x(0, 0, 1, 30)30 — AIC:20557.55841401075ARIMA(0, 1, 1)x(0, 1, 0, 30)30 — AIC:21199.715947455446ARIMA(0, 1, 1)x(0, 1, 1, 30)30 — AIC:20053.5642980343ARIMA(0, 1, 1)x(1, 0, 0, 30)30 — AIC:20605.04579611025ARIMA(0, 1, 1)x(1, 0, 1, 30)30 — AIC:20558.518359458823ARIMA(0, 1, 1)x(1, 1, 0, 30)30 — AIC:20318.704493510733ARIMA(0, 1, 1)x(1, 1, 1, 30)30 — AIC:20052.683141618152ARIMA(1, 0, 0)x(0, 0, 0, 30)30 — AIC:21416.419225045975ARIMA(1, 0, 0)x(0, 0, 1, 30)30 — AIC:20711.450678416928ARIMA(1, 0, 0)x(0, 1, 0, 30)30 — AIC:21291.435652014574ARIMA(1, 0, 0)x(0, 1, 1, 30)30 — AIC:20153.52687638274ARIMA(1, 0, 0)x(1, 0, 0, 30)30 — AIC:20711.50592060887ARIMA(1, 0, 0)x(1, 0, 1, 30)30 — AIC:20713.27590112759ARIMA(1, 0, 0)x(1, 1, 0, 30)30 — AIC:20352.99101787769ARIMA(1, 0, 0)x(1, 1, 1, 30)30 — AIC:20154.2263185353ARIMA(1, 0, 1)x(0, 0, 0, 30)30 — AIC:21284.027618019507ARIMA(1, 0, 1)x(0, 0, 1, 30)30 — AIC:20579.426784812895ARIMA(1, 0, 1)x(0, 1, 0, 30)30 — AIC:21204.887071198293ARIMA(1, 0, 1)x(0, 1, 1, 30)30 — AIC:20053.382389648694ARIMA(1, 0, 1)x(1, 0, 0, 30)30 — AIC:20603.300227055624ARIMA(1, 0, 1)x(1, 0, 1, 30)30 — AIC:20580.575096299734ARIMA(1, 0, 1)x(1, 1, 0, 30)30 — AIC:20294.59890330388ARIMA(1, 0, 1)x(1, 1, 1, 30)30 — AIC:20052.39937573138ARIMA(1, 1, 0)x(0, 0, 0, 30)30 — AIC:21339.144939704944ARIMA(1, 1, 0)x(0, 0, 1, 30)30 — AIC:20633.488277537876ARIMA(1, 1, 0)x(0, 1, 0, 30)30 — AIC:21267.37941006447ARIMA(1, 1, 0)x(0, 1, 1, 30)30 — AIC:20104.39550167636ARIMA(1, 1, 0)x(1, 0, 0, 30)30 — AIC:20633.842799559407ARIMA(1, 1, 0)x(1, 0, 1, 30)30 — AIC:20634.703230420448ARIMA(1, 1, 0)x(1, 1, 0, 30)30 — AIC:20327.151794423327ARIMA(1, 1, 0)x(1, 1, 1, 30)30 — AIC:20104.25721828914ARIMA(1, 1, 1)x(0, 0, 0, 30)30 — AIC:21260.535839126904ARIMA(1, 1, 1)x(0, 0, 1, 30)30 — AIC:20554.5720707192ARIMA(1, 1, 1)x(0, 1, 0, 30)30 — AIC:21194.875022056338ARIMA(1, 1, 1)x(0, 1, 1, 30)30 — AIC:20053.71537879714ARIMA(1, 1, 1)x(1, 0, 0, 30)30 — AIC:20578.52682146238ARIMA(1, 1, 1)x(1, 0, 1, 30)30 — AIC:20555.633154745876ARIMA(1, 1, 1)x(1, 1, 0, 30)30 — AIC:20289.608637369725ARIMA(1, 1, 1)x(1, 1, 1, 30)30 — AIC:20052.89612096206
We can also print the stats summary for the best combination (0,1,1,1,1,1) and create a graph with predictions for the same period of time (84 days):
mod = sm.tsa.statespace.SARIMAX(y,                                order=(0, 1, 1),                                seasonal_order=(1, 1, 1, 30),                                enforce_stationarity=False,                                enforce_invertibility=False)results = mod.fit()print(results.summary().tables[1])
import datetime as dtpred_uc = results.get_forecast(steps=84)index_date = pd.date_range(y.index[-1], periods = 84, freq = ‘D’)forecast_series = pd.Series(list(pred_uc.predicted_mean), index = index_date)pred_ci = pred_uc.conf_int()ax = y.plot(label=’observed’, figsize=(14, 4))#print(pred_uc.predicted_mean)forecast_series.plot(ax=ax, label=’Forecast’)ax.fill_between(forecast_series.index, pred_ci.iloc[:, 0], pred_ci.iloc[:, 1], color=’k’, alpha=.1)ax.set_xlabel(‘Date’)ax.set_ylabel(‘Sales’)plt.legend()plt.show()
We can now zoom-in to the predicted period, the yellow line represents SARIMA’s result, the red line FB Prophet result and the blue line represents real sales for the first 43 days of the total 84 predicted. We can see that SARIMA’s numbers are closer to the real value (94% accuracy), but the line looks plane. On the other hand, Prophet presents a trend that harmoniously changes with daily sales, but on a different scale (61% accuracy).
In a perfect scenario, we would like to have SARIMA’s proportions with daily changes similar to Prophet’s model:
What is butter?
scipy.signal.butter is a library on Python created to filter signals, we will use a frequency of 11 (I tried a couple of them but I want to keep this article short), and we will apply this filter to prophet’s result (prophet_df).
The output of that filter will be then added to SARIMA’s results and compared to the real data (data_df).
filter_freq = 11sos = signal.butter(10, filter_freq, 'high', fs=91, output='sos')filtered = signal.sosfilt(sos, list(prophet_df.yhat))sarima_df['ensembled'] = sarima_df.sales + filteredensembled_list = np.array(sarima_df.loc[(sarima_df['date'] >= data_df['Date'].iloc[0]) & ((sarima_df['date'] <= data_df['Date'].iloc[-1])),'ensembled'])real_list = np.array(data_df.Real
The result
As the graphic below shows, the ensemble of SARIMA and Prophet follows better the real trend than SARIMA (better RMSE) and is more accurate than SARIMA or Prophet (95%):
Final Notes
Particular situations like COVID-19 forced my company to adapt the daily forecasting to increase accuracy and decrease RMSE. It doesn’t mean that this ensemble is going to improve your particular dataset’s performance, if your SARIMA monthly numbers are more accurate but Prophet shows better daily changes, this may help.
You should also consider the risk of overfitting when trying to fit your prediction so close to a particular period of time. I did this same exercise for 6 months and tried different cycles for SARIMA and different frequencies for butter to make sure I was choosing a combination that was beneficial for all months.