Prerequisites

This tutorial assumes basic familiarity with StatsForecast. For a minimal example visit the Quick Start

Introduction

When we generate a forecast, we usually produce a single value known as the point forecast. This value, however, doesn’t tell us anything about the uncertainty associated with the forecast. To have a measure of this uncertainty, we need prediction intervals.

A prediction interval is a range of values that the forecast can take with a given probability. Hence, a 95% prediction interval should contain a range of values that include the actual future value with probability 95%. Probabilistic forecasting aims to generate the full forecast distribution. Point forecasting, on the other hand, usually returns the mean or the median or said distribution. However, in real-world scenarios, it is better to forecast not only the most probable future outcome, but many alternative outcomes as well.

The problem is that some timeseries models provide forecast distributions, but some other ones only provide point forecasts. How can we then estimate the uncertainty of predictions?

Prediction Intervals

For models that already provide the forecast distribution, check Prediction Intervals.

Conformal Prediction

For a video introduction, see the PyData Seattle presentation.

Multi-quantile losses and statistical models can provide provide prediction intervals, but the problem is that these are uncalibrated, meaning that the actual frequency of observations falling within the interval does not align with the confidence level associated with it. For example, a calibrated 95% prediction interval should contain the true value 95% of the time in repeated sampling. An uncalibrated 95% prediction interval, on the other hand, might contain the true value only 80% of the time, or perhaps 99% of the time. In the first case, the interval is too narrow and underestimates the uncertainty, while in the second case, it is too wide and overestimates the uncertainty.

Statistical methods also assume normality. Here, we talk about another method called conformal prediction that doesn’t require any distributional assumptions. More information on the approach can be found in this repo owned by Valery Manokhin.

Conformal prediction intervals use cross-validation on a point forecaster model to generate the intervals. This means that no prior probabilities are needed, and the output is well-calibrated. No additional training is needed, and the model is treated as a black box. The approach is compatible with any model.

Statsforecast now supports Conformal Prediction on all available models.

Install libraries

We assume that you have StatsForecast already installed. If not, check this guide for instructions on how to install StatsForecast

Install the necessary packages using pip install statsforecast

pip install statsforecast -U

Load and explore the data

For this example, we’ll use the hourly dataset from the M4 Competition. We first need to download the data from a URL and then load it as a pandas dataframe. Notice that we’ll load the train and the test data separately. We’ll also rename the y column of the test data as y_test.

import pandas as pd 

train = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly.csv')
test = pd.read_csv('https://auto-arima-results.s3.amazonaws.com/M4-Hourly-test.csv').rename(columns={'y': 'y_test'})
train.head()
unique_iddsy
0H11605.0
1H12586.0
2H13586.0
3H14559.0
4H15511.0

Since the goal of this notebook is to generate prediction intervals, we’ll only use the first 8 series of the dataset to reduce the total computational time.

n_series = 8 
uids = train['unique_id'].unique()[:n_series] # select first n_series of the dataset
train = train.query('unique_id in @uids')
test = test.query('unique_id in @uids')

We can plot these series using the statsforecast.plot method from the StatsForecast class. This method has multiple parameters, and the required ones to generate the plots in this notebook are explained below.

  • df: A pandas dataframe with columns [unique_id, ds, y].
  • forecasts_df: A pandas dataframe with columns [unique_id, ds] and models.
  • plot_random: bool = True. Plots the time series randomly.
  • models: List[str]. A list with the models we want to plot.
  • level: List[float]. A list with the prediction intervals we want to plot.
  • engine: str = matplotlib. It can also be matplotlib. plotly generates interactive plots, while matplotlib generates static plots.
from statsforecast import StatsForecast

StatsForecast.plot(train, test, plot_random = False, engine = 'plotly')

Train models

StatsForecast can train multiple models on different time series efficiently. Most of these models can generate a probabilistic forecast, which means that they can produce both point forecasts and prediction intervals.

For this example, we’ll use SimpleExponentialSmoothing and ADIDA which do not provide a prediction interval natively. Thus, it makes sense to use Conformal Prediction to generate the prediction interval.

We’ll also show using it with ARIMA to provide prediction intervals that don’t assume normality.

To use these models, we first need to import them from statsforecast.models and then we need to instantiate them.

from statsforecast.models import SeasonalExponentialSmoothing, ADIDA, ARIMA
from statsforecast.utils import ConformalIntervals

# Create a list of models and instantiation parameters 
intervals = ConformalIntervals(h=24, n_windows=2)
# P.S. n_windows*h should be less than the count of data elements in your time series sequence.
# P.S. Also value of n_windows should be atleast 2 or more.

models = [
    SeasonalExponentialSmoothing(season_length=24,alpha=0.1, prediction_intervals=intervals),
    ADIDA(prediction_intervals=intervals),
    ARIMA(order=(24,0,12), season_length=24, prediction_intervals=intervals),
]

To instantiate a new StatsForecast object, we need the following parameters:

  • df: The dataframe with the training data.
  • models: The list of models defined in the previous step.
  • freq: A string indicating the frequency of the data. See pandas’ available frequencies.
  • n_jobs: An integer that indicates the number of jobs used in parallel processing. Use -1 to select all cores.
sf = StatsForecast(
    df=train, 
    models=models, 
    freq='H', 
)

Now we’re ready to generate the forecasts and the prediction intervals. To do this, we’ll use the forecast method, which takes two arguments:

  • h: An integer that represent the forecasting horizon. In this case, we’ll forecast the next 24 hours.
  • level: A list of floats with the confidence levels of the prediction intervals. For example, level=[95] means that the range of values should include the actual future value with probability 95%.
levels = [80, 90] # confidence levels of the prediction intervals 

forecasts = sf.forecast(h=24, level=levels)
forecasts = forecasts.reset_index()
forecasts.head()
unique_iddsSeasonalESSeasonalES-lo-90SeasonalES-lo-80SeasonalES-hi-80SeasonalES-hi-90ADIDAADIDA-lo-90ADIDA-lo-80ADIDA-hi-80ADIDA-hi-90ARIMAARIMA-lo-90ARIMA-lo-80ARIMA-hi-80ARIMA-hi-90
0H1701624.132690561.315369565.365356682.900024686.950012747.292542668.049988672.099976822.485107826.535095634.355164581.760376585.810364682.900024686.950012
1H1702555.698181501.886902510.377441601.018921609.509460747.292542560.200012570.400024924.185059934.385071578.701355540.992310542.581909614.820801616.410400
2H1703514.403015468.656036471.506042557.299988560.150024747.292542546.849976549.700012944.885071947.735107544.308960528.375244531.132568557.485352560.242676
3H1704482.057892438.715790442.315796521.799988525.400024747.292542508.600006512.200012982.385071985.985107516.846619504.739288504.785309528.907959528.953979
4H1705460.222534419.595062422.745056497.700012500.850006747.292542486.149994489.2999881005.2850951008.435059502.623077485.736938488.473846516.772339519.509277

Plot prediction intervals

Here we’ll plot the different intervals using matplotlib for one timeseries.

import matplotlib.pyplot as plt
import numpy as np

def _plot_fcst(fcst, train, model): 
    fig, ax = plt.subplots(1, 1, figsize = (20,7))
    plt.plot(np.arange(0, len(train['y'])), train['y'])
    plt.plot(np.arange(len(train['y']), len(train['y']) + 24), fcst[model], label=model)
    plt.plot(np.arange(len(train['y']), len(train['y']) + 24), fcst[f'{model}-lo-90'], color = 'r', label='lo-90')
    plt.plot(np.arange(len(train['y']), len(train['y']) + 24), fcst[f'{model}-hi-90'], color = 'r', label='hi-90')
    plt.plot(np.arange(len(train['y']), len(train['y']) + 24), fcst[f'{model}-lo-80'], color = 'g', label='lo-80')
    plt.plot(np.arange(len(train['y']), len(train['y']) + 24), fcst[f'{model}-hi-80'], color = 'g', label='hi-80')
    plt.legend()
id = "H105"
temp_train = train.loc[train['unique_id'] == id]
temp_forecast = forecasts.loc[forecasts['unique_id'] == id]

The prediction interval with the SeasonalExponentialSmoothing seen below. Even if the model generates a point forecast, we are able to get a prediction interval. The 80% prediction interval does not cross the 90% prediction interval, which is a sign that the intervals are calibrated.

_plot_fcst(temp_forecast, temp_train, "SeasonalES")

For weaker fitting models, the conformal prediction interval can be larger. A better model corresponds to a narrower interval.

_plot_fcst(temp_forecast, temp_train,"ADIDA")

ARIMA is an example of a model that provides a forecast distribution, but we can still use conformal prediction to generate the prediction interval. As mentioned earlier, this method has the benefit of not assuming normality.

_plot_fcst(temp_forecast, temp_train,"ARIMA")

StatsForecast Object

Alternatively, the prediction interval can be defined on the StatsForecast object. This will apply to all models that don’t have the prediction_intervals defined.

from statsforecast.models import SimpleExponentialSmoothing, ADIDA
from statsforecast.utils import ConformalIntervals
from statsforecast import StatsForecast

models = [
    SimpleExponentialSmoothing(alpha=0.1),
    ADIDA()
]

res = StatsForecast(
    df=train, 
    models=models, 
    freq='H').forecast(h=24, prediction_intervals=ConformalIntervals(h=24, n_windows=2), level=[80]) 

res.head().reset_index()
unique_iddsSESSES-lo-80SES-hi-80ADIDAADIDA-lo-80ADIDA-hi-80
0H1701742.669067672.099976813.238159747.292542672.099976822.485107
1H1702742.669067570.400024914.938110747.292542570.400024924.185059
2H1703742.669067549.700012935.638123747.292542549.700012944.885071
3H1704742.669067512.200012973.138123747.292542512.200012982.385071
4H1705742.669067489.299988996.038147747.292542489.2999881005.285095

Future work

Conformal prediction has become a powerful framework for uncertainty quantification, providing well-calibrated prediction intervals without making any distributional assumptions. Its use has surged in both academia and industry over the past few years. We’ll continue working on it, and future tutorials may include:

  • Exploring larger datasets
  • Incorporating industry-specific examples
  • Investigating specialized methods like the jackknife+ that are closely related to conformal prediction (for details on the jackknife+ see here).

If you’re interested in any of these, or in any other related topic, please let us know by opening an issue on GitHub

Acknowledgements

We would like to thank Kevin Kho for writing this tutorial, and Valeriy Manokhin for his expertise on conformal prediction, as well as for promoting this work.

References

Manokhin, Valery. (2022). Machine Learning for Probabilistic Prediction. 10.5281/zenodo.6727505.