In this document, the utsf package is described. This package offers a meta-engine for applying different regression models to univariate time series forecasting using an autoregressive approach. One of the main features of the package is its extensibility, which allows you to use machine learning models not directly supported by the package (such as neural networks) or your own custom models. Currently, the package is mainly focused on regression tree models. An explanation of how to forecast univariate time series using regression trees can be found here: https://arxiv.org/abs/2602.00077.
A univariate time series forecasting method is one in which the future values of a series are predicted using only information from the series itself—for example, using its historical mean value as a forecast. An advantage of this type of prediction is that, apart from the series being forecasted, there is no need to collect any further information to train the forecasting model.
An autoregressive model is a type of univariate time series forecasting model in which a value of a time series is expressed as a function of some of its past values. That is, an autoregressive model is a regression model in which the independent variables are lagged values (previous values) of the response variable. For example, given a time series with the following historical values: \(t = \{1, 3, 6, 7, 9, 11, 16\}\), suppose that we want to develop an autoregressive model in which a target “is explained” by its first, second, and fourth past values (in this context, a previous value is also called a lag, so lag 1 is the value immediately preceding a given value in the series). Given the series \(t\) and lags (1, 2, and 4), the training set would be:
| Lag 4 | Lag 2 | Lag 1 | Target |
|---|---|---|---|
| 1 | 6 | 7 | 9 |
| 3 | 7 | 9 | 11 |
| 6 | 9 | 11 | 16 |
Given a model trained with the previous dataset, the next future value of the series is predicted as \(f(\text{Lag}_4, \text{Lag}_2, \text{Lag}_1)\), where \(f\) is the regression function and \(\text{Lag}_4\), \(\text{Lag}_2\), and \(\text{Lag}_1\) are the fourth, second, and first lagged values of the next future value. Therefore, the next future value of the series \(t\) is predicted as \(f(7, 11, 16)\), producing a value that will be denoted as \(F_1\).
Suppose that the forecast horizon (the number of future values to be forecasted into the future) is greater than 1. If the regression function only predicts the next future value of the series, a recursive approach can be applied to forecast all the values within the horizon. Using this approach, the regression function is applied recursively until all multi-step-ahead values are forecasted. For instance, following the previous example, suppose that the forecast horizon is 3. As explained, to forecast the next value of the series (one-step-ahead) the regression function is fed with the vector \([7, 11, 16]\), producing \(F_1\). To forecast the two-step-ahead value the regression function is fed with the vector \([9, 16, F_1]\). The forecast for the one-step-ahead value, \(F_1\), is used as the first lag for the two-step-ahead forecast, because the actual value is unknown. Finally, to predict the three-step-ahead value, the regression function is fed with the vector [\(11, F_1, F_2]\). This example of recursive forecast is summarized in the table below:
| Steps ahead | Autoregressive values | Forecast |
|---|---|---|
| 1 | 7, 11, 16 | \(F_1\) |
| 2 | 9, 16, \(F_1\) | \(F_2\) |
| 3 | 11, \(F_1\), \(F_2\) | \(F_3\) |
The recursive approach to multi-step-ahead forecasting is employed in classical statistical models, such as ARIMA and exponential smoothing.
The utsf package simplifies the use of classical regression models for univariate time series forecasting, employing the autoregressive approach and the recursive prediction strategy explained in the previous section. All the supported models are applied using a uniform interface:
create_model() function to build the forecasting
model, andforecast() function for using the trained model to
predict future values.Let us see an example in which a regression tree model is used to forecast the future values of a time series:
In this example, an autoregressive tree model
(method = "rt", Regression Tree) is trained using the
historical values of the AirPassengers time series and a
forecast for its next 12 values (h = 12) is generated. The
create_model() function returns an S3 object of class
utsf with information about the trained model. The forecast
information is included in the object returned by the
forecast() function, as a component named pred
of class ts (a time series):
f$pred
#> Jan Feb Mar Apr May Jun Jul Aug
#> 1961 460.2980 428.4915 467.0304 496.9833 499.9819 554.7891 627.5849 628.0503
#> Sep Oct Nov Dec
#> 1961 533.2803 482.4221 448.7926 453.6920
library(ggplot2)
autoplot(f)The training set used to fit the model is built from the historical
values of the time series using the autoregressive approach explained in
the previous section. The lags parameter of the
create_model() function specifies the autoregressive lags.
In this example: lags = 1:12, meaning each target is a
function of its 12 previous values. Next, we check the first targets
(and their associated features) used to train the regression model:
head(m$targets) # first targets
#> [1] -11.6666667 -0.9166667 13.4166667 6.6666667 -3.8333333 19.8333333
head(m$features) # and its associated features
#> Lag12 Lag11 Lag10 Lag9 Lag8 Lag7 Lag6
#> 1 -14.6666667 -8.666667 5.333333 2.333333 -5.666667 8.333333 21.333333
#> 2 -8.9166667 5.083333 2.083333 -5.916667 8.083333 21.083333 21.083333
#> 3 4.4166667 1.416667 -6.583333 7.416667 20.416667 20.416667 8.416667
#> 4 0.6666667 -7.333333 6.666667 19.666667 19.666667 7.666667 -9.333333
#> 5 -7.8333333 6.166667 19.166667 19.166667 7.166667 -9.833333 -24.833333
#> 6 5.8333333 18.833333 18.833333 6.833333 -10.166667 -25.166667 -11.166667
#> Lag5 Lag4 Lag3 Lag2 Lag1
#> 1 21.333333 9.333333 -7.666667 -22.666667 -8.666667
#> 2 9.083333 -7.916667 -22.916667 -8.916667 -11.916667
#> 3 -8.583333 -23.583333 -9.583333 -12.583333 -1.583333
#> 4 -24.333333 -10.333333 -13.333333 -2.333333 12.666667
#> 5 -10.833333 -13.833333 -2.833333 12.166667 6.166667
#> 6 -14.166667 -3.166667 11.833333 5.833333 -4.166667The curious reader may have noticed that the features and targets are not on the same scale as the original time series. This is because, by default, a transformation is applied to the examples of the training set. This transformation will be explained later.
Let us inspect the training set associated with the example from the previous section:
t <- ts(c(1, 3, 6, 7, 9, 11, 16))
out <- create_model(t, lags = c(1, 2, 4), trend = "none")
cbind(out$features, Target = out$targets)
#> Lag4 Lag2 Lag1 Target
#> 1 1 6 7 9
#> 2 3 7 9 11
#> 3 6 9 11 16No transformation has been applied this time
(trend = "none").
Prediction intervals for a forecast can be computed:
m <- create_model(USAccDeaths, method = "mt")
f <- forecast(m, h = 12, PI = TRUE, level = 90)
f
#> Point Forecast Lo 90 Hi 90
#> Jan 1979 8195.911 7654.387 8750.039
#> Feb 1979 7446.227 6885.823 8024.522
#> Mar 1979 7775.062 7072.970 8419.338
#> Apr 1979 8289.710 7645.091 8977.396
#> May 1979 9048.237 8329.607 9716.592
#> Jun 1979 9237.905 8561.585 9916.638
#> Jul 1979 10297.351 9014.763 10837.597
#> Aug 1979 9741.415 8632.913 10399.199
#> Sep 1979 9103.687 8274.895 9827.232
#> Oct 1979 8948.850 8141.232 9703.285
#> Nov 1979 8750.469 8083.375 9465.097
#> Dec 1979 9247.210 7985.251 9815.854
library(ggplot2)
autoplot(f)Prediction intervals are calculated following the guidelines in Forecasting:
Principles and Practice. Random errors are assumed to follow a
normal distribution. In this example, a 90% prediction interval
(level = 90) was computed.
The create_model() and forecast() functions
provide a unified interface for applying an autoregressive approach to
time series forecasting with various regression models. These models are
implemented across several R packages. Currently, our project focuses
primarily on regression tree models, supporting the following
approaches:
FNN::knn.reg() is used, as regression function, to
recursively predict the future values of the time series.stats::lm() and its associated method
stats::predict.lm() is applied recursively for the
forecasts, i.e., as regression function.rpart::rpart() and its associated method
rpart::predict.rpart() is used for the forecasts.Cubist::cubist() and its associated method
Cubist::predict.cubist() is used for predictions.ipred::bagging() and its associated method
ipred::predict.regbagg() is used for forecasting.ranger::ranger() and its associated method
ranger::predict.ranger() is used for predictions.xgboost::xgboost() and its associated method
xgboost::predict.xgboost() is used for predictions.The S3 object of class utsf returned by the
create_model() function contains a component with the
trained autoregressive model:
m <- create_model(fdeaths, lags = 1:12, method = "rt")
m$model
#> n= 60
#>
#> node), split, n, deviance, yval
#> * denotes terminal node
#>
#> 1) root 60 1967124.00 -6.465278
#> 2) Lag12< 73 38 212851.90 -124.414500
#> 4) Lag6>=-66.45833 30 57355.07 -153.847200
#> 8) Lag12< -170.7083 10 13293.09 -195.991700 *
#> 9) Lag12>=-170.7083 20 17419.67 -132.775000 *
#> 5) Lag6< -66.45833 8 32051.01 -14.041670 *
#> 3) Lag12>=73 22 312482.00 197.265200
#> 6) Lag5>=-131.7917 7 24738.12 114.500000 *
#> 7) Lag5< -131.7917 15 217416.40 235.888900 *In this case, the model is obtained by training a regression tree
using the rpart::rpart() function on a training set
composed of the features m$features and targets
m$targets. Once trained, the
rpart::predict.rpart() function can be used recursively
within forecast() to predict future values of the time
series.
An appealing feature of the utsf package is its extensibility. Beyond the natively supported models, you can leverage the package’s infrastructure to perform autoregressive time series forecasting with custom regression models. This allows user-defined models to benefit from built-in functionality, such as training set construction, recursive forecasting, prediction intervals, preprocessing, accuracy evaluation, and hyperparameter tuning.
To use a custom regression model, pass a function capable of training
your model to the method parameter of the
create_model() function. We refer to this function as the
modeling function, and it must return an object containing the
trained regression model. The modeling function must accept three input
parameters:
X: a data frame containing the features of the
training examples. This data frame is constructed from the time series
according to the autoregressive lags, as explained in a previous
section. It is identical to the features component of the
object returned by create_model().
y: a vector containing the targets of the training
examples, constructed as described in a previous section. It is
identical to the targets component of the object returned
by create_model().
...: additional arguments passed to fine-tune model
behavior. This parameter is not used in this section and will be
detailed in the next.
Furthermore, if the modeling function returns an object of class
model_class, an S3 method with the signature
predict.model_class(object, new_value) must be implemented.
This method uses the model to predict a single new value (acting as the
prediction function associated with the model). Its parameters are
described below:
object: the object of class model_class
returned by your modeling function.new_value: a one-row data frame with the same structure
as X, containing the features of the instance to be
predicted.In summary, to use a custom regression model, you must provide:
The following examples demonstrate how to use this functionality to apply custom models to autoregressive time series forecasting, leveraging the capabilities of the utsf package.
The k-nearest neighbors (k-NN) algorithm is a special case because it is a lazy learner and builds no explicit model. Instead, it simply stores the training set, leaving all the computation to the prediction function. Below is an initial example using the k-NN implementation from the FNN package:
# Modeling function: in this case (k-NN) just stores the training set
my_knn_model <- function(X, y, ...) { structure(list(X = X, y = y), class = "my_knn") }
# Regression function
predict.my_knn <- function(object, new_value) {
FNN::knn.reg(train = object$X, test = new_value, y = object$y)$pred
}
m <- create_model(AirPassengers, lags = 1:12, method = my_knn_model)
f <- forecast(m, h = 12)
f$pred
#> Jan Feb Mar Apr May Jun Jul Aug
#> 1961 455.9167 434.3264 480.7703 490.1678 506.1262 568.0534 640.6689 640.8636
#> Sep Oct Nov Dec
#> 1961 549.5467 495.4255 441.6554 476.7934
autoplot(f)The modeling function (my_knn_model()) creates a “model”
that simply stores the training set. The regression function
(predict.my_knn()) uses FNN::knn.reg() to find
the \(k\)-nearest neighbors and compute
their mean response value. The default number of neighbors in
FNN::knn.reg() (3) is used. Later, we will modify this
example to allow users to specify the value of \(k\).
The \(k\)-nearest neighbors algorithm is simple enough to be easily implemented from scratch, without relying on external R packages:
# Modeling function
my_knn_model2 <- function(X, y, ...) {
structure(list(X = X, y = y), class = "my_knn2")
}
# Regression function
predict.my_knn2 <- function(object, new_value) {
k <- 3 # number of nearest neighbors
distances <- sapply(1:nrow(object$X), function(i) sum((object$X[i, ] - new_value)^2))
k_nearest <- order(distances)[1:k]
mean(object$y[k_nearest])
}
m2 <- create_model(AirPassengers, lags = 1:12, method = my_knn_model2)
forecast(m2, h = 12)$pred
#> Jan Feb Mar Apr May Jun Jul Aug
#> 1961 455.9167 434.3264 480.7703 490.1678 506.1262 568.0534 640.6689 640.8636
#> Sep Oct Nov Dec
#> 1961 549.5467 495.4255 441.6554 476.7934The random forest algorithm is natively supported in the
utsf package via the ranger package
implementation. Here, we build an autoregressive model using the random
forest implementation provided by the randomForest
package:
library(randomForest)
#> randomForest 4.7-1.2
#> Type rfNews() to see new features/changes/bug fixes.
#>
#> Adjuntando el paquete: 'randomForest'
#> The following object is masked from 'package:ggplot2':
#>
#> margin
my_model <- function(X, y, ...) { randomForest(x = X, y = y) }
m <- create_model(USAccDeaths, lags = 1:12, method = my_model)
f <- forecast(m, h = 12)
library(ggplot2)
autoplot(f)print(m$model)
#>
#> Call:
#> randomForest(x = X, y = y)
#> Type of random forest: regression
#> Number of trees: 500
#> No. of variables tried at each split: 4
#>
#> Mean of squared residuals: 190450.2
#> % Var explained: 77.39The modeling function (my_model()) simply calls
randomForest::randomForest(), which returns an S3 object of
class randomForest containing the trained model. In this
case, we do not need to implement a regression function, as the existing
predict.randomForest() S3 method already performs the
required task.
As another example, we implement an autoregressive neural network model using the nnet package:
library(nnet)
my_model <- function(X, y, ...) {
nnet(x = X, y = y, size = 5, linout = TRUE, trace = FALSE)
}
m <- create_model(USAccDeaths, lags = 1:12, method = my_model)
f <- forecast(m, h = 12)
library(ggplot2)
autoplot(f)In this case, nnet() returns an S3 object of class
nnet. Once again, the class’s default prediction method,
predict.nnet(), provides the required functionality. Given
that neural networks are sensitive to feature scales, preprocessing the
time series would likely improve forecast accuracy.
Typically, a regression model can be tuned using hyperparameters. By default, the models supported by the package use specific default values defined by their underlying training functions (listed in a previous section). However, users can customize these hyperparameters to adjust model behavior. Below is an example:
# A bagging model set with default parameters
m <- create_model(USAccDeaths, lags = 1:12, method = "bagging")
length(m$model$mtrees) # number of regression trees (25 by default)
#> [1] 25
# A bagging model set with 3 regression trees
m2 <- create_model(USAccDeaths,
lags = 1:12,
method = "bagging",
nbagg = 3
)
length(m2$model$mtrees) # number of regression trees
#> [1] 3In the previous example, two bagging models (using regression trees)
are trained with create_model(). The first model uses 25
trees (the default for ipred::ipredbagg()) while the second
sets the number of trees to 3. To customize hyperparameters, consult the
parameter documentation of the training function used internally by
utsf. In this case, ipred::ipredbagg()
uses the nbagg parameter to specify the number of trees in
the ensemble.
When using a custom regression model, its parameters can also be tuned. As explained previously, custom models require a modeling function with three parameters:
X: a data frame containing the training features.y: a vector containing the target values....: additional arguments passed to adjust model
behavior.The ... parameter allows the modeling function to accept
an arbitrary number of arguments to tune model behavior.
Below are several examples of passing arguments to the custom regression models implemented in the previous section:
In this case, the model can be tuned by specifying the value of \(k\). The modeling function accepts a
parameter named k to set the number of nearest
neighbors:
# Function to train the model
my_knn_model <- function(X, y, ...) {
param <- list(...)
k <- if ("k" %in% names(param)) param$k else 3
structure(list(X = X, y = y, k = k), class = "my_knn")
}
# Regression function for object of class my_knn
predict.my_knn <- function(object, new_value) {
FNN::knn.reg(train = object$X, test = new_value,
y = object$y, k = object$k)$pred
}
# The model is trained with default parameters (k = 3)
m <- create_model(AirPassengers, lags = 1:12, method = my_knn_model)
print(m$model$k)
#> [1] 3
# The model is trained with k = 5
m2 <- create_model(AirPassengers, method = my_knn_model, k = 5)
print(m2$model$k)
#> [1] 5Additional arguments specified in create_model() (such
as k = 5) are passed directly to the custom modeling
function (my_knn_model()).
As another example, suppose we create a random forest model using the
randomForest package. By default, the model relies on
the default parameters of randomForest::randomForest(),
except for the number of trees, which is set to 200
(ntree = 200). However, users can override this and train
the model with custom parameter values:
library(randomForest)
my_model <- function(X, y, ...) {
param <- list(...)
args <- list(x = X, y = y, ntree = 200) # default parameters
args <- args[!(names(args) %in% names(param))]
args <- c(args, param)
do.call(randomForest::randomForest, args = args)
}
# The random forest is built with our default parameters
m <- create_model(USAccDeaths, method = my_model)
print(m$model$ntree)
#> [1] 200
print(m$model$mtry)
#> [1] 4
# The random forest is built with ntree = 400 and mtry = 6
m <- create_model(USAccDeaths, method = my_model, ntree = 400, mtry = 6)
print(m$model$ntree)
#> [1] 400
print(m$model$mtry)
#> [1] 6This section demonstrates how to estimate the forecast accuracy of a regression model when predicting a time series with utsf. Below is an example:
m <- create_model(UKgas, lags = 1:4, method = "knn")
r <- efa(m, h = 4, type = "normal", size = 8)
r$per_horizon
#> Horizon 1 Horizon 2 Horizon 3 Horizon 4
#> MAE 40.956667 37.205000 42.730417 51.424167
#> MAPE 4.118140 5.030128 7.468098 7.434864
#> sMAPE 4.261377 5.208551 7.814417 7.732037
#> RMSE 58.920772 47.660996 49.733955 55.023038
r$global
#> MAE MAPE sMAPE RMSE
#> 43.079063 6.012808 6.254095 52.834690To assess model forecast accuracy, use the efa()
function on the fitted model. In this example, the forecasting method is
a \(k\)-nearest neighbors algorithm
(method = "knn") with autoregressive lags 1 to 4, applied
to the UKgas time series. The function estimates forecast
accuracy across a 4-step-ahead horizon (h = 4) using
multiple evaluation metrics.
The efa() function returns a list with two
components:
per_horizon: Contains accuracy estimates for each
individual forecasting step.global: Contains the overall accuracy estimates,
calculated as the row means of the per_horizon matrix.Currently, the following forecast accuracy measures are computed:
Next, we describe how the forecasting accuracy measures are computed for a forecasting horizon \(h\) (\(y_t\) and \(\hat{y}_t\) are the actual future value and its forecast for horizon \(t\) respectively):
\[ MAE = \frac{1}{h}\sum_{t=1}^{h} |y_t-\hat{y}_t| \]
\[ MAPE = \frac{1}{h}\sum_{t=1}^{h} 100\frac{|y_t-\hat{y}_t|}{y_t} \] \[ sMAPE = \frac{1}{h}\sum_{t=1}^{h} 200\frac{\left|y_{t}-\hat{y}_{t}\right|}{|y_t|+|\hat{y}_t|} \]
\[ RMSE = \sqrt{\frac{1}{h}\sum_{t=1}^{h} (y_t-\hat{y}_t)^2} \]
Forecast accuracy is estimated using the well-known rolling origin
evaluation strategy. This technique generates multiple training and test
sets from the time series, as illustrated below for a 4-step-ahead
forecasting horizon (h = 4):
In this setup, the last nine observations of the time series are reserved to construct the test sets. Six models are fitted using the expanding training sets, and their forecasts are evaluated against the corresponding test values. Consequently, the forecast error for each step of the horizon is averaged across six evaluation folds.
The number of trailing observations used to form the test sets can be
controlled via the size or prop
parameters:
m <- create_model(UKgas, lags = 1:4, method = "knn")
# Use the last 9 observations of the series to build test sets
r1 <- efa(m, h = 4, type = "normal", size = 9)
# Use the last 20% observations of the series to build test sets
r2 <- efa(m, h = 4, type = "normal", prop = 0.2)If the series is short, the training sets may be too small to fit a
meaningful model. In such cases, an alternative rolling origin scheme
can be applied by setting the type parameter to
"minimum":
m <- create_model(UKgas, lags = 1:4, method = "knn")
r <- efa(m, h = 4, type = "minimum")
r$per_horizon
#> Horizon 1 Horizon 2 Horizon 3 Horizon 4
#> MAE 47.893750 44.934722 44.918229 26.961198
#> MAPE 5.745458 6.757300 8.250563 3.444200
#> sMAPE 5.761843 6.791762 8.329877 3.385892
#> RMSE 56.558584 52.858868 46.912818 26.961198
r$global
#> MAE MAPE sMAPE RMSE
#> 41.176975 6.049380 6.067343 45.822867When this option is selected, the last h observations (4
in this example) are used to construct the test sets, as illustrated
below for h = 4:
In this case, the estimated forecast error for horizon 1 is based on 4 errors, for horizon 2 on 3 errors, for horizon 3 on 2 errors, and for horizon 4 on 1 error.
The efa() function also returns the test sets (and their
associated predictions) used during accuracy assessment:
timeS <- ts(1:25)
m <- create_model(timeS, lags = 1:3, method = "mt")
r <- efa(m, h = 5, size = 7)
r$test_sets
#> h=1 h=2 h=3 h=4 h=5
#> [1,] 19 20 21 22 23
#> [2,] 20 21 22 23 24
#> [3,] 21 22 23 24 25
r$predictions
#> h=1 h=2 h=3 h=4 h=5
#> [1,] 19 20 21 22 23
#> [2,] 20 21 22 23 24
#> [3,] 21 22 23 24 25Each row in these tables represents a test set or a set of
predictions. Below are the test sets generated when using the
"minimum" evaluation scheme:
Another key feature of utsf is parameter tuning. The
tune_grid() function estimates the forecast accuracy of a
model across various parameter combinations. The optimal configuration
(the one achieving the best forecast accuracy) is then used to train a
final model on the full historical series and generate future forecasts.
Below is an example:
m <- create_model(UKgas, lags = 1:4, method = "knn")
r <- tune_grid(m, h = 4, tuneGrid = expand.grid(k = 1:7), type = "normal", size = 8)
# To check the estimated forecast accuracy with the different configurations
r$tuneGrid
#> k MAE MAPE sMAPE RMSE
#> 1 1 27.50551 4.195273 4.344649 38.72080
#> 2 2 41.67751 5.782108 5.995799 50.67769
#> 3 3 43.07906 6.012808 6.254095 52.83469
#> 4 4 43.58916 6.206692 6.382526 55.14836
#> 5 5 46.03185 6.332209 6.503322 58.41501
#> 6 6 47.64410 6.441446 6.583134 62.46201
#> 7 7 48.95917 6.841679 6.886563 63.75997In this example, the tuneGrid parameter specifies (as a
data frame) the set of hyperparameters to evaluate. The forecast
accuracy for each parameter combination is estimated as described in the
previous section, using the most recent observations of the time series
as a validation set. The size parameter controls the length
of this test set.
The tuneGrid component of the list returned by
tune_grid() contains these estimation results. Here, the
\(k\)-nearest neighbors model with
\(k = 1\) achieves the best performance
across all forecast accuracy metrics, which is driven by the increasing
variance of the series. The optimal parameter combination according to
RMSE is then used to generate the final time series forecast:
r$best
#> $k
#> [1] 1
r$forecast
#> Qtr1 Qtr2 Qtr3 Qtr4
#> 1987 1217.9250 661.4063 388.1828 817.3785Below, we plot the \(k\) values against their estimated forecast accuracy in terms of RMSE:
plot(r$tuneGrid$k, r$tuneGrid$RMSE, type = "o", pch = 19, xlab = "k (number of nearest neighbors)", ylab = "RMSE", main = "Estimated accuracy")The following example illustrates tuning a model with multiple hyperparameters. Here, we evaluate a random forest model using three tuning parameters:
m <- create_model(UKDriverDeaths, lags = 1:12, method = "rf")
r <- tune_grid(m,
h = 12,
tuneGrid = expand.grid(num.trees = c(200, 500), replace = c(TRUE, FALSE), mtry = c(4, 8)),
type = "normal",
size = 12
)
r$tuneGrid
#> num.trees replace mtry MAE MAPE sMAPE RMSE
#> 1 200 TRUE 4 103.4673 7.003647 7.386769 103.4673
#> 2 500 TRUE 4 105.3515 7.058263 7.479477 105.3515
#> 3 200 FALSE 4 110.0918 7.297095 7.771264 110.0918
#> 4 500 FALSE 4 107.0101 7.159315 7.578507 107.0101
#> 5 200 TRUE 8 105.4484 7.173581 7.590381 105.4484
#> 6 500 TRUE 8 107.4666 7.316988 7.739419 107.4666
#> 7 200 FALSE 8 107.9476 7.327047 7.740572 107.9476
#> 8 500 FALSE 8 106.6378 7.219095 7.632296 106.6378Model forecast accuracy can often be improved by applying transformations or preprocessing techniques to the target time series. Currently, utsf focuses on preprocessing methods designed for trended time series. This is because most models currently integrated into the package (such as regression trees and \(k\)-nearest neighbors) predict averages of the target values in the training set (i.e., historical observations). When a series exhibits a trend, these historical averages typically fall outside the range of future values.
The following example demonstrates forecasting a trended series (the annual revenue passenger miles flown by commercial airlines in the United States) using a random forest model:
m <- create_model(airmiles, lags = 1:4, method = "rf", trend = "none")
f <- forecast(m, h = 4)
autoplot(f)In this case, no preprocessing is applied to handle the trend
(trend = "none"). As shown below, the forecast fails to
capture the upward trajectory of the series because regression tree
models can only predict average values of the training targets.
The following subsections explain how to handle trended series using three different transformations:
A common approach for handling trended series is first-differencing (computing the differences between consecutive observations). The model is then trained on the differenced series, and the resulting forecasts are back-transformed to the original scale. Below is an example:
m <- create_model(airmiles, lags = 1:4, method = "rf", trend = "differences", nfd = 1)
f <- forecast(m, h = 4)
autoplot(f)In this example, the trend parameter is set to
"differences" to apply first-differencing. The order of
differencing is specified via nfd (which typically defaults
to 1). Setting nfd = -1 automatically estimates the
required order of differencing using the ndiffs() function
from the forecast package. In this case, if the
estimated order is 0, no differencing is applied.
This transformation is also used for dealing with trended series in other R packages, such as tsfknn or tsfgrnn. The additive transformation modifies the targets of the training set as follows:
The effect of the additive transformation on the training examples can be easily inspected using the package API. For instance, consider the training set of a model fit without preprocessing:
timeS <- ts(c(1, 3, 7, 9, 10, 12))
m <- create_model(timeS, lags = 1:2, trend = "none")
cbind(m$features, Targets = m$targets)
#> Lag2 Lag1 Targets
#> 1 1 3 7
#> 2 3 7 9
#> 3 7 9 10
#> 4 9 10 12Now, consider the effect of the additive transformation on the training examples:
timeS <- ts(c(1, 3, 7, 9, 10, 12))
m <- create_model(timeS, lags = 1:2, trend = "additive", transform_features = FALSE)
cbind(m$features, Targets = m$targets)
#> Lag2 Lag1 Targets
#> 1 1 3 5.0
#> 2 3 7 4.0
#> 3 7 9 2.0
#> 4 9 10 2.5In addition to transforming the targets, the features can also be transformed. A feature vector is transformed by subtracting its mean from each element. The effect of this transformation is shown below:
timeS <- ts(c(1, 3, 7, 9, 10, 12))
m <- create_model(timeS, lags = 1:2, trend = "additive", transform_features = TRUE)
cbind(m$features, Targets = m$targets)
#> Lag2 Lag1 Targets
#> 1 -1.0 1.0 5.0
#> 2 -2.0 2.0 4.0
#> 3 -1.0 1.0 2.0
#> 4 -0.5 0.5 2.5Transforming the features removes level effects from the feature space, which empirically improves forecast accuracy.
Below, we forecast the airmiles time series using the
additive transformation with a random forest model:
By default, the additive transformation is applied to both targets and features.
The multiplicative transformation is similar to the additive transformation, but it is designed for series with exponential trends, whereas the additive transformation is better suited for linear trends. Under the multiplicative transformation, training examples are processed as follows:
Below is an example using a synthetic time series with an exponential trend, comparing forecasts generated with the additive and multiplicative transformations:
t <- ts(10 * 1.05^(1:20))
m_m <- create_model(t, lags = 1:3, method = "rf", trend = "multiplicative")
f_m <- forecast(m_m, h = 4)
m_a <- create_model(t, lags = 1:3, method = "rf", trend = "additive")
f_a <- forecast(m_a, h = 4)
library(vctsfr)
plot_predictions(t, predictions = list(Multiplicative = f_m$pred, Additive = f_a$pred))In this case, the forecast generated with the multiplicative transformation captures the exponential trend perfectly.
As an additional example, we compare the additive and multiplicative
transformations applied to the AirPassengers series (a
monthly series exhibiting an exponential trend):
m_m <- create_model(AirPassengers, lags = 1:12, method = "knn", trend = "multiplicative")
f_m <- forecast(m_m, h = 36)
m_a <- create_model(AirPassengers, lags = 1:12, method = "knn", trend = "additive")
f_a <- forecast(m_a, h = 36)
library(vctsfr)
plot_predictions(AirPassengers, predictions = list(Multiplicative = f_m$pred, Additive = f_a$pred))The create_model() function requires only one mandatory
parameter: the time series used to train the model. Below, we describe
the default settings for the remaining parameters:
lags: An integer vector defining the autoregressive
lags. If frequency(ts) == f (where ts is the
input time series and \(f > 1\)),
the default lags are 1:f (e.g., 1:4 for
quarterly data and 1:12 for monthly data), capturing
potential seasonal patterns. If frequency(ts) == 1:
stats::pacf()).1:5 are
selected.1:5 are used instead. This prevents applying
feature-level transformations when only one lag is present, as scaling a
single feature by its own mean is uninformative.method: Defaults to the \(k\)-nearest neighbors algorithm.trend: Defaults to the additive transformation
("additive"). This method has proven effective for trended
time series and generally improves performance across most series
types.transform_features: Defaults to TRUE,
ensuring that both features and targets are transformed when using the
additive or multiplicative transformation.In previous sections we have explained how to leverage the package’s features to apply your own models for time series forecasting. In this section we describe how to take advantage of other package’s features. We will employ the random forest model implemented in the randomForest package used previously. First, let us examine the computation of prediction intervals:
library(randomForest)
myModel <- function(X, y, ...) {
param <- list(...)
args <- list(x = X, y = y, ntree = 200)
args <- args[!(names(args) %in% names(param))]
args <- c(args, param)
do.call(randomForest::randomForest, args = args)
}
m <- create_model(USAccDeaths, method = myModel)
f <- forecast(m, h = 12, level = 90, PI = TRUE)
f
#> Point Forecast Lo 90 Hi 90
#> Jan 1979 8140.180 7616.195 8714.630
#> Feb 1979 7293.334 6860.925 7963.039
#> Mar 1979 8056.349 7475.799 8598.615
#> Apr 1979 8280.013 7752.210 8867.134
#> May 1979 9080.205 8481.096 9648.541
#> Jun 1979 9466.613 8784.123 10066.069
#> Jul 1979 10178.016 9331.077 10673.154
#> Aug 1979 9599.156 8840.990 10107.774
#> Sep 1979 9075.167 8472.870 9703.990
#> Oct 1979 8974.975 8306.085 9549.678
#> Nov 1979 8656.216 8103.380 9277.757
#> Dec 1979 9030.775 8161.252 9449.128As can be seen, all these features are used in the same way as with the built-in models. For example, let us estimate the forecast accuracy of the previous model for a forecasting horizon of 5:
efa(m, h = 5, type = "normal", size = 8)$per_horizon
#> Horizon 1 Horizon 2 Horizon 3 Horizon 4 Horizon 5
#> MAE 336.606849 512.539793 587.175618 602.430610 618.444333
#> MAPE 3.433026 5.342922 6.146178 6.509639 6.824317
#> sMAPE 3.505668 5.515537 6.363258 6.753901 7.092678
#> RMSE 371.407984 547.846512 613.880093 639.427652 653.309994By default, the additive transformation is applied. Let us select the multiplicative transformation to estimate the forecast accuracy under this setting:
m <- create_model(USAccDeaths, lags = 1:12, method = myModel,
trend = "multiplicative")
efa(m, h = 5, type = "normal", size = 8)$per_horizon
#> Horizon 1 Horizon 2 Horizon 3 Horizon 4 Horizon 5
#> MAE 332.535554 488.467860 582.657426 602.744565 590.058133
#> MAPE 3.390765 5.085486 6.089902 6.514085 6.510790
#> sMAPE 3.472280 5.248379 6.307826 6.762925 6.757952
#> RMSE 391.876819 533.569133 617.532382 644.986167 627.151852Finally, we perform parameter tuning over different values for the
mtry and ntree arguments:
m <- create_model(USAccDeaths, method = myModel)
r <- tune_grid(m, h = 6,
tuneGrid = expand.grid(mtry = 3:6, ntree = c(200, 500)),
type = "normal"
)
r$tuneGrid
#> mtry ntree MAE MAPE sMAPE RMSE
#> 1 3 200 626.2959 6.694872 6.952252 626.2959
#> 2 4 200 611.8487 6.549357 6.796386 611.8487
#> 3 5 200 541.4217 5.811333 6.010729 541.4217
#> 4 6 200 538.5158 5.776617 5.972405 538.5158
#> 5 3 500 616.4211 6.589849 6.838681 616.4211
#> 6 4 500 585.6115 6.269034 6.499482 585.6115
#> 7 5 500 574.3956 6.164767 6.391123 574.3956
#> 8 6 500 572.5947 6.142637 6.366733 572.5947