Forecasting Manhattan Valley Condo Sales with NumPyro
By: Michael Grogan
Tags: Bayesian Statistics, Statistics, Data Science, Probability, Bayesian Machine Learning

Forecasting Manhattan Valley Condo Sales with NumPyro

Source: Photo by StockSnap from Pixabay

NumPyro is an increasingly popular probabilistic programming tool that is used for Bayesian analysis. One of the main advantages of using NumPyro compared to other Bayesian libraries is speed. With NumPyro being powered by JAX, this allows for use of Autograd to differentiate between native Python and NumPy functions. What’s more, XLA can be used to run NumPy programs on both GPUs and TPUs — which altogether allows for particularly fast performance.

NumPyro itself is built on PyTorch, whereas PyMC3 (another particularly popular Bayesian library) is built on Theano.

Aside from the speed advantages of using NumPyro, the library comes with a wide arsenal of tools for probabilistic programming — we will look at the use-case of time series forecasting in this instance.

For this example, NumPyro will be used to forecast total condo sales per quarter for the Manhattan Valley.

Data

The data was sourced from NYC Open Data, and the sale prices for Condos — Elevator Apartments across the Manhattan Valley were aggregated by quarter from 2003 to 2015.

When it comes to working with time series in general — a good rule of thumb is that the longer the time series — the more meaningful the data.

Let’s consider this in the context of real estate sales.

In Manhattan Valley — there are occasions where a highly expensive property sale will take place, which can greatly skew the total sales recorded for that period. For instance, the average condo price in 2004 was recorded at $825,983, but the most expensive condo sold for over $45,388,000!

In this regard, working with a monthly time series in this case would have resulted in an abnormally large peak in July 2004 (when the sale of the $45 million property took place). Using quarterly data can help to smooth out these anomalies to some degree.

When combining the sales data by quarter, we see that there are certain quarters where overall sales see a significant peak.

Source: Jupyter Notebook Output

Time Series

When using a traditional time series model such as ARIMA, a particularly important consideration is the seasonal trend of a particular series. If a time series shows a repeating pattern at certain intervals (distinct from a cyclical time series where these repeating patterns happen randomly), then this makes the series easier to predict.

We can see that, strictly speaking, the time series does not follow a seasonal pattern in this case, or at least not a significant one. The autocorrelation lags lie below the demonstrated 95% confidence interval (shaded in blue).

Source: Jupyter Notebook Output

That said, the purpose of using Bayesian analysis is to provide a probability forecast. In other words, while we cannot know definitively whether a time series follows a particular pattern — we can generate a probability assessment of whether it does.

In the above autocorrelation function, we can see that the correlations start to peak at roughly every 9 lags.

Across the plot itself, we see that sales seem to peak at approximately every 8–10 quarters.

Source: Jupyter Notebook Output

From this standpoint, let’s assume a seasonal factor of 10 for the purposes of this forecast. Note that when running the model, it is indeed possible to experiment with varying seasonal factors to determine which ones provide the best forecasts.

Forecasting Using Seasonal, Global Trend (SGT) model

In this example, the template as provided by the num.pyro.ai website is used — which replicates the Rlgt: Bayesian Exponential Smoothing Models with Trend Modifications originally devised in R.

The SGT model uses the Student’s t-distribution in place of a normal distribution to represent the likelihood, given the heavier tails of the former.

The model consists of a trend and seasonal component, and the model updates the trend values over time using what are called the level and s components.

For instance, as defined under the function for the SGT model, both level_sm and s_sm are defined as follows:

level_sm = numpyro.sample("level_sm", dist.Beta(1, 2))

s_sm = numpyro.sample("s_sm", dist.Uniform(0, 1))

Essentially, this simply represents that with the beta and uniform distributions both serving as continuous probability distributions, the random components of the model are being updated over time based on the calculated probability.

This serves as one of the key strengths of the model. For instance, we saw in the autocorrelation function that a seasonal pattern of a sort may be present in the model — but it has not been shown to be statistically significant. It could be the case that over a longer time period, we may observe a significant seasonal pattern — but simply do not have enough data to investigate whether this is the case. However, this model can be used to define a probability of observing a seasonal pattern at a particular period, and generate probability-based forecasts accordingly.

The model can also be more adept at handling uncertainty — we may not be able to say with certainty when a big spike in condo sales will take place — but we can attempt to put a probability on it.

Given that we have 52 quarters across the devised dataset in total, the first 40 quarters will be used for training purposes, with the remaining 12 used for test purposes.

y_train, y_test = jnp.array(data[:40], dtype=jnp.float32), data[40:]

Upon defining the function, training and test set, 5000 MCMC (Markov Chain Monte Carlo) samples can be generated. NUTS (or the No-U-Turn Sampler) is a means of speeding up the sampling process by avoiding the generation of samples by the random walk method — instead opting for identification of likely candidate points that allows for faster generation to a target distribution.

The model is run as below:

%%time

kernel = NUTS(sgt)

mcmc = MCMC(kernel, num_warmup=5000, num_samples=5000, num_chains=4)

mcmc.run(random.PRNGKey(0), y_train, seasonality=10)

mcmc.print_summary()

samples = mcmc.get_samples()

The model generates the following results:

Source: Jupyter Notebook Output

Now, the predictions can be made and forecast accuracy can be estimated.

>>> predictive = Predictive(sgt, samples, return_sites=["y_forecast"])

>>> forecast_marginal = predictive(random.PRNGKey(1), y_train, seasonality=10, future=12)[

>>> "y_forecast"

>>> ] >>> y_pred = jnp.mean(forecast_marginal, axis=0)

>>> sMAPE = jnp.mean(jnp.abs(y_pred - y_test) / (y_pred + y_test)) * 200

>>> msqrt = jnp.sqrt(jnp.mean((y_pred - y_test) ** 2))

>>> print("sMAPE: {:.2f}, rmse: {:.2f}".format(sMAPE, msqrt)) sMAPE: 38.90, rmse: 22167788.00

Let’s try to make more sense of these figures by looking at a graph of the forecasts.

Source: Jupyter Notebook Output

We can see that the model generally captures the overall trend — i.e. a decline in sales across the first few quarters in the test set, followed by a significant uptrend. While the main forecast (orange line) does not capture a big spike in condo sales across one particular quarter, the probability zones (in light blue) broadly indicate that a potentially big spike could be ahead.

On an RMSE (root mean squared error) basis, the RMSE of 22,167,788 is significant compared to the overall mean sales of 37,448,102 as calculated across the test set.

However, since RMSE is significantly influenced by outliers — we can expect the figure to be large in this instance. When looking at the analysis of the lynx dataset as illustrated in the original example, the calculated RMSE of 1249.29 is also quite significant compared to the mean of 1978.41 as calculated across the test set.

In this regard, a Bayesian-led time series model has value in that it can help to provide forecasts for a range of scenarios and remove uncertainty that is inherent when working with smaller datasets. In this particular instance, the model has been of use in that while we cannot define a seasonal factor with certainty — we can use probability-based methods to generate estimates of fluctuations in both the trend and seasonal factors.

Conclusion

To conclude, you have seen in this example how NumPyro can be used to:

Define an SGT (Seasonal, Global Trend) model

Use such a model to update probability estimates over time

Leverage the No-U-Turn Sampler to generate MCMC samples

Generate probability-led forecasts and determine the accuracy of the same

Many thanks for your time, and any questions or feedback are greatly welcomed. You can find more of my content at michael-grogan.com.

Disclaimer: This article is written on an “as is” basis and without warranty. It was written with the intention of providing an overview of data science concepts, and should not be interpreted as professional advice. The findings and interpretations in this article are those of the author and are not endorsed by or affiliated with any third-party mentioned in this article.