I often like to centralize my libraries, particularly if I am going to cite them later. (I don’t like to forget any citations of the packages.) Use the cite() function for full details.
library(car)
## Loading required package: carData
library(eia)
## EIA_KEY not found. See `vignette("api", "eia")` for key storage options.
library(fpp3)
## Warning: package 'fpp3' was built under R version 4.4.3
## Registered S3 method overwritten by 'tsibble':
## method from
## as_tibble.grouped_df dplyr
## ── Attaching packages ──────────────────────────────────────────── fpp3 1.0.1 ──
## ✔ tibble 3.2.1 ✔ tsibble 1.1.5
## ✔ dplyr 1.1.4 ✔ tsibbledata 0.4.1
## ✔ tidyr 1.3.1 ✔ feasts 0.3.2
## ✔ lubridate 1.9.3 ✔ fable 0.3.4
## ✔ ggplot2 3.5.1
## ── Conflicts ───────────────────────────────────────────────── fpp3_conflicts ──
## ✖ lubridate::date() masks base::date()
## ✖ dplyr::filter() masks stats::filter()
## ✖ tsibble::intersect() masks base::intersect()
## ✖ tsibble::interval() masks lubridate::interval()
## ✖ dplyr::lag() masks stats::lag()
## ✖ dplyr::recode() masks car::recode()
## ✖ tsibble::setdiff() masks base::setdiff()
## ✖ tsibble::union() masks base::union()
library(fredr)
library(ggplot2)
library(httr)
library(jsonlite)
library(kableExtra)
##
## Attaching package: 'kableExtra'
## The following object is masked from 'package:dplyr':
##
## group_rows
library(latex2exp)
library(magrittr)
##
## Attaching package: 'magrittr'
## The following object is masked from 'package:tidyr':
##
## extract
library(readxl)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.0 ✔ readr 2.1.5
## ✔ purrr 1.0.2 ✔ stringr 1.5.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ magrittr::extract() masks tidyr::extract()
## ✖ dplyr::filter() masks stats::filter()
## ✖ purrr::flatten() masks jsonlite::flatten()
## ✖ kableExtra::group_rows() masks dplyr::group_rows()
## ✖ tsibble::interval() masks lubridate::interval()
## ✖ dplyr::lag() masks stats::lag()
## ✖ dplyr::recode() masks car::recode()
## ✖ purrr::set_names() masks magrittr::set_names()
## ✖ purrr::some() masks car::some()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
I have API keys for foundational TS data (Fred and EIA). Obviously, I keep them hidden. :)
So the easiest way to get FRED data is to invoke the fredr package.
unemp=fredr(
series_id = "UNRATE",
observation_start = as.Date("2020-04-01"),
observation_end = as.Date("2025-03-31")
)
A quick peak…
rbind(head(unemp),tail(unemp))
## # A tibble: 12 × 5
## date series_id value realtime_start realtime_end
## <date> <chr> <dbl> <date> <date>
## 1 2020-04-01 UNRATE 14.8 2025-07-06 2025-07-06
## 2 2020-05-01 UNRATE 13.2 2025-07-06 2025-07-06
## 3 2020-06-01 UNRATE 11 2025-07-06 2025-07-06
## 4 2020-07-01 UNRATE 10.2 2025-07-06 2025-07-06
## 5 2020-08-01 UNRATE 8.4 2025-07-06 2025-07-06
## 6 2020-09-01 UNRATE 7.8 2025-07-06 2025-07-06
## 7 2024-10-01 UNRATE 4.1 2025-07-06 2025-07-06
## 8 2024-11-01 UNRATE 4.2 2025-07-06 2025-07-06
## 9 2024-12-01 UNRATE 4.1 2025-07-06 2025-07-06
## 10 2025-01-01 UNRATE 4 2025-07-06 2025-07-06
## 11 2025-02-01 UNRATE 4.1 2025-07-06 2025-07-06
## 12 2025-03-01 UNRATE 4.2 2025-07-06 2025-07-06
I keep it easy and drop unnecessary variables. Tsibbles are also particularly about dates, so I force the months to be compliant.
unemp$Month=seq(as.Date("2020/4/1"), as.Date("2025/3/31"), by="1 month")
unemp$Month=yearmonth(unemp$Month)
unemp$series_id=NULL
mydata=unemp
str(mydata)
## tibble [60 × 5] (S3: tbl_df/tbl/data.frame)
## $ date : Date[1:60], format: "2020-04-01" "2020-05-01" ...
## $ value : num [1:60] 14.8 13.2 11 10.2 8.4 7.8 6.9 6.7 6.7 6.4 ...
## $ realtime_start: Date[1:60], format: "2025-07-06" "2025-07-06" ...
## $ realtime_end : Date[1:60], format: "2025-07-06" "2025-07-06" ...
## $ Month : mth [1:60] 2020 Apr, 2020 May, 2020 Jun, 2020 Jul, 2020 Aug, 2020 Sep,...
Even when I am working on exploration, I like a train / test split. This is a foundational principal for ML. I define the TS and then set the split.
myts <- mydata %>%as_tsibble(index = Month)
train <- myts[1:48, ]
test <- myts[49:nrow(myts), ]
I am going to look at the data and fit a LOESS to the train set. I will then (for simplicity) use a linear forecast of the last 6 months of LOESS data and forecast the test set. Finally, I calculate metrics for this super simple forecasting method.
# Create the tsibble
myts <- mydata %>%
as_tsibble(index = Month)
# Split train/test
train <- myts[1:48, ]
test <- myts[49:nrow(myts), ]
# Convert Month to numeric for modeling
myts2 <- myts %>%
mutate(x_numeric = as.numeric(Month))
train2 <- myts2[1:48, ]
test2 <- myts2[49:nrow(myts2), ]
# Fit LOESS on training data
loess_model <- loess(
value ~ x_numeric,
data = train2,
span = 0.75
)
# Get LOESS fitted values for all train data
train2 <- train2 %>%
mutate(
loess_fit = predict(loess_model)
)
# Select last 6 months of training for linear extension
last6 <- tail(train2, 6)
# Fit linear model on last 6 LOESS points
tail_model <- lm(
loess_fit ~ x_numeric,
data = last6
)
# Predict over test set with confidence intervals
predictions <- predict(
tail_model,
newdata = test2,
interval = "confidence",
level = 0.95
)
# Bind predictions into test2
test2 <- test2 %>%
mutate(
linear_extension = predictions[,"fit"],
lower = predictions[,"lwr"],
upper = predictions[,"upr"]
)
# Compute forecast error metrics
errors <- test2$value - test2$linear_extension
ME <- mean(errors)
MPE <- mean(errors / test2$value) * 100
MAE <- mean(abs(errors))
RMSE <- sqrt(mean(errors^2))
MAPE <- mean(abs(errors) / test2$value) * 100
# For MASE: compute naive forecast MAE on training data
train_naive_errors <- train2$value[-1] - train2$value[-nrow(train2)]
naive_mae <- mean(abs(train_naive_errors))
MASE <- MAE / naive_mae
# Create a formatted metrics label
metrics_label <- sprintf(
"Test Set Forecast Metrics:\nME = %.3f\nMPE = %.2f%%\nMAE = %.3f\nRMSE = %.3f\nMAPE = %.2f%%\nMASE = %.3f",
ME, MPE, MAE, RMSE, MAPE, MASE
)
# Plot everything with metrics annotation
ggplot() +
geom_line(data = train2, aes(x = Month, y = value, color = "Train Actual"), size = 1) +
geom_line(data = test2, aes(x = Month, y = value, color = "Test Actual"), size = 1) +
geom_line(data = train2, aes(x = Month, y = loess_fit, color = "LOESS Fit"), size = 1) +
geom_line(data = test2, aes(x = Month, y = linear_extension, color = "Linear Extension"), size = 1) +
geom_ribbon(
data = test2,
aes(x = Month, ymin = lower, ymax = upper, fill = "95% CI"),
alpha = 0.3,
color = "gray"
) +
annotate(
"text",
x = min(test2$Month)-10 , # adjust x position if needed
y = max(train2$value, na.rm=TRUE), # adjust y position if needed
label = metrics_label,
hjust = 0,
vjust = 1,
size = 4,
color = "black",
fontface = "italic"
) +
scale_color_manual(
name = "Legend",
values = c(
"Train Actual" = "black",
"Test Actual" = "red",
"LOESS Fit" = "green",
"Linear Extension" = "blue"
)
) +
scale_fill_manual(
name = "",
values = c("95% CI" = "gray")
) +
scale_y_continuous(limits = c(0, NA)) +
labs(
title = "Monthly Unemployment Rate with LOESS, Linear Extension Forecast, and Error Metrics",
x = "Month",
y = "Unemployment Rate"
)
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
When we look at the data, there is clearly a leveling that occurs from a
high of near 15% during the COVID spike to under 5%. Pushing a linear
forecast forward from a LOESS fit of the previous 6 months is better
than a simple naive forecast (MASE=0.8 = MAE Model Forecast / MAE Naive
Model Forecast). The forecast is biased negatively (ME and MPE), and the
MAE, RMSE, and MAPE suggest reasonably low variance. And we haven’t even
decomposed the data yet.
Here, we take a look at some typical TS plots
myts%>%gg_season(value)
myts%>%gg_subseries(value)
myts%>%gg_lag(value, geom="point")
myts%>%ACF(value)%>%autoplot()+ylim(-1,1)
myts%>%PACF(value)%>%autoplot()+ylim(-1,1)
The seasonality ggseason plot reveals two things, a trend and a seasonal
component. We can see that 2020 exhibited the highest unemployment
followed by 2021, 2025, 2024, 2022, 2023. So we conclude that 2020 and
2021 exhibit true nonstationarity and that the trend effect is
nonlinear. There is no obvious seasonal component from observing the
data, but we will look further.
The ggsub_series plot appears to show a declining unemployment rate associated with the months April through December, but a word of caution…The outlier years have affected that appearance dramatically.
The lag plot shows obvious autocorrelation at lag 1 and some at 2 and 3 with it decreasing rapidly beyond that. The 45 degree reference line is the line if \(y_t=y_{t-k}\) where \(k\) is the lag. You can see how there is increasing spread and deviation from the 45 degree line as we increase lags. We will test to see what lags are appropriate at some point.
The ACF plot shows significant autocorrelation through 9 periods, finally dropping off at period 10. Recall, ACF is defined as \(r_{k} = \frac{\sum\limits_{t=k+1}^T (y_{t}-\bar{y})(y_{t-k}-\bar{y})}{\sum\limits_{t=1}^T (y_{t}-\bar{y})^2},\) This is just the correlation between \(y_t\) and \(y_{t-k}\)=$r_k = \(r_k=\frac{Cov(y_t, y_{t-k})}{Var(y_t)}\).
The PACF plot shows significant correlation only for 1 period. This plot removes the preceeding autocorrelation to see if there is remaining autocorrelation. If we want the PACF for \(y_{t-k}\), then we regress \(y_t\) on lags \(1, 2,...k-1\) and regress \(y_{t-k}\) on lags \(1, 2, k-1\). We correlate the residuals of both = PACF! What we don’t have is white noise,
Our next step is transformation of the data prior to decomposition. In our case, we can use a Box-Cox transformation since the data are strictly non-negative. That transformation is defined as follows.
\[ w_t = \begin{cases} \log(y_t), & \text{if }\lambda = 0 \\[8pt] \displaystyle\frac{\operatorname{sign}(y_t)\,\lvert y_t\rvert^{\,\lambda}\,-\,1}{\lambda}, & \text{otherwise} \end{cases} \]
# Compute lambda using Guerrero method
lambda <- myts %>%
features(value, features = guerrero) %>%
pull(lambda_guerrero)
# Plot Box-Cox transformed series with LaTeX lambda label
myts %>%
autoplot(box_cox(value, lambda)) +
labs(
y = "",
title = latex2exp::TeX(paste0(
"Transformed series with $\\lambda = ",
round(lambda, 2),
"$"
))
)
We can see here that the lambda selected is almost a recipricol. But
that lambda helped none.
We can decompose our data into different components: Seasonal, Trend, Residual. Additive decomposition assumes \(y_{t} = S_{t} + T_{t} + R_t\) whereas multiplicative decomposition is additive in the logs: \(y_{t} = S_{t} \times T_{t} \times R_t\). One of the common techniques is STL: Seasonal decomposition of time series by LOESS. The trend and seasonal components are measured using LOESS smoothing. Think of fitting a curve through all Januaries, all Februaries, etc. Think of fitting a nonlinear function for trend. Recall, LOESS fits a locally weighted polynomial regression:
\[ \hat{y}(x_0) = \sum_{i=1}^{n} w_i(x_0)\, y_i \]
\[ \sum_{i=1}^{n} K\left( \frac{x_i - x_0}{h} \right) \Bigl( y_i \;-\; p(x_i) \Bigr)^2 \] where
\[ K(u) = (1 - |u|^3)^3 \quad \text{for } |u| \le 1 \] - \(h\) is the bandwidth parameter controlling the span of the weights - \(p(x)\) is a local polynomial (usually linear or quadratic) - \(w_i(x_0)\) are the values of the kernel function applied to the scaled distance between \(x_i\) and \(x_0\): \(K \frac{x_i-x_0}{h}\) - \(x_0\) is the point at which the fitted value is estimated
comp=myts%>%model(stl=STL(value))%>%components()
comp2=myts%>%mutate(logvalue=log(value))%>%
model(stl=STL(logvalue))%>%components()
comp%>%as_tsibble() %>%autoplot(value)+geom_line(aes(y=trend), colour = "red")+
geom_line(aes(y=season_adjust), colour = "blue")
comp2%>%as_tsibble() %>%autoplot(exp(logvalue))+geom_line(aes(y=trend), colour = "red")+
geom_line(aes(y=season_adjust), colour = "blue")
comp%>%autoplot()
comp2%>%autoplot()+labs(
title = "log(value) = trend + seasonality + remainder",
subtitle = "All components are in log scale"
)
So we have run both additive and multiplicative decomposition. Which is
more appropriate for the model?
m1 <- myts %>% model(add = STL(value))
m2 <- myts %>% model(mult = STL(log(value)))
rbind(accuracy(m1),accuracy(m2))
## # A tibble: 2 × 10
## .model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 add Training -0.0856 0.491 0.311 -2.03 5.77 0.217 0.209 0.613
## 2 mult Training -0.0252 0.403 0.208 -1.09 3.47 0.146 0.172 0.543
Looking at the full data,the multiplicative has the lower ME and MPE (bias measures). It has the lower RMSE, MAE, and MAPE (variance measures). It has the smaller comparative measures (MASE and RMSSE). RMSSE is the ratio of mean squared error of the model versus a naive. Multiplicative wins. We use the log going forward.
Now, we cannnot forecast with a traditional STL. We have to use other decomposition methods. Why? The LOESS component…We would need more traditional decompositions to do so.
So next we move to same basic forecasting models. We will use log(value) to help us since that appears to be a better solution by multiplicative decomposition.
#Log
train$logvalue=log(train$value)
#Naive Models
m1=train |>model(Mean_Model=MEAN(logvalue))
m2=train |>model(Drift_Model=RW(logvalue~drift()))
m3=train |>model(Naive_Model=NAIVE(logvalue))
m4=train |>model(Seasonal_Naive_Model=SNAIVE(logvalue))
#Basic Models
m5=train |>model(TS_Model = TSLM(logvalue))
m6=train |>model(ETS_Model=ETS(logvalue))
m7=train |>model(ARIMA_Model=ARIMA(logvalue))
#Dynamic Ensemble Models
m8=train|>model(Ensemble_Model=(ETS(logvalue)+ARIMA(logvalue))/2)
m9=train|>model(Ensemble_Model_2=(ETS(logvalue)+TSLM(logvalue~fourier(K=2))+ARIMA(logvalue))/3)
The first model is nothing more than a mean model. We acquire the mean of the TS and use that for foreasting. Simple.
The second model is a drift model (not a linear fit). We acquire the first and last observations, and use these to generate a drift component. To easy.
The third model is a naive model. Next period forecast is last period observation. Obviously, it runs out of juice for longer forecasts.
The fourth model is a seasonal naive. Next January forecast is this January’s observation. This type of forecast can work well for data that only has seasonality (no trend).
The fifth model is a time series linear regression model, a model that works well for data that only has a trend.
The sixth model is an Error, Trend, and Seasonality Model. We allow the software to auto-choose the terms. More on this later in the course.
The seventh model is an ARIMA, autoregressive integrated moving average. Again, later in the course.
Model 8 and 9 are simple ensembles. We will discuss later.
Of course, we want complete reports for the estimates.
for (i in 1:9){report(eval(parse(text=paste0('m',i))))}
## Series: logvalue
## Model: MEAN
##
## Mean: 1.5606
## sigma^2: 0.1472
## Series: logvalue
## Model: RW w/ drift
##
## Drift: -0.0284 (se: 0.0079)
## sigma^2: 0.003
## Series: logvalue
## Model: NAIVE
##
## sigma^2: 0.003
## Series: logvalue
## Model: SNAIVE
##
## sigma^2: 0.081
## Series: logvalue
## Model: TSLM
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.3369 -0.2797 -0.1997 0.2477 1.1340
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.56064 0.05537 28.19 <2e-16 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.3836 on 47 degrees of freedom
## Series: logvalue
## Model: ETS(M,Ad,N)
## Smoothing parameters:
## alpha = 0.5870324
## beta = 0.3298261
## phi = 0.8427385
##
## Initial states:
## l[0] b[0]
## 2.922793 -0.2324905
##
## sigma^2: 6e-04
##
## AIC AICc BIC
## -121.7800 -119.7312 -110.5528
## Series: logvalue
## Model: ARIMA(0,2,2)
##
## Coefficients:
## ma1 ma2
## -0.8881 0.4431
## s.e. 0.1357 0.1359
##
## sigma^2 estimated as 0.001706: log likelihood=81.89
## AIC=-157.78 AICc=-157.21 BIC=-152.3
## Series: logvalue
## Model: COMBINATION
## Combination: logvalue * 0.5
##
## ===========================
##
## Series: logvalue
## Model: COMBINATION
## Combination: logvalue + logvalue
##
## ================================
##
## Series: logvalue
## Model: ETS(M,Ad,N)
## Smoothing parameters:
## alpha = 0.5870324
## beta = 0.3298261
## phi = 0.8427385
##
## Initial states:
## l[0] b[0]
## 2.922793 -0.2324905
##
## sigma^2: 6e-04
##
## AIC AICc BIC
## -121.7800 -119.7312 -110.5528
##
## Series: logvalue
## Model: ARIMA(0,2,2)
##
## Coefficients:
## ma1 ma2
## -0.8881 0.4431
## s.e. 0.1357 0.1359
##
## sigma^2 estimated as 0.001706: log likelihood=81.89
## AIC=-157.78 AICc=-157.21 BIC=-152.3
##
##
## Series: logvalue
## Model: COMBINATION
## Combination: logvalue * 0.333333333333333
##
## =========================================
##
## Series: logvalue
## Model: COMBINATION
## Combination: logvalue + logvalue
##
## ================================
##
## Series: logvalue
## Model: COMBINATION
## Combination: logvalue + logvalue
##
## ================================
##
## Series: logvalue
## Model: ETS(M,Ad,N)
## Smoothing parameters:
## alpha = 0.5870324
## beta = 0.3298261
## phi = 0.8427385
##
## Initial states:
## l[0] b[0]
## 2.922793 -0.2324905
##
## sigma^2: 6e-04
##
## AIC AICc BIC
## -121.7800 -119.7312 -110.5528
##
## Series: logvalue
## Model: TSLM
##
## Residuals:
## Min 1Q Median 3Q Max
## -0.4506 -0.2499 -0.1057 0.1795 1.0351
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 1.56064 0.05583 27.951 <2e-16 ***
## fourier(K = 2)C1_12 -0.11102 0.07896 -1.406 0.167
## fourier(K = 2)S1_12 0.06551 0.07896 0.830 0.411
## fourier(K = 2)C2_12 -0.03336 0.07896 -0.423 0.675
## fourier(K = 2)S2_12 -0.04850 0.07896 -0.614 0.542
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.3868 on 43 degrees of freedom
## Multiple R-squared: 0.06969, Adjusted R-squared: -0.01685
## F-statistic: 0.8053 on 4 and 43 DF, p-value: 0.52862
##
##
## Series: logvalue
## Model: ARIMA(0,2,2)
##
## Coefficients:
## ma1 ma2
## -0.8881 0.4431
## s.e. 0.1357 0.1359
##
## sigma^2 estimated as 0.001706: log likelihood=81.89
## AIC=-157.78 AICc=-157.21 BIC=-152.3
And we want plots.
myts$logvalue=log(myts$value)
test$logvalue=log(test$value)
myplot=function(model){
model|>forecast(test)|>autoplot(myts)+geom_line(aes(y=.fitted,col=.model),
data=augment(model))+ggtitle(names(model))}
for (i in 1:9){print(myplot(eval(parse(text=paste0('m',i)))))} #NNETAR takes long to plot
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 12 rows containing missing values or values outside the scale range
## (`geom_line()`).
And of course, accuracy metrics.
# Initialize an empty tibble to collect all results
all_metrics <- tibble()
# Loop over your 9 models
for (i in 1:9) {
# Get the model object
mod <- eval(parse(text = paste0("m", i)))
# Forecast over the test set
fc <- forecast(mod, new_data = test)
# Compute accuracy using the *full dataset* so MASE/RMSSE are available
acc <- accuracy(fc, data = myts)
# Add a model name column
acc <- acc %>% mutate(Model = names(mod))
# Append to the collection tibble
all_metrics <- bind_rows(all_metrics, acc)
}
# Clean up the output to only keep the metrics you care about
metrics_table <- all_metrics %>%
select(Model, ME, MPE, RMSE, MAE, MAPE, MASE, RMSSE)
# Arrange by MAPE for example
metrics_table %>% arrange(MAPE)
## # A tibble: 9 × 8
## Model ME MPE RMSE MAE MAPE MASE RMSSE
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 ETS_Model 0.0172 1.20 0.0258 0.0213 1.50 0.0712 0.0664
## 2 Ensemble_Model -0.0411 -2.91 0.0598 0.0454 3.22 0.152 0.154
## 3 Naive_Model 0.0498 3.50 0.0546 0.0498 3.50 0.166 0.140
## 4 Ensemble_Model_2 -0.0773 -5.50 0.0804 0.0773 5.50 0.258 0.207
## 5 ARIMA_Model -0.0994 -7.03 0.125 0.0994 7.03 0.332 0.322
## 6 Seasonal_Naive_Model 0.101 7.16 0.108 0.101 7.16 0.338 0.278
## 7 Mean_Model -0.150 -10.7 0.152 0.150 10.7 0.501 0.390
## 8 TS_Model -0.150 -10.7 0.152 0.150 10.7 0.501 0.390
## 9 Drift_Model 0.234 16.5 0.258 0.234 16.5 0.783 0.664
The metrics show a clear winner in terms of bias. The lowest bias (ME and MPE measures) belong to the ETS model. In terms of variance, the ETS wins RMSE, MPE, and MAPE. Comparatively, the ETS wins MASE and RMSSE as well. ETS it is. Now, looking at the components of ETS, we have multiplicate error, additive trend, and no seasonality. More on this later.
So let’s look at some TS features. First, let’s include CPI in our analysis.
unemp <- fredr(
series_id = "UNRATE",
observation_start = as.Date("2020-04-01"),
observation_end = as.Date("2025-03-31")
)
# Download CPI
cpi <- fredr(
series_id = "CPIAUCSL",
observation_start = as.Date("2020-04-01"),
observation_end = as.Date("2025-03-31")
)
# Clean unemployment data
unemp$Month <- seq(as.Date("2020/4/1"), as.Date("2025/3/31"), by = "1 month")
unemp$Month <- yearmonth(unemp$Month)
unemp <- unemp %>% select(Month, Unemployment = value)
# Clean CPI data
cpi$Month <- seq(as.Date("2020/4/1"), as.Date("2025/3/31"), by = "1 month")
cpi$Month <- yearmonth(cpi$Month)
cpi <- cpi %>% select(Month, CPI = value)
# Merge into a single dataset
mydata <- left_join(unemp, cpi, by = "Month")
# Inspect structure
str(mydata)
## tibble [60 × 3] (S3: tbl_df/tbl/data.frame)
## $ Month : mth [1:60] 2020 Apr, 2020 May, 2020 Jun, 2020 Jul, 2020 Aug, 2020 Sep,...
## $ Unemployment: num [1:60] 14.8 13.2 11 10.2 8.4 7.8 6.9 6.7 6.7 6.4 ...
## $ CPI : num [1:60] 256 256 257 258 259 ...
# Create a tsibble
myts <- mydata %>% as_tsibble(index = Month)
# View first rows
print(myts)
## # A tsibble: 60 x 3 [1M]
## Month Unemployment CPI
## <mth> <dbl> <dbl>
## 1 2020 Apr 14.8 256.
## 2 2020 May 13.2 256.
## 3 2020 Jun 11 257.
## 4 2020 Jul 10.2 258.
## 5 2020 Aug 8.4 259.
## 6 2020 Sep 7.8 260.
## 7 2020 Oct 6.9 260.
## 8 2020 Nov 6.7 261.
## 9 2020 Dec 6.7 262.
## 10 2021 Jan 6.4 263.
## # ℹ 50 more rows
So now we pivot longer and generate features (i.e. quantiles).
myts_long <- myts %>%
pivot_longer(
cols = c(Unemployment, CPI),
names_to = "Variable",
values_to = "Value"
)
myts_long <- myts_long %>%
as_tsibble(
key = Variable,
index = Month
)
myfeatures <- myts_long %>%
features(Value, c(quantile,mean, feat_acf, feat_pacf, feat_stl, coef_hurst,
feat_spectral,box_pierce,ljung_box,unitroot_kpss, unitroot_pp,
unitroot_ndiffs,unitroot_nsdiffs, var_tiled_mean, shift_level_max,
shift_var_max, shift_kl_max, n_crossing_points,
stat_arch_lm, guerrero))
## New names:
## • `` -> `...6`
myfeatures <- myfeatures %>% ungroup()
temp <- myfeatures %>%
pivot_longer(
cols = -Variable,
names_to = "Code",
values_to = "Value"
)
# 3. Pivot wider to get CPI and Unemployment columns
temp <- temp %>%
distinct() %>% # <= Important: remove any accidental duplicates
pivot_wider(
names_from = Variable,
values_from = Value
)
lookup <- tibble(
Code = c("0%", "25%", "50%", "75%", "100%", "...6", "acf1", "acf10", "diff1_acf1", "diff1_acf10",
"diff2_acf1", "diff2_acf10", "season_acf1", "pacf5", "diff1_pacf5", "diff2_pacf5",
"season_pacf", "trend_strength", "seasonal_strength_year", "seasonal_peak_year",
"seasonal_trough_year", "spikiness", "linearity", "curvature", "stl_e_acf1",
"stl_e_acf10", "coef_hurst", "spectral_entropy", "bp_stat", "bp_pvalue", "lb_stat",
"lb_pvalue", "kpss_stat", "kpss_pvalue", "pp_stat", "pp_pvalue", "ndiffs", "nsdiffs",
"var_tiled_mean", "shift_level_max", "shift_level_index", "shift_var_max",
"shift_var_index", "shift_kl_max", "shift_kl_index", "n_crossing_points",
"stat_arch_lm", "lambda_guerrero"),
Name = c(
"Minimum", "25th Percentile", "Median", "75th Percentile", "Maximum", "Mean",
"Lag-1 Autocorrelation", "Sum of First 10 ACFs",
"ACF of 1st Difference", "Sum ACFs of 1st Diff",
"ACF of 2nd Difference", "Sum ACFs of 2nd Diff",
"Seasonal Lag-1 ACF", "PACF Lag-5", "PACF Lag-5 (1st Diff)",
"PACF Lag-5 (2nd Diff)", "Seasonal PACF", "Trend Strength",
"Seasonal Strength (Year)", "Seasonal Peak (Month)",
"Seasonal Trough (Month)", "Spikiness", "Linearity",
"Curvature", "Residual ACF Lag-1", "Residual ACF Sum (10)",
"Hurst Exponent", "Spectral Entropy", "Box-Pierce Statistic",
"Box-Pierce p-value", "Ljung-Box Statistic", "Ljung-Box p-value",
"KPSS Statistic", "KPSS p-value", "Phillips-Perron Statistic",
"Phillips-Perron p-value", "Required Differences",
"Seasonal Differences", "Tiled Mean Variance", "Max Level Shift",
"Level Shift Index", "Max Variance Shift", "Variance Shift Index",
"Max KL Divergence", "KL Divergence Index", "Zero Crossing Count",
"ARCH LM Statistic", "Guerrero Lambda"
),
Description = c(
"Smallest observed value.",
"Value below which 25% of observations fall.",
"Middle value of the distribution.",
"Value below which 75% of observations fall.",
"Largest observed value.",
"Average value of the series.",
"Correlation between series and itself lagged by 1 period.",
"Cumulative autocorrelation over first 10 lags.",
"Lag-1 autocorrelation of first differenced series.",
"Sum of autocorrelations over first 10 lags (1st differenced).",
"Lag-1 autocorrelation of second differenced series.",
"Sum of autocorrelations over first 10 lags (2nd differenced).",
"Lag-1 autocorrelation at seasonal period.",
"Partial autocorrelation at lag 5.",
"Partial autocorrelation at lag 5 (1st differenced).",
"Partial autocorrelation at lag 5 (2nd differenced).",
"Partial autocorrelation at seasonal lag.",
"Proportion of variance explained by trend.",
"Proportion of variance explained by yearly seasonality.",
"Month of peak seasonal component.",
"Month of lowest seasonal component.",
"Measure of sudden large deviations in the series.",
"Strength and direction of linear trend.",
"Degree of nonlinearity in trend.",
"Lag-1 autocorrelation of STL residuals.",
"Sum of STL residual autocorrelations over 10 lags.",
"Degree of long-term memory (0.5=random walk, >0.5 persistence).",
"Complexity and unpredictability of frequency spectrum.",
"Chi-squared test statistic for autocorrelation.",
"p-value for Box-Pierce test.",
"Adjusted chi-squared test statistic (Ljung-Box).",
"p-value for Ljung-Box test.",
"KPSS test statistic for stationarity.",
"KPSS p-value.",
"Phillips-Perron test statistic for unit root.",
"Phillips-Perron p-value.",
"Minimum number of differences needed for stationarity.",
"Minimum number of seasonal differences needed.",
"Variability of the mean across time tiles.",
"Largest shift in level between adjacent windows.",
"Index of maximum level shift.",
"Largest variance change between windows.",
"Index of maximum variance change.",
"Largest KL divergence between windows.",
"Index of maximum KL divergence.",
"Number of times the series crosses its mean.",
"ARCH LM test statistic for conditional heteroskedasticity.",
"Box-Cox lambda estimated by Guerrero method."
)
)
result_table <- lookup %>%
left_join(temp, by = "Code") %>%
select(Name, Description, everything(), -Code)
numeric_cols <- c("CPI", "Unemployment")
# Round and format
result_table <- result_table %>%
mutate(across(all_of(numeric_cols), ~format(round(.x, 3), nsmall = 3, scientific = FALSE)))
knitr::kable(result_table, align = "l", caption = "Summary of Time Series Features")
| Name | Description | CPI | Unemployment |
|---|---|---|---|
| Minimum | Smallest observed value. | 255.802 | 3.400 |
| 25th Percentile | Value below which 25% of observations fall. | 271.651 | 3.675 |
| Median | Middle value of the distribution. | 297.200 | 4.000 |
| 75th Percentile | Value below which 75% of observations fall. | 309.000 | 5.500 |
| Maximum | Largest observed value. | 319.775 | 14.800 |
| Mean | Average value of the series. | 291.204 | 4.967 |
| Lag-1 Autocorrelation | Correlation between series and itself lagged by 1 period. | 0.959 | 0.828 |
| Sum of First 10 ACFs | Cumulative autocorrelation over first 10 lags. | 5.811 | 2.276 |
| ACF of 1st Difference | Lag-1 autocorrelation of first differenced series. | 0.368 | 0.603 |
| Sum ACFs of 1st Diff | Sum of autocorrelations over first 10 lags (1st differenced). | 0.528 | 0.981 |
| ACF of 2nd Difference | Lag-1 autocorrelation of second differenced series. | -0.231 | -0.712 |
| Sum ACFs of 2nd Diff | Sum of autocorrelations over first 10 lags (2nd differenced). | 0.516 | 0.951 |
| Seasonal Lag-1 ACF | Lag-1 autocorrelation at seasonal period. | 0.426 | 0.171 |
| PACF Lag-5 | Partial autocorrelation at lag 5. | 0.925 | 0.697 |
| PACF Lag-5 (1st Diff) | Partial autocorrelation at lag 5 (1st differenced). | 0.403 | 0.540 |
| PACF Lag-5 (2nd Diff) | Partial autocorrelation at lag 5 (2nd differenced). | 0.534 | 0.639 |
| Seasonal PACF | Partial autocorrelation at seasonal lag. | -0.044 | -0.023 |
| Trend Strength | Proportion of variance explained by trend. | 0.998 | 0.953 |
| Seasonal Strength (Year) | Proportion of variance explained by yearly seasonality. | 0.000 | 0.324 |
| Seasonal Peak (Month) | Month of peak seasonal component. | 0.000 | 1.000 |
| Seasonal Trough (Month) | Month of lowest seasonal component. | 2.000 | 8.000 |
| Spikiness | Measure of sudden large deviations in the series. | 0.001 | 0.000 |
| Linearity | Strength and direction of linear trend. | 157.158 | -12.132 |
| Curvature | Degree of nonlinearity in trend. | -21.860 | 10.849 |
| Residual ACF Lag-1 | Lag-1 autocorrelation of STL residuals. | 0.675 | 0.613 |
| Residual ACF Sum (10) | Sum of STL residual autocorrelations over 10 lags. | 0.687 | 0.771 |
| Hurst Exponent | Degree of long-term memory (0.5=random walk, >0.5 persistence). | 0.997 | 0.995 |
| Spectral Entropy | Complexity and unpredictability of frequency spectrum. | 0.226 | 0.501 |
| Box-Pierce Statistic | Chi-squared test statistic for autocorrelation. | 55.129 | 41.158 |
| Box-Pierce p-value | p-value for Box-Pierce test. | 0.000 | 0.000 |
| Ljung-Box Statistic | Adjusted chi-squared test statistic (Ljung-Box). | 57.932 | 43.250 |
| Ljung-Box p-value | p-value for Ljung-Box test. | 0.000 | 0.000 |
| KPSS Statistic | KPSS test statistic for stationarity. | 1.566 | 0.931 |
| KPSS p-value | KPSS p-value. | 0.010 | 0.010 |
| Phillips-Perron Statistic | Phillips-Perron test statistic for unit root. | -1.564 | -12.743 |
| Phillips-Perron p-value | Phillips-Perron p-value. | 0.100 | 0.010 |
| Required Differences | Minimum number of differences needed for stationarity. | 1.000 | 2.000 |
| Seasonal Differences | Minimum number of seasonal differences needed. | 0.000 | 0.000 |
| Tiled Mean Variance | Variability of the mean across time tiles. | 1.185 | 0.833 |
| Max Level Shift | Largest shift in level between adjacent windows. | 21.721 | 6.825 |
| Level Shift Index | Index of maximum level shift. | 21.000 | 2.000 |
| Max Variance Shift | Largest variance change between windows. | 49.840 | 8.224 |
| Variance Shift Index | Index of maximum variance change. | 29.000 | 9.000 |
| Max KL Divergence | Largest KL divergence between windows. | 0.231 | 11.536 |
| KL Divergence Index | Index of maximum KL divergence. | 18.000 | 4.000 |
| Zero Crossing Count | Number of times the series crosses its mean. | 1.000 | 4.000 |
| ARCH LM Statistic | ARCH LM test statistic for conditional heteroskedasticity. | 0.996 | 0.865 |
| Guerrero Lambda | Box-Cox lambda estimated by Guerrero method. | 2.000 | -0.900 |
We readily generate the 5-number summary here along with the mean. We also look at a variety of TS features.
This appendix provides the formulae and interpretation for selected metrics.
Formula:
\[ \rho_1 = \frac{\sum_{t=2}^{n} (x_t - \bar{x})(x_{t-1} - \bar{x})}{\sum_{t=1}^{n} (x_t - \bar{x})^2} \]
Interpretation: Measures the correlation of the series with itself lagged by 1 period. High values indicate strong persistence.
Formula:
\[ \sum_{k=1}^{10} \rho_k \]
Interpretation: Cumulative autocorrelation over the first 10 lags. Higher sums imply strong persistence.
Formula: Same as Lag-1 autocorrelation but applied to the differenced series:
\[ \rho_1(\Delta x_t) \]
Interpretation: Autocorrelation after first differencing. Lower values indicate stationarity achieved by differencing.
Formula:
\[ \sum_{k=1}^{10} \rho_k(\Delta x_t) \]
Interpretation: Aggregate autocorrelation of the first differenced series.
Formula:
\[ \rho_1(\Delta^2 x_t) \]
Interpretation: Lag-1 autocorrelation after second differencing.
Formula:
\[ \sum_{k=1}^{10} \rho_k(\Delta^2 x_t) \]
Interpretation: Cumulative autocorrelation of the twice differenced series.
Formula:
\[ \rho_s \]
where \(s\) = seasonal lag (e.g., 12).
Interpretation: Measures seasonality correlation.
Formula: Partial autocorrelation at lag 5, defined recursively via the Yule-Walker equations.
Interpretation: Correlation at lag 5 controlling for lags 1–4.
Formula: Proportion of variance explained by the trend component:
\[ 1 - \frac{\text{Var}(R)}{\text{Var}(X)} \]
where \(R\) = remainder (residual).
Interpretation: Near 1 means almost all variance comes from the trend.
Formula: Proportion of variance explained by seasonality:
\[ 1 - \frac{\text{Var}(R + T)}{\text{Var}(X)} \]
where \(T\) = trend.
Interpretation: Higher values mean stronger seasonality.
Definition:
Spikiness measures how much the local variance of residuals fluctuates
over time. It helps detect sudden bursts of volatility (e.g., outliers,
volatility clustering).
Formula:
Let residuals be: \[ e_1, e_2, \dots, e_n. \]
Compute the leave-one-out variance for each observation \(i\): \[ v_i = \frac{1}{n - 2}\sum_{j \neq i}\left(e_j - \bar{e}_{(-i)}\right)^2, \] where \(\bar{e}_{(-i)}\) is the mean of all residuals excluding observation \(i\).
The spikiness is: \[ \text{Spikiness} = \text{Var}\left\{v_1, v_2, \dots, v_n\right\}. \]
Interpretation:
Formula: Estimated slope coefficient in a linear model:
\[ y_t = \beta_0 + \beta_1 t + \varepsilon_t \]
Interpretation: Positive indicates upward trend; negative indicates downward.
Formula: Quadratic coefficient from:
\[ y_t = \beta_0 + \beta_1 t + \beta_2 t^2 + \varepsilon_t \]
Interpretation: Curvature in trend (concave or convex).
Formula: Lag-1 autocorrelation of residuals:
\[ \rho_1(\hat{\varepsilon}) \]
Interpretation: Should be near zero if model is adequate.
Formula: Cumulative ACF of residuals over 10 lags:
\[ \sum_{k=1}^{10} \rho_k(\hat{\varepsilon}) \]
Interpretation: Measures long-term memory.
Formula: Entropy of normalized power spectrum:
\[ H = -\sum_{i} p_i \log(p_i) \]
Interpretation: Lower entropy = more predictable series.
Formula:
\[ Q = n \sum_{k=1}^{h} \hat{\rho}_k^2 \]
Interpretation: Large \(Q\) indicates autocorrelation.
Formula:
\[ Q = n(n+2)\sum_{k=1}^{h} \frac{\hat{\rho}_k^2}{n-k} \]
Interpretation: Adjusted for small samples.
Formula: Sum of squared partial sums of residuals divided by variance estimate.
Interpretation: Higher values indicate nonstationarity.
Interpretation: Tests for unit root. Negative statistics with low p-values reject unit root hypothesis.
Interpretation: Minimum differencing needed to achieve stationarity.
Interpretation: Box-Cox transformation parameter estimate.
| # Time Series Diagnostic Statistics: Definitions, Formulae, and Interpretations |
| This appendix describes each diagnostic statistic, shows formulas where applicable, and explains how to interpret the results. |
Formula: \(\rho_1 = \frac{\sum_{t=2}^{n} (x_t - \bar{x})(x_{t-1} - \bar{x})}{\sum_{t=1}^{n} (x_t - \bar{x})^2}\)
Interpretation: - Measures persistence from one observation to the next. - CPI: 0.959 (very strong persistence) - Unemployment: 0.828 (strong persistence)
Formula: \(\sum_{k=1}^{10} \rho_k\)
Interpretation: - Captures cumulative autocorrelation. - CPI: 5.811 (high persistence) - Unemployment: 2.276 (moderate)
Interpretation: - Autocorrelation after differencing once. - Lower is better if goal is stationarity. - CPI: 0.368 (residual correlation) - Unemployment: 0.603 (notable persistence)
Interpretation: - Aggregated persistence post-differencing. - CPI: 0.528 - Unemployment: 0.981
Interpretation: - Autocorrelation after differencing twice. - Negative values may indicate over-differencing. - CPI: -0.231 - Unemployment: -0.712
Interpretation: - CPI: 0.516 - Unemployment: 0.951
Interpretation: - Autocorrelation at seasonal lag (e.g., lag 12). - CPI: 0.426 (moderate seasonality) - Unemployment: 0.171 (low)
Interpretation: - Direct dependence at lag 5. - CPI: 0.925 - Unemployment: 0.697
Interpretation: - CPI: 0.403 - Unemployment: 0.540
Interpretation: - CPI: 0.534 - Unemployment: 0.639
Interpretation: - CPI: -0.044 - Unemployment: -0.023
Interpretation: - Proportion of variance explained by trend. - Close to 1 means very strong trend. - CPI: 0.998 - Unemployment: 0.953
Interpretation: - Proportion explained by yearly seasonality. - CPI: 0.000 - Unemployment: 0.324
Interpretation: - Month of peak seasonal component (0–11). - CPI: 0 (January) - Unemployment: 1 (February)
Interpretation: - Month of lowest seasonal component. - CPI: 2 (March) - Unemployment: 8 (September)
Interpretation: - Measures sudden large deviations. - Close to 0 means smooth. - CPI: 0.001 - Unemployment: 0.000
Interpretation: - Positive indicates upward trend. - Negative indicates downward. - CPI: 157.158 (very strong upward trend) - Unemployment: -12.132 (weak negative trend)
Interpretation: - Non-linear trends. - Positive: convex; Negative: concave. - CPI: -21.860 - Unemployment: 10.849
Interpretation: - Should be near 0 for good model fit. - CPI: 0.675 - Unemployment: 0.613
Interpretation: - Sum over 10 lags. - CPI: 0.687 - Unemployment: 0.771
Interpretation: - \(H = 0.5\): random walk. - \(H > 0.5\): long memory. - CPI: 0.997 - Unemployment: 0.995
Interpretation: - Lower entropy: more predictable. - CPI: 0.226 (predictable) - Unemployment: 0.501 (less predictable)
Interpretation: - Large values = autocorrelation present. - CPI: 55.129 - Unemployment: 41.158
Interpretation: - p < 0.05: reject null of no autocorrelation. - Both p ≈ 0.
Interpretation: - Adjusted test for autocorrelation. - CPI: 57.932 - Unemployment: 43.250
Interpretation: - Both p ≈ 0: autocorrelation detected.
Interpretation: - High: non-stationarity. - CPI: 1.566 - Unemployment: 0.931
Interpretation: - p = 0.01: evidence of non-stationarity.
Interpretation: - Negative: reject unit root. - CPI: -1.564 - Unemployment: -12.743
Interpretation: - CPI: 0.10 (weak evidence of stationarity) - Unemployment: 0.01 (stationary)
Interpretation: - Min differences to stationarity. - CPI: 1 - Unemployment: 2
Interpretation: - Seasonal differencing needed. - Both: 0
Interpretation: - Variability over time tiles. - CPI: 1.185 - Unemployment: 0.833
Interpretation: - Largest shift in level. - CPI: 21.721 - Unemployment: 6.825
Interpretation: - Index of max shift. - CPI: 21 - Unemployment: 2
Interpretation: - Largest change in variance. - CPI: 49.840 - Unemployment: 8.224
Interpretation: - Index of change. - CPI: 29 - Unemployment: 9
Interpretation: - Largest divergence between windows. - CPI: 0.231 - Unemployment: 11.536
Interpretation: - Index of divergence. - CPI: 18 - Unemployment: 4
Interpretation: - Number of mean crossings. - CPI: 1 - Unemployment: 4
Interpretation: - Test for conditional heteroskedasticity. - CPI: 0.996 - Unemployment: 0.865
Interpretation: - Box-Cox transformation parameter. - CPI: 2.000 - Unemployment: -0.900
Note: Metrics like Max Level Shift, Variance Shift, and KL Divergence are calculated numerically, so no standard formula is shown.
We can use a simple algorithm to prepare data for forecasting. First, we tidy the data. Then we visualize, estimate, and generate forecasts which are evaluated. The visualization, estimation, and forecast generation process is repeated. Hyndman illustrates.
Let’s do that with our Unemployment and CPI data. We will create a proxy for the misery index.
myts <- myts %>%
arrange(Month) %>%
mutate(
CPI_inflation_mom = 100 * (CPI / lag(CPI, 1) - 1)
)
myts$Misery=myts$Unemployment+myts$CPI_inflation_mom
myts |>
autoplot(Misery) +
labs(y = "Misery Index", title = "Misery Index")
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
myts%>%gg_season(Misery)
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
myts%>%gg_subseries(Misery)
myts%>%gg_lag(Misery, geom="point")
## Warning: Removed 1 rows containing missing values (gg_lag).
myts%>%ACF(Misery)%>%autoplot()+ylim(-1,1)
myts%>%PACF(Misery)%>%autoplot()+ylim(-1,1)
## Guerrero
# Compute lambda using Guerrero method
lambda <- myts %>%filter(!is.na(Misery), Misery > 0) %>%
features(Misery, features = guerrero) %>%
pull(lambda_guerrero)
# Plot Box-Cox transformed series with LaTeX lambda label
myts %>%
autoplot(box_cox(Misery, lambda)) +
labs(
y = "",
title = latex2exp::TeX(paste0(
"Transformed series with $\\lambda = ",
round(lambda, 2),
"$"
))
)
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
Nearly inverse! But we will ignore for now (and for interpretability).
train2 <- myts %>%
slice(2:48)
test2 <- myts %>%
slice(49:n())
#Log
train2$logMisery=log(train2$Misery)
#Naive Models
m1=train2 |>model(Mean_Model=MEAN(logMisery))
m2=train2 |>model(Drift_Model=RW(logMisery~drift()))
m3=train2 |>model(Naive_Model=NAIVE(logMisery))
m4=train2 |>model(Seasonal_Naive_Model=SNAIVE(logMisery))
#Basic Models
m5=train2 |>model(TS_Model = TSLM(logMisery))
m6=train2 |>model(ETS_Model=ETS(logMisery))
m7=train2 |>model(ARIMA_Model=ARIMA(logMisery))
#Dynamic Ensemble Models
m8=train2|>model(Ensemble_Model=(ETS(logMisery)+ARIMA(logMisery))/2)
m9=train2|>model(Ensemble_Model_2=(ETS(logMisery)+TSLM(logMisery~fourier(K=2))+ARIMA(logMisery))/3)
myts$logMisery=log(myts$Misery)
test2$logMisery=log(test2$Misery)
myplot=function(model){
model|>forecast(test2)|>autoplot(myts)+geom_line(aes(y=.fitted,col=.model),
data=augment(model))+ggtitle(names(model))}
for (i in 1:9){print(myplot(eval(parse(text=paste0('m',i)))))} #NNETAR takes long to plot
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 12 rows containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
# Initialize an empty tibble to collect all results
all_metrics <- tibble()
# Loop over your 9 models
for (i in 1:9) {
# Get the model object
mod <- eval(parse(text = paste0("m", i)))
# Forecast over the test set
fc <- forecast(mod, new_data = test2)
# Compute accuracy using the *full dataset* so MASE/RMSSE are available
acc <- accuracy(fc, data = myts)
# Add a model name column
acc <- acc %>% mutate(Model = names(mod))
# Append to the collection tibble
all_metrics <- bind_rows(all_metrics, acc)
}
# Clean up the output to only keep the metrics you care about
metrics_table <- all_metrics %>%
select(Model, ME, MPE, RMSE, MAE, MAPE, MASE, RMSSE)
# Arrange by MAPE for example
metrics_table %>% arrange(MAPE)
## # A tibble: 9 × 8
## Model ME MPE RMSE MAE MAPE MASE RMSSE
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Ensemble_Model -0.0160 -1.13 0.0334 0.0246 1.71 0.0880 0.101
## 2 Naive_Model 0.0110 0.706 0.0350 0.0317 2.17 0.114 0.106
## 3 ETS_Model 0.0265 1.77 0.0433 0.0378 2.57 0.135 0.131
## 4 ARIMA_Model -0.0584 -4.04 0.0687 0.0584 4.04 0.209 0.208
## 5 Ensemble_Model_2 -0.0671 -4.67 0.0821 0.0671 4.67 0.240 0.248
## 6 Seasonal_Naive_Model 0.0744 5.07 0.0920 0.0784 5.35 0.281 0.278
## 7 Drift_Model 0.170 11.6 0.200 0.171 11.6 0.611 0.604
## 8 TS_Model -0.169 -11.6 0.172 0.169 11.6 0.604 0.520
## 9 Mean_Model -0.169 -11.6 0.172 0.169 11.6 0.604 0.520