Forecasting Stock Prices using Prophet | by Yibin Ng | Towards Data Science

Forecasting is a hard science and requires substantial expertise. For these reasons Facebook open-sourced Prophet, which allows anyone with Python programming skills to implement forecasts on time series easily. You can read more about Prophet on Facebook’s blog or in the paper.
In this article, we will experiment with using Prophet to forecast stock prices. Prophet uses a decomposable time series model with three main model components: growth, seasonality and holidays. They are combined using the equation
y(t) = g(t) + s(t) + h(t) + e(t),
where g(t) represents the growth function which models non-periodic changes, s(t) represents periodic changes due to weekly or yearly seasonality, h(t) represents the effects of holidays, and e(t) represents the error term. Such decomposable time series are very common in forecasting and later in the article we will see how to tune each component of the above equation.
Note from Towards Data Science’s editors: While we allow independent authors to publish articles in accordance with our rules and guidelines, we do not endorse each author’s contribution. You should not rely on an author’s works without seeking professional advice. See our Reader Terms for details.
We aim to predict the daily adjusted closing prices of Vanguard Total Stock Market ETF (VTI), using data from the previous N days. In this experiment, we will use 6 years of historical prices for VTI from 2013–01–02 to 2018–12–28, which can be easily downloaded from yahoo finance. After downloading, the dataset looks like this:
Altogether, we have 1509 days of data to play with. Note that Saturdays and Sundays are not included in the dataset above. A plot of the adjusted closing price in the entire dataset is shown below:
To effectively evaluate the performance of Prophet, running one forecast at a single date is not enough. Instead, we will perform various forecasts at different dates in this dataset, and average the results. For all forecasts, we will compare the Prophet method with the Last Value method.
To evaluate the effectiveness of our methods, we will use the root mean square error (RMSE), mean absolute percentage error (MAPE), and mean absolute error (MAE) metrics. For all metrics, the lower the value, the better the prediction.
To perform a forecast, we need training and validation data. We will use 3 years of data as the train set, which corresponds to 756 days since there are about 252 trading days in a year (252*3 = 756). We will use the next 1 year of data to perform validation, which corresponds to 252 days. In other words, for each forecast we make, we need 756+252 = 1,008 days of data for model training and validation. The model will be trained using the train set, and model hyperparameters will be tuned using the validation set.
To tune the hyperparameters, we will use the moving window validation method. This is best explained using an example. Suppose we have 896 days of data in all and we wish to perform a forecast at the 857th day with forecast horizon = 40 days. As a heuristic, for a forecast horizon H, we generally make a forecast every H/2 periods. With a train set of 756 days we will be able to do validation 4 times (see diagram below). The error metrics from these 4 validations will be averaged, and model hyperparameters will be chosen based on the combination that gives the lowest averaged error metrics. With these optimum hyperparameters, we will then do a forecast on the 857th day and report the results.
In what follows, we will first perform a forecast on the 1009th day of our dataset, with a forecast horizon of 21 days (note there are about 21 trading days in a month, excluding weekends). For Prophet, we will use the first 1008 days as training and validation set, with a 756:252 split as mentioned above. We will first explain the Last Value method in the next section.
In the Last Value method, we simply set the prediction as the last observed value. In our context, this means we set current adjusted closing price as the previous day’s adjusted closing price. This is the most cost-effective forecasting model and is commonly used as a benchmark against which more sophisticated models can be compared. There are no hyperparameters to be tuned here. Our forecast on the 1009th day of our dataset, for a forecast horizon of 21 days are shown below.
The forecast above gives a RMSE of 1.89, MAPE of 1.59%, and MAE of 1.80.
To run a forecast using Prophet, use the code below.
Note that you will need to remove weekends from your future dataframe. Failing to do so will give you bad forecast results. For more info on this, see here or here. Another thing to note is that when we create future dataframe, we create it for a period bigger than H. This is because if we create future dataframe for period of H, subsequently after we removed weekends from the dataframe, we may end up with less than H days in the dataframe.
For a quick visualization, you can plot your forecast using Prophet as such:
m.plot(forecast)
The Prophet visualization is nice, but for a more detailed look I will still use Matplotlib:
The forecast above gives a RMSE of 3.40, MAPE of 2.87%, and MAE of 3.25. At this point, the Last Value method is superior. Below we will tune the hyperparameters and observe if the forecast improves.
Time series usually have abrupt changes in their trajectories. Prophet by default employs automatic changepoint detection. However, the strength of this changepoint detection can be adjusted by using the parameter changepoint_prior_scale. Increasing changepoint_prior_scale will make the trend more flexible and result in overfitting. Decreasing the changepoint_prior_scale will make the trend less flexible and result in underfitting. By default, this parameter is set to 0.05.
Changepoint prior scale can be set in Prophet as such:
m = Prophet(changepoint_prior_scale=changepoint_prior_scale)
We run our forecast using the training and validation sets and below shows the results:
The above process took 8 mins in total. From the above, we observe that the optimum value for changepoint_prior_scale is 1. Next, we use this value and run our predictions on the test set. Results are shown below.
The forecast above gives a RMSE of 0.64, MAPE of 0.52%, and MAE of 0.58.
Monthly seasonality can be set in Prophet as such:
m = Prophet()m.add_seasonality(name='monthly', period=21, fourier_order=fourier_order)
Fourier order above refers to the number of terms in the partial sum which is used to estimate seasonalities. For more details about the fourier order, refer to Prophet documentation here.
We run our forecast using the training and validation sets and below shows the results:
The above process took 17 mins in total. From the above, we observe that the optimum value for fourier_order is 3. Next, we use this value and run our predictions on the test set. Results are shown below.
The forecast above gives a RMSE of 3.52, MAPE of 2.98%, and MAE of 3.37.
Prophet is able to take holidays into account when performing predictions. First we need to construct a holiday dataframe which looks like the one below, which can be easily constructed from a csv file.
The columns lower_window and upper_window extend the holiday out to [lower_window, upper_window] days around the date. For more information, see here.
Holidays can be set in prophet as such (holidays is the dataframe shown above):
m = Prophet(holidays=holidays)
We run our forecast using the training and validation sets and below shows the results:
The above process took 9 mins in total. From the above, we observe that the optimum value for window size is None ie. not considering holidays give a better forecast. Next, we use this value and run our predictions on the test set. Results are shown below.
The forecast above gives a RMSE of 3.40, MAPE of 2.87%, and MAE of 3.25.
Here we tune changepoint, fourier order, and window size to observe if the performance can be improved further. We run our forecast using the training and validation sets and below shows the results:
Testing each of the above hyperparameters in a grid search manner would have taken 8 mins * 17 mins * 9 mins= 20 hours in total. We implemented a parallelized implementation in Python over 8 cores in our machine that reduced this process to 2 hours. You can check out the Jupyter notebook for this implementation (link provided at the end of the article).
From the above, we observe that the optimum values for changepoint_prior_scale, fourier_order and window are 2.5, 10, and not using holidays respectively. Next, we use these values and run our predictions on the test set. Results are shown below.
The forecast above gives a RMSE of 0.55, MAPE of 0.41%, and MAE of 0.47.
At this point it will be good to consolidate our results from above:
By tuning changepoint, fourier order, and window size together, we were able to achieve superior performance using Prophet for our forecast on the 1009th day. Next, let’s see how Prophet works for other days.
Having observed the effect of hyperparameter tuning above, here we will extend our methodology to run forecasts on multiple days. We will start by performing a forecast on the 1009th day of our dataset, and repeat this forecast every 42 days. Since we have 1509 days in our dataset, we will make 12 forecasts in all. For each forecast, we will stick to a forecast horizon of 21 days. Also, for each forecast on day t, we use the previous 1008 days as training and validation set, to train and tune the hyperparameters as above. After the hyperparameters are tuned, we will run the forecast on day t.
To make 12 forecasts, without our parallelized implementation, it would have taken 12 * 20 hours = 10 days. With our parallelized implementation above it took 12 * 2 hours = 24 hours.
Results are shown below.
From the above, we observe that not all forecasts are good. There are certain days where the direction and level of the forecasts match the actual values well, and certain days where direction and level of the forecasts are totally off. Let’s compare the above results with the Last Value method shown below.
Prophet returned a mean RMSE (averaged over the 11 forecasts) of 3.16, mean MAPE of 2.15%, and mean MAE of 2.86. While this seems not too bad, Last Value method actually returned better results — mean RMSE of 2.53, mean MAPE of 1.69% and mean MAE of 2.26.
You can find my Jupyter notebook here. Feel free to play with it, and if you manage to find hyperparameters such that Prophet can consistently beat the Last Value method, please share in the comments below!