Dicssuion Week 6

Discussion 6

Part1.

library(fpp3)
Warning: package 'fpp3' was built under R version 4.5.2
── Attaching packages ──────────────────────────────────────────── fpp3 1.0.3 ──
✔ tibble      3.3.0     ✔ tsibble     1.2.0
✔ dplyr       1.2.1     ✔ tsibbledata 0.4.1
✔ tidyr       1.3.1     ✔ ggtime      0.2.0
✔ lubridate   1.9.4     ✔ feasts      0.5.0
✔ ggplot2     4.0.3     ✔ fable       0.5.0
Warning: package 'dplyr' 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.2
Warning: package 'ggtime' was built under R version 4.5.2
Warning: package 'feasts' was built under R version 4.5.2
Warning: package 'fabletools' was built under R version 4.5.2
Warning: package 'fable' was built under R version 4.5.2
── 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()
tourism_6<- tourism|>
  aggregate_key(
    State/Region,
    Trips=sum(Trips)
  )

tourism_6|>
  filter(is_aggregated(Region))|>
  autoplot(Trips)+
  facet_wrap(~State, scales = "free_y")

The state level series will show the pattern of tourism respectively. States such as Victoria and New South Wales display bigger volume of tourism.

train<- tourism_6|>
  filter_index(.~"2014 Q4")

test<- tourism_6|>
  filter_index("2015 Q1"~.)

Fits

fit <- train |>
  model(
    ets = ETS(Trips)
  )

reconciled_fit <- fit |>
  reconcile(
    BottomUp = bottom_up(ets),
    
    TopDown = top_down(
      ets,
      method = "forecast_proportions"
    ),
    
    MiddleOut = middle_out(
      ets,
      split = 1
    )
  )

Comparing Forecasts

fc <- reconciled_fit |>
  forecast(h = "2 years")

fc|>
  filter(
    is_aggregated(State),
    is_aggregated(Region)
  )|>
  autoplot(tourism_6)

Forecast Accuracy

fc_accuracy<- fc|>
  accuracy(data=test)

fc_accuracy|>
  select(.model,RMSE, MAPE) |>
  arrange(RMSE)
# A tibble: 340 × 3
   .model     RMSE  MAPE
   <chr>     <dbl> <dbl>
 1 TopDown    7.60  69.2
 2 BottomUp   7.70  69.2
 3 ets        7.70  69.2
 4 MiddleOut  7.71  67.6
 5 BottomUp   8.31  17.5
 6 ets        8.31  17.5
 7 MiddleOut  8.89  18.1
 8 TopDown    9.48  18.5
 9 BottomUp   9.69  22.8
10 ets        9.69  22.8
# ℹ 330 more rows
fc_accuracy |>
  filter(!is_aggregated(Region)) |>
  group_by(.model)|>
  summarise(
    Mean_RMSE = mean(RMSE, na.rm = TRUE),
    Mean_MAE  = mean(MAE, na.rm = TRUE)
  ) |>
  arrange(Mean_RMSE)
# A tibble: 4 × 3
  .model    Mean_RMSE Mean_MAE
  <chr>         <dbl>    <dbl>
1 TopDown        40.6     33.5
2 MiddleOut      43.8     36.3
3 BottomUp       48.7     41.0
4 ets            48.7     41.0

Topdown model was the model that has the lowest mean RMSE and mean MAE indicating that topdown had the lowset errors while forecasting and accuracy of Topdown was beating all the other models. My hypothesis why Top down model is a winning model is that the total tourism series was smoother and less affected by regional noise.

Reconciliation Techniques

#recon fits(OLS,Mint)


recon_fit <- fit |>
  reconcile(
    OLS = min_trace(
      ets,
      method = "ols"
    ),
    
    MinT = min_trace(
      ets,
      method = "mint_shrink"
    )
  )

recon_fit <- recon_fit |>
  forecast(h = "2 years")

recon_accuracy<- recon_fit|>
  accuracy(data=test)


recon_accuracy|>
  select(.model,RMSE, MAPE) |>
  arrange(RMSE)
# A tibble: 255 × 3
   .model  RMSE  MAPE
   <chr>  <dbl> <dbl>
 1 OLS     6.10  30.8
 2 MinT    7.69  69.0
 3 ets     7.70  69.2
 4 ets     8.31  17.5
 5 MinT    8.39  17.6
 6 MinT    9.43  24.8
 7 ets     9.69  22.8
 8 OLS     9.85 128. 
 9 ets     9.90  37.2
10 MinT    9.91  37.2
# ℹ 245 more rows
recon_accuracy |>
  filter(!is_aggregated(Region)) |>
  group_by(.model)|>
  summarise(
    Mean_RMSE = mean(RMSE, na.rm = TRUE),
    Mean_MAE  = mean(MAE, na.rm = TRUE)
  ) |>
  arrange(Mean_RMSE)
# A tibble: 3 × 3
  .model Mean_RMSE Mean_MAE
  <chr>      <dbl>    <dbl>
1 OLS         43.5     36.0
2 MinT        45.3     37.6
3 ets         48.7     41.0

The OLS reconciliation method produced the lowest mean RMSE and MAE, showing that it generated the most accurate forecasts among the three approaches. OLS performed better than MinT is that OLS uses a simpler error structure. It assumes similar forecast-error variance across the series and does not attempt to estimate complex covariance relationships.

Conclustion

Overall, all methods produced coherent forecasts that respected the hierarchical structure. However, the Top-Down approach achieved the best forecasting accuracy. While OLS and MinT effectively reconciled forecasts across all levels, incorporating information from more variable state and regional series slightly reduced their overall accuracy. These results suggest that, for this dataset, forecasting the stable aggregate series first provided the best balance between accuracy and coherence.

Part2.

aus_retail
# A tsibble: 64,532 x 5 [1M]
# Key:       State, Industry [152]
   State                        Industry           `Series ID`    Month Turnover
   <chr>                        <chr>              <chr>          <mth>    <dbl>
 1 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Apr      4.4
 2 Australian Capital Territory Cafes, restaurant… A3349849A   1982 May      3.4
 3 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Jun      3.6
 4 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Jul      4  
 5 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Aug      3.6
 6 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Sep      4.2
 7 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Oct      4.8
 8 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Nov      5.4
 9 Australian Capital Territory Cafes, restaurant… A3349849A   1982 Dec      6.9
10 Australian Capital Territory Cafes, restaurant… A3349849A   1983 Jan      3.8
# ℹ 64,522 more rows
#Create the grouped structure
retail_grouped <- aus_retail |>
  aggregate_key(
    State * Industry,
    Turnover = sum(Turnover)
    
  )

#Select rerpesentative states and industries

selected_states <- c(
  "New South Wales",
  "Victoria",
  "Queensland"
)

selected_industries <- c(
  "Food retailing",
  "Household goods retailing",
  "Clothing, footwear and personal accessory retailing"
)

retail_selected <- aus_retail |>
  filter(
    State %in% selected_states,
    Industry %in% selected_industries
  )

The three states are the representative large retail markets.

retail_grouped <- retail_selected |>
  aggregate_key(
    State * Industry,
    Turnover = sum(Turnover)
  )



retail_grouped |>
  filter(
    !is_aggregated(State),
    is_aggregated(Industry),
    as.character(State) %in% selected_states
  ) |>
  mutate(State = as.character(State)) |>
  ggplot(
    aes(
      x = Month,
      y = Turnover,
      colour = State
    )
  ) +
  geom_line() +
  labs(
    title = "Retail Turnover for Selected States",
    x = "Month",
    y = "Turnover",
    colour = "State"
  )

retail_grouped |>
  filter(
    is_aggregated(State),
    !is_aggregated(Industry),
    as.character(Industry) %in% selected_industries
  ) |>
  mutate(Industry = as.character(Industry)) |>
  ggplot(
    aes(
      x = Month,
      y = Turnover,
      colour = Industry
    )
  ) +
  geom_line() +
  labs(
    title = "Retail Turnover for Selected Industries",
    x = "Month",
    y = "Turnover",
    colour = "Industry"
  )

retail_grouped |>
  filter(
    !is_aggregated(State),
    !is_aggregated(Industry),
    as.character(State) %in% selected_states,
    as.character(Industry) %in% selected_industries
  ) |>
  mutate(
    State = as.character(State),
    Industry = as.character(Industry)
  ) |>
  ggplot(
    aes(
      x = Month,
      y = Turnover,
      colour = Industry
    )
  ) +
  geom_line() +
  labs(
    title = "Selected Retail Industries by State",
    x = "Month",
    y = "Turnover",
    colour = "Industry"
  )

Train and Test Split

retail_train <- retail_grouped |>
  filter_index(. ~ "2017 Dec")

retail_test <- retail_grouped |>
  filter_index("2018 Jan" ~ "2019 Dec")

Fit

#Base Model
fit<- retail_train|>
  model(
    ets=ETS(Turnover)
  )

base_fc <- fit |>
  forecast(h = "2 years")


#Recon Models

reconciled_fit <- fit |>
  reconcile(
    OLS = min_trace(
      ets,
      method = "ols"
    ))

grouped_fc <- reconciled_fit |>
  forecast(h = "2 years")
accuracy_retail<- grouped_fc|>
  accuracy(data=retail_test)
Warning: The future dataset is incomplete, incomplete out-of-sample data will be treated as missing. 
12 observations are missing between 2019 Jan and 2019 Dec
accuracy_retail|>
  select(.model,RMSE, MAPE) |>
  arrange(RMSE)
# A tibble: 32 × 3
   .model  RMSE  MAPE
   <chr>  <dbl> <dbl>
 1 ets     9.67  2.05
 2 OLS    12.7   3.04
 3 ets    22.8   2.44
 4 OLS    23.5   2.09
 5 ets    25.2   2.47
 6 OLS    25.8   2.78
 7 OLS    26.5   3.32
 8 ets    27.8   3.27
 9 ets    32.4   2.19
10 OLS    32.8   2.16
# ℹ 22 more rows
accuracy_retail |>
  filter(!is_aggregated(Industry)) |>
  group_by(.model)|>
  summarise(
    Mean_RMSE = mean(RMSE, na.rm = TRUE),
    Mean_MAE  = mean(MAE, na.rm = TRUE)
  ) |>
  arrange(Mean_RMSE)
# A tibble: 2 × 3
  .model Mean_RMSE Mean_MAE
  <chr>      <dbl>    <dbl>
1 ets         42.4     32.9
2 OLS         42.8     33.4
accuracy_retail |>
  filter(!is_aggregated(State)) |>
  group_by(.model)|>
  summarise(
    Mean_RMSE = mean(RMSE, na.rm = TRUE),
    Mean_MAE  = mean(MAE, na.rm = TRUE)
  ) |>
  arrange(Mean_RMSE)
# A tibble: 2 × 3
  .model Mean_RMSE Mean_MAE
  <chr>      <dbl>    <dbl>
1 OLS         40.8     33.1
2 ets         41.2     33.3

The OLS reconciliation method produced a lower mean RMSE and MAE than the ETS model .This delivers that reconciliation improved forecast accuracy ensuring coherence across the total,state, industry and state-industry levels. On the other hand, Mint could not be estimated because some grouped series produced missing residual values.

Part3. Volatility Modeling(S&P 500)

library(quantmod)
Warning: package 'quantmod' was built under R version 4.5.2
Loading required package: xts
Loading required package: zoo

Attaching package: 'zoo'
The following object is masked from 'package:tsibble':

    index
The following objects are masked from 'package:base':

    as.Date, as.Date.numeric

######################### Warning from 'xts' package ##########################
#                                                                             #
# The dplyr lag() function breaks how base R's lag() function is supposed to  #
# work, which breaks lag(my_xts). Calls to lag(my_xts) that you type or       #
# source() into this session won't work correctly.                            #
#                                                                             #
# Use stats::lag() to make sure you're not using dplyr::lag(), or you can add #
# conflictRules('dplyr', exclude = 'lag') to your .Rprofile to stop           #
# dplyr from breaking base R's lag() function.                                #
#                                                                             #
# Code in packages is not affected. It's protected by R's namespace mechanism #
# Set `options(xts.warn_dplyr_breaks_lag = FALSE)` to suppress this warning.  #
#                                                                             #
###############################################################################

Attaching package: 'xts'
The following objects are masked from 'package:dplyr':

    first, last
Loading required package: TTR
Registered S3 method overwritten by 'quantmod':
  method            from
  as.zoo.data.frame zoo 
library(rugarch)
Warning: package 'rugarch' was built under R version 4.5.2
Loading required package: parallel

Attaching package: 'rugarch'
The following object is masked from 'package:fabletools':

    report
library(FinTS)

Attaching package: 'FinTS'
The following object is masked from 'package:fable':

    ARIMA
getSymbols(
  "^GSPC",
  src="yahoo",
  from="2015-01-01",
  to="2025-12-31"
)
[1] "GSPC"
head(GSPC)
           GSPC.Open GSPC.High GSPC.Low GSPC.Close GSPC.Volume GSPC.Adjusted
2015-01-02   2058.90   2072.36  2046.04    2058.20  2708700000       2058.20
2015-01-05   2054.44   2054.44  2017.34    2020.58  3799120000       2020.58
2015-01-06   2022.15   2030.25  1992.44    2002.61  4460110000       2002.61
2015-01-07   2005.55   2029.61  2005.55    2025.90  3805480000       2025.90
2015-01-08   2030.61   2064.08  2030.61    2062.14  3934010000       2062.14
2015-01-09   2063.45   2064.43  2038.33    2044.81  3364140000       2044.81
sp500_returns<- dailyReturn(
  Cl(GSPC),
  type="log"
)*100

sp500_returns<- na.omit(sp500_returns)

sp500_returns
           daily.returns
2015-01-02    0.00000000
2015-01-05   -1.84472135
2015-01-06   -0.89332547
2015-01-07    1.15627358
2015-01-08    1.77301681
2015-01-09   -0.84393221
2015-01-12   -0.81266168
2015-01-13   -0.25818854
2015-01-14   -0.58300290
2015-01-15   -0.92909030
       ...              
2025-12-16   -0.23867640
2025-12-17   -1.16598566
2025-12-18    0.79029515
2025-12-19    0.87794074
2025-12-22    0.64158706
2025-12-23    0.45400670
2025-12-24    0.32163043
2025-12-26   -0.03044099
2025-12-29   -0.34981629
2025-12-30   -0.13766143

Visual plots for seeing the evidence of volatility clustering

autoplot(sp500_returns) +
  labs(
    title = "S&P 500 Daily Log Returns",
    x = "Date",
    y = "Return (%)"
  ) +
  theme_minimal()

As seen in the plot of S&P 500, there is some volatility that follows up with outbreaks in the financial market, while calm periods tend to remain calm.

Uncondoitonal Variance

constant_variance <- var(sp500_returns)


constant_variance
              daily.returns
daily.returns      1.279301

ARCH test

ArchTest(
  sp500_returns,
  lags=12,
  demean =TRUE
)

    ARCH LM-test; Null hypothesis: no ARCH effects

data:  sp500_returns
Chi-squared = 888.45, df = 12, p-value < 2.2e-16

The Arch test produced a p-value bleow 0.05. This means that there is conditional heteroskedasticity, so the assumption of constant variance was rejected, supporting the use of a GARCH model.

GARCH 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"
)


garch_fit<-ugarchfit(
  spec=garch_spec,
  data=sp500_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.079248    0.014024   5.6508        0
omega   0.039375    0.005855   6.7249        0
alpha1  0.174529    0.018406   9.4820        0
beta1   0.793957    0.018556  42.7876        0

Robust Standard Errors:
        Estimate  Std. Error  t value Pr(>|t|)
mu      0.079248    0.012778   6.2020  0.0e+00
omega   0.039375    0.009917   3.9706  7.2e-05
alpha1  0.174529    0.028394   6.1466  0.0e+00
beta1   0.793957    0.027936  28.4206  0.0e+00

LogLikelihood : -3571.861 

Information Criteria
------------------------------------
                   
Akaike       2.5865
Bayes        2.5951
Shibata      2.5865
Hannan-Quinn 2.5896

Weighted Ljung-Box Test on Standardized Residuals
------------------------------------
                        statistic p-value
Lag[1]                     0.9972  0.3180
Lag[2*(p+q)+(p+q)-1][2]    1.0055  0.4965
Lag[4*(p+q)+(p+q)-1][5]    2.2785  0.5542
d.o.f=0
H0 : No serial correlation

Weighted Ljung-Box Test on Standardized Squared Residuals
------------------------------------
                        statistic p-value
Lag[1]                     0.7029  0.4018
Lag[2*(p+q)+(p+q)-1][5]    2.8819  0.4290
Lag[4*(p+q)+(p+q)-1][9]    5.3036  0.3862
d.o.f=2

Weighted ARCH LM Tests
------------------------------------
            Statistic Shape Scale P-Value
ARCH Lag[3]   0.03765 0.500 2.000  0.8462
ARCH Lag[5]   4.38613 1.440 1.667  0.1422
ARCH Lag[7]   5.51563 2.315 1.543  0.1773

Nyblom stability test
------------------------------------
Joint Statistic:  1.4069
Individual Statistics:             
mu     0.1051
omega  0.4405
alpha1 0.2571
beta1  0.5891

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           3.1721 0.001530 ***
Negative Sign Bias  0.1054 0.916095    
Positive Sign Bias  0.3139 0.753610    
Joint Effect       19.7857 0.000188 ***


Adjusted Pearson Goodness-of-Fit Test:
------------------------------------
  group statistic p-value(g-1)
1    20     131.9    6.235e-19
2    30     143.2    4.695e-17
3    40     155.7    7.195e-16
4    50     179.1    1.026e-16


Elapsed time : 0.04293704 
coef(garch_fit)
        mu      omega     alpha1      beta1 
0.07924799 0.03937539 0.17452941 0.79395685 
garch_fit@fit$matcoef
         Estimate  Std. Error   t value     Pr(>|t|)
mu     0.07924799 0.014024253  5.650781 1.597202e-08
omega  0.03937539 0.005855138  6.724929 1.756773e-11
alpha1 0.17452941 0.018406467  9.481961 0.000000e+00
beta1  0.79395685 0.018555750 42.787646 0.000000e+00
conditional_sd <- sigma(garch_fit)

conditional_sd
           m.c.seq.row..seq.n...seq.col..drop...FALSE.
2015-01-02                                   1.1314151
2015-01-05                                   1.0280154
2015-01-06                                   1.2347023
2015-01-07                                   1.1894712
2015-01-08                                   1.1683963
2015-01-09                                   1.2743409
2015-01-12                                   1.2155090
2015-01-13                                   1.1624350
2015-01-14                                   1.0639954
2015-01-15                                   1.0073467
       ...                                            
2025-12-16                                   0.7028283
2025-12-17                                   0.6702276
2025-12-18                                   0.8164872
2025-12-19                                   0.8104984
2025-12-22                                   0.8199182
2025-12-23                                   0.7926639
2025-12-24                                   0.7501619
2025-12-26                                   0.7045725
2025-12-29                                   0.6600100
2025-12-30                                   0.6460369
conditional_variance <- conditional_sd^2

conditional_variance
           m.c.seq.row..seq.n...seq.col..drop...FALSE.
2015-01-02                                   1.2801001
2015-01-05                                   1.0568158
2015-01-06                                   1.5244897
2015-01-07                                   1.4148416
2015-01-08                                   1.3651499
2015-01-09                                   1.6239447
2015-01-12                                   1.4774621
2015-01-13                                   1.3512552
2015-01-14                                   1.1320862
2015-01-15                                   1.0147475
       ...                                            
2025-12-16                                   0.4939676
2025-12-17                                   0.4492050
2025-12-18                                   0.6666513
2025-12-19                                   0.6569077
2025-12-22                                   0.6722659
2025-12-23                                   0.6283161
2025-12-24                                   0.5627429
2025-12-26                                   0.4964224
2025-12-29                                   0.4356133
2025-12-30                                   0.4173637
plot(
  conditional_variance,
  main = "Conditional and Unconditional Variance",
  ylab = "Variance",
  xlab = "Date"
)

abline(
  h = constant_variance,
  lty = 2,
  lwd = 2
)

legend(
  "topright",
  legend = c(
    "GARCH Conditional Variance",
    "Constant Unconditional Variance"
  ),
  lty = c(1, 2),
  lwd = c(1, 2)

)

constant_variance
              daily.returns
daily.returns      1.279301

The unconditional variance remained constatnt at 1.279 throughout the sample period. In contrast, GARCH conditional variance has high response to the market conditions. In my case, it was able to be seen that there was volatility in the market that shakes the return of the sp&500. While during the COVID-19 market crash in 2020, the variance jumped dramatically nearly 60 before gradually returning to lower levels. This indicates that financial market volatility is time varying and shows volatility clustering.