Does “soft” data enhance “hard” data forecasts?

1 Start and end dates

start_date = lubridate::ymd(20100101)
end_date = lubridate::ymd(20211031)

2 Retrieving raw data from FRED API

employment = fredr_series_observations(
  series_id = "PAYEMS",
  frequency = "m",
  aggregation_method = "avg",
  observation_start =  start_date,
  observation_end = end_date
)

fear = fredr_series_observations(
  series_id = "VIXCLS",
  frequency = "m",
  aggregation_method = "avg",
  observation_start =  start_date,
  observation_end = end_date
)

uncertainty = fredr_series_observations(
  series_id = "USEPUINDXD",
  frequency = "m",
  aggregation_method = "avg",
  observation_start =  start_date,
  observation_end = end_date
)

3 Creating time series from raw data

employment_ts = ts(employment$value, start = c(year(start_date), month(start_date)), frequency = 12)
fear_ts = ts(fear$value, start = c(year(start_date), month(start_date)), frequency = 12)
uncertainty_ts = ts(uncertainty$value, start = c(year(start_date), month(start_date)), frequency = 12)

4 Splitting raw data into training and testing datasets

employment_split <- ts_split(ts.obj = employment_ts, sample.out = 12)
employment_training <- employment_split$train
employment_testing <- employment_split$test

fear_split <- ts_split(ts.obj = fear_ts, sample.out = 12)
fear_training <- fear_split$train
fear_testing <- fear_split$test

uncertainty_split <- ts_split(ts.obj = uncertainty_ts, sample.out = 12)
uncertainty_training <- uncertainty_split$train
uncertainty_testing <- uncertainty_split$test

5 Plots from raw data

autoplot(log(employment_training)) + 
  autolayer(log(fear_training)) + 
  autolayer(log(uncertainty_training))

Looking the graph of the raw data, the external regressors apparently have some correlation with the employment time series.

7 Analizing how many differences the model will need

7.1 Using ndiff function to identify the needed number of differences

ndiffs(employment_training, alpha = 0.05, test = "adf")
## [1] 1

The function calculate the need of just one difference.

7.2 using auto.arima

employment_arima = auto.arima(employment_training)
summary(employment_arima)
## Series: employment_training 
## ARIMA(0,1,0) 
## 
## sigma^2 estimated as 3573478:  log likelihood=-1156
## AIC=2315   AICc=2315   BIC=2317
## 
## Training set error measures:
##                ME RMSE MAE   MPE  MAPE  MASE   ACF1
## Training set 97.1 1883 441 0.062 0.323 0.152 0.0039

The auto.arima() results (ARIMA(0,1,0)) confirmed the initial analysis with ndiffs().

tsdisplay(diff(employment_training))

Applying one difference, the results of the ACF and the PACF are much better, and the graph is showing a stationary data.

The graph also shows a sharp drop in the number of employments around March 2020, probably caused by the COVID-19 pandemic’s lockdowns. However, this is not affecting the stationarity of the chart.

8 Running the arima model

employment_arima = Arima(employment_training, order = c(0, 1, 0))
summary(employment_arima)
## Series: employment_training 
## ARIMA(0,1,0) 
## 
## sigma^2 estimated as 3573478:  log likelihood=-1156
## AIC=2315   AICc=2315   BIC=2317
## 
## Training set error measures:
##                ME RMSE MAE   MPE  MAPE  MASE   ACF1
## Training set 97.1 1883 441 0.062 0.323 0.152 0.0039

The arima model is showing a RMSE of 1909.

8.1 Forecasting with the arima model and plotting the forecast

employment_arima_fc = forecast(employment_arima, h = length(employment_testing))
autoplot(employment_arima_fc, col = "blue") +
  autolayer(employment_arima_fc$mean, alpha = 0.5, color = "red") 

The mean of the arima model looks like a naive model, always repeating the value of the last period. This is due to having AR(p) and MA(q) igual to zero.

8.2 Calculating the accuracy of the predictions

accuracy(employment_arima_fc$mean, employment_testing)
##            ME RMSE  MAE  MPE MAPE  ACF1 Theil's U
## Test set 2758 3287 2758 1.89 1.89 0.767      6.27

The RMSE (3287) of the testing time-series is much higher than the RMSE (1909) of the training time-series, which is something expected because the model was created based on the data of the training time-series.

The forecast data is presenting a linear behavior with zero slope because the model is not using past periods values (p) or error values of past periods forecasts (q) to make the predictions.

I will try to get a model with better RMSE by adding 1 q to the Arima function. The function auto.arima not necessarily find the model with the best RMSE, the function calculates a trade off between accuracy and parsimony based on the Akaike’s Information Criterion (AIC) or/and the Bayesian Information Criterion (BIC).

If our focus is to find the best prediction model regardless of its complexity, we should try other combinations of p, d, and q. I can probably find models with better RMSE, however, they will have worse (higher) AIC and BIC, and may present overfitting issues.

8.2.1 Function to test the arima function with many different combinations of p, d, and q

arima_brute_force <-
  function(training_ts,
           testing_ts,
           p_vec = c(0:10),
           d_vec = c(0:10),
           q_vec = c(0:10)) {
    best_model = auto.arima(training_ts)
    best_RMSE = accuracy(best_model)[2]
    for (p in p_vec) {
      for (d in d_vec) {
        for (q in q_vec) {
          err <- FALSE
          tryCatch({
            result <- Arima(training_ts,
                            order = c(p, d, q))
          },
          error = function(e) {
            err <<- TRUE
          },
          warning = function(w) {
          })
          if (!err) {
            r = accuracy(result)
            if (!is.na(accuracy(result)[2])) {
              if (accuracy(result)[2] < best_RMSE) {
                best_model = result
                best_RMSE = accuracy(result)[2]
              }
            }
          }
        }
      }
    }
    result <- best_model
  }

8.2.2 Testing the create function above with p, d, and q values from 0 to 10

best_model = arima_brute_force(employment_training, employment_testing, c(0:10), c(0:10), c(0:10))
summary(best_model)
## Series: training_ts 
## ARIMA(3,0,8) with non-zero mean 
## 
## Coefficients:
## Warning in sqrt(diag(x$var.coef)): NaNs produced
##         ar1    ar2     ar3     ma1     ma2     ma3    ma4    ma5    ma6    ma7
##       0.999  0.995  -0.997  -0.113  -1.304  -0.025  0.178  0.053  0.059  0.085
## s.e.    NaN    NaN   0.009   0.103   0.107   0.144  0.146  0.142  0.144  0.112
##         ma8    mean
##       0.067  138874
## s.e.  0.112    1680
## 
## sigma^2 estimated as 3375787:  log likelihood=-1159
## AIC=2344   AICc=2347   BIC=2381
## 
## Training set error measures:
##                ME RMSE MAE     MPE  MAPE  MASE     ACF1
## Training set 3.07 1750 489 -0.0177 0.349 0.168 -0.00612

8.2.3 Forecasting with the arima model with 1q and plotting the forecast

employment_arima_fc_best_model = forecast(best_model, h = length(employment_testing))
autoplot(employment_arima_fc_best_model, col = "blue") +
  autolayer(employment_arima_fc_best_model$mean, alpha = 0.5, color = "red") 

8.2.4 Calculating the accuracy of the predictions of the model with 1q

accuracy(employment_arima_fc_best_model$mean, employment_testing)
##            ME RMSE  MAE  MPE MAPE  ACF1 Theil's U
## Test set 4548 5364 4548 3.11 3.11 0.767      10.2

The ARIMA(3,0,8) model is clearly suffering from overfitting since the forecasting of the testing time series is presenting a higher RMSE (4458) than the initial ARIMA(0,1,0) model RMSE (3287).

So, I will keep with the initial model and leave the ARIMA(3,0,8) model.

9 Combining regressors into a dataframe

regressors_training_ts = cbind(uncertainty_training, fear_training)
regressors_testing_ts = cbind(uncertainty_testing, fear_testing)

10 Running the arimax model

employment_arimax = auto.arima(employment_training, xreg = regressors_training_ts)
summary(employment_arimax)
## Series: employment_training 
## Regression with ARIMA(1,1,1) errors 
## 
## Coefficients:
##         ar1     ma1  drift  uncertainty_training  fear_training
##       0.687  -0.860  126.8                -30.51          164.1
## s.e.  0.231   0.173   61.6                  3.37           29.7
## 
## sigma^2 estimated as 2074536:  log likelihood=-1119
## AIC=2250   AICc=2250   BIC=2267
## 
## Training set error measures:
##                 ME RMSE MAE      MPE  MAPE  MASE    ACF1
## Training set -3.16 1407 733 -0.00938 0.531 0.252 -0.0203

The arimax model presented significant improvement of RMSE (1422) with the training time-series.

10.1 Forecasting with the arimax model and plotting the forecast

employment_arimax_fc = forecast(employment_arimax, xreg = regressors_testing_ts)
## Warning in forecast.forecast_ARIMA(employment_arimax, xreg =
## regressors_testing_ts): xreg contains different column names from the xreg used
## in training. Please check that the regressors are in the same order.
autoplot(employment_arimax_fc, col = "blue") +
  autolayer(employment_arimax_fc$mean, alpha = 0.5, color = "red") 

The graph is also showing some significant improvement.

10.2 Calculating the accuracy of the predictions

accuracy(employment_arimax_fc$mean, employment_testing)
##             ME RMSE  MAE    MPE  MAPE  ACF1 Theil's U
## Test set -1140 1419 1191 -0.788 0.823 0.416      2.74

The RMSE is worse than the RMSE using the training time-series. However, comparing the RMSE of the two testing time-series, the RMSE (1909) of the arimax model is showing a significant improvement in relation to the RMSE (3287) to the arima model

11 Running the NNETS model

employment_nnets = nnetar(employment_training, 
                          P=3, 
                          size = 10, 
                          decay = 0.05, 
                          xreg = regressors_training_ts)
accuracy(employment_nnets)
##                 ME RMSE MAE      MPE  MAPE   MASE    ACF1
## Training set -1.03  292 164 -0.00242 0.115 0.0564 -0.0915

11.1 Forecasting with the NNETS model and plotting the forecast

employment_nnets_fc = forecast(employment_nnets, xreg = regressors_testing_ts)
## Warning in forecast.nnetar(employment_nnets, xreg = regressors_testing_ts): xreg
## contains different column names from the xreg used in training. Please check
## that the regressors are in the same order.
autoplot(employment_ts, col = "blue") +
  autolayer(employment_nnets_fc$mean, color="red") 

11.2 Calculating the accuracy of the predictions

accuracy(employment_nnets_fc, employment_testing)
##                   ME RMSE  MAE      MPE  MAPE   MASE    ACF1 Theil's U
## Training set   -1.03  292  164 -0.00242 0.115 0.0564 -0.0915        NA
## Test set     -864.14 2245 1757 -0.60799 1.215 0.6035  0.7885      4.35

The RMSE (308) of the training set is by far the best one. however, it is presenting a worse RMSE (2512) than the arimax model RMSE (1986). This is a indication of overfitting. If I have to choose one model now I would choose the arimax model with regressors.

12 Running the TBATS model

employment_tbats = tbats(employment_training, num.cores = 8)
accuracy(employment_tbats)
##                ME RMSE MAE    MPE  MAPE MASE     ACF1
## Training set 85.4 1886 449 0.0532 0.329 1.01 -0.00425

The TBATS model RMSE (1912) for the training time-series is the worst until now

12.1 Forecasting with the TBATS model and plotting the forecast

employment_tbats_fc = forecast(employment_tbats, h = length(employment_testing))
autoplot(employment_ts, col = "blue") +
  autolayer(employment_tbats_fc, alpha = 0.5, color = "red") 

12.2 Calculating the accuracy of the predictions

accuracy(employment_tbats_fc$mean, employment_testing)
##            ME RMSE  MAE  MPE MAPE  ACF1 Theil's U
## Test set 2751 3282 2751 1.88 1.88 0.767      6.26

The tbats is presenting the worst RMSE for the training (1912) and testing (3276) sets. So I will keep the arimax model with regressors.

13 Running the hybrid model

employment_hybrid = forecastHybrid::hybridModel(employment_training, 
                                                a.args = list(xreg = regressors_training_ts),
                                                n.args = list(xreg = regressors_training_ts), 
                                                models ="ant")
## Fitting the auto.arima model
## Fitting the nnetar model
## Fitting the tbats model
accuracy(employment_hybrid)
##            ME RMSE MAE    MPE  MAPE    ACF1 Theil's U
## Test set 30.6 1212 422 0.0152 0.306 -0.0301     0.613

The TBATS model RMSE (1912) for the training time-series is the worst until now

13.1 Forecasting with the hybrid model and plotting the forecast

employment_hybrid_fc = forecast(employment_hybrid,
                                h = length(employment_testing),
                                xreg = regressors_testing_ts)
## Warning in forecast.forecast_ARIMA(object$auto.arima, h = h, xreg = xregA, :
## xreg contains different column names from the xreg used in training. Please
## check that the regressors are in the same order.
## Warning in forecast.nnetar(object$nnetar, h = h, xreg = xregN, PI = PI, : xreg
## contains different column names from the xreg used in training. Please check
## that the regressors are in the same order.
autoplot(employment_ts, col = "blue") +
  autolayer(employment_hybrid_fc, alpha = 0.5, color = "red") 

13.2 Calculating the accuracy of the predictions

accuracy(employment_hybrid_fc$mean, employment_testing)
##            ME RMSE  MAE  MPE MAPE  ACF1 Theil's U
## Test set 1495 2585 2129 1.01 1.46 0.835      4.93

The hybrid model is the clear winner. The best RMSE (308) for the training set was the nnetar model. However, the nnetar presented low accuracy because of overfitting. The hybrid model had the second-best RMSE (1224) for the training set and the best RMSE (1176) for the testing set, showing no signs of overfitting.