library(fpp3)
## Warning: package 'fpp3' was built under R version 4.5.3
## ── Attaching packages ──────────────────────────────────────────── fpp3 1.0.3 ──
## ✔ tibble      3.3.1     ✔ tsibble     1.2.0
## ✔ dplyr       1.1.4     ✔ tsibbledata 0.4.1
## ✔ tidyr       1.3.2     ✔ ggtime      0.2.0
## ✔ lubridate   1.9.4     ✔ feasts      0.5.0
## ✔ ggplot2     4.0.2     ✔ fable       0.5.0
## Warning: package 'tibble' was built under R version 4.5.2
## Warning: package 'tidyr' was built under R version 4.5.2
## Warning: package 'ggplot2' was built under R version 4.5.2
## Warning: package 'tsibble' was built under R version 4.5.3
## Warning: package 'tsibbledata' was built under R version 4.5.3
## Warning: package 'ggtime' was built under R version 4.5.3
## Warning: package 'feasts' was built under R version 4.5.3
## Warning: package 'fabletools' was built under R version 4.5.3
## Warning: package 'fable' was built under R version 4.5.3
## ── 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()
## ✖ tsibble::setdiff()   masks base::setdiff()
## ✖ tsibble::union()     masks base::union()
library(rugarch)
## Warning: package 'rugarch' was built under R version 4.5.3
## Loading required package: parallel
## 
## Attaching package: 'rugarch'
## The following object is masked from 'package:fabletools':
## 
##     report
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.2
## Warning: package 'readr' was built under R version 4.5.2
## Warning: package 'purrr' was built under R version 4.5.2
## Warning: package 'stringr' was built under R version 4.5.2
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats 1.0.1     ✔ readr   2.1.6
## ✔ purrr   1.2.1     ✔ stringr 1.6.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter()     masks stats::filter()
## ✖ tsibble::interval() masks lubridate::interval()
## ✖ dplyr::lag()        masks stats::lag()
## ✖ purrr::reduce()     masks rugarch::reduce()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Part 1

Create the Hierarchiary

tourism_h <- tourism %>%
  aggregate_key(
    State / Region,
    Trips = sum(Trips)
  )

Visualize

tourism_h %>%
  filter(
    !is_aggregated(State),
    is_aggregated(Region)
  ) %>%
  autoplot(Trips) +
  facet_wrap(vars(State), scales = "free_y") +
  labs(
    title = "Tourism Trips by State",
    x = "Quarter",
    y = "Trips"
  )

Create training/testing data

tourism_train <- tourism_h %>%
  filter(year(Quarter) <= 2015)

tourism_test <- tourism_h %>%
  filter(year(Quarter) > 2015)

fit models

tourism_fit <- tourism_train %>%
  model(base = ETS(Trips)) %>%
  reconcile(
    bottom_up = bottom_up(base),
    top_down = top_down(
      base,
      method = "average_proportions"
    ),
    middle_out = middle_out(
      base,
      split = 1
    ),
    ols = min_trace(
      base,
      method = "ols"
    ),
    mint = min_trace(
      base,
      method = "mint_shrink"
    )
  )

plot forecasts

tourism_fc <- tourism_fit %>%
  forecast(h = "2 years")

tourism_fc %>%
  filter(
    is_aggregated(State),
    is_aggregated(Region)
  ) %>%
  autoplot(tourism_h, level = NULL) +
  labs(
    title = "Hierarchical Forecast Comparison",
    x = "Quarter",
    y = "Trips"
  )

compare forecast accuracy

tourism_fc %>%
  accuracy(tourism_test) %>%
  group_by(.model) %>%
  summarise(
    RMSE = mean(RMSE, na.rm = TRUE),
    MAPE = mean(MAPE, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(RMSE)
## # A tibble: 6 × 3
##   .model      RMSE  MAPE
##   <chr>      <dbl> <dbl>
## 1 ols         90.1  16.9
## 2 middle_out  94.7  16.1
## 3 base        96.2  16.7
## 4 mint       101.   15.9
## 5 top_down   108.   21.6
## 6 bottom_up  113.   16.8

comparisons

tourism_fc %>%
  accuracy(tourism_test) %>%
  mutate(
    Level = case_when(
      is_aggregated(State) ~ "Australia",
      is_aggregated(Region) ~ "State",
      TRUE ~ "Region"
    )
  ) %>%
  group_by(Level, .model) %>%
  summarise(
    RMSE = mean(RMSE, na.rm = TRUE),
    MAPE = mean(MAPE, na.rm = TRUE),
    .groups = "drop"
  ) %>%
  arrange(Level, RMSE)
## # A tibble: 18 × 4
##    Level     .model       RMSE  MAPE
##    <chr>     <chr>       <dbl> <dbl>
##  1 Australia base       1721.   5.22
##  2 Australia top_down   1721.   5.22
##  3 Australia ols        1761.   5.37
##  4 Australia middle_out 2027.   6.48
##  5 Australia mint       2143.   7.07
##  6 Australia bottom_up  2514.   8.73
##  7 Region    middle_out   47.0 16.9 
##  8 Region    ols          47.0 17.9 
##  9 Region    mint         48.7 16.7 
## 10 Region    base         52.6 17.5 
## 11 Region    bottom_up    52.6 17.5 
## 12 Region    top_down     61.4 22.8 
## 13 State     ols         291.   8.70
## 14 State     middle_out  307.   9.83
## 15 State     base        307.   9.83
## 16 State     mint        339.  10.0 
## 17 State     top_down    347.  12.6 
## 18 State     bottom_up   389.  11.3

The tourism data shows differences in both scale/movement for the Australian states. With that, most states generally increased near the end of the series. The reconciliation methods also performed differently depending on the level of hierarchy. Overall, OLS had the lowest RMSE, but MinT had the lowest MAPE. At the national level, the base/top down forecasts performed the best while middle out had the lowest regional RMSE and OLS had the lowest state-level RMSE. This shows that no single method was the unanimous best, but the reconciliation ensured that everything stayed consistent.

Part 2

Create dataset

industries <- aus_retail %>%
  distinct(Industry) %>%
  slice(1:3) %>%
  pull(Industry)

retail_grouped <- aus_retail %>%
  filter(Industry %in% industries) %>%
  aggregate_key(
    State * Industry,
    RetailSales = sum(Turnover)
  )

visualize

retail_grouped %>%
  filter(
    !is_aggregated(State),
    !is_aggregated(Industry)
  ) %>%
  autoplot(RetailSales) +
  facet_wrap(
    vars(State, Industry),
    scales = "free_y"
  ) +
  labs(
    title = "Retail Sales by State and Industry",
    x = "Month",
    y = "Retail Sales"
  ) +
  theme(legend.position = "none")

retail_grouped %>%
  filter(
    !is_aggregated(State),
    is_aggregated(Industry)
  ) %>%
  autoplot(RetailSales) +
  facet_wrap(vars(State), scales = "free_y") +
  labs(
    title = "Total Retail Sales by State",
    x = "Month",
    y = "Retail Sales"
  ) +
  theme(legend.position = "none")

training sets

last_month <- max(retail_grouped$Month)

retail_train <- retail_grouped %>%
  filter(Month <= last_month - 24)

retail_test <- retail_grouped %>%
  filter(Month > last_month - 24)

fit models

retail_fit <- retail_train %>%
  model(
    ETS = ETS(RetailSales)
  )

Reconcile

retail_reconciled <- retail_fit %>%
  reconcile(
    BottomUp = bottom_up(ETS),
    MinTrace = min_trace(ETS, method = "mint_shrink")
  )

forecast

retail_forecasts <- retail_reconciled %>%
  forecast(h = 24)

plot

retail_forecasts %>%
  filter(
    !is_aggregated(State),
    !is_aggregated(Industry)
  ) %>%
  autoplot(
    retail_grouped %>%
      filter(Month > last_month - 60)
  ) +
  facet_wrap(
    vars(State, Industry),
    scales = "free_y"
  ) +
  labs(
    title = "Reconciled Retail Sales Forecasts",
    x = "Month",
    y = "Retail Sales"
  )

accuracy - grouped

grouped_accuracy <- retail_forecasts %>%
  accuracy(retail_test) %>%
  filter(
    !is_aggregated(State),
    !is_aggregated(Industry)
  ) %>%
  select(.model, RMSE, MAE, MAPE)

grouped_accuracy
## # A tibble: 72 × 4
##    .model     RMSE    MAE  MAPE
##    <chr>     <dbl>  <dbl> <dbl>
##  1 BottomUp  2.72   2.18   5.55
##  2 BottomUp  3.33   2.74   4.10
##  3 BottomUp  2.47   2.05   9.72
##  4 BottomUp 61.7   56.2    7.47
##  5 BottomUp 29.9   21.7    1.66
##  6 BottomUp 17.0   13.0    2.48
##  7 BottomUp  2.22   1.58   8.43
##  8 BottomUp  4.30   3.76   9.71
##  9 BottomUp  0.805  0.737  8.61
## 10 BottomUp 41.1   36.2    9.34
## # ℹ 62 more rows

fit flat models

retail_flat <- aus_retail %>%
  filter(Industry %in% industries) %>%
  select(Month, State, Industry, Turnover)

flat_train <- retail_flat %>%
  filter(Month <= last_month - 24)

flat_test <- retail_flat %>%
  filter(Month > last_month - 24)

flat_fit <- flat_train %>%
  model(
    FlatETS = ETS(Turnover)
  )

flat_forecasts <- flat_fit %>%
  forecast(h = 24)

evaluate flat forecast

flat_accuracy <- flat_forecasts %>%
  accuracy(flat_test) %>%
  select(.model, RMSE, MAE, MAPE)

flat_accuracy
## # A tibble: 24 × 4
##    .model    RMSE    MAE  MAPE
##    <chr>    <dbl>  <dbl> <dbl>
##  1 FlatETS  2.72   2.18   5.55
##  2 FlatETS  3.33   2.74   4.10
##  3 FlatETS  2.47   2.05   9.72
##  4 FlatETS 61.7   56.2    7.47
##  5 FlatETS 29.9   21.7    1.66
##  6 FlatETS 17.0   13.0    2.48
##  7 FlatETS  2.22   1.58   8.43
##  8 FlatETS  4.30   3.76   9.71
##  9 FlatETS  0.805  0.737  8.61
## 10 FlatETS 41.1   36.2    9.34
## # ℹ 14 more rows

compare

bind_rows(
  grouped_accuracy %>%
    mutate(Type = "Grouped and Reconciled"),
  flat_accuracy %>%
    mutate(Type = "Flat")
) %>%
  group_by(Type, .model) %>%
  summarise(
    RMSE = mean(RMSE, na.rm = TRUE),
    MAE = mean(MAE, na.rm = TRUE),
    MAPE = mean(MAPE, na.rm = TRUE),
    .groups = "drop"
  )
## # A tibble: 4 × 5
##   Type                   .model    RMSE   MAE  MAPE
##   <chr>                  <chr>    <dbl> <dbl> <dbl>
## 1 Flat                   FlatETS   16.5  13.9  7.18
## 2 Grouped and Reconciled BottomUp  16.5  13.9  7.18
## 3 Grouped and Reconciled ETS       16.5  13.9  7.18
## 4 Grouped and Reconciled MinTrace  16.6  14.2  6.97

The retail data showed a strong upward trend across most states, but the size and growth of sales differed by both state and industry. The flat ETS, base ETS, and bottom-up forecasts produced the same average accuracy. MinTrace had a slightly higher RMSE and MAE but the lowest MAPE, so the grouped forecasts did not clearly outperform the flat models. In a regional sales setting, one region’s forecast can affect the reconciled totals that connect all regions and industries. Reconciliation improves reliability by adjusting the forecasts so that individual state and industry values add correctly to the overall totals, preventing conflicting forecasts across categories.

Part 3

create dax returns

data("EuStockMarkets")

dax_prices <- EuStockMarkets[, "DAX"]

dax_returns <- diff(log(dax_prices)) * 100
dax_returns <- as.numeric(na.omit(dax_returns))

plot

returns_data <- tibble(
  Time = seq_along(dax_returns),
  Return = dax_returns
)

ggplot(returns_data, aes(x = Time, y = Return)) +
  geom_line() +
  labs(
    title = "Daily DAX Returns",
    x = "Observation",
    y = "Percent Return"
  )

plot squared returns

ggplot(returns_data, aes(x = Time, y = Return^2)) +
  geom_line() +
  labs(
    title = "Squared DAX Returns",
    x = "Observation",
    y = "Squared Return"
  )

specify model

garch_spec <- ugarchspec(
  variance.model = list(
    model = "sGARCH",
    garchOrder = c(1, 1)
  ),
  mean.model = list(
    armaOrder = c(0, 0),
    include.mean = TRUE
  ),
  distribution.model = "norm"
)

fit model

garch_fit <- ugarchfit(
  spec = garch_spec,
  data = dax_returns
)

garch_fit
## 
## *---------------------------------*
## *          GARCH Model Fit        *
## *---------------------------------*
## 
## Conditional Variance Dynamics    
## -----------------------------------
## GARCH Model  : sGARCH(1,1)
## Mean Model   : ARFIMA(0,0,0)
## Distribution : norm 
## 
## Optimal Parameters
## ------------------------------------
##         Estimate  Std. Error  t value Pr(>|t|)
## mu      0.065353    0.021576   3.0290 0.002454
## omega   0.047563    0.012813   3.7121 0.000206
## alpha1  0.068454    0.014975   4.5713 0.000005
## beta1   0.887569    0.023897  37.1417 0.000000
## 
## Robust Standard Errors:
##         Estimate  Std. Error  t value Pr(>|t|)
## mu      0.065353    0.022151   2.9503 0.003175
## omega   0.047563    0.034132   1.3935 0.163465
## alpha1  0.068454    0.025102   2.7270 0.006391
## beta1   0.887569    0.045481  19.5153 0.000000
## 
## LogLikelihood : -2594.796 
## 
## Information Criteria
## ------------------------------------
##                    
## Akaike       2.7959
## Bayes        2.8078
## Shibata      2.7959
## Hannan-Quinn 2.8003
## 
## Weighted Ljung-Box Test on Standardized Residuals
## ------------------------------------
##                         statistic p-value
## Lag[1]                     0.1997  0.6550
## Lag[2*(p+q)+(p+q)-1][2]    0.3475  0.7699
## Lag[4*(p+q)+(p+q)-1][5]    0.7977  0.9035
## d.o.f=0
## H0 : No serial correlation
## 
## Weighted Ljung-Box Test on Standardized Squared Residuals
## ------------------------------------
##                         statistic p-value
## Lag[1]                     0.1255  0.7231
## Lag[2*(p+q)+(p+q)-1][5]    0.3366  0.9797
## Lag[4*(p+q)+(p+q)-1][9]    0.4989  0.9985
## d.o.f=2
## 
## Weighted ARCH LM Tests
## ------------------------------------
##             Statistic Shape Scale P-Value
## ARCH Lag[3]   0.01578 0.500 2.000  0.9000
## ARCH Lag[5]   0.32054 1.440 1.667  0.9348
## ARCH Lag[7]   0.37485 2.315 1.543  0.9883
## 
## Nyblom stability test
## ------------------------------------
## Joint Statistic:  2.5476
## Individual Statistics:             
## mu     0.7035
## omega  0.1271
## alpha1 0.2864
## beta1  0.1054
## 
## Asymptotic Critical Values (10% 5% 1%)
## Joint Statistic:          1.07 1.24 1.6
## Individual Statistic:     0.35 0.47 0.75
## 
## Sign Bias Test
## ------------------------------------
##                    t-value   prob sig
## Sign Bias           1.4183 0.1563    
## Negative Sign Bias  0.8066 0.4200    
## Positive Sign Bias  0.4291 0.6679    
## Joint Effect        4.2421 0.2365    
## 
## 
## Adjusted Pearson Goodness-of-Fit Test:
## ------------------------------------
##   group statistic p-value(g-1)
## 1    20     97.12    1.776e-12
## 2    30    132.02    4.270e-15
## 3    40    130.52    8.456e-12
## 4    50    195.57    2.093e-19
## 
## 
## Elapsed time : 0.127902

view params

coef(garch_fit)
##         mu      omega     alpha1      beta1 
## 0.06535253 0.04756287 0.06845367 0.88756875
garch_fit@fit$matcoef
##          Estimate  Std. Error   t value     Pr(>|t|)
## mu     0.06535253  0.02157585  3.028967 2.453911e-03
## omega  0.04756287  0.01281283  3.712129 2.055231e-04
## alpha1 0.06845367  0.01497478  4.571263 4.847931e-06
## beta1  0.88756875  0.02389685 37.141671 0.000000e+00

calc volatility

parameters <- coef(garch_fit)

persistence <- parameters["alpha1"] + parameters["beta1"]

persistence
##    alpha1 
## 0.9560224

compare the variance

conditional_variance <- sigma(garch_fit)^2
unconditional_variance <- var(dax_returns)

variance_data <- tibble(
  Time = seq_along(conditional_variance),
  ConditionalVariance = conditional_variance,
  UnconditionalVariance = unconditional_variance
)

ggplot(variance_data, aes(x = Time)) +
  geom_line(aes(y = ConditionalVariance)) +
  geom_line(
    aes(y = UnconditionalVariance),
    linetype = "dashed"
  ) +
  labs(
    title = "Conditional and Unconditional Variance",
    x = "Observation",
    y = "Variance"
  )
## Don't know how to automatically pick scale for object of type <xts/zoo>.
## Defaulting to continuous.

forecast

garch_forecast <- ugarchforecast(
  garch_fit,
  n.ahead = 20
)

garch_forecast
## 
## *------------------------------------*
## *       GARCH Model Forecast         *
## *------------------------------------*
## Model: sGARCH
## Horizon: 20
## Roll Steps: 0
## Out of Sample: 0
## 
## 0-roll forecast [T0=1975-02-03]:
##       Series Sigma
## T+1  0.06535 1.527
## T+2  0.06535 1.509
## T+3  0.06535 1.491
## T+4  0.06535 1.475
## T+5  0.06535 1.458
## T+6  0.06535 1.442
## T+7  0.06535 1.427
## T+8  0.06535 1.412
## T+9  0.06535 1.398
## T+10 0.06535 1.384
## T+11 0.06535 1.371
## T+12 0.06535 1.358
## T+13 0.06535 1.346
## T+14 0.06535 1.334
## T+15 0.06535 1.322
## T+16 0.06535 1.311
## T+17 0.06535 1.300
## T+18 0.06535 1.290
## T+19 0.06535 1.280
## T+20 0.06535 1.270

plot volatility

forecast_variance <- as.numeric(
  sigma(garch_forecast)^2
)

forecast_data <- tibble(
  Day = 1:20,
  ForecastVariance = forecast_variance
)

ggplot(
  forecast_data,
  aes(x = Day, y = ForecastVariance)
) +
  geom_line() +
  labs(
    title = "GARCH Variance Forecast",
    x = "Forecast Day",
    y = "Forecast Conditional Variance"
  )

The DAX returns clearly show volatility clustering because periods of relatively small movements are followed by periods with much larger positive and negative returns. The GARCH model captured this changing variance better than a constant-variance model. The alpha estimate shows that recent shocks affected current volatility, while the beta estimate shows that volatility was highly persistent and faded slowly over time. The Ljung-Box and ARCH tests had large p-values, indicating that little serial correlation or remaining ARCH behavior was left after fitting the model. The forecast also shows conditional volatility gradually declining toward its longer-run level. In a broader forecasting workflow, ETS or ARIMA could model the expected value of the series, while GARCH could model changing uncertainty and produce more realistic prediction intervals.