STAT 411 · Time Series Analysis & Forecasting

Study Guide — Weeks 1–15

For every week: the concepts, the right R code, how to read the plots, and the conclusions on the patterns. Built on the course lectures (Krispin / TSstudio) and the official worksheet & homework solutions. Press Print → Save as PDF to keep it offline.

The reading rules that cover the whole course

  1. ACF lag axis = cycles, not steps. On a monthly ts the axis "1,2,3" means lags 12, 24, 36 (×frequency).
  2. Model fingerprints: AR(p) → PACF cuts off at p, ACF tails off. MA(q) → ACF cuts off at q, PACF tails off. ARMA → both tail off (use AIC). White noise → both inside the band.  The letter that CUTS names the order: PACF→AR, ACF→MA.
  3. Seasonal ACF: +peak at the seasonal lag (12, 24 = same season), −trough at the half-season lag (6, 18 = opposite season).
  4. Slow-decaying ACF = non-stationary → difference first. (A lone PACF spike + slow ACF is the trap: it's not AR(1).)
  5. CCF: with ccf(x, y), a peak at a negative lag means x leads y; never judge two series by lag-0 cor() alone.
  6. Decomposition random panel: flat noise = the model fits; a wave or megaphone = wrong model → switch additive↔multiplicative (or log).
  7. Additive vs multiplicative: constant seasonal amplitude = additive; swings that grow with the level = multiplicative.
  8. "Model is adequate" = flat residual ACF and Ljung–Box p > 0.05 and lowest validation MASE.
  9. Hypotheses (H₀): Ljung–Box = no autocorrelation; ADF = non-stationary (small p = stationary); KPSS = stationary (large p = stationary); coefficient z-test = coef is 0.
Jump to a week
  1. 1 · The ts object
  2. 2 · Decomposition
  3. 3 · Seasonality
  4. 4 · Correlation
  5. 5 · Forecasting strategies
  6. 6 · Linear regression
  7. 7 · Multiseasonality
  8. 8 · MA models
  9. 9 · Exp. smoothing
  10. 10 · Stationarity / AR
  11. 11 · MA / ARMA
  12. 12 · ARIMA / SARIMA
  13. 13 · ARIMA errors
  14. 14 · Project
  15. 15 · Final review

Week 1 — The Time Series Object · components, ts, window, lags

Concepts

A ts = values + start + frequency. Components: trend, seasonal, cycle, irregular. Additive vs multiplicative. Inspect, create, subset (window), change frequency (aggregate), and lag a series.

R code

library(TSstudio); library(dplyr); library(lubridate)
load("NGC_monthly.RData")              # instructor's NGC series
ts_info(NGC); frequency(NGC); deltat(NGC)   # 12 ; 1/12
head(cycle(NGC)); head(time(NGC)); start(NGC); end(NGC)
y <- ts(x, start = c(2020,1), frequency = 12)   # build one yourself
window(NGC, start=c(2010,1), end=c(2012,12))   # subset
aggregate(NGC, nfrequency = 4, FUN = "mean")      # monthly -> quarterly
stats::lag(NGC, k = -12)                        # seasonal lag
ts_plot(NGC, title="US Natural Gas", slide=TRUE)
How to read the time plot: scan for a drift (trend), a repeating shape (seasonality), swings that grow with the level (→ multiplicative / log), and one-off spikes (outliers). ts_info() confirms the frequency is right.
Conclusions: set the frequency correctly or every seasonal tool later fails silently; decide additive vs multiplicative here (constant swing = additive; growing swing = multiplicative).

Week 2 — Decomposition · moving average, white noise, decompose

Concepts

Moving-average smoothing (order = n_left + n_right + 1; one-sided vs two-sided; symmetric only for odd order). White noise & its ACF / Ljung–Box test. Classical decomposition into trend + seasonal + random.

R code

library(TSstudio); library(forecast)
ts_ma(USVSales, n_left=11, plot=FALSE)        # one-sided order-12 (trailing)
ts_ma(USVSales, n=6)                        # symmetric order-13
set.seed(1234); wn <- ts(rnorm(120))
acf(wn); Box.test(wn, lag=24, type="Ljung-Box")
plot(decompose(USVSales))                   # additive (default)
plot(decompose(AirPassengers, type="multiplicative"))
How to read: an MA of order = frequency removes the seasonality → the trend. White noise = ACF bars inside ±1.96/√n and Ljung–Box p > 0.05 (one stray spike among many lags is chance). In decompose(), the random panel is the judge: flat noise = the model fits; a leftover wave / megaphone = wrong model.
Conclusions: decide additive vs multiplicative, then confirm with the random panel; use order = frequency to estimate the trend (R centers an even order with the 2×12 MA).

Week 3 — Seasonality Analysis · bar / box / density / seasonal plot / heatmap

Concepts

Single vs multiple seasonality; descriptive stats by frequency unit; density plots; ggseasonplot (normal + polar); ts_seasonal (normal/cycle/box); heatmap; quantile plots. Seasonal (fixed period) vs cycle (variable).

R code

library(forecast); library(TSstudio); library(dplyr)
df <- data.frame(month=factor(month.abb[cycle(USgas)], levels=month.abb), y=as.numeric(USgas))
df %>% group_by(month) %>% summarize(mean=mean(y), sd=sd(y))   # bar-chart data
ggseasonplot(USgas, year.labels=TRUE); ggseasonplot(USgas, polar=TRUE)
ts_seasonal(USgas, type="all")            # normal + cycle + box
ts_heatmap(USgas); ts_quantile(UKgrid)
How to read: monthly-mean bar chart with a hump = seasonality; flat bars = no seasonality (a cycle, e.g. sunspots). Seasonal-plot year-lines sharing a shape = consistent season; lines stacking bottom→top = a trend. Box width = within-month variability (but a trend inflates it). Heatmap color horizontal = seasonal, vertical = cycle.
Conclusions: classify seasonal vs cycle; detrend before comparing monthly distributions when a trend is present; pick the plot that answers your exact question (consistency → normal seasonal plot; outlier month-year → heatmap).

Week 4 — Correlation Analysis · ACF, PACF, lag plots, CCF

Concepts

Autocorrelation (ACF) and partial autocorrelation (PACF); lag plots; causation vs correlation; cross-correlation (CCF) for two series.

R code

acf(nottem, lag.max=60); pacf(nottem, lag.max=60)
ts_lags(nottem, lags=c(1,6,12,24,36))            # scatter vs each lag
ccf(x=advertising, y=sales, lag.max=12)        # lead-lag of two series
cor(sales, advertising)                        # lag-0 only (can mislead)
How to read: remember the axis is in cycles (×12). Seasonal series: +peak at lag 12 (same month a year ago), −trough at lag 6 (opposite season). PACF significant spikes = candidate AR lags (Nile: only lag 1 → AR(1)). Lag plot: tight rising diagonal = strong + correlation at that lag; quarterly JohnsonJohnson peaks at lags 4, 8, 12. CCF: ccf(x,y) peak at negative lag = x leads y (advertising leads sales by 3 weeks at lag −3).
Conclusions: ACF reads seasonality & MA order; PACF reads AR order; the lag plot, ACF and PACF must agree. For two series, ignore lag 0 and read the CCF; correlation ≠ causation (watch for a confounder).

Week 5 — Forecasting Strategies · split, backtest, residuals, scoring

Concepts

Workflow: prepare → train → test → evaluate → refit. Train/test split (train ≥70%, test = horizon); backtesting (expanding vs sliding); residual analysis; scores MSE/RMSE/MAE/MAPE/MASE; benchmarks; finalize on full data; prediction intervals.

R code

library(TSstudio); library(forecast)
sp <- ts_split(USgas, sample.out=12); train <- sp$train; test <- sp$test
md <- auto.arima(train); fc <- forecast(md, h=12)
accuracy(fc, test)        # score vs actuals + benchmark
checkresiduals(md)
fc_final <- forecast(auto.arima(USgas), h=12)   # refit on ALL data
How to read: checkresiduals() → residuals ~0, flat ACF, bell histogram, Ljung–Box p > 0.05 = white noise. Accuracy table → compare test RMSE/MAPE/MASE to the naïve; big test-vs-train gap = overfit; MASE < 1 beats naïve. Backtest error stable across partitions = robust.
Conclusions: keep a model only if it beats the benchmark on the held-out set and has white-noise residuals; refit on all data before the real forecast.

Week 6 — Forecasting with Linear Regression · trend + season, polynomial

Concepts

OLS review and the time-series problem (autocorrelated errors); features trend = 1:n and season = factor(month); trend-only, season-only, trend+season, polynomial trend.

R code

train$trend <- 1:nrow(train)
train$seasonal <- factor(month(train$ds, label=TRUE), ordered=FALSE)
md1 <- lm(y ~ trend + seasonal, data=train)            # workhorse
md2 <- lm(y ~ trend + I(trend^2) + seasonal, data=train)  # curved trend
md  <- tslm(train.ts ~ trend + season)                  # on the ts directly
How to read: fitted vs actual — trend-only leaves seasonal waves in residuals; season-only repeats the same annual shape and misses the climb; trend+season tracks both. Each season coefficient = that month minus January; trend coefficient = change per period. Residual ACF with spikes = autocorrelated errors → OLS standard errors are wrong.
Conclusions: trend+season is the default; add I(trend^2) only if the trend curves (don't extrapolate it far); autocorrelated residuals → regression with ARIMA errors (Week 13).

Week 7 — Multiseasonality (Case Study) · events, breaks, UKgrid

Concepts

tslm; modeling outliers, structural breaks (H_t = max(0, t−k) = slope change), recurring non-seasonal events; engineering wday, month, lag365 for several seasonal cycles; anova selection; forecasting with future regressors; Box–Cox.

R code

k <- which(time(USgas) >= 2010)[1]; trend <- 1:length(USgas)
s_break <- pmax(trend - k, 0)                  # slope-change feature
md <- tslm(USgas ~ season + trend + s_break)
md3 <- tslm(train_ts ~ season + trend + wday + month + lag365, data=train_df)
anova(md3)                                     # which terms are significant
fc <- forecast(final_md, h=h, newdata=UK_fc_df)   # needs FUTURE predictors
How to read: a structural break shows slope β₁ before k and β₁+β₂ after. Each differing wday/month profile is a separate seasonal cycle. anova p-value small = the term carries signal. Even high R²/low MAPE can hide autocorrelated residuals — check the ACF.
Conclusions: engineer one feature per seasonal cycle, keep the anova-significant ones; to forecast you must supply the predictors' future values; non-white residuals → add ARIMA errors.

Week 8 — Forecasting with Moving-Average Models · SMA, weighted MA

Concepts

Simple MA forecast (average the last m; m=1 = naïve); window-length trade-off; limitations (no trend/season, flattens); weighted MA (weights ≥ 0, sum to 1); seasonal weighting.

R code

# all weight on lag 12 == seasonal naive
fc_a <- sma_forecast(USgas_df, h=24, m=12, w=c(1, rep(0,11)))
# blend seasonal lag (0.8) + recent (0.2)
fc_b <- sma_forecast(USgas_df, h=24, m=12, w=c(0.8, rep(0,10), 0.2))
How to read: short m reacts fast but is noisy; long m is smooth but flat; the forecast converges to a constant → it can't hold a trend and averages seasonality away. Weighted MA with all weight on lag 12 reproduces the seasonal oscillation.
Conclusions: SMA only for series without a persistent trend/season; put weight on the lags the Week-4 ACF flagged; choose m/weights by test error.

Week 9 — Exponential Smoothing · SES, Holt, Holt–Winters

Concepts

SES (level only, flat forecast; weight decays by (1−α); α=1 = naïve); Holt (+trend); Holt–Winters (+season, additive vs multiplicative); ets() auto-selects.

R code

fc_ses  <- ses(train, h=12)                     # flat
fc_holt <- holt(train, h=8)                     # + trend
md_hw   <- HoltWinters(train); fc_hw <- forecast(md_hw, h=12)
fit <- ets(train); fc <- forecast(fit, h=12)   # auto E,T,S
How to read: SES forecast is flat, Holt sloped, Holt–Winters seasonal. Large α/β/γ = adapts fast to recent data; small = smooth. Residual ACF flat = the smoothing captured the structure.
Conclusions: match the method to the components (level? trend? season?); additive HW if the seasonal swing is constant, multiplicative if it grows; a very large α may be chasing noise.

Week 10 — Stationarity, Differencing & AR · unit root, random walk, AR(p)

Concepts

Weak stationarity (constant mean/variance/autocovariance); differencing (1st, 2nd, seasonal); log for variance; random walk (non-stationary; its first difference is white noise); AR(1)/AR(p); AR(1) property ρₘ = φᵐ.

R code

diff(y); diff(log(AirPassengers))             # difference / log-then-difference
library(tseries); adf.test(y); kpss.test(y)    # stationarity tests
ndiffs(y); nsdiffs(y)                     # how many differences
ar2 <- arima.sim(list(order=c(2,0,0), ar=c(0.9,-0.3)), n=500)
How to read: a trend + a slowly-decaying ACF = non-stationary → difference. After differencing, even fluctuation around 0 = stationary. AR fingerprint: ACF tails off, PACF cuts off at p (φ>0 smooth decay; φ<0 alternating). ADF: small p = stationary; KPSS: large p = stationary.
Conclusions: difference to the smallest d that gives stationarity; read p from the PACF cutoff. Trap: a lone PACF spike + slow ACF = non-stationary, not AR(1).

Week 11 — MA Process, ARMA & Forecasting · MA(q), ARMA, AIC

Concepts

MA(1)/MA(q) (a weighted sum of recent shocks); identify MA; MA(1) has only lag-1 ACF nonzero; ARMA(p,q); order selection by AIC/BIC; residual check; forecasting.

R code

ma2 <- arima.sim(list(order=c(0,0,2), ma=c(0.5,-0.3)), n=500)
md_ma  <- arima(ma2, order=c(0,0,2), method="ML")
arma_md<- arima(arma, order=c(1,0,2)); AIC(md_a, md_b); BIC(md_a)
checkresiduals(arma_md); forecast(ar(ar2), h=100)
How to read: MA(q) → ACF cuts off after lag q, PACF tails off. ARMA → both tail off → use AIC/BIC (lower = better balance of fit vs parameters). checkresiduals Ljung–Box p > 0.05 = adequate. Forecast reverts to the mean; interval widens then stabilizes.
Conclusions: q from the ACF cutoff; when both tail off, compare AICs + residuals; a stationary series' forecast reverts to its mean (always report the interval).

Week 12 — ARIMA & Seasonal ARIMA · (p,d,q)(P,D,Q)ₛ, grid search

Concepts

ARIMA(p,d,q) (I = differencing); special cases (000 = white noise, 010 = random walk); choosing d; SARIMA(p,d,q)(P,D,Q)ₛ; seasonal AR/MA/differencing; tuning; grid-search AIC; USgas case study.

R code

arima(robusta_price, order=c(1,1,0)); lmtest::coeftest(md, df=Inf)
d12 <- diff(train, 12); d12_1 <- diff(d12, 1)        # seasonal then ordinary
best <- arima(train, order=c(1,1,1), seasonal=list(order=c(0,1,2)))
fc <- forecast(best, h=12); accuracy(fc, test); checkresiduals(best)
How to read: ACF slowly decaying at short lags → ordinary diff (d); tall spikes at 12, 24 → seasonal diff (D). On the differenced series, read short lags for (p,q) and seasonal lags for (P,Q). Grid AIC → take the lowest; coeftest → drop a clearly non-significant term. Ljung–Box p > 0.05 = adequate.
Conclusions: smallest d, D (seasonal difference first); USgas winner SARIMA(1,1,1)(0,1,2)₁₂ with test MAPE 3.87% beat the seasonal-naïve 5.22% — complexity earned its keep.

Week 13 — Linear Regression with ARIMA Errors · dynamic regression

Concepts

OLS on time series leaves autocorrelated errors → understated standard errors; fix = regression with ARIMA errors (yₜ = β′xₜ + nₜ, nₜ ~ ARIMA); forecasting needs future predictor values.

R code

X  <- cbind(trend=seq_along(train), fourier(train, K=3))
Xf <- cbind(trend=length(train)+1:h, fourier(train, K=3, h=h))
fit <- auto.arima(train, xreg=X, lambda=0)   # regression + ARIMA errors
fc  <- forecast(fit, xreg=Xf, h=h)           # MUST pass future xreg
checkresiduals(fit)
How to read: a plain OLS/tslm residual ACF with spikes = autocorrelated errors (t-values untrustworthy). After the ARIMA-errors fit, the residual ACF should be flat → the ARIMA part absorbed the dynamics and the coefficients now have correct standard errors.
Conclusions: autocorrelated regression residuals → switch to ARIMA errors; interpret the predictor effects from the corrected model; supply future predictor values to forecast.

Week 14 — Project · the whole workflow, end to end

Concepts & workflow

No new plots — chain the reads: time plot → ACF/PACF → candidate fit → checkresiduals → forecast, each feeding the next.

y <- ...                               # your ts
sp <- ts_split(y, sample.out=h); train <- sp$train; test <- sp$test
models <- list(snaive=snaive(train,h), ets=forecast(ets(train),h),
               arima=forecast(auto.arima(train, lambda=0),h))
sapply(models, function(m) accuracy(m, test)["Test set","MASE"])
final <- auto.arima(y, lambda=0); forecast(final, h=24)   # refit on all data
Conclusion / grading target: pick the simplest model whose residuals are white noise and whose test MASE < 1; refit on all data; forecast with 80/95% intervals; report the accuracy table + residual diagnostics.

Week 15 — Final Review · the decision map

The one-page decision map

You seeConclusion → move
ACF decays slowly, stays outside bandNon-stationary → diff()
Tall ACF spikes at 12, 24…Seasonal → diff(y,12) (D=1)
PACF cuts off at p · ACF tails offAR(p) → c(p,d,0)
ACF cuts off at q · PACF tails offMA(q) → c(0,d,q)
Both tail offARMA → compare AIC
Residual ACF flat + Ljung–Box p > 0.05Adequate → forecast
Decompose random panel flat / wavyModel fits / switch add↔mult
Know each H₀: Ljung–Box = no autocorrelation (adequacy); ADF = non-stationary; KPSS = stationary; coefficient z-test = coefficient is 0. The exam rewards the path (stationary? → difference → ACF/PACF → orders → fit → residuals → forecast) more than any formula.
Adequate model = white-noise residuals + Ljung–Box p > 0.05 + lowest validation MASE among the candidates.

STAT 411 · Study Guide · built from the course lectures (Krispin / TSstudio) and the official worksheet & homework solution keys.