library(fpp3)
## Registered S3 method overwritten by 'tsibble':
## method from
## as_tibble.grouped_df dplyr
## ── Attaching packages ──────────────────────────────────────────── fpp3 1.0.2 ──
## ✔ tibble 3.2.1 ✔ tsibble 1.1.6
## ✔ dplyr 1.1.4 ✔ tsibbledata 0.4.1
## ✔ tidyr 1.3.1 ✔ feasts 0.4.2
## ✔ lubridate 1.9.4 ✔ fable 0.5.0
## ✔ ggplot2 4.0.0
## Warning: package 'dplyr' was built under R version 4.3.3
## Warning: package 'lubridate' was built under R version 4.3.3
## Warning: package 'tsibble' was built under R version 4.3.3
## Warning: package 'tsibbledata' was built under R version 4.3.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()
# View(aus_livestock)
Filtering & Visualizing the data for Pigs in Victoria
pigs_vic <- aus_livestock |>
filter(Animal == "Pigs", State == "Victoria")
pigs_vic |>
autoplot(Count) +
labs(title = "Trends of Pigs Slaughtered in Victoria", y = "Number of Pigs")
Fitting and Estimating the ETS Model (SES)
ETS(A,N,N) model
To perform Simple Exponential Smoothing within the ETS framework, we use error(“A”), trend(“N”), and season(“N”)
aus_fit <- pigs_vic |>
model(ANN = ETS(Count ~ error("A") + trend("N") + season("N")))
report(aus_fit)
## Series: Count
## Model: ETS(A,N,N)
## Smoothing parameters:
## alpha = 0.3221247
##
## Initial states:
## l[0]
## 100646.6
##
## sigma^2: 87480760
##
## AIC AICc BIC
## 13737.10 13737.14 13750.07
Interpretation
This model assumes the data has an additive error structure but no trend and no seasonality
Smoothing Parameter (Alpha): A moderate alpha value suggests the model places roughly 32% weight on the most recent observation and 68% weight on older data. It produces a balanced forecast that doesn’t overreact to monthly fluctuations.
Initial state: This represents the estimated baseline level of pig slaughters at the very start of the time series.
Residual Variance: The value of 87,480,760 indicates the variability of the series.
Since there is no trend component, the forecast for the next four months will be a flat horizontal line based on the last estimated level.
Generating Forecasts for 4 Months
aus_forecast <- aus_fit |>
forecast(h = 4)
# View the forecast table
# aus_forecast
Visualize the Results
aus_forecast |>
autoplot(pigs_vic |>
filter(Month >= yearmonth("2015 Jan"))) +
labs(title = "4-Month ETS Forecast for Victoria Pigs",
y = "Pigs Slaughtered")
### b. Compute a 95% prediction interval for the first forecast using
^y±1.96s where s is the standard deviation of the residuals. Compare
your interval with the interval produced by R.
Extracting the residuals and calculate their standard deviation
res_std <- augment(aus_fit) |>
pull(.resid) |>
sd(na.rm = TRUE)
# Getting the first forecast value (Point Forecast)
first_fc <- aus_forecast |> filter(row_number() == 1)
y_hat <- first_fc |> pull(.mean)
#Calculating the manual boundaries
lower_manual <- y_hat - 1.96 * res_std
upper_manual <- y_hat + 1.96 * res_std
cat("Manual 95% PI: [", lower_manual, ", ", upper_manual, "]\n")
## Manual 95% PI: [ 76871.01 , 113502.1 ]
Extracting R’s Prediction Interval
# Extract R's calculated interval
r_interval <- aus_forecast |>
hilo(level = 95) |>
filter(row_number() == 1) |>
unpack_hilo("95%")
cat("R's 95% PI: [", r_interval$`95%_lower`, ", ", r_interval$`95%_upper`, "]\n")
## R's 95% PI: [ 76854.79 , 113518.3 ]
Interpretation:
Alpha: The model is stable, favoring long-term history over recent fluctuations.
Point Forecast: The forecast is a flat line at 95,186.56 because the model assumes no trend.
Initial State: The series is estimated to have started at a baseline level of 100,647.
Interval Match: Your manual calculation and R’s results are nearly identical for the first month.
Precision Gap: Small differences exist because R uses a precise multiplier and optimized error values.
While the prediction is flat, the fan of uncertainty widens the further you look ahead.
Filtering for Australia and plotting
library(fpp3)
# Filtering for Australia and plotting
aus_exports <- global_economy |>
filter(Country == "Australia")
aus_exports
## # A tsibble: 58 x 9 [1Y]
## # Key: Country [1]
## Country Code Year GDP Growth CPI Imports Exports Population
## <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Australia AUS 1960 18573188487. NA 7.96 14.1 13.0 10276477
## 2 Australia AUS 1961 19648336880. 2.49 8.14 15.0 12.4 10483000
## 3 Australia AUS 1962 19888005376. 1.30 8.12 12.6 13.9 10742000
## 4 Australia AUS 1963 21501847911. 6.21 8.17 13.8 13.0 10950000
## 5 Australia AUS 1964 23758539590. 6.98 8.40 13.8 14.9 11167000
## 6 Australia AUS 1965 25931235301. 5.98 8.69 15.3 13.2 11388000
## 7 Australia AUS 1966 27261731437. 2.38 8.98 15.1 12.9 11651000
## 8 Australia AUS 1967 30389741292. 6.30 9.29 13.9 12.9 11799000
## 9 Australia AUS 1968 32657632434. 5.10 9.52 14.5 12.3 12009000
## 10 Australia AUS 1969 36620002240. 7.04 9.83 13.3 12.0 12263000
## # ℹ 48 more rows
aus_exports |>
autoplot(Exports) +
labs(title = "Annual Exports for Australia",
y = "% of GDP", x = "Year")
Overall Trend: The data shows a clear, long-term upward trajectory,
indicating exports are taking up a larger share of the economy over
time.
Cyclicality: Visible cycles appear throughout the decades
Seasonality: There is no seasonality to analyze because the data is recorded on an annual basis.
Structural Shift: A sharp acceleration in growth is evident starting in the 1980s and peaking after 2000.
b. Use an ETS(A,N,N) model to forecast the series, and plot the forecasts.
library(fpp3)
# Filtering the data for Australia
aus_exports <- global_economy |>
filter(Country == "Australia")
# Fitting the ETS(A,N,N) model
aus_exports_fit <- aus_exports |>
model(ANN = ETS(Exports ~ error("A") + trend("N") + season("N")))
report(aus_exports_fit)
## Series: Exports
## Model: ETS(A,N,N)
## Smoothing parameters:
## alpha = 0.5659948
##
## Initial states:
## l[0]
## 12.98943
##
## sigma^2: 1.3621
##
## AIC AICc BIC
## 257.3943 257.8387 263.5756
components(aus_exports_fit) |> autoplot()
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_line()`).
Now we left Join the fitted values to the
components
components(aus_exports_fit) |>
left_join(fitted(aus_exports_fit), by = c("Country", ".model", "Year"))
## # A dable: 59 x 7 [1Y]
## # Key: Country, .model [1]
## # : Exports = lag(level, 1) + remainder
## Country .model Year Exports level remainder .fitted
## <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Australia ANN 1959 NA 13.0 NA NA
## 2 Australia ANN 1960 13.0 13.0 0.00503 13.0
## 3 Australia ANN 1961 12.4 12.7 -0.589 13.0
## 4 Australia ANN 1962 13.9 13.4 1.28 12.7
## 5 Australia ANN 1963 13.0 13.2 -0.380 13.4
## 6 Australia ANN 1964 14.9 14.2 1.77 13.2
## 7 Australia ANN 1965 13.2 13.6 -0.951 14.2
## 8 Australia ANN 1966 12.9 13.2 -0.701 13.6
## 9 Australia ANN 1967 12.9 13.0 -0.353 13.2
## 10 Australia ANN 1968 12.3 12.6 -0.739 13.0
## # ℹ 49 more rows
Generating forecasts for the next 10 years
aus_exports_forecast <- aus_exports_fit |>
forecast(h = 10)
aus_exports_forecast
## # A fable: 10 x 5 [1Y]
## # Key: Country, .model [1]
## Country .model Year
## <fct> <chr> <dbl>
## 1 Australia ANN 2018
## 2 Australia ANN 2019
## 3 Australia ANN 2020
## 4 Australia ANN 2021
## 5 Australia ANN 2022
## 6 Australia ANN 2023
## 7 Australia ANN 2024
## 8 Australia ANN 2025
## 9 Australia ANN 2026
## 10 Australia ANN 2027
## # ℹ 2 more variables: Exports <dist>, .mean <dbl>
# Plotting the forecasts alongside historical data
aus_exports_forecast |>
autoplot(aus_exports) +
labs(title = "Australia Exports Forecast Using ETS(A,N,N) Model",
y = "% of GDP", x = "Year")
Interpretation
The predicted exports remain at a constant horizontal level because the model assumes no trend.
The forecast starts from the last estimated level, essentially freezing the most recent smoothed value into the future.
The shaded intervals widen as time progresses, reflecting increasing uncertainty.
Model Limitation: This model ignores the clear historical upward trend, acting as a simple baseline rather than a growth predictor.
calculating the Root Mean Squared Error (RMSE) - accuracy metrics for the training data
tidy(aus_exports_fit) # esimates
## # A tibble: 2 × 4
## Country .model term estimate
## <fct> <chr> <chr> <dbl>
## 1 Australia ANN alpha 0.566
## 2 Australia ANN l[0] 13.0
accuracy(aus_exports_fit) # accuracy metrics
## # A tibble: 1 × 11
## Country .model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
## <fct> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Australia ANN Training 0.232 1.15 0.914 1.09 5.41 0.928 0.928 0.0125
Interpretation of the SES model’s performance on the Australian exports data:
RMSE (1.15): On average, the model’s predictions deviate from the actual export values by 1.15 percentage points of the GDP.
MAE (0.91): The “Mean Absolute Error” shows that the typical error is slightly smaller than the RMSE, suggesting there are few extreme outliers in the data.
MAPE (5.41%): The model is off by an average of 5.41% relative to the actual export values, which is generally considered a very accurate fit for macroeconomic data.
MASE (0.93): Since this value is less than 1, the SES model is performing better than a standard “Naive” benchmark (which simply uses the previous year’s value as the forecast).
Now evaluate whether adding a trend component improves the model.
# Filtering the data for Australia
aus_exports <- global_economy |>
filter(Country == "Australia")
# Fit both SES (A,N,N) and Holt's Linear Trend (A,A,N)
aus_exports_comparison_fit <- aus_exports |>
model(
SES_ANN = ETS(Exports ~ error("A") + trend("N") + season("N")),
Holt_AAN = ETS(Exports ~ error("A") + trend("A") + season("N"))
)
report(aus_exports_comparison_fit)
## Warning in report.mdl_df(aus_exports_comparison_fit): Model reporting is only
## supported for individual models, so a glance will be shown. To see the report
## for a specific model, use `select()` and `filter()` to identify a single model.
## # A tibble: 2 × 10
## Country .model sigma2 log_lik AIC AICc BIC MSE AMSE MAE
## <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Australia SES_ANN 1.36 -126. 257. 258. 264. 1.32 1.79 0.914
## 2 Australia Holt_AAN 1.34 -124. 258. 259. 269. 1.25 1.59 0.893
tidy(aus_exports_comparison_fit) # esimates
## # A tibble: 6 × 4
## Country .model term estimate
## <fct> <chr> <chr> <dbl>
## 1 Australia SES_ANN alpha 0.566
## 2 Australia SES_ANN l[0] 13.0
## 3 Australia Holt_AAN alpha 0.441
## 4 Australia Holt_AAN beta 0.000100
## 5 Australia Holt_AAN l[0] 12.7
## 6 Australia Holt_AAN b[0] 0.137
accuracy(aus_exports_comparison_fit) # accuracy metrics
## # A tibble: 2 × 11
## Country .model .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
## <fct> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Australia SES_ANN Train… 2.32e-1 1.15 0.914 1.09 5.41 0.928 0.928 0.0125
## 2 Australia Holt_AAN Train… -7.46e-7 1.12 0.893 -0.387 5.39 0.907 0.904 0.109
Accuracy Comparison (RMSE & MAE)
Holt_AAN has a lower RMSE (1.117) compared to SES_ANN (1.147).
Holt_AAN also shows a lower MAE (0.893) than SES_ANN (0.914).
The trended model (Holt_AAN) fits the historical training data more accurately because it accounts for the upward trajectory of Australia’s exports.
Complexity and Selection (AICc)
SES_ANN has a lower AICc (257.84) compared to Holt_AAN (259.47).
Although the Holt model is more accurate, the AICc suggests that the improvement in fit is not quite large enough to offset the penalty for adding an extra parameter (the trend).
Even though the SES model is slightly less accurate, its lower AICc makes it the preferred choice because it is simpler and avoids unnecessary complexity.
# Generating forecasts for the next 10 years
aus_exports_comparison_forecast <- aus_exports_comparison_fit |>
forecast(h = 10)
aus_exports_comparison_forecast
## # A fable: 20 x 5 [1Y]
## # Key: Country, .model [2]
## Country .model Year
## <fct> <chr> <dbl>
## 1 Australia SES_ANN 2018
## 2 Australia SES_ANN 2019
## 3 Australia SES_ANN 2020
## 4 Australia SES_ANN 2021
## 5 Australia SES_ANN 2022
## 6 Australia SES_ANN 2023
## 7 Australia SES_ANN 2024
## 8 Australia SES_ANN 2025
## 9 Australia SES_ANN 2026
## 10 Australia SES_ANN 2027
## 11 Australia Holt_AAN 2018
## 12 Australia Holt_AAN 2019
## 13 Australia Holt_AAN 2020
## 14 Australia Holt_AAN 2021
## 15 Australia Holt_AAN 2022
## 16 Australia Holt_AAN 2023
## 17 Australia Holt_AAN 2024
## 18 Australia Holt_AAN 2025
## 19 Australia Holt_AAN 2026
## 20 Australia Holt_AAN 2027
## # ℹ 2 more variables: Exports <dist>, .mean <dbl>
Visualize the Forecasts
aus_exports_comparison_fit |>
forecast(h = 10) |>
autoplot(aus_exports, level = NULL) +
labs(title = "Australia Exports Comparison ANN vs. AAN Forecast",
y = "% of GDP")
Model Comparison
The SES_ANN model produces a completely flat point forecast of 20.61% for the entire 10-year period, whereas the Holt_AAN model captures the historical growth by projecting a steady upward slope that reaches 22.07% by 2027.
The Holt_AAN model fits the historical training data more closely, with a lower RMSE (1.116) and MAE (0.892) compared to the SES_ANN model’s RMSE (1.146) and MAE (0.913).
Despite the better fit, the SES_ANN model is statistically “preferred” by the AICc metric (257.84 vs 259.47) because it is simpler and uses one fewer parameter than the Holt model.
The SES_ANN model shows much higher uncertainty over time, with the variance of the distribution increasing from 1.4 to 5.3, while the Holt_AAN model’s variance remains tighter, growing only from 1.3 to 3.7.
To calculate the prediction intervals manually, we use the formula for a 95% interval under the assumption of normality. This assumes that for the first forecast step (h=1), the standard deviation of the forecast distribution is approximately equal to the RMSE of the residuals.
We calculate RMSE and first forecast values to calculate the lower and upper bounds manually.
# Extract the point forecasts (.mean) for the first year (2018)
# SES_ANN = 20.60716 and Holt_AAN = 20.83864
first_forecasts <- aus_exports_comparison_forecast |>
filter(Year == 2018) |>
pull(.mean)
# Extract RMSE values from previous accuracy test
# SES_ANN = 1.146794, Holt_AAN = 1.116727
rmse_values <- accuracy(aus_exports_comparison_fit)$RMSE
# Manual Calculation: Point Forecast +/- (1.96 * RMSE)
# We use 1.96 for a 95% confidence level under a Normal distribution
manual_intervals <- tibble(
Model = c("SES_ANN", "Holt_AAN"),
Manual_Lower = first_forecasts - (1.96 * rmse_values),
Manual_Upper = first_forecasts + (1.96 * rmse_values)
)
manual_intervals
## # A tibble: 2 × 3
## Model Manual_Lower Manual_Upper
## <chr> <dbl> <dbl>
## 1 SES_ANN 18.4 22.9
## 2 Holt_AAN 18.6 23.0
SES_ANN Interval: The model predicts with 95% confidence that 2018 exports will fall between 18.36% and 22.85% of GDP.
Holt_AAN Interval: This model provides a slightly higher and tighter range, estimating between 18.65% and 23.03% of GDP.
The Holt_AAN interval is narrower because its lower RMSE indicates a more precise fit to the historical data.
Both intervals are centered around their respective point forecasts (20.61 for SES vs. 20.84 for Holt), showing how the trend component shifts the entire range upward.
Intervals Using R
r_intervals <- aus_exports_comparison_forecast |>
filter(Year == 2018) |>
hilo(level = 95) |>
unpack_hilo("95%") |>
select(.model, Year, .mean, `95%_lower`, `95%_upper`)
r_intervals
## # A tsibble: 2 x 5 [?]
## # Key: .model [2]
## .model Year .mean `95%_lower` `95%_upper`
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 SES_ANN 2018 20.6 18.3 22.9
## 2 Holt_AAN 2018 20.8 18.6 23.1
Manual calculations are very similar to the R results.
R Intervals are Wider: R produces slightly wider intervals than my manual calculation.
RMSE vs Variance: This happens because R uses residual variance and accounts for estimated parameters, while I used a simple training RMSE.Normal Distribution: Both methods rely on the same assumption that errors follow a normal distribution.
Both results show the Holt_AAN model has a tighter, more certain range than the SES_ANN model.
Prepare China GDP data
library(fpp3)
china_gdp <- global_economy |>
filter(Country == "China")
Experiment with different ETS models
china_fit <- china_gdp |>
model(
# Simple Automatic Selection
auto = ETS(GDP),
# Damped Trend (Ad) to prevent "infinite" growth projection
damped = ETS(GDP ~ error("A") + trend("Ad") + season("N")),
# Box-Cox transformation to stabilize variance
box_cox = ETS(box_cox(GDP, 0.2) ~ error("A") + trend("A") + season("N"))
)
report(china_fit)
## Warning in report.mdl_df(china_fit): Model reporting is only supported for
## individual models, so a glance will be shown. To see the report for a specific
## model, use `select()` and `filter()` to identify a single model.
## # A tibble: 3 × 10
## Country .model sigma2 log_lik AIC AICc BIC MSE AMSE MAE
## <fct> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 China auto 1.08e- 2 -1546. 3102. 3103. 3112. 4.00e22 1.61e23 7.93e- 2
## 2 China damped 3.96e+22 -1624. 3260. 3262. 3273. 3.62e22 1.30e23 9.49e+10
## 3 China box_cox 3.94e+ 2 -289. 588. 589. 598. 3.67e 2 1.32e 3 1.58e+ 1
Generating 20-year forecasts and Plotting to Compare
china_fc <- china_fit |>
forecast(h = "20 years")
# Plot the results to compare intuition
china_fc |>
autoplot(china_gdp, level = NULL) +
labs(title = "Chinese GDP Forecast ETS Variations",
y = "GDP (current US$)")
Auto Model: This model automatically identifies the increasing variance
in China’s growth, leading to an aggressive and slightly exponential
forecast that follows the recent sharp upward trend.
Damped Model: This bends the trend line downward, providing a more conservative forecast by assuming that the explosive growth seen in the past will eventually slow down.
Box-Cox Model: By transforming the scale of the data, this model stabilizes the variance, resulting in an even more aggressive forecast than the Auto model as it interprets the historical growth as a strong, persistent signal.
Model Selection (AICc): The Box-Cox model has a significantly lower AICc (589.15) compared to the Auto or Damped models, indicating it is the most statistically efficient way to handle this specific type of rapid economic growth.
Prepare the Gas data
gas_data <- aus_production |>
select(Gas) |>
filter(!is.na(Gas))
Fit models: Automatic (Multiplicative) vs. Damped
gas_fit <- gas_data |>
model(
auto = ETS(Gas),
damped = ETS(Gas ~ error("M") + trend("Ad") + season("M"))
)
report(gas_fit)
## Warning in report.mdl_df(gas_fit): Model reporting is only supported for
## individual models, so a glance will be shown. To see the report for a specific
## model, use `select()` and `filter()` to identify a single model.
## # A tibble: 2 × 9
## .model sigma2 log_lik AIC AICc BIC MSE AMSE MAE
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 auto 0.00324 -831. 1681. 1682. 1711. 21.1 32.2 0.0413
## 2 damped 0.00329 -832. 1684. 1685. 1718. 21.1 32.0 0.0417
Forecast for the next 5 years (20 quarters)
gas_fc <- gas_fit |>
forecast(h = 20)
# Compare results
gas_fc |>
autoplot(gas_data, level = NULL) +
labs(title = "Australian Gas Production ETS Forecasts",
y = "Petajoules")
Why Multiplicative Seasonality is Necessary:
The seasonal swings in the gas data get larger as the production level increases over time.
Multiplicative seasonality is necessary because the seasonal pattern is not constant; it changes in proportion to the level of the series.
Using multiplicative seasonality allows the model to track these growing seasonal variations more accurately than an additive model could.
Damped Trend Experiment Results:
Statistical Preference: The auto model is better because its AICc (1681.79) is lower than the damped model’s (1685.09).
Better Accuracy: The auto model is more precise, with a smaller MAE (0.0413) compared to the damped model (0.0417).
Auto Model Intuition: This model predicts that the current strong upward trend in gas production will keep going at the same speed.
Damped Model Intuition: This model “bends” the forecast line down, assuming the growth will eventually level off.
Final Verdict: Adding a damped trend does not improve the forecast since the standard model is both more accurate and statistically favored.
library(fpp3)
# Selecting unique retail series
set.seed(12345678)
myseries <- aus_retail |>
filter(`Series ID` == sample(aus_retail$`Series ID`, 1))
# Plotting the data to inspect seasonal swings
myseries |>
autoplot(Turnover) +
labs(title = "Retail Turnover",
y = "Turnover ($Millions)")
# Fit an Automatic ETS model
# R will likely choose a model with Multiplicative seasonality (M)
retail_fit <- myseries |>
model(ets_auto = ETS(Turnover))
# View the chosen model components
report(retail_fit)
## Series: Turnover
## Model: ETS(M,A,M)
## Smoothing parameters:
## alpha = 0.5262368
## beta = 0.0001026197
## gamma = 0.0001037781
##
## Initial states:
## l[0] b[0] s[0] s[-1] s[-2] s[-3] s[-4] s[-5]
## 2.347713 0.04437301 0.8398709 0.7617432 0.833525 1.373239 0.9998723 1.033418
## s[-6] s[-7] s[-8] s[-9] s[-10] s[-11]
## 1.032927 1.119514 1.140226 1.019343 0.9810424 0.8652791
##
## sigma^2: 0.0045
##
## AIC AICc BIC
## 1774.530 1776.274 1841.014
Why Multiplicative Seasonality is Necessary
In your retail data, the size of the seasonal fluctuations, the peaks and drop grows as the overall trend increases.
The December spending spikes are not a constant dollar amount; instead, they represent a percentage of the total turnover, which rises over time.
Multiplicative seasonality treats the seasonal component as a ratio, which effectively stabilizes the seasonal pattern across different levels of sales.
Plot shows a wider shape at he end because the seasonal swings get larger as the sales numbers grow.
Holt-Winters Multiplicative Experiments
retail_comparison_fit <- myseries |>
model(
`Holt-Winters Multi` = ETS(Turnover ~ error("M") + trend("A") + season("M")),
`Damped Multi` = ETS(Turnover ~ error("M") + trend("Ad") + season("M"))
)
report(retail_comparison_fit)
## Warning in report.mdl_df(retail_comparison_fit): Model reporting is only
## supported for individual models, so a glance will be shown. To see the report
## for a specific model, use `select()` and `filter()` to identify a single model.
## # A tibble: 2 × 11
## State Industry .model sigma2 log_lik AIC AICc BIC MSE AMSE MAE
## <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Northern… Clothin… Holt-… 0.00447 -870. 1775. 1776. 1841. 0.376 0.473 0.0511
## 2 Northern… Clothin… Dampe… 0.00457 -872. 1781. 1783. 1851. 0.379 0.479 0.0505
Compare the model parameters and metrics
retail_comparison_fit |> report()
## Warning in report.mdl_df(retail_comparison_fit): Model reporting is only
## supported for individual models, so a glance will be shown. To see the report
## for a specific model, use `select()` and `filter()` to identify a single model.
## # A tibble: 2 × 11
## State Industry .model sigma2 log_lik AIC AICc BIC MSE AMSE MAE
## <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Northern… Clothin… Holt-… 0.00447 -870. 1775. 1776. 1841. 0.376 0.473 0.0511
## 2 Northern… Clothin… Dampe… 0.00457 -872. 1781. 1783. 1851. 0.379 0.479 0.0505
retail_comparison_fit |> accuracy()
## # A tibble: 2 × 12
## State Industry .model .type ME RMSE MAE MPE MAPE MASE RMSSE
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Northern … Clothin… Holt-… Trai… -0.0128 0.613 0.450 -0.469 5.15 0.513 0.529
## 2 Northern … Clothin… Dampe… Trai… 0.0398 0.616 0.444 -0.0723 5.06 0.507 0.531
## # ℹ 1 more variable: ACF1 <dbl>
Forecast and Plot
retail_comparison_fit |>
forecast(h = "2 years") |>
autoplot(myseries, level = NULL) +
labs(title = "Holt-Winters Multiplicative - Standard vs. Damped",
y = "Turnover")
Experimenting with Damped Trend Bending:
The standard Holt-Winters method assumes the current trend will continue in a straight line forever, while the Damped model bends the trend into a flat line over time.
Check the AICc in your report(); if the Damped model has a lower value, the extra complexity of the damping parameter is worth it.
Damped trends are often more accurate for long-term retail forecasts because they prevent over-optimistic projections of infinite growth.
In the Damped model, you will see a new parameter called phi if phi is significantly less than 1 e.g., 0.85, the model is aggressively slowing down the projected growth.
retail_comparison_fit |>
tidy()
## # A tibble: 35 × 5
## State Industry .model term estimate
## <chr> <chr> <chr> <chr> <dbl>
## 1 Northern Territory Clothing, footwear and personal acc… Holt-… alpha 0.526
## 2 Northern Territory Clothing, footwear and personal acc… Holt-… beta 0.000103
## 3 Northern Territory Clothing, footwear and personal acc… Holt-… gamma 0.000104
## 4 Northern Territory Clothing, footwear and personal acc… Holt-… l[0] 2.35
## 5 Northern Territory Clothing, footwear and personal acc… Holt-… b[0] 0.0444
## 6 Northern Territory Clothing, footwear and personal acc… Holt-… s[0] 0.840
## 7 Northern Territory Clothing, footwear and personal acc… Holt-… s[-1] 0.762
## 8 Northern Territory Clothing, footwear and personal acc… Holt-… s[-2] 0.834
## 9 Northern Territory Clothing, footwear and personal acc… Holt-… s[-3] 1.37
## 10 Northern Territory Clothing, footwear and personal acc… Holt-… s[-4] 1.000
## # ℹ 25 more rows
retail_comparison_fit |>
accuracy()
## # A tibble: 2 × 12
## State Industry .model .type ME RMSE MAE MPE MAPE MASE RMSSE
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Northern … Clothin… Holt-… Trai… -0.0128 0.613 0.450 -0.469 5.15 0.513 0.529
## 2 Northern … Clothin… Dampe… Trai… 0.0398 0.616 0.444 -0.0723 5.06 0.507 0.531
## # ℹ 1 more variable: ACF1 <dbl>
Model Preference
The Holt-Winters Multi model has a slightly lower RMSE (0.613) than the Damped Multi model (0.616).
There is only a very tiny difference of 0.002 between the two models, meaning they fit the training data almost exactly the same.
The Damped Multi model has a lower MAE (0.444) and MAPE (5.06%), suggesting its average errors are slightly smaller.
Even though the Damped model has a better MAE, the Holt-Winters Multi model is usually preferred when it has the lowest RMSE and fewer parameters.
I prefer the Holt-Winters Multi model because it is simpler and provides the most accurate fit for this specific series.
To confirm if the preferred Holt-Winters Multiplicative model has captured all the available information in your retail series, we will examine the residuals
Residual Analysis
retail_comparison_fit |>
select(`Holt-Winters Multi`) |>
gg_tsresiduals()
## Warning: `gg_tsresiduals()` was deprecated in feasts 0.4.2.
## ℹ Please use `ggtime::gg_tsresiduals()` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
The top plot shows that the residuals are scattered randomly around zero
with no clear patterns or trends remaining.
The histogram is shaped like a bell curve and centered at zero, which confirms that the errors follow a normal distribution.
In the ACF plot, almost all bars are within the blue dashed lines, meaning the model has successfully captured the seasonal patterns.
There is one small spike at lag 12 that pokes slightly above the line, but it is minor enough that the residuals can still be considered white noise.
Since these residuals look clean, you can trust that the prediction intervals for your retail forecast are accurate.
library(fpp3)
# Training ends in Dec 2010
myseries_train <- myseries |>
filter(year(Month) <= 2010)
# Fit both models to the training data
# We specify Holt-Winters Multiplicative (M,A,M)
fit_comparison <- myseries_train |>
model(
`Holt-Winters Multi` = ETS(Turnover ~ error("M") + trend("A") + season("M")),
`SNAIVE` = SNAIVE(Turnover)
)
# Forecast over the test set period
# R automatically knows the length of the test set from myseries
fc_comparison <- fit_comparison |>
forecast(new_data = anti_join(myseries, myseries_train, by = "Month"))
# Compare accuracy on the test set
test_accuracy <- fc_comparison |>
accuracy(myseries)
test_accuracy
## # A tibble: 2 × 12
## .model State Industry .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Holt-W… Nort… Clothin… Test -1.54 1.78 1.60 -12.3 12.6 1.75 1.47 0.495
## 2 SNAIVE Nort… Clothin… Test 0.836 1.55 1.24 5.94 9.06 1.36 1.28 0.601
The Seasonal Naive approach actually better here because its MASE is 1.00, while your other models are both higher (around 1.35 and 1.74).
Neither the Holt-Winters nor the other model managed to beat the simple baseline on this specific test set.
he model with the lower RMSE (1.55) is your best performer among the complex models, but it still falls short of the seasonal naive benchmark.
With a MAPE of 9.06%, your best model is off by nearly double the error margin of a typical winning forecast for this type of data.
In this specific case, you cannot beat the seasonal naive approach; the simple method of repeating last year’s numbers is more accurate for this period.
library(fpp3)
# Select the series
set.seed(12345678)
myseries <- aus_retail |>
filter(`Series ID` == sample(aus_retail$`Series ID`, 1))
# Split data: Training ends Dec 2010
myseries_train <- myseries |>
filter(year(Month) <= 2010)
# Fit the hybrid model: Box-Cox -> STL -> ETS
# Note: season("N") is used because STL already removed seasonality
fit_hybrid <- myseries_train |>
model(
`STL-ETS Box-Cox` = decomposition_model(
STL(box_cox(Turnover, 0.21) ~ season(window = "periodic")),
ETS(season_adjust ~ error("A") + trend("A") + season("N"))
),
`Holt-Winters Multi` = ETS(Turnover ~ error("M") + trend("A") + season("M")),
`SNAIVE` = SNAIVE(Turnover)
)
# Evaluate on the test set
fit_hybrid |>
forecast(new_data = anti_join(myseries, myseries_train, by = "Month")) |>
accuracy(myseries)
## # A tibble: 3 × 12
## .model State Industry .type ME RMSE MAE MPE MAPE MASE RMSSE ACF1
## <chr> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Holt-W… Nort… Clothin… Test -1.54 1.78 1.60 -12.3 12.6 1.75 1.47 0.495
## 2 SNAIVE Nort… Clothin… Test 0.836 1.55 1.24 5.94 9.06 1.36 1.28 0.601
## 3 STL-ET… Nort… Clothin… Test -3.86 4.29 3.87 -29.7 29.8 4.23 3.54 0.842
The SNAIVE (Seasonal Naive) approach is the best performer because it has the lowest RMSE (1.55) and MAPE (9.06%).
All three models have a MASE greater than 1.0, meaning that for this specific test period, none of these models are actually better than a simple naive forecast.
The STL-ETS Box-Cox model performed the worst on the test set, with a much higher RMSE (4.29) and MAPE (29.80%).
Both the Holt-Winters and STL-ETS models have negative ME values (Mean Error), which means they are consistently forecasting numbers that are too low compared to the actual retail sales.
In this specific case, the simple “repeat last year’s values” method (SNAIVE) is more accurate than the complex ETS and Box-Cox combinations.