How to Predict a Time Series Part 1 | by Bryant Crocker | Towards Data Science

Time series forecasts are quite different from other supervised regression problems. My background is in business analytics, so I’ve learned quite a bit about classical forecasting methodology (arima, exponential smoothing state space models , moving average etc...). When talking to many data scientists, I have found that many of them know little about predicting time series and treat it like other supervised learning problems with little success (usually because they aren’t engineering the right features).
The R forecast library is one of the most complete and popular libraries for handling and forecasting time series. While I do recognize that python has become more popular among data scientists, this does not mean that it is the best language for everything. Time series forecasting in R is much more mature and routine.
The goal of this article is walk through forecasting workflow and evaluation. Thus, I will leave the math to a minimum in this blog post. There are many blog posts that deal with the math behind these methods and I will link to a few. In this blog post I ‘d like to share a few things I learned from forecasting a lot of time series using the forecast package. In follow up blog posts I will cover workflow with facebook prophet and workflow for one step ahead predictions using supervised machine learning algorithms.
Not all the code used to write this article will be presented in the blog post. The full code with even more examples can be found on my github.
It is unprincipled to use k fold randomized cross-validation (because maintaining temporal order is important)We are extending a trend outside the range of observed dataPopular machine learning algorithms (xgboost, linear regression, MLP neural networks etc...) typically won’t work well without heavy feature engineeringUnderstanding uncertainty around point estimations can be more important than the point estimates themselves for decision making, especially in a business setting
It is unprincipled to use k fold randomized cross-validation (because maintaining temporal order is important)
We are extending a trend outside the range of observed data
Popular machine learning algorithms (xgboost, linear regression, MLP neural networks etc...) typically won’t work well without heavy feature engineering
Understanding uncertainty around point estimations can be more important than the point estimates themselves for decision making, especially in a business setting
The time series that I will try to predict is the weekly recorded bike path volume in the city of Seattle. The script seatleBike.r creates this dataset (pulls together 13 public datasets from Seattle open data portal, aggregates to weekly frequency).
I’ll start off with a quick line plot to visualize the time series.
df_day %>%   ggplot(aes(ds, y))+  geom_line()+   labs(x = "Date", y = "Traffic", title = "Seattle Bike Traffic")+  theme_minimal()
Observations and important notes:
We can see that the time series has seasonality. Seasonality refers to a periodic pattern, within years, that is related to the calendar day, month, quarter etc...
We can see that the time series does not appear to have a trend. a trend is a long run upward or downward direction in the series.
It appears that bike traffic is much higher during summer weeks than winter weeks. We would expect this because Seattle is cold and rainy in the winter.
When predicting a time series, we typically use previous values of the series to predict a future value. Because we use these previous values, it’s useful to plot the correlation of the y vector (the volume of traffic on bike paths in a given week) with previous y vector values. This plot is called an auto correlation plot (auto correlation because it correlation with itself).
ggAcf(df_day %>% convert()) + theme_light() + labs(title = "ACF plot of Seattle Bikes Series")
Notes:
The strongest correlation is with the most recent value. This is typical with time series
We can see there is a strong negative correlation around 26 weeks. We should expect this given the seasonality of the time series
There is a strong correlation with the observation a year (52 weeks) prior, this is expected.
We can see that the correlation drops off around 3 or 4 lags. Classical time series models will learn this structure and we can create lagged variables to incorporate this into our machine learning models.
Our time series does not appear to have a trend, but it does have seasonality. When there is seasonality in a time series (which is typically the case in most real world time series) a good baseline model is a seasonal naive model.
A seasonal naive model predicts the last value of the same season (same week last year) when forecasting. We can fit these models with the snaive() function from the forecast package. I previously stated that I would limit the math in order to zoom in on forecasting workflow. This is the only formula that I will present in the blog post. I opted to present formula to introduce notation frequently used in forecasting. I will link to more info on the formulas/math behind the rest of the models presented.
I fit the model on the train set, and predict on a held out test set to see how the model performs forecasting data that it hasn’t seen.
naiv = train %>%   snaive(h=length(test), bootstrap = T, level = 0.89) naiv %>%   forecast_eval(model_name = "Seaonal Naive")
The snaive forecast looks pretty good. The forecast is off by about 56953.55 on average on the test set (MAE). This is pretty good for a simple model on real world data.
The prediction interval covers most of the points
The model would perform much worse if the data had a trend
The model misses the large May and April spikes which can been seen reaching far above the forecast’s predictive interval
The uncertainty around the predictions grows with time
A model must perform better than this to even be viable
Another way that we can evaluate forecasts is by checking the models residuals on the training set with the checkresiduals() functions. The function returns the test statistic for a Ljung-Box test, which is a null hypothesis test to see whether the residuals are white noise. I don’t like null hypothesis testing so I will only do this once.
Another issue with residual analysis is that is stresses the accuracy of point estimates rather than the accuracy of the predictive interval. These tests also come with a laundry list of assumptions that often aren’t met with real world time series.
auto.arima() is one of the most popular functions in the forecast package. Despite its popularity, I am not a big fan of it. In my experience, arima models work best with monthly, quarterly or yearly data, but fail to capture the seasonality of weekly or daily data without Fourier terms (which auto.arima doesn’t use). Still, auto.arima() is often the only forecasting function that data scientists use.
aarim = train %>%   auto.arima() %>%   forecast(h = length(test), level = 0.89)aarim %>%   forecast_eval(model_name = "Arima")
The auto.arima() function performs worse than just guessing last years value (the snaive() model)
arima models don’t tend to work well on weekly data (Dr. Hyndman wrote the package, so if you don’t trust me trust him)
The auto.arima function chose an SARIMA(1,0,0)(1,1,0)[52] with drift. Lets slow down and explain this notation.
SARIMA models follow the form (p,d,q)x(P,D,Q)m, arima models follow the form (p, d, q):
p = number of autoregressive terms
d = number of differences
q = number of moving average terms
P=number of seasonal autoregressive terms
D=number of seasonal differences
Q=number of seasonal moving average terms
m = frequency
drift = drift coefficient or constant (both arimas and sarimas can include drift)
So that means that our model is an SARIMA model that has 1 autoregressive term, 1 seasonal autoregressive term, and 1 seasonal difference term. The model has no moving average or seasonal moving average terms. It also has a drift coefficient. We can confirm this with the summary function:
Those interested in learning more about arima models can refer to the package author’s book or this link.
When an arima model is created with additional exogenous regressors and domain knowledge, they tend to perform quite well. when analysts opt to just use the auto.arima function, models don’t tend to perform well (especially for weekly data).
Exponential smoothing methods are a family of related models, that use exponentially decreasing weights for previous values to predict the current value of a time series. These methods are extremely popular in the business analytics and supply chain domains. For the sake of brevity, I am going to use the ETS function which fits a variety of exponential smoothing methods and selects the best one. I highly recommend that you read more about the function here. This article is getting rather long, so I will not explain further.
ETS = train %>%   ets() %>%   forecast(h = length(test), bootstrap = T, level = 0.89, lamda = 'auto')ETS %>% forecast_eval()
There is a large amount of uncertainty in the forecast distribution based on the ETS model
The model totally fails to fit the seasonality because it does not handle a frequency greater than 24
Exponential smoothing models tend to work well daily data (if it doesn’t have a yearly seasonality so frequency can be set to 7), monthly and yearly data, but not weekly data. In many business situations we need to forecast weekly data and understand uncertainty.
TBATS is typically my go to method for forecasting time series with the forecast package. Like other methods in the forecast package, it is fully automatic. The model itself is flexible enough to fit a bunch of different time series and is still relatively fast to fit.
TBATS is an extension of exponential smoothing methods, that adds ARMA errors and trigonometric terms. One could write a whole blog post about this method, so I will opt to link to a couple of good posts on the method for those who are interested because a full explanation would simply make this article too long.
TBATS = train %>%   tbats()TBATS_for = TBATS %>%   forecast(h = length(test))TBATS_for %>%   forecast_eval("Tbats")
TBATS performs the best of all methods presented thus far
TBATS does a good job of representing where it is uncertain about predictions
TBATS intervals can sometimes be too wide as the package author has pointed out
I know the subtitle is incorrect the prediction interval is in grey.
Uncertainty is much higher in the summer and early fall than the rest of the year
Several successive points fall outside the prediction interval
TBATS is a flexible, accurate, algorithm that often outperforms other classical methods
Classical time series methods fail to predict large anomalies
All of the models presented used automatic model selection procedures
To find out more about forecasting I highly recommend checking out Rob Hyndman, the package author’s book. Dr. Hyndman is also working on a new version of the book using the new fable package which brings forecasting to the tidyverse (to check it out just change the 2 to a 3 in the URL). I have not been able to get fable to run on my machine yet. It will also likely be a while until it as mature as the forecast package, so for now I would stick to using the forecast package.
CODE
“The data made available here has been modified for use from its original source, which is the City of Seattle. Neither the City of Seattle nor the Office of the Chief Technology Officer (OCTO) makes any claims as to the completeness, timeliness, accuracy or content of any data contained in this application; makes any representation of any kind, including, but not limited to, warranty of the accuracy or fitness for a particular use; nor are any such warranties to be implied or inferred with respect to the information or data furnished herein. The data is subject to change as modifications and updates are complete. It is understood that the information contained in the web feed is being used at one’s own risk.”
The modifications that have been made to the dataset can all be read clearly from my code.
license for this article: https://creativecommons.org/licenses/by-sa/3.0/