Quickstart¶
In this notebook, we go over the main functionalities of the library:
We will only show some minimal “get started” examples here. For more in depth information, you can refer to our user guide and example notebooks.
Installing Darts¶
We recommend using some virtual environment. Then there are mainly two ways.
With pip:
pip install darts
With conda
conda install -c conda-forge -c pytorch u8darts-all
Consult the detailed install guide if you run into issues or want to install a different flavour (avoiding certain dependencies).
First, let’s import a few things:
[1]:
%matplotlib inline
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from darts import TimeSeries
from darts.datasets import AirPassengersDataset
Building and manipulating TimeSeries
¶
TimeSeries
is the main data class in Darts. A TimeSeries
represents a univariate or multivariate time series, with a proper time index. The time index can either be of type pandas.DatetimeIndex
(containing datetimes), or of type pandas.RangeIndex
(containing integers; useful for representing sequential data without specific timestamps). In some cases, TimeSeries
can even represent probabilistic series, in order for instance to obtain confidence intervals. All models in
Darts consume TimeSeries
and produce TimeSeries
.
Read data and build a TimeSeries
¶
TimeSeries
can be built easily using a few factory methods:
From an entire Pandas
DataFrame
, usingTimeSeries.from_dataframe()
(docs).From a time index and an array of corresponding values, using
TimeSeries.from_times_and_values()
(docs).From a NumPy array of values, using
TimeSeries.from_values()
(docs).From a Pandas
Series
, usingTimeSeries.from_series()
(docs).From an
xarray.DataArray
, usingTimeSeries.from_xarray()
(docs).From a CSV file, using
TimeSeries.from_csv()
(docs).
Below, we get a TimeSeries
by directly loading the air passengers series from one of the datasets available in Darts:
[2]:
series = AirPassengersDataset().load()
series.plot()
[2]:
<Axes: xlabel='Month'>
Some TimeSeries
Operations¶
TimeSeries
support different kinds of operations - here are a few examples.
splitting
We can also split at a fraction of the series, at a pandas Timestamp
or at an integer index value.
[3]:
series1, series2 = series.split_after(0.75)
series1.plot()
series2.plot()
[3]:
<Axes: xlabel='Month'>
slicing:
[4]:
series1, series2 = series[:-36], series[-36:]
series1.plot()
series2.plot()
[4]:
<Axes: xlabel='Month'>
arithmetic operations:
[5]:
series_noise = TimeSeries.from_times_and_values(
series.time_index, np.random.randn(len(series))
)
(series / 2 + 20 * series_noise - 10).plot()
[5]:
<Axes: xlabel='Month'>
stacking
Concatenating a new dimension to produce a new single multivariate series.
[6]:
(series / 50).stack(series_noise).plot()
[6]:
<Axes: xlabel='Month'>
mapping:
[7]:
series.map(np.log).plot()
[7]:
<Axes: xlabel='Month'>
mapping on both timestamps and values:
[8]:
series.map(lambda ts, x: x / ts.days_in_month).plot()
[8]:
<Axes: xlabel='Month'>
Adding some datetime attribute as an extra dimension (yielding a multivariate series):
[9]:
(series / 20).add_datetime_attribute("month").plot()
[9]:
<Axes: xlabel='Month'>
Adding some binary holidays component:
[10]:
(series / 200).add_holidays("US").plot()
[10]:
<Axes: xlabel='Month'>
differencing:
[11]:
series.diff().plot()
[11]:
<Axes: xlabel='Month'>
Filling missing values (using a ``utils`` function).
Missing values are represented by np.nan
.
[12]:
from darts.utils.missing_values import fill_missing_values
values = np.arange(50, step=0.5)
values[10:30] = np.nan
values[60:95] = np.nan
series_ = TimeSeries.from_values(values)
(series_ - 10).plot(label="with missing values (shifted below)")
fill_missing_values(series_).plot(label="without missing values")
[12]:
<Axes: xlabel='time'>
Creating a training and validation series¶
For what follows, we will split our TimeSeries
into a training and a validation series. Note: in general, it is also a good practice to keep a test series aside and never touch it until the end of the process. Here, we just build a training and a validation series for simplicity.
The training series will be a TimeSeries
containing values until January 1958 (excluded), and the validation series a TimeSeries
containing the rest:
[13]:
train, val = series.split_before(pd.Timestamp("19580101"))
train.plot(label="training")
val.plot(label="validation")
[13]:
<Axes: xlabel='Month'>
Training forecasting models and making predictions¶
Playing with toy models¶
There is a collection of “naive” baseline models in Darts, which can be very useful to get an idea of the bare minimum accuracy that one could expect. For example, the NaiveSeasonal(K)
model always “repeats” the value that occured K
time steps ago.
In its most naive form, when K=1
, this model simply always repeats the last value of the training series:
[14]:
from darts.models import NaiveSeasonal
naive_model = NaiveSeasonal(K=1)
naive_model.fit(train)
naive_forecast = naive_model.predict(36)
series.plot(label="actual")
naive_forecast.plot(label="naive forecast (K=1)")
/Users/dennisbader/miniconda3/envs/darts310/lib/python3.10/site-packages/statsforecast/utils.py:237: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.
"ds": pd.date_range(start="1949-01-01", periods=len(AirPassengers), freq="M"),
[14]:
<Axes: xlabel='Month'>
It’s very easy to fit models and produce predictions on TimeSeries
. All the models have a fit()
and a predict()
function. This is similar to Scikit-learn, except that it is specific to time series. The fit()
function takes in argument the training time series on which to fit the model, and the predict()
function takes in argument the number of time steps (after the end of the training series) over which to forecast.
Inspect Seasonality¶
Our model above is perhaps a bit too naive. We can already improve by exploiting the seasonality in the data. It seems quite obvious that the data has a yearly seasonality, which we can confirm by looking at the auto-correlation function (ACF), and highlighting the lag m=12
:
[15]:
from darts.utils.statistics import plot_acf, check_seasonality
plot_acf(train, m=12, alpha=0.05)
The ACF presents a spike at x = 12, which suggests a yearly seasonality trend (highlighted in red). The blue zone determines the significance of the statistics for a confidence level of \(\alpha = 5\%\). We can also run a statistical check of seasonality for each candidate period m
:
[16]:
for m in range(2, 25):
is_seasonal, period = check_seasonality(train, m=m, alpha=0.05)
if is_seasonal:
print("There is seasonality of order {}.".format(period))
There is seasonality of order 12.
A less naive model¶
Let’s try the NaiveSeasonal
model again with a seasonality of 12:
[17]:
seasonal_model = NaiveSeasonal(K=12)
seasonal_model.fit(train)
seasonal_forecast = seasonal_model.predict(36)
series.plot(label="actual")
seasonal_forecast.plot(label="naive forecast (K=12)")
[17]:
<Axes: xlabel='Month'>
This is better, but we are still missing the trend. Fortunately, there is also another naive baseline model capturing the trend, which is called NaiveDrift
. This model simply produces linear predictions, with a slope that is determined by the first and last values of the training set:
[18]:
from darts.models import NaiveDrift
drift_model = NaiveDrift()
drift_model.fit(train)
drift_forecast = drift_model.predict(36)
combined_forecast = drift_forecast + seasonal_forecast - train.last_value()
series.plot()
combined_forecast.plot(label="combined")
drift_forecast.plot(label="drift")
[18]:
<Axes: xlabel='Month'>
What happened there? We simply fit a naive drift model, and add its forecast to the seasonal forecast we had previously. We also subtract the last value of the training set from the result, so that the resulting combined forecast starts off with the right offset.
Computing error metrics¶
This looks already like a fairly decent forecast, and we did not use any non-naive model yet. In fact - any model should be able to beat this.
So what’s the error we will have to beat? We will use the Mean Absolute Percentage Error (MAPE) (note that in practice there are often good reasons not to use the MAPE - we use it here as it is quite convenient and scale independent). In Darts it is a simple function call:
[19]:
from darts.metrics import mape
print(
"Mean absolute percentage error for the combined naive drift + seasonal: {:.2f}%.".format(
mape(series, combined_forecast)
)
)
Mean absolute percentage error for the combined naive drift + seasonal: 5.66%.
darts.metrics
contains many more metrics to compare time series. The metrics will compare only common slices of series when the two series are not aligned, and parallelize computation over a large number of pairs of series - but let’s not get ahead of ourselves.
Quickly try out several models¶
Darts is built to make it easy to train and validate several models in a unified way. Let’s train a few more and compute their respective MAPE on the validation set:
[20]:
from darts.models import ExponentialSmoothing, TBATS, AutoARIMA, Theta
def eval_model(model):
model.fit(train)
forecast = model.predict(len(val))
print("model {} obtains MAPE: {:.2f}%".format(model, mape(val, forecast)))
eval_model(ExponentialSmoothing())
eval_model(TBATS())
eval_model(AutoARIMA())
eval_model(Theta())
model ExponentialSmoothing() obtains MAPE: 5.11%
model TBATS() obtains MAPE: 5.87%
model AutoARIMA() obtains MAPE: 11.65%
model Theta() obtains MAPE: 8.15%
Here, we did only built these models with their default parameters. We can probably do better if we fine-tune to our problem. Let’s try with the Theta method.
Searching for hyper-parameters with the Theta method¶
The model Theta
contains an implementation of Assimakopoulos and Nikolopoulos’ Theta method. This method has had some success, particularly in the M3-competition.
Though the value of the Theta parameter is often set to 0 in applications, our implementation supports a variable value for parameter tuning purposes. Let’s try to find a good value for Theta:
[21]:
# Search for the best theta parameter, by trying 50 different values
thetas = 2 - np.linspace(-10, 10, 50)
best_mape = float("inf")
best_theta = 0
for theta in thetas:
model = Theta(theta)
model.fit(train)
pred_theta = model.predict(len(val))
res = mape(val, pred_theta)
if res < best_mape:
best_mape = res
best_theta = theta
[22]:
best_theta_model = Theta(best_theta)
best_theta_model.fit(train)
pred_best_theta = best_theta_model.predict(len(val))
print(
"The MAPE is: {:.2f}, with theta = {}.".format(
mape(val, pred_best_theta), best_theta
)
)
The MAPE is: 4.40, with theta = -3.5102040816326543.
[23]:
train.plot(label="train")
val.plot(label="true")
pred_best_theta.plot(label="prediction")
[23]:
<Axes: xlabel='Month'>
We can observe that the model with best_theta
is so far the best we have, in terms of MAPE.
Backtesting: simulate historical forecasting¶
So at this point we have a model that performs well on our validation set, and that’s good. But, how can we know the performance we would have obtained if we had been using this model historically?
Backtesting simulates predictions that would have been obtained historically with a given model. It can take a while to produce, since the model is (by default) re-trained every time the simulated prediction time advances.
Such simulated forecasts are always defined with respect to a forecast horizon, which is the number of time steps that separate the prediction time from the forecast time. In the example below, we simulate forecasts done for 3 months in the future (compared to prediction time). The result of calling historical_forecasts()
is (by default) a TimeSeries
that contains those 3-months ahead forecasts:
[24]:
historical_fcast_theta = best_theta_model.historical_forecasts(
series, start=0.6, forecast_horizon=3, verbose=True
)
series.plot(label="data")
historical_fcast_theta.plot(label="backtest 3-months ahead forecast (Theta)")
print("MAPE = {:.2f}%".format(mape(historical_fcast_theta, series)))
MAPE = 7.70%
So it seems that our best model on validation set is not doing so great anymore when we backtest it (did I hear overfitting :D)
To have a closer look at the errors, we can also use the backtest()
method to obtain all the raw errors (say, MAPE errors) that would have been obtained by our model:
[25]:
best_theta_model = Theta(best_theta)
raw_errors = best_theta_model.backtest(
series, start=0.6, forecast_horizon=3, metric=mape, reduction=None, verbose=True
)
from darts.utils.statistics import plot_hist
plot_hist(
raw_errors,
bins=np.arange(0, max(raw_errors), 1),
title="Individual backtest error scores (histogram)",
)
Finally, using backtest()
we can also get a simpler view of the average error over the historical forecasts:
[26]:
average_error = best_theta_model.backtest(
series,
start=0.6,
forecast_horizon=3,
metric=mape,
reduction=np.mean, # this is actually the default
verbose=True,
)
print("Average error (MAPE) over all historical forecasts: %.2f" % average_error)
Average error (MAPE) over all historical forecasts: 6.36
We could also for instance have specified the argument reduction=np.median
to get the median MAPE instead.
Let’s look at the fitted value residuals of our current Theta
model, i.e. the difference between the 1-step forecasts at every point in time obtained by fitting the model on all previous points, and the actual observed values:
[27]:
from darts.utils.statistics import plot_residuals_analysis
plot_residuals_analysis(best_theta_model.residuals(series))
We can see that the distribution is not centered at 0, which means that our Theta
model is biased. We can also make out a large ACF value at lag equal to 12, which indicates that the residuals contain information that was not used by the model.
Could we maybe do better with a simple ExponentialSmoothing
model?
[28]:
model_es = ExponentialSmoothing(seasonal_periods=12)
historical_fcast_es = model_es.historical_forecasts(
series, start=0.6, forecast_horizon=3, verbose=True
)
series.plot(label="data")
historical_fcast_es.plot(label="backtest 3-months ahead forecast (Exp. Smoothing)")
print("MAPE = {:.2f}%".format(mape(historical_fcast_es, series)))
MAPE = 4.45%
This much better! We get a mean absolute percentage error of about 4-5% when backtesting with a 3-months forecast horizon in this case.
[29]:
plot_residuals_analysis(model_es.residuals(series))
The residual analysis also reflects an improved performance in that we now have a distribution of the residuals centred at value 0, and the ACF values, although not insignificant, have lower magnitudes.
Machine learning and global models¶
Darts has a rich support for machine learning and deep learning forecasting models; for instance:
RegressionModel can wrap around any sklearn-compatible regression model to produce forecasts (it has its own section below).
RNNModel is a flexible RNN implementation, which can be used like DeepAR.
NBEATSModel implements the N-BEATS model.
TFTModel implements the Temporal Fusion Transformer model.
TCNModel implements temporal convolutional networks.
…
In addition to supporting the same basic fit()
/predict()
interface as the other models, these models are also global models, as they support being trained on multiple time series (sometimes referred to as meta learning).
This is a key point of using ML-based models for forecasting: more often than not, ML models (especially deep learning models) need to be trained on large amounts of data, which often means a large amount of separate yet related time series.
In Darts, the basic way to specify multiple TimeSeries
is using a Sequence
of TimeSeries
(for instance, a simple list of TimeSeries
).
A toy example with two series¶
These models can be trained on thousands of series. Here, for the sake of illustration, we will load two distinct series - the air traffic passenger count and another series containing the number of pounds of milk produced per cow monthly. We also cast our series to np.float32
as that will slightly speedup the training:
[30]:
from darts.datasets import AirPassengersDataset, MonthlyMilkDataset
series_air = AirPassengersDataset().load().astype(np.float32)
series_milk = MonthlyMilkDataset().load().astype(np.float32)
# set aside last 36 months of each series as validation set:
train_air, val_air = series_air[:-36], series_air[-36:]
train_milk, val_milk = series_milk[:-36], series_milk[-36:]
train_air.plot()
val_air.plot()
train_milk.plot()
val_milk.plot()
[30]:
<Axes: xlabel='Month'>
First, let’s scale these two series between 0 and 1, as that will benefit most ML models. We will use a Scaler for this:
[31]:
from darts.dataprocessing.transformers import Scaler
scaler = Scaler()
train_air_scaled, train_milk_scaled = scaler.fit_transform([train_air, train_milk])
train_air_scaled.plot()
train_milk_scaled.plot()
[31]:
<Axes: xlabel='Month'>
Note how we can scale several series in one go. We can also parallelize this sort of operations over multiple processors by specifying n_jobs
.
Using deep learning: example with N-BEATS¶
Next, we will build an N-BEATS model. This model can be tuned with many hyper-parameters (such as number of stacks, layers, etc). Here, for simplicity, we will use it with default hyper-parameters. The only two hyper-parameters that we have to provide are:
input_chunk_length
: this is the “lookback window” of the model - i.e., how many time steps of history the neural network takes as input to produce its output in a forward pass.output_chunk_length
: this is the “forward window” of the model - i.e., how many time steps of future values the neural network outputs in a forward pass.
The random_state
parameter is just here to get reproducible results.
Most neural networks in Darts require these two parameters. Here, we will use multiples of the seasonality. We are now ready to fit our model on our two series (by giving a list containing the two series to fit()
):
[32]:
from darts.models import NBEATSModel
model = NBEATSModel(input_chunk_length=24, output_chunk_length=12, random_state=42)
model.fit([train_air_scaled, train_milk_scaled], epochs=50, verbose=True);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
| Name | Type | Params
---------------------------------------------------
0 | criterion | MSELoss | 0
1 | train_metrics | MetricCollection | 0
2 | val_metrics | MetricCollection | 0
3 | stacks | ModuleList | 6.2 M
---------------------------------------------------
6.2 M Trainable params
1.4 K Non-trainable params
6.2 M Total params
24.787 Total estimated model params size (MB)
`Trainer.fit` stopped: `max_epochs=50` reached.
Let’s now get some forecasts 36 months ahead, for our two series. We can just use the series
argument of the fit()
function to tell the model which series to forecast. Importantly, the output_chunk_length
does not directly constrain the forecast horizon n
that can be used with predict()
. Here, we trained the model with output_chunk_length=12
and produce forecasts for n=36
months ahead; this is simply done in an auto-regressive way behind the scenes (where the network
recursively consumes its previous outputs).
[33]:
pred_air = model.predict(series=train_air_scaled, n=36)
pred_milk = model.predict(series=train_milk_scaled, n=36)
# scale back:
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])
plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)")
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
[33]:
<Axes: xlabel='Month'>
Our forecasts are actually not so terrible, considering that we use one model with default hyper-parameters to capture both air passengers and milk production!
The model seems quite OK at capturing the yearly seasonality, but misses the trend for the air series. In the next section, we will try to solve this issue using external data (covariates).
Covariates: using external data¶
In addition to the target series (the series we are interested to forecast), many models in Darts also accept covariates series in input. Covariates are series that we do not want to forecast, but which can provide helpful additional information to the models. Both the targets and covariates can be multivariate or univariate.
There are two kinds of covariate time series in Darts:
past_covariates
are series not necessarily known ahead of the forecast time. Those can for instance represent things that have to be measured and are not known upfront. Models do not use the future values ofpast_covariates
when making forecasts.future_covariates
are series which are known in advance, up to the forecast horizon. This can represent things such as calendar information, holidays, weather forecasts, etc. Models that acceptfuture_covariates
will look at the future values (up to the forecast horizon) when making forecasts.
Each covariate can potentially be multivariate. If you have several covariate series (such as month and year values), you should stack()
or concatenate()
them to obtain a multivariate series.
The covariates you provide can be longer than necessary. Darts will try to be smart and slice them in the right way for forecasting the target, based on the time indexes of the different series. You will receive an error if your covariates do not have a sufficient time span, though.
Let’s now build some external covariates containing both monthly and yearly values for our air and milk series. In the cell below, we use the darts.utils.timeseries_generation.datetime_attribute_timeseries()
function to generate series containing the month and year values, and we concatenate()
these series along the "component"
axis in order to obtain one covariate series with two components (month and year), per target series. For simplicity, we directly scale the month and year
values to have them between (roughly) 0 and 1:
[34]:
from darts import concatenate
from darts.utils.timeseries_generation import datetime_attribute_timeseries as dt_attr
air_covs = concatenate(
[
dt_attr(series_air.time_index, "month", dtype=np.float32) / 12,
(dt_attr(series_air.time_index, "year", dtype=np.float32) - 1948) / 12,
],
axis="component",
)
milk_covs = concatenate(
[
dt_attr(series_milk.time_index, "month", dtype=np.float32) / 12,
(dt_attr(series_milk.time_index, "year", dtype=np.float32) - 1962) / 13,
],
axis="component",
)
air_covs.plot()
plt.title(
"one multivariate time series of 2 dimensions, containing covariates for the air series:"
);
Not all models support all types of covariates. NBEATSModel
supports only past_covariates
. Therefore, even though our covariates represent calendar information and are known in advance, we will use them as past_covariates
with N-BEATS. To train, all we have to do is give them as past_covariates
to the fit()
function, in the same order as the targets:
[35]:
model = NBEATSModel(input_chunk_length=24, output_chunk_length=12, random_state=42)
model.fit(
[train_air_scaled, train_milk_scaled],
past_covariates=[air_covs, milk_covs],
epochs=50,
verbose=True,
);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
| Name | Type | Params
---------------------------------------------------
0 | criterion | MSELoss | 0
1 | train_metrics | MetricCollection | 0
2 | val_metrics | MetricCollection | 0
3 | stacks | ModuleList | 6.6 M
---------------------------------------------------
6.6 M Trainable params
1.7 K Non-trainable params
6.6 M Total params
26.314 Total estimated model params size (MB)
`Trainer.fit` stopped: `max_epochs=50` reached.
Then to produce forecasts, we again have to provide our covariates as past_covariates
to the predict()
function. Even though the covariates time series also contains “future” values of the covariates up to the forecast horizon, the model will not consume those future values, because it uses them as past covariates (and not future covariates).
[36]:
pred_air = model.predict(series=train_air_scaled, past_covariates=air_covs, n=36)
pred_milk = model.predict(series=train_milk_scaled, past_covariates=milk_covs, n=36)
# scale back:
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])
plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)")
`predict()` was called with `n > output_chunk_length`: using auto-regression to forecast the values after `output_chunk_length` points. The model will access `(n - output_chunk_length)` future values of your `past_covariates` (relative to the first predicted time step). To hide this warning, set `show_warnings=False`.
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
`predict()` was called with `n > output_chunk_length`: using auto-regression to forecast the values after `output_chunk_length` points. The model will access `(n - output_chunk_length)` future values of your `past_covariates` (relative to the first predicted time step). To hide this warning, set `show_warnings=False`.
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
[36]:
<Axes: xlabel='Month'>
It seems that now the model captures better the trend of the air series (which also perturbs a bit the forecasts of the milk series).
Encoders: using covariates for free¶
Using covariates related to the calendar or time axis (such as months and years as in our example above) is so frequent that deep learning models in Darts have a built-in functionality to use such covariates out of the box.
To easily integrate such covariates to your model, you can simply specify the add_encoders
parameter at model creation. This parameter has to be a dictionary containing informations about what should be encoded as extra covariates. Here is an example of what such a dictionary could look like, for a model supporting both past and future covariates:
[37]:
def extract_year(idx):
"""Extract the year each time index entry and normalized it."""
return (idx.year - 1950) / 50
encoders = {
"cyclic": {"future": ["month"]},
"datetime_attribute": {"future": ["hour", "dayofweek"]},
"position": {"past": ["absolute"], "future": ["relative"]},
"custom": {"past": [extract_year]},
"transformer": Scaler(),
}
In the above dictionary, the following things are specified:
The month should be used as a future covariate, with a cyclic (sin/cos) encoding.
The hour and day-of-the-week should be used as future covariates.
The absolute position (time step in the series) should be used as past covariates.
The relative position (w.r.t the forecasting time) should be used as future covariates.
An additional custom function of the year should be used as past covariates.
All the above covariates should be scaled using a
Scaler
, which will be fit upon calling the modelfit()
function and used afterwards to transform the covariates.
We refer to the API doc for more informations about how to use encoders. Note that lambda functions cannot be used as they are not pickable.
To replicate our example with month and year used as past covariates with N-BEATS, we can use some encoders as follows:
[38]:
encoders = {"datetime_attribute": {"past": ["month", "year"]}, "transformer": Scaler()}
Now, the whole training of the N-BEATS model with these covariates looks as follows:
[39]:
model = NBEATSModel(
input_chunk_length=24,
output_chunk_length=12,
add_encoders=encoders,
random_state=42,
)
model.fit([train_air_scaled, train_milk_scaled], epochs=50, verbose=True);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
| Name | Type | Params
---------------------------------------------------
0 | criterion | MSELoss | 0
1 | train_metrics | MetricCollection | 0
2 | val_metrics | MetricCollection | 0
3 | stacks | ModuleList | 6.6 M
---------------------------------------------------
6.6 M Trainable params
1.7 K Non-trainable params
6.6 M Total params
26.314 Total estimated model params size (MB)
`Trainer.fit` stopped: `max_epochs=50` reached.
And get some forecasts for the air passengers series:
[40]:
pred_air = model.predict(series=train_air_scaled, n=36)
# scale back:
pred_air = scaler.inverse_transform(pred_air)
plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
pred_air.plot(label="forecast (air)")
`predict()` was called with `n > output_chunk_length`: using auto-regression to forecast the values after `output_chunk_length` points. The model will access `(n - output_chunk_length)` future values of your `past_covariates` (relative to the first predicted time step). To hide this warning, set `show_warnings=False`.
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
[40]:
<Axes: xlabel='Month'>
Regression forecasting models¶
RegressionModel’s are forecasting models which wrap around sklearn-compatible regression models. The inner regression model is used to predict future values of the target series, as a function of certain lags of the target, past and future covariates. Behind the scenes, the time series are tabularized in order to build a training dataset in the right format.
By default, the RegressionModel
will do a linear regression. It is very easy to use any desired sklearn-compatible regression model by specifying the model
parameter, but for convenience Darts also provides a couple of ready-made models out of the box:
RandomForest wraps around
sklearn.ensemble.RandomForestRegressor
.LightGBMModel wraps around
lightbm
.LinearRegressionModel wraps around
sklearn.linear_model.LinearRegression
(accepting the same kwargs).
For example, this is what fitting a Bayesian ridge regression to our toy two-series problem looks like:
[41]:
from darts.models import RegressionModel
from sklearn.linear_model import BayesianRidge
model = RegressionModel(lags=72, lags_future_covariates=[-6, 0], model=BayesianRidge())
model.fit(
[train_air_scaled, train_milk_scaled], future_covariates=[air_covs, milk_covs]
);
Several things happened above:
lags=72
is telling theRegressionModel
to look at the past 72 lags of the target.In addition,
lags_future_covariates=[-6, 0]
means that the model will also look at lags of thefuture_covariates
we provide. Here we enumerate the precise lags we want the models to take into account; the “-6th” and the “0th” lags. The “0th” lag means the “current” lag (i.e., at the time step being forecasted); obviously, knowning this lag requires knowing the data in advance (hence the fact we are usingfuture_covariates
). Similarly,-6
means we also look at the value of the covariates 6 months before the forecasted time step (which also requires to know the covariates in advance if we are forecasting at a horizon more than 6 steps ahead).model=BayesianRidge()
provides the actual inner regression model.
Now let’s get some forecasts:
[42]:
pred_air, pred_milk = model.predict(
series=[train_air_scaled, train_milk_scaled],
future_covariates=[air_covs, milk_covs],
n=36,
)
# scale back:
pred_air, pred_milk = scaler.inverse_transform([pred_air, pred_milk])
plt.figure(figsize=(10, 6))
series_air.plot(label="actual (air)")
series_milk.plot(label="actual (milk)")
pred_air.plot(label="forecast (air)")
pred_milk.plot(label="forecast (milk)")
[42]:
<Axes: xlabel='Month'>
Note how we obtained the forecasts for the two time series at once above. Similarly, we can also get some metrics over sequences of series:
[43]:
mape([series_air, series_milk], [pred_air, pred_milk])
[43]:
[3.4170475, 5.2831783]
or the average metric over “all” series:
[44]:
mape([series_air, series_milk], [pred_air, pred_milk], component_reduction=np.mean)
[44]:
[3.4170475, 5.2831783]
By the way: similarly to transformers such as Scaler
, computing metrics can be parallelized over N
processors when executed over many series pairs by specifying n_jobs=N
.
It seems that this model performs well on the Air traffic series, how does it do when we backtest it on this one series?
[45]:
bayes_ridge_model = RegressionModel(
lags=72, lags_future_covariates=[0], model=BayesianRidge()
)
backtest = bayes_ridge_model.historical_forecasts(
series_air, future_covariates=air_covs, start=0.6, forecast_horizon=3, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
`enable_optimization=True` is ignored because `retrain` is not `False` or `0`.To hide this warning, set `show_warnings=False` or `enable_optimization=False`.
`enable_optimization=True` is ignored because `forecast_horizon > model.output_chunk_length`.To hide this warning, set `show_warnings=False` or `enable_optimization=False`.
MAPE = 3.66
[45]:
<Axes: xlabel='time'>
Our best model so far!
Probabilistic forecasts¶
Some models can produce probabilistic forecasts. This is the case for all deep learning models (such as RNNModel
, NBEATSModel
, etc …), as well as for ARIMA
and ExponentialSmoothing
. The full list is available on the Darts README page.
For ARIMA
and ExponentialSmoothing
, one can simply specify a num_samples
parameter to the predict()
function. The returned TimeSeries
will then be composed of num_samples
Monte Carlo samples describing the distribution of the time series’ values. The advantage of relying on Monte Carlo samples (in contrast to, say, explicit confidence intervals) is that they can be used to describe any parametric or non-parametric joint distribution over components, and compute arbitrary
quantiles.
[46]:
model_es = ExponentialSmoothing()
model_es.fit(train)
probabilistic_forecast = model_es.predict(len(val), num_samples=500)
series.plot(label="actual")
probabilistic_forecast.plot(label="probabilistic forecast")
plt.legend()
plt.show()
With neural networks¶
With neural networks, one has to give a Likelihood
object to the model. The likelihoods specify which distribution the model will try to fit, along with potential prior values for the distributions’ parameters. The full list of available likelihoods is available in the docs.
Using likelihoods is easy. For instance, here is what training an TCNModel
to fit a Laplace likelihood looks like:
[47]:
from darts.models import TCNModel
from darts.utils.likelihood_models import LaplaceLikelihood
model = TCNModel(
input_chunk_length=24,
output_chunk_length=12,
random_state=42,
likelihood=LaplaceLikelihood(),
)
model.fit(train_air_scaled, epochs=400, verbose=True);
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
| Name | Type | Params
----------------------------------------------------
0 | criterion | MSELoss | 0
1 | train_metrics | MetricCollection | 0
2 | val_metrics | MetricCollection | 0
3 | dropout | MonteCarloDropout | 0
4 | res_blocks | ModuleList | 166
----------------------------------------------------
166 Trainable params
0 Non-trainable params
166 Total params
0.001 Total estimated model params size (MB)
`Trainer.fit` stopped: `max_epochs=400` reached.
Then to get probabilistic forecasts, we again only need to specify some num_samples >> 1
:
[48]:
pred = model.predict(n=36, num_samples=500)
# scale back:
pred = scaler.inverse_transform(pred)
series_air.plot()
pred.plot()
GPU available: True (mps), used: True
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
/Users/dennisbader/miniconda3/envs/darts310/lib/python3.10/site-packages/torch/_tensor_str.py:137: UserWarning: MPS: nonzero op is supported natively starting from macOS 13.0. Falling back on CPU. This may have performance implications. (Triggered internally at /Users/runner/work/pytorch/pytorch/pytorch/aten/src/ATen/native/mps/operations/Indexing.mm:283.)
nonzero_finite_vals = torch.masked_select(
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
Cell In[48], line 1
----> 1 pred = model.predict(n=36, num_samples=500)
3 # scale back:
4 pred = scaler.inverse_transform(pred)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/utils/torch.py:112, in random_method.<locals>.decorator(self, *args, **kwargs)
110 with fork_rng():
111 manual_seed(self._random_instance.randint(0, high=MAX_TORCH_SEED_VALUE))
--> 112 return decorated(self, *args, **kwargs)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/models/forecasting/torch_forecasting_model.py:1401, in TorchForecastingModel.predict(self, n, series, past_covariates, future_covariates, trainer, batch_size, verbose, n_jobs, roll_size, num_samples, num_loader_workers, mc_dropout, predict_likelihood_parameters, show_warnings)
1382 super().predict(
1383 n,
1384 series,
(...)
1389 show_warnings=show_warnings,
1390 )
1392 dataset = self._build_inference_dataset(
1393 target=series,
1394 n=n,
(...)
1398 bounds=None,
1399 )
-> 1401 predictions = self.predict_from_dataset(
1402 n,
1403 dataset,
1404 trainer=trainer,
1405 verbose=verbose,
1406 batch_size=batch_size,
1407 n_jobs=n_jobs,
1408 roll_size=roll_size,
1409 num_samples=num_samples,
1410 num_loader_workers=num_loader_workers,
1411 mc_dropout=mc_dropout,
1412 predict_likelihood_parameters=predict_likelihood_parameters,
1413 )
1415 return predictions[0] if called_with_single_series else predictions
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/utils/torch.py:112, in random_method.<locals>.decorator(self, *args, **kwargs)
110 with fork_rng():
111 manual_seed(self._random_instance.randint(0, high=MAX_TORCH_SEED_VALUE))
--> 112 return decorated(self, *args, **kwargs)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/models/forecasting/torch_forecasting_model.py:1546, in TorchForecastingModel.predict_from_dataset(self, n, input_series_dataset, trainer, batch_size, verbose, n_jobs, roll_size, num_samples, num_loader_workers, mc_dropout, predict_likelihood_parameters)
1541 self.trainer = self._setup_trainer(
1542 trainer=trainer, model=self.model, verbose=verbose, epochs=self.n_epochs
1543 )
1545 # prediction output comes as nested list: list of predicted `TimeSeries` for each batch.
-> 1546 predictions = self.trainer.predict(self.model, pred_loader)
1547 # flatten and return
1548 return [ts for batch in predictions for ts in batch]
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/trainer/trainer.py:863, in Trainer.predict(self, model, dataloaders, datamodule, return_predictions, ckpt_path)
861 self.state.status = TrainerStatus.RUNNING
862 self.predicting = True
--> 863 return call._call_and_handle_interrupt(
864 self, self._predict_impl, model, dataloaders, datamodule, return_predictions, ckpt_path
865 )
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/trainer/call.py:44, in _call_and_handle_interrupt(trainer, trainer_fn, *args, **kwargs)
42 if trainer.strategy.launcher is not None:
43 return trainer.strategy.launcher.launch(trainer_fn, *args, trainer=trainer, **kwargs)
---> 44 return trainer_fn(*args, **kwargs)
46 except _TunerExitException:
47 _call_teardown_hook(trainer)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/trainer/trainer.py:902, in Trainer._predict_impl(self, model, dataloaders, datamodule, return_predictions, ckpt_path)
898 assert self.state.fn is not None
899 ckpt_path = self._checkpoint_connector._select_ckpt_path(
900 self.state.fn, ckpt_path, model_provided=model_provided, model_connected=self.lightning_module is not None
901 )
--> 902 results = self._run(model, ckpt_path=ckpt_path)
904 assert self.state.stopped
905 self.predicting = False
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/trainer/trainer.py:986, in Trainer._run(self, model, ckpt_path)
981 self._signal_connector.register_signal_handlers()
983 # ----------------------------
984 # RUN THE TRAINER
985 # ----------------------------
--> 986 results = self._run_stage()
988 # ----------------------------
989 # POST-Training CLEAN UP
990 # ----------------------------
991 log.debug(f"{self.__class__.__name__}: trainer tearing down")
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/trainer/trainer.py:1027, in Trainer._run_stage(self)
1025 return self._evaluation_loop.run()
1026 if self.predicting:
-> 1027 return self.predict_loop.run()
1028 if self.training:
1029 with isolate_rng():
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/loops/utilities.py:182, in _no_grad_context.<locals>._decorator(self, *args, **kwargs)
180 context_manager = torch.no_grad
181 with context_manager():
--> 182 return loop_run(self, *args, **kwargs)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/loops/prediction_loop.py:124, in _PredictionLoop.run(self)
122 self.batch_progress.is_last_batch = data_fetcher.done
123 # run step hooks
--> 124 self._predict_step(batch, batch_idx, dataloader_idx, dataloader_iter)
125 except StopIteration:
126 # this needs to wrap the `*_step` call too (not just `next`) for `dataloader_iter` support
127 break
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/loops/prediction_loop.py:253, in _PredictionLoop._predict_step(self, batch, batch_idx, dataloader_idx, dataloader_iter)
247 # configure step_kwargs
248 step_args = (
249 self._build_step_args_from_hook_kwargs(hook_kwargs, "predict_step")
250 if not using_dataloader_iter
251 else (dataloader_iter,)
252 )
--> 253 predictions = call._call_strategy_hook(trainer, "predict_step", *step_args)
254 if predictions is None:
255 self._warning_cache.warn("predict returned None if it was on purpose, ignore this warning...")
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/trainer/call.py:309, in _call_strategy_hook(trainer, hook_name, *args, **kwargs)
306 return None
308 with trainer.profiler.profile(f"[Strategy]{trainer.strategy.__class__.__name__}.{hook_name}"):
--> 309 output = fn(*args, **kwargs)
311 # restore current_fx when nested context
312 pl_module._current_fx_name = prev_fx_name
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/pytorch_lightning/strategies/strategy.py:438, in Strategy.predict_step(self, *args, **kwargs)
436 if self.model != self.lightning_module:
437 return self._forward_redirection(self.model, self.lightning_module, "predict_step", *args, **kwargs)
--> 438 return self.lightning_module.predict_step(*args, **kwargs)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/models/forecasting/pl_forecasting_module.py:292, in PLForecastingModule.predict_step(self, batch, batch_idx, dataloader_idx)
286 input_data_tuple_samples = self._sample_tiling(
287 input_data_tuple, batch_sample_size
288 )
290 # get predictions for 1 whole batch (can include predictions of multiple series
291 # and for multiple samples if a probabilistic forecast is produced)
--> 292 batch_prediction = self._get_batch_prediction(
293 self.pred_n, input_data_tuple_samples, self.pred_roll_size
294 )
296 # reshape from 3d tensor (num_series x batch_sample_size, ...)
297 # into 4d tensor (batch_sample_size, num_series, ...), where dim 0 represents the samples
298 out_shape = batch_prediction.shape
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/models/forecasting/pl_forecasting_module.py:678, in PLPastCovariatesModule._get_batch_prediction(self, n, input_batch, roll_size)
673 input_past[:, :, n_targets : n_targets + n_past_covs] = (
674 future_past_covariates[:, left_past:right_past, :]
675 )
677 # take only last part of the output sequence where needed
--> 678 out = self._produce_predict_output(x=(input_past, static_covariates))[
679 :, self.first_prediction_index :, :
680 ]
682 batch_prediction.append(out)
683 prediction_length += self.output_chunk_length
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/models/forecasting/pl_forecasting_module.py:480, in PLForecastingModule._produce_predict_output(self, x)
478 return self.likelihood.predict_likelihood_parameters(output)
479 else:
--> 480 return self.likelihood.sample(output)
481 else:
482 return self(x).squeeze(dim=-1)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/darts/utils/likelihood_models.py:1051, in LaplaceLikelihood.sample(self, model_output)
1049 def sample(self, model_output) -> torch.Tensor:
1050 mu, b = self._params_from_output(model_output)
-> 1051 distr = _Laplace(mu, b)
1052 return distr.sample()
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/torch/distributions/laplace.py:52, in Laplace.__init__(self, loc, scale, validate_args)
50 else:
51 batch_shape = self.loc.size()
---> 52 super().__init__(batch_shape, validate_args=validate_args)
File ~/miniconda3/envs/darts310/lib/python3.10/site-packages/torch/distributions/distribution.py:68, in Distribution.__init__(self, batch_shape, event_shape, validate_args)
66 valid = constraint.check(value)
67 if not valid.all():
---> 68 raise ValueError(
69 f"Expected parameter {param} "
70 f"({type(value).__name__} of shape {tuple(value.shape)}) "
71 f"of distribution {repr(self)} "
72 f"to satisfy the constraint {repr(constraint)}, "
73 f"but found invalid values:\n{value}"
74 )
75 super().__init__()
ValueError: Expected parameter scale (Tensor of shape (32, 24, 1)) of distribution Laplace(loc: torch.Size([32, 24, 1]), scale: torch.Size([32, 24, 1])) to satisfy the constraint GreaterThan(lower_bound=0.0), but found invalid values:
tensor([[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[3.5466e+02],
[0.0000e+00],
[4.6437e+02],
[7.5609e+02],
[3.2690e+02],
[0.0000e+00],
[3.2321e+02],
[1.1282e+03],
[2.8581e+03],
[3.9684e+03],
[8.2819e+03],
[2.1521e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[4.2196e+02],
[2.9085e+02],
[1.4671e+02],
[4.2493e+02],
[1.8432e+03],
[5.2108e+02],
[1.8184e+02],
[2.8255e+02],
[4.0458e+03],
[1.3649e+03],
[5.2962e+03],
[3.4843e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.6621e+02],
[9.4351e+01],
[5.9659e+02],
[0.0000e+00],
[0.0000e+00],
[3.0769e+02],
[0.0000e+00],
[2.4108e+02],
[0.0000e+00],
[3.4435e+02],
[0.0000e+00],
[9.6189e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[6.2573e+02],
[1.9478e+01],
[0.0000e+00],
[2.6027e+02],
[7.2585e+03],
[8.6589e+03],
[1.2641e+04],
[1.2864e+04],
[2.2555e+04],
[2.1138e+04],
[2.4303e+04],
[1.9105e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[4.0673e+02],
[0.0000e+00],
[4.7850e+02],
[1.6997e+02],
[2.0591e+03],
[6.3764e+01],
[0.0000e+00],
[0.0000e+00],
[1.4656e+04],
[3.9371e+04],
[4.8098e+04],
[8.9642e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.1685e+02],
[6.7294e+01],
[1.8114e+02],
[7.7117e+01],
[4.1540e+03],
[1.0585e+03],
[4.6140e+03],
[2.0682e+02],
[9.2396e+03],
[1.0781e+03],
[6.3496e+03],
[0.0000e+00]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[4.2792e+02],
[3.4131e+02],
[2.9428e+02],
[2.8990e+02],
[9.5690e+02],
[2.9681e+03],
[3.5354e+03],
[2.1584e+03],
[1.3484e+03],
[7.0866e+03],
[4.9625e+03],
[4.5635e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[4.9794e+02],
[6.3204e+02],
[0.0000e+00],
[0.0000e+00],
[3.6342e+03],
[1.5193e+04],
[1.7998e+04],
[3.3042e+04],
[3.7157e+04],
[5.3951e+04],
[5.3066e+04],
[6.3337e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[3.8738e+02],
[5.9231e+02],
[1.2937e+02],
[0.0000e+00],
[8.7417e+02],
[4.4924e+03],
[7.7686e+03],
[5.2749e+03],
[1.1464e+04],
[7.3656e+03],
[1.5769e+04],
[5.8549e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.2412e+02],
[5.9308e+01],
[0.0000e+00],
[7.0189e+02],
[4.2594e+03],
[1.7311e+03],
[3.6755e+03],
[5.0991e+03],
[9.1981e+03],
[4.6576e+03],
[7.5442e+03],
[1.0303e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[0.0000e+00],
[2.2418e+02],
[7.0614e+02],
[3.5272e+02],
[0.0000e+00],
[0.0000e+00],
[1.7541e+02],
[7.4891e+01],
[5.0964e+03],
[0.0000e+00],
[1.0632e+04],
[1.6849e+02]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.4644e+02],
[6.0210e+02],
[2.9503e+02],
[6.2108e+02],
[3.4425e+02],
[2.4112e+03],
[1.8081e+02],
[5.3398e+03],
[3.0440e+03],
[5.6225e+03],
[7.2210e+03],
[7.2862e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.1805e+02],
[4.2366e+01],
[5.6576e+02],
[5.2030e+02],
[4.2446e+03],
[0.0000e+00],
[8.7383e+03],
[2.5814e+03],
[2.0124e+04],
[1.0774e+04],
[4.8236e+04],
[3.5690e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.8773e+02],
[2.1789e+02],
[4.3082e+02],
[3.2359e+02],
[4.0239e+02],
[2.4006e+02],
[8.2602e+01],
[0.0000e+00],
[0.0000e+00],
[0.0000e+00],
[1.8908e+03],
[8.6257e+02]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[3.5584e+02],
[1.1363e+03],
[0.0000e+00],
[0.0000e+00],
[0.0000e+00],
[1.8747e+04],
[4.2485e+03],
[2.7998e+04],
[3.0196e+03],
[4.8885e+04],
[5.8029e+03],
[4.8085e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[4.0141e+02],
[2.3429e+02],
[8.7342e+02],
[0.0000e+00],
[8.6362e+02],
[7.0546e+02],
[7.8284e+03],
[2.4537e+03],
[1.5170e+04],
[1.7069e+04],
[1.8793e+04],
[2.3392e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[4.7611e+02],
[3.5129e+02],
[1.8447e+02],
[0.0000e+00],
[1.5156e+03],
[6.0635e+03],
[1.2944e+04],
[2.0738e+04],
[1.6236e+04],
[2.7564e+04],
[2.1402e+04],
[3.6097e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[0.0000e+00],
[4.1882e+00],
[6.6243e+00],
[0.0000e+00],
[4.2323e-04],
[0.0000e+00],
[0.0000e+00],
[3.9317e+01],
[0.0000e+00],
[0.0000e+00],
[0.0000e+00],
[0.0000e+00]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.8740e+02],
[5.7409e+02],
[3.1118e+02],
[4.6254e+02],
[4.6531e+02],
[2.5792e+03],
[5.5729e+02],
[4.7599e+03],
[6.3608e+03],
[6.3185e+03],
[9.6536e+03],
[6.8044e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[3.8756e+02],
[4.2308e+02],
[2.3249e+02],
[2.0505e+02],
[9.1562e+02],
[1.4755e+03],
[5.3977e+02],
[4.2784e+02],
[1.1924e+03],
[1.4245e+03],
[3.7625e+03],
[6.6584e+02]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.8670e+02],
[1.3137e+02],
[5.9928e+02],
[3.8858e+02],
[5.2856e+02],
[0.0000e+00],
[2.4692e+02],
[1.6345e+02],
[6.7857e+03],
[0.0000e+00],
[4.9448e+03],
[1.6755e+02]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.1253e+02],
[2.0230e+02],
[8.7402e+00],
[0.0000e+00],
[4.2190e+03],
[4.5460e+03],
[6.9655e+03],
[4.3069e+03],
[1.0163e+04],
[5.1115e+03],
[1.3262e+04],
[2.0912e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.1422e+02],
[2.6167e+02],
[0.0000e+00],
[5.9992e+02],
[3.1619e+03],
[7.3641e+03],
[8.3763e+03],
[1.9238e+04],
[1.0471e+04],
[2.9842e+04],
[1.2521e+04],
[3.7745e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[3.4059e+02],
[5.0451e+02],
[0.0000e+00],
[1.0669e+02],
[4.7835e+02],
[1.7370e+03],
[0.0000e+00],
[2.0056e+02],
[1.6877e+02],
[1.5789e+03],
[8.3318e+01],
[0.0000e+00]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[3.7563e+02],
[3.7193e+02],
[4.0011e+02],
[5.6188e+02],
[6.5350e+02],
[0.0000e+00],
[5.6529e+02],
[1.0223e+03],
[1.6954e+03],
[0.0000e+00],
[6.2738e+03],
[6.0970e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.7755e+02],
[9.4972e+01],
[1.3695e+00],
[1.9749e+02],
[4.7334e+03],
[8.4419e+03],
[1.4042e+04],
[1.6364e+04],
[1.7950e+04],
[3.9612e+04],
[2.3361e+04],
[4.9177e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.3864e+02],
[1.3122e-04],
[1.5656e+02],
[4.4812e+02],
[4.3967e+03],
[2.1135e+02],
[4.8541e+03],
[0.0000e+00],
[7.9736e+03],
[5.3526e+02],
[5.9979e+03],
[0.0000e+00]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.8944e+02],
[7.1204e+02],
[1.1715e+02],
[0.0000e+00],
[0.0000e+00],
[7.1381e+03],
[6.9366e+03],
[1.4242e+04],
[6.5499e+03],
[2.5540e+04],
[1.8810e+04],
[3.7684e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.3328e+02],
[2.7684e+02],
[5.5600e+02],
[5.2213e+02],
[6.6743e+01],
[1.7771e+01],
[1.4544e+02],
[1.0106e+03],
[3.4816e+03],
[2.7806e+03],
[4.9556e+03],
[1.0809e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.9472e+02],
[0.0000e+00],
[4.8687e+02],
[3.7346e+02],
[4.9578e-07],
[2.0248e+02],
[5.1683e+02],
[4.9718e+02],
[0.0000e+00],
[0.0000e+00],
[0.0000e+00],
[9.4098e+03]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[2.1485e+02],
[3.6214e+02],
[5.5538e+02],
[4.7417e+02],
[1.2258e+02],
[0.0000e+00],
[1.2590e+03],
[1.3526e+02],
[2.0405e+03],
[0.0000e+00],
[1.7554e+04],
[1.7065e+04]],
[[1.0175e+00],
[3.7658e-01],
[2.3018e-01],
[2.9205e-01],
[2.3816e+01],
[3.4126e+01],
[7.2803e+01],
[8.8747e+01],
[1.5841e+02],
[1.8664e+02],
[2.5827e+02],
[2.8993e+02],
[5.2569e+02],
[1.1581e+02],
[5.3897e+02],
[0.0000e+00],
[3.6477e+03],
[3.4733e+03],
[1.8417e+04],
[1.3479e+04],
[2.8909e+04],
[1.9235e+04],
[5.3059e+04],
[3.4125e+04]]], device='mps:0')
Furthermore, we could also for instance specify that we have some prior belief that the scale of the distribution is about \(0.1\) (in the transformed domain), while still capturing some time dependency of the distribution, by specifying prior_b=.1
.
Behind the scenes this will regularize the training loss with a Kullback-Leibler divergence term.
[ ]:
model = TCNModel(
input_chunk_length=24,
output_chunk_length=12,
random_state=42,
likelihood=LaplaceLikelihood(prior_b=0.1),
)
model.fit(train_air_scaled, epochs=400, verbose=True);
[ ]:
pred = model.predict(n=36, num_samples=500)
# scale back:
pred = scaler.inverse_transform(pred)
series_air.plot()
pred.plot()
By default TimeSeries.plot()
shows the median as well as the 5th and 95th percentiles (of the marginal distributions, if the TimeSeries
is multivariate). It is possible to control this:
[ ]:
pred.plot(low_quantile=0.01, high_quantile=0.99, label="1-99th percentiles")
pred.plot(low_quantile=0.2, high_quantile=0.8, label="20-80th percentiles")
Types of distributions¶
The likelihood has to be compatible with the domain of your time series’ values. For instance PoissonLikelihood can be used on discrete positive values, ExponentialLikelihood can be used on real positive values, and BetaLikelihood on real values in \((0,1)\).
It is also possible to use QuantileRegression to apply a quantile loss and fit some desired quantiles directly.
Evaluating Probabilistic Forecasts¶
How can we evaluate the quality of probabilistic forecasts? By default, most metrics functions (such as mape()
) will keep working but look only at the median forecast. It is also possible to use the Mean Quantile Loss metric mql()
, which quantifies the error for each predicted quantiles. For quantile=0.5 (the median), it is identical to the Mean Absolute Error (MAE):
[52]:
from darts.metrics import mql, mae
print("MAPE of median forecast: %.2f" % mape(series_air, pred))
print("MAE of median forecast: %.2f" % mae(series_air, pred))
for q in [0.05, 0.1, 0.5, 0.9, 0.95]:
q_loss = mql(series_air, pred, q=q)
print("quantile loss at quantile %.2f: %.2f" % (q, q_loss))
MAPE of median forecast: 11.78
MAE of median forecast: 50.12
quantile loss at quantile 0.05: 5.01
quantile loss at quantile 0.10: 11.73
quantile loss at quantile 0.50: 50.12
quantile loss at quantile 0.90: 20.56
quantile loss at quantile 0.95: 12.13
Using Quantile Loss¶
Could we do better by fitting these quantiles directly? We can just use a QuantileRegression
likelihood:
[ ]:
from darts.utils.likelihood_models import QuantileRegression
model = TCNModel(
input_chunk_length=24,
output_chunk_length=12,
random_state=42,
likelihood=QuantileRegression([0.05, 0.1, 0.5, 0.9, 0.95]),
)
model.fit(train_air_scaled, epochs=400, verbose=True);
[54]:
pred = model.predict(n=36, num_samples=500)
# scale back:
pred = scaler.inverse_transform(pred)
series_air.plot()
pred.plot()
print("MAPE of median forecast: %.2f" % mape(series_air, pred))
for q in [0.05, 0.1, 0.5, 0.9, 0.95]:
q_loss = mql(series_air, pred, q=q)
print("quantile loss at quantile %.2f: %.2f" % (q, q_loss))
GPU available: False, used: False
TPU available: False, using: 0 TPU cores
IPU available: False, using: 0 IPUs
HPU available: False, using: 0 HPUs
MAPE of median forecast: 4.90
quantile loss at quantile 0.05: 8.27
quantile loss at quantile 0.10: 12.76
quantile loss at quantile 0.50: 19.86
quantile loss at quantile 0.90: 12.17
quantile loss at quantile 0.95: 7.57
Ensembling models¶
Ensembling is about combining the forecasts produced by several models, in order to obtain a final - and hopefully better forecast.
For instance, in our example of a less naive model above, we manually combined a naive seasonal model with a naive drift model. Here, we will show how models forecasts can be automatically combined, naively using a NaiveEnsembleModel
, or learned using RegressionEnsembleModel
.
It is of course also possible to use past
and/or future_covariates
with ensemble model but they will be passed only to the models supporting them when calling fit()
and predict()
.
Naive Ensembling¶
Naive ensembling just takes the average of the forecasts of several models. Darts provides a NaiveEnsembleModel
, which allows to do this while still manipulating only one forecasting model (which, for instance, allows for easier backtesting):
[ ]:
from darts.models import NaiveEnsembleModel
models = [NaiveDrift(), NaiveSeasonal(12)]
ensemble_model = NaiveEnsembleModel(forecasting_models=models)
backtest = ensemble_model.historical_forecasts(
series_air, start=0.6, forecast_horizon=3, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
Learned Ensembling¶
As expected in this case, the naive ensemble doesn’t give great results (although in some cases it could!)
We can sometimes do better if we see the ensembling as a supervised regression problem: given a set of forecasts (features), find a model that combines them in order to minimise errors on the target. This is what the RegressionEnsembleModel
does. It accepts three parameters:
forecasting_models
is a list of forecasting models whose predictions we want to ensemble.regression_train_n_points
is the number of time steps to use for fitting the “ensemble regression” model (i.e., the inner model that combines the forecasts).regression_model
is, optionally, a sklearn-compatible regression model or a DartsRegressionModel
to be used for the ensemble regression. If not specified, a linear regression is used. Using a sklearn model is easy out-of-the-box, but using aRegressionModel
allows to potentially take arbitrary lags of the individual forecasts as inputs of the regression model.
Once these elements are in place, a RegressionEnsembleModel
can be used like a regular forecasting model:
[ ]:
from darts.models import RegressionEnsembleModel
models = [NaiveDrift(), NaiveSeasonal(12)]
ensemble_model = RegressionEnsembleModel(
forecasting_models=models, regression_train_n_points=12
)
backtest = ensemble_model.historical_forecasts(
series_air, start=0.6, forecast_horizon=3, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
We can also inspect the coefficients used to weigh the two inner models in the linear combination:
[ ]:
ensemble_model.fit(series_air)
ensemble_model.regression_model.model.coef_
By using a probabilistic regression model, the RegressionEnsembleModel
can also generate probabilistic forecasts:
[ ]:
from darts.models import LinearRegressionModel
quantiles = [0.25, 0.5, 0.75]
models = [NaiveDrift(), NaiveSeasonal(12)]
regression_model = LinearRegressionModel(
quantiles=quantiles,
lags_future_covariates=[0],
likelihood="quantile",
fit_intercept=False,
)
ensemble_model = RegressionEnsembleModel(
forecasting_models=models,
regression_train_n_points=12,
regression_model=regression_model,
)
backtest = ensemble_model.historical_forecasts(
series_air, start=0.6, forecast_horizon=3, num_samples=500, verbose=True
)
print("MAPE = %.2f" % (mape(backtest, series_air)))
series_air.plot()
backtest.plot()
RegressionEnsembleModel
uses the stacking technique to train and combine the forecasting_models
: each one of them is trained independently and the regression_model
is then trained using their predictions as future_covariates
.
Filtering models¶
In addition to forecasting models, which are able to predict future values of series, Darts also contains a couple of helpful filtering models, which can model “in sample” series’ values distributions.
Fitting a Kalman Filter¶
KalmanFilter
implements a Kalman Filter. The implementation relies on nfoursid, so it is for instance possible to provide a nfoursid.kalman.Kalman
object containing a transition matrix, process noise covariance, observation noise covariance etc.
It is also possible to do system identification by calling fit()
to “train” the Kalman Filter using the N4SID system identification algorithm:
[ ]:
from darts.models import KalmanFilter
kf = KalmanFilter(dim_x=3)
kf.fit(train_air_scaled)
filtered_series = kf.filter(train_air_scaled, num_samples=100)
train_air_scaled.plot()
filtered_series.plot()
Inferring missing values with Gaussian Processes¶
Darts also contains a GaussianProcessFilter
which can be used for probabilistic modeling of series:
[ ]:
from darts.models import GaussianProcessFilter
from sklearn.gaussian_process.kernels import RBF
# create a series with holes:
values = train_air_scaled.values()
values[20:22] = np.nan
values[28:32] = np.nan
values[55:59] = np.nan
values[72:80] = np.nan
series_holes = TimeSeries.from_times_and_values(train_air_scaled.time_index, values)
series_holes.plot()
kernel = RBF()
gpf = GaussianProcessFilter(kernel=kernel, alpha=0.1, normalize_y=True)
filtered_series = gpf.filter(series_holes, num_samples=100)
filtered_series.plot()
A Word of Caution¶
So is N-BEATS, exponential smoothing, or a Bayesian ridge regression trained on milk production the best approach for predicting the future number of airline passengers? Well, at this point it’s actually hard to say exactly which one is best. Our time series is small, and our validation set is even smaller. In such cases, it’s very easy to overfit the whole forecasting exercise to such a small validation set. That’s especially true if the number of available models and their degrees of freedom is high (such as for deep learning models), or if we played with many models on a single test set (as done in this notebook).
As data scientists, it is our responsibility to understand the extent to which our models can be trusted. So always take results with a grain of salt, especially on small datasets, and apply the scientific method before making any kind of forecast :) Happy modeling!