Each week builds on the last. You are not starting over โ you are adding tools.
Linear model
Assumptions
Interpretation
Quadratic
Log models
Elasticity
Odds
Sigmoid
Confusion
Distance
Probabilities
Lift ยท ROC
Splits
Forests
Ensembles
Every predictive model you build follows this same cycle.
In earlier weeks, row order usually did not matter. In time series analysis, order is the story.
Rows describe different people, firms, or products at one moment in time.
Shuffling rows usually changes nothing important.
Rows describe the same process unfolding over time.
Shuffling rows destroys the signal we want to forecast.
Time series data are observations collected sequentially over time.
โForecasting is about predicting the future as accurately as possible, given all of the information available.โ
Quarterly GDP
Daily stock prices
Monthly demand
Annual population
Some series are much easier to predict than others.
Clear patterns are easier to project.
More data gives us better evidence.
Forecasting breaks down when the process changes too much.
When homework asks for three components, this is the answer students should know.
Long-run direction plus slower wave-like movement.
Patterns that repeat on a fixed calendar rhythm.
Noise and unexplained movement left after removing structure.
global_economyStudents need this because GDP per capita is not a built-in column. They must compute it.
head(global_economy)
global_economy %>%
mutate(GDP_per_capita = GDP / Population) %>%
autoplot(GDP_per_capita) +
theme(legend.position = "none") +
labs(
title = "GDP Per Capita by Country Over Time",
y = "$US"
)
global_economy %>%
mutate(GDP_per_capita = GDP / Population) %>%
filter(GDP_per_capita == max(GDP_per_capita, na.rm = TRUE))
global_economy %>%
filter(Country == "United States") %>%
mutate(GDP_per_capita = GDP / Population) %>%
autoplot(GDP_per_capita) +
labs(title = "US GDP Per Capita Over Time", y = "$US")Students should learn to diagnose this visually before touching code.
Seasonal swings stay roughly the same size over time.
Seasonal swings grow as the series level grows.
This is the logic behind many official seasonal-adjustment systems.
This code is suitable for homework preparation and lecture demonstration.
Gas <- tail(aus_production, 5 * 4) %>%
select(Quarter, Gas)
autoplot(Gas, Gas) +
labs(
title = "Australian Gas Production (Last 5 Years)",
y = "Petajoules"
)
Gas %>%
model(
classical_decomposition(Gas, type = "multiplicative")
) %>%
components() %>%
autoplot() +
labs(title = "Classical Multiplicative Decomposition of Gas Production")Removing seasonal effects helps reveal the underlying movement that decision-makers care about.
dcmp <- Gas %>%
model(
classical_decomposition(Gas, type = "multiplicative")
)
components(dcmp) %>%
as_tsibble() %>%
autoplot(Gas, colour = "gray") +
geom_line(aes(y = season_adjust), colour = "blue", linewidth = 1) +
labs(
title = "Seasonally Adjusted Gas Production",
y = "Petajoules"
)Classical decomposition is simple, but it is not robust.
Gas_clean <- tail(aus_production, 5 * 4) %>%
select(Quarter, Gas)
Gas_mid_outlier <- Gas_clean %>%
mutate(Gas = if_else(row_number() == 10, Gas + 300, Gas))
dcmp_mid <- Gas_mid_outlier %>%
model(
classical_decomposition(Gas, type = "multiplicative")
)
components(dcmp_mid) %>%
as_tsibble() %>%
autoplot(Gas, colour = "gray") +
geom_line(aes(y = season_adjust), colour = "red", linewidth = 1) +
labs(title = "Seasonally Adjusted โ Outlier in Middle")STL is more flexible and more robust than classical decomposition.
us_retail_employment <- us_employment %>%
filter(year(Month) >= 1990, Title == "Retail Trade") %>%
select(-Series_ID)
dcmp <- us_retail_employment %>%
model(stl = STL(Employed))
components(dcmp) %>% autoplot()
us_retail_employment %>%
model(
STL(Employed ~ trend(window = 7) +
season(window = "periodic"),
robust = TRUE)
) %>%
components() %>%
autoplot()Official seasonal adjustment systems are built on the same core decomposition ideas, but with stronger endpoint and robustness handling.
Simple forecasting methods are not bad models. They are the baseline every complex model must beat.
Students often understand these methods much faster when they see the forecast shape rather than just the formula.
This is a great example of how model() can fit multiple forecasting methods in one clean pipeline.
train <- aus_production %>%
filter_index("1992 Q1" ~ "2006 Q4")
beer_fit <- train %>%
model(
Mean = MEAN(Beer),
Naive = NAIVE(Beer),
Seasonal_naive = SNAIVE(Beer),
Drift = RW(Beer ~ drift())
)
beer_fc <- beer_fit %>% forecast(h = 14)
beer_fc %>%
autoplot(train, level = NULL) +
autolayer(
filter_index(aus_production, "2007 Q1" ~ .),
colour = "black"
) +
labs(
y = "Megalitres",
title = "Forecasts for quarterly beer production"
) +
guides(colour = guide_legend(title = "Forecast"))The drift method is especially teachable because it matches a simple geometric idea.
fb_stock <- gafa_stock %>%
filter(Symbol == "FB") %>%
mutate(trading_day = row_number()) %>%
update_tsibble(index = trading_day, regular = TRUE)
fb_stock %>%
autoplot(Close) +
labs(title = "Facebook Daily Closing Stock Price", y = "$US")
fb_fit <- fb_stock %>%
model(Drift = RW(Close ~ drift()))
fb_fit %>%
forecast(h = 30) %>%
autoplot(fb_stock) +
labs(title = "Drift Forecast for Facebook Stock Price", y = "$US")This is a strong lecture dataset because it has long-run movement, seasonality, and a familiar business context.
head(us_employment)
tail(us_employment)
str(us_employment)
glimpse(us_employment)
dim(us_employment)
nrow(us_employment)
ncol(us_employment)
names(us_employment)
colnames(us_employment)
summary(us_employment$Employed)
us_retail_employment <- us_employment %>%
filter(year(Month) >= 1990, Title == "Retail Trade")
us_retail_employment <- us_retail_employment[,
names(us_retail_employment) != "Series_ID"
]
head(us_retail_employment)A big part of time-series work is learning to read the pattern before choosing the model.
plot <- autoplot(us_retail_employment, Employed) +
labs(
y = "Persons (thousands)",
title = "Total employment in US retail"
)
print(plot)
gg_season(us_retail_employment, Employed) +
labs(title = "Seasonal Plot: US Retail Employment", y = "Persons (thousands)")
gg_subseries(us_retail_employment, Employed) +
labs(title = "Month Plot: US Retail Employment", y = "Persons (thousands)")
us_retail_quarterly <- us_retail_employment %>%
index_by(Quarter = yearquarter(Month)) %>%
summarise(Employed = mean(Employed))
gg_subseries(us_retail_quarterly, Employed) +
labs(title = "Quarterly Plot: US Retail Employment", y = "Persons (thousands)")
gg_season(us_retail_employment, Employed, polar = TRUE) +
labs(title = "Polar Seasonal Plot: US Retail Employment", y = "Persons (thousands)")Moving averages are one of the clearest ways to explain smoothing before formal forecasting models.
us_retail_employment %>%
mutate(
`3-Month MA` = slider::slide_dbl(Employed, mean, .before = 1, .after = 1, .complete = TRUE),
`5-Month MA` = slider::slide_dbl(Employed, mean, .before = 2, .after = 2, .complete = TRUE)
) %>%
tidyr::pivot_longer(
cols = c(Employed, `3-Month MA`, `5-Month MA`),
names_to = "Series",
values_to = "Value"
) %>%
ggplot(aes(x = Month, y = Value, color = Series)) +
geom_line() +
labs(
title = "US Retail Employment with Moving Averages",
y = "Persons (thousands)",
x = "Month"
)ETS gives a more formal smoothing framework than moving averages.
fit_ets <- us_retail_employment %>%
model(ETS(Employed))
report(fit_ets)
components(fit_ets) %>%
autoplot() +
labs(
title = "Exponential Smoothing Decomposition: US Retail Employment",
y = "Persons (thousands)"
)This example shows how to split time-ordered data into training and test sets and compare multiple simple models.
google_data <- gafa_stock %>%
filter(Symbol == "GOOG") %>%
select(Date, Close) %>%
as_tsibble(index = Date)
google_train <- google_data %>%
filter(row_number() <= 200)
google_test <- google_data %>%
filter(row_number() > 200)
models_google <- google_train %>%
model(
Naive = NAIVE(Close),
Drift = RW(Close ~ drift()),
Mean = MEAN(Close)
)
google_fc <- models_google %>%
forecast(new_data = google_test)
google_acc <- google_fc %>%
accuracy(google_data) %>%
mutate(
MSE = RMSE^2,
MAD = MAE
)
google_acc %>%
select(.model, MSE, MAD, MAPE)Unlike ordinary cross-validation, time-series cross-validation respects temporal order.
google_stretch <- google_data %>%
stretch_tsibble(.init = 40, .step = 5)
cv_models <- google_stretch %>%
model(
Naive = NAIVE(Close),
Drift = RW(Close ~ drift()),
Mean = MEAN(Close)
)
cv_fc <- cv_models %>%
forecast(new_data = google_data)
cv_fc %>%
accuracy(google_data)Forecasting is not only about fitting models. It is also about inspecting the process for dependence, outliers, and structural changes.
google_data %>%
ACF(Close) %>%
autoplot() +
labs(title = "Autocorrelation of Google Closing Prices")
google_data %>%
ggplot(aes(x = Date, y = Close)) +
geom_line() +
geom_point(
data = google_data %>% filter(abs(scale(Close)) > 2),
aes(x = Date, y = Close),
size = 2
) +
labs(title = "Potential Outliers in Google Closing Prices", y = "Close Price")
google_data %>%
mutate(Return = difference(log(Close))) %>%
ggplot(aes(x = Date, y = Return)) +
geom_line() +
labs(title = "Google Daily Log Returns", y = "Log Return")This gives students a first look at a more flexible time-series model after the benchmark methods.
bricks_train <- aus_production %>%
filter(year(Quarter) >= 1970, year(Quarter) <= 2004) %>%
select(Quarter, Bricks)
bricks_arima <- bricks_train %>%
model(ARIMA = ARIMA(Bricks))
report(bricks_arima)
fc_arima <- bricks_arima %>%
forecast(h = "2 years")
autoplot(bricks_train, Bricks) +
autolayer(fc_arima, level = NULL) +
labs(
title = "ARIMA Forecast: Australian Clay Bricks",
y = "Million Bricks"
)
gg_tsresiduals(bricks_arima)A forecast is only useful if we know how well it performs on data it did not see during training.
Average absolute error
Penalizes large mistakes more heavily
Percent error, when meaningful
This version explicitly defines the evaluation set before calling accuracy().
recent_production <- aus_production %>%
filter_index("2007 Q1" ~ .)
accuracy(beer_fc, recent_production)Students often want to know how to fetch real economic data directly from a public source.
library(quantmod)
library(ggfortify)
getSymbols("TRP6001", src = "FRED")
autoplot(TRP6001) +
labs(title = "Real Personal Consumption Expenditures", y = "Index")
# Fallback if autoplot methods are unavailable
plot(TRP6001, main = "Real Personal Consumption Expenditures")If students remember these ideas, they will be ready for forecasting work and the homework set.