The data set I chose and used for this discussion was the
tourism dataset from the fpp3 package. It
contains quarterly overnight trips (in thousands) for Australia, broken
down by Region, State, and Purpose of travel.
library(fpp3)
library(dplyr)
data(tourism)
tourism
## # A tsibble: 24,320 x 5 [1Q]
## # Key: Region, State, Purpose [304]
## Quarter Region State Purpose Trips
## <qtr> <chr> <chr> <chr> <dbl>
## 1 1998 Q1 Adelaide South Australia Business 135.
## 2 1998 Q2 Adelaide South Australia Business 110.
## 3 1998 Q3 Adelaide South Australia Business 166.
## 4 1998 Q4 Adelaide South Australia Business 127.
## 5 1999 Q1 Adelaide South Australia Business 137.
## 6 1999 Q2 Adelaide South Australia Business 200.
## 7 1999 Q3 Adelaide South Australia Business 169.
## 8 1999 Q4 Adelaide South Australia Business 134.
## 9 2000 Q1 Adelaide South Australia Business 154.
## 10 2000 Q2 Adelaide South Australia Business 169.
## # ℹ 24,310 more rows
aus_total <- tourism %>%
summarise(Trips = sum(Trips))
aus_total
## # A tsibble: 80 x 2 [1Q]
## Quarter Trips
## <qtr> <dbl>
## 1 1998 Q1 23182.
## 2 1998 Q2 20323.
## 3 1998 Q3 19827.
## 4 1998 Q4 20830.
## 5 1999 Q1 22087.
## 6 1999 Q2 21458.
## 7 1999 Q3 19914.
## 8 1999 Q4 20028.
## 9 2000 Q1 22339.
## 10 2000 Q2 19941.
## # ℹ 70 more rows
aus_total %>%
autoplot(Trips) +
labs(title = "Total Quarterly Overnight Trips in Australia",
y = "Trips ('000)", x = "Quarter")
aus_total %>%
gg_season(Trips, labels = "both") +
labs(title = "Seasonal Plot: Total Overnight Trips",
y = "Trips ('000)")
aus_total %>%
gg_subseries(Trips) +
labs(title = "Subseries Plot: Total Overnight Trips",
y = "Trips ('000)")
Analysis:
aus_total_dcmp_classical <- aus_total %>%
model(classical_decomposition(Trips, type = "additive")) %>%
components()
aus_total_dcmp_classical %>%
autoplot() +
labs(title = "Classical Additive Decomposition: Total Overnight Trips")
aus_total_dcmp_stl <- aus_total %>%
model(STL(Trips ~ trend(window = 7) + season(window = "periodic"),
robust = TRUE)) %>%
components()
aus_total_dcmp_stl %>%
autoplot() +
labs(title = "STL Decomposition: Total Overnight Trips")
bind_rows(
aus_total_dcmp_classical %>% as_tibble() %>%
transmute(Quarter, Trend = trend, Method = "Classical"),
aus_total_dcmp_stl %>% as_tibble() %>%
transmute(Quarter, Trend = trend, Method = "STL")
) %>%
ggplot(aes(x = Quarter, y = Trend, colour = Method)) +
geom_line(linewidth = 0.9) +
labs(title = "Trend Comparison: Classical vs STL",
y = "Trend component")
Interpretation:
Classical decomposition assumes a fixed seasonal pattern that does not change over time and uses a symmetric moving average for the trend. This causes it to lose observations at the start and end of the series, which is visible in the trend comparison chart where the red (Classical) line starts slightly later than the STL line.
STL decomposition allows the seasonal component to evolve gradually over time, is more robust to outliers, and does not lose values at the edges of the series, which is why the teal (STL) line extends further on both ends of the trend comparison chart.
In this specific series, both methods produce nearly identical trends, as seen in the trend comparison chart where the two lines overlap almost completely throughout. This suggests the data does not have strong structural breaks or changing seasonality that would cause the two methods to diverge significantly.
The seasonal components in both decompositions look very consistent and stable over time, with swings roughly between -500 and +1,000, confirming that a fixed seasonal assumption works reasonably well here.
The remainder (Classical) and remainder (STL) are both relatively small and random-looking, though the Classical random component appears slightly noisier, which is expected given its edge-value limitations.
n <- nrow(aus_total)
train_n <- floor(0.7 * n)
aus_train <- aus_total %>% slice(1:train_n)
aus_test <- aus_total %>% slice((train_n + 1):n)
c(train_obs = nrow(aus_train), test_obs = nrow(aus_test))
## train_obs test_obs
## 56 24
aus_fit <- aus_train %>%
model(
Mean = MEAN(Trips),
Naive = NAIVE(Trips),
SNaive = SNAIVE(Trips ~ lag("year")),
Drift = RW(Trips ~ drift())
)
aus_fit
## # A mable: 1 x 4
## Mean Naive SNaive Drift
## <model> <model> <model> <model>
## 1 <MEAN> <NAIVE> <SNAIVE> <RW w/ drift>
aus_fc <- aus_fit %>%
forecast(h = nrow(aus_test))
aus_fc %>%
autoplot(aus_total, level = NULL) +
labs(title = "Forecasts vs Actuals: Total Overnight Trips",
y = "Trips ('000)") +
guides(colour = guide_legend(title = "Forecast Method"))
acc <- accuracy(aus_fc, aus_total) %>%
mutate(MSE = RMSE^2) %>%
select(.model, ME, MPE, MAPE, MSE, MAE) %>%
arrange(MAPE)
acc
## # A tibble: 4 × 6
## .model ME MPE MAPE MSE MAE
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Mean 3078. 12.1 12.8 14844228. 3209.
## 2 SNaive 3404. 13.5 13.9 17125593. 3481.
## 3 Naive 3583. 14.3 14.5 18211801. 3623.
## 4 Drift 4278. 17.1 17.2 25431634. 4304.
acc %>%
knitr::kable(digits = 3, caption = "Hold-out accuracy (30% test set)")
| .model | ME | MPE | MAPE | MSE | MAE |
|---|---|---|---|---|---|
| Mean | 3077.734 | 12.134 | 12.792 | 14844228 | 3208.567 |
| SNaive | 3403.924 | 13.510 | 13.901 | 17125593 | 3481.200 |
| Naive | 3583.297 | 14.288 | 14.490 | 18211801 | 3623.094 |
| Drift | 4278.425 | 17.103 | 17.235 | 25431634 | 4304.318 |
Interpretation:
After looking at the hold-out accuracy resultsfor all models, the Mean model performs best, as it has the lowest values across every error metric.
This is somewhat surprising since SNaive is usually expected to perform well on seasonal data, but in this case simply averaging historical values outperformed all other benchmarks.
The Drift model performs worst by a significant margin, with nearly double the MSE of the Mean model, suggesting the data does not follow a consistent linear trend. All four models show large positive ME values, meaning every model is systematically over-forecasting. The predictions are consistently higher than the actual values, which is a sign of bias worth investigating.
Overall, while the Mean model wins among these simple benchmarks, a MAPE of 12.8% still leaves meaningful room for improvement, and any advanced model such as ETS or ARIMA should aim to beat that number to be considered worth using.
Total Australian overnight trips show a clear upward trend, particularly accelerating after 2013, with a strong and stable quarterly seasonal pattern that remains consistent throughout the entire series.
Classical and STL decomposition produce nearly identical trend and seasonal components for this dataset, confirming that the fixed seasonal assumption of Classical decomposition works well here since the seasonal swings do not grow or change meaningfully over time. STL still has the advantage of handling the edges of the series without losing observations.
Among simple forecasting benchmarks fit on 70% of the data and evaluated on the remaining 30%, the Mean model actually performed best with the lowest error across all metrics. This suggests that the strong upward trend in recent years may have made last-season values a less reliable reference point, while the overall mean provided a more stable estimate.