How to run Facebook-Prophet predict X100 faster
By: Oren Matar
Tags: Time Series Forecasting, Editors Pick, Vectorization, Prophet, Efficiency

Note that as time increases the width of the confidence interval increases, (which intuitively makes sense). This is because the trend-changes could have a greater impact the more we forecast into the future.

Now let’s vectorize

Remember our goal is to create a matrix where each row is a sampled trend, and each column is some date in the future. E.g.

(Image by Author)

But with 1000+ rows.

First, we need to sample the number and location of slope-changes. This is where Prophet samples from a Poisson distribution for each row, and then randomly assigns the number of changepoints. We’re going to get the same outcome without Poisson. The likelihood of a changepoint occurring at any time step is determined by the number of changepoint observed in the training data, divided by the length of the train data. Assuming we have a trained prophet_obj and a forecast_df with the future dates to forecast, the likelihood is:

prophet_obj.changepoints_t is a list of times where a changepoint occurred in the training data. What is single_diff? It is a consequence of how Prophet models time progression.

Prophet’s time progression

In both the train and predict functions, Prophet converts the time (“ds”) column in the Pandas DataFrame to an array called t which signifies time progression. For all kinds of reasons, it is convenient to have t in the range of 0-1 during training. So, in training, t is created by setting the interval between any two elements as 1/training_length. E.g., if there are 50 training data points, t is equal to [0, 0.02, 0.04, ..., 0.98, 1.]. For the forecast, it will continue with the same intervals [1.02, 1.04 ...] up to the length of forecast_df.

Since the t array represents the progression of time, it can be used to calculate progress over time. For example, some_coef*t creates a linear trend, where some_coef represents the slope. Thus, a slope of 4 means that in each time step the series adds 4*single_diff (4*0.02 in this example).

values (y-axis) with slope=4 over time (x-axis). Image by Author

Back to vectorizing slope changes

We now have the likelihood of a slope-change at any given point. Let’s create a matrix of slope-changes:

where k is the number of samples (rows). The result is a Boolean matrix:

Matrix of random boolean change-points. (Image by Author)

And this is where the magic happens. Take the sum of each row, plot the distribution, and you will get:

The rows-sum of a random boolean matrix is a Poisson Distribution. (Image by Author)

A Poisson distribution with a mean of likelihood * len(future_t_time)! Exactly what Prophet samples from to set the n_changes for each row!

This derives straight from the definition of Poisson – if you have q independent draws where each has a likelihood of l being positive, you get a Poisson distribution with a mean of q*l.

We replaced Prophet’s for-loop sampling of the number of changepoints in a row - and then sampling their placement in the time-steps - and replaced it with a single line for an entire matrix!

But we don’t need a Boolean value of whether a change occurs – we need the delta of the change. This part is simple:

We create a new matrix of samples, this one from a Laplace distribution with a scale of the mean absolute training data deltas. Why Laplace? I don’t know if there’s a theoretical justification, but that’s what Prophet does, so we follow it (except that Prophet performs the sampling separately for each row, and we sample a matrix). Take the product of the two matrices to get:

Matrix of slope-change values. (Image by Author)

The change in slope for each sampled trend, in each time step (remember row=sampled trend, column=time step).

We are almost done! Now we need to transition from slope changes to actual forecast values.

Slope-change to actual values

Imagine we have the following time series of deltas of slope (the change in slope values between adjacent steps): [0,1,0,0,-4,0,2]. Assuming we start with an actual slope of 0, the slope at each step will be [0,1,1,1,-3,-3,-1], as for each time step we take the sum of deltas up to it, or in other words: “cumsum”.

Deltas to actual values example. (Image by Author)

But this only gives us the slope at each point (instead of the slope-delta). We want the actual values! A linear slope means we add the slope value at each iteration. If we have a slope of 2 for 3 time-steps, the actual values will be [2,4,6]; it should be obvious that this is another cumsum. Hence, to transition from a matrix of slope-deltas to the actual values of the time series:

Remember that a slope of 4 means that 4*single_diff is the actual value, so we take the product of this new matrix with single_diff.

Adding Gaussian residuals

Now we have a matrix of sample trends, we only need to add the Gaussian noise:

Where sigma is the standard deviation around the trend in the training data.

We finished vectorizing the creation of a matrix of samples. If we want the confidence interval between 10%-90%, we set

Simply adding the lower and upper edges of the interval to the yhat forecast. All done! Except for a few small issues (feel free to skip the next 3 sections).

Mid-step trend change

There’s a tiny modeling difference between the vectorized code and Prophet. Prophet allows the slope-changes to occur between time-steps. E.g if t = [1.02, 1.04, 1.06…], a slope change can be set at 1.028. Or in other words: if your data represents weekly values on Mondays, Prophet allows for slope changes Saturdays or any other day.

I’m not sure this modeling always makes sense for all datasets. Also, from my experience, the difference is negligible.

However, for the sake of completion: Let’s consider a trend change occurring before the first actual step (in 1.0–1.02). If it is close to 1.0 (Previous Monday), by the time-step 1.02 we will get a full step-worth of change (the 4*single_diff). But if it is close to 1.02 (Sunday), the slope change will not make a big difference for that time step. The changes can occur anywhere on that spectrum, making the best correction the mean of the two extremes: each time step with the one before it (with 0 for the first one).

Another approach is to create a matrix with k times more columns, and a chengepoint likelihood of 1/k of the previous likelihood. This matrix will represent smaller intervals than the ones in the time series. After the cumsum.cumsum we could extract every k-th column. But again — the difference is probably insignificant.

Logistic growth

By default, Prophet assumes the growth is linear, but you can set it to logistic, in which case the creation of the slope-changes remains identical, but the transition from the matrix to the sampled trends is completely different (non-linear). Prophet’s code for that transition is monstrous, but it is trivial to convert the code to a vectorized version. I will not explain it in this post, but the vectorized code is given below for your use.

Training data

One final issue — all the above applies to forecasting into the future, where the trend values are not known and therefore need to be sampled. However, sometimes we call the predict function on the training data. The uncertainty there only depends on the Gaussian noise around the trend, so we can set the sample_trends to be zeros.

Time and accuracy comparison

How much faster is the vectorized version?

%%timeit

add_prophet_uncertainty(p, forecast_df)

output:

2.13 ms ± 139 µs per loop

Compared with 1s+ for the original, non-vectorized, version of Prophet. This is a 500x improvement.

And does it return the same results as Prophet? Pretty close.