Time Series (ts & forecasting basics)
Reviewed & published by Brayan K
A time series is a sequence of values measured at regular time intervals, and R's ts object stores that data together with its start date and seasonal frequency so it can be plotted, decomposed, and forecast.
Part of the free R course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll build ts objects, plot them over time, split them into trend, seasonal, and random parts, read an ACF plot, and produce a simple forecast.
What You'll Learn in This Lesson
- How ts() encodes start time and frequency
- How to plot a series so time runs on the x-axis
- How decompose() splits trend, season, and noise
- When to use additive vs multiplicative decomposition
- How to read an ACF (autocorrelation) plot
- How to forecast with HoltWinters and the forecast package
1️⃣ Building and Plotting a ts Object
Wrap a numeric vector in ts(), telling it when the data start and how many points make one cycle via frequency. Once it's a ts, plot() automatically puts time on the x-axis.
# A ts object is a numeric series that knows its time structure:
# when it starts and how many observations make up one cycle (frequency).
# 24 monthly sales figures over two years
sales <- c(120, 135, 150, 165, 160, 155, 140, 130, 145, 170, 200, 230,
130, 150, 165, 175, 170, 165, 150, 140, 158, 185, 215, 250)
# frequency = 12 means monthly data with a yearly season
y <- ts(sales, start = c(2023, 1), frequency = 12)
# Printing a ts shows it laid out by year and month, with attributes:
print(y)
start(y) # when the series begins
end(y) # when it ends
frequency(y) # observations per cycle -> 12
# Plot it — time runs along the x-axis automatically:
plot(y, main = "Monthly Sales", ylab = "Units")2️⃣ Decomposing into Trend, Season, and Noise
decompose() separates a series into a smooth trend, a repeating seasonal shape, and a leftover random component. Plotting the result stacks all four panels (observed, trend, seasonal, random) so you can see each piece.
# Real series mix three things: a long-run TREND, a repeating SEASONAL
# pattern, and leftover RANDOM noise. decompose() splits them apart.
y <- ts(sales, start = c(2023, 1), frequency = 12)
parts <- decompose(y) # additive by default: trend + seasonal + random
# parts$trend -> the smooth long-term movement
# parts$seasonal -> the repeating within-year shape
# parts$random -> what's left after removing trend & season
plot(parts) # four stacked panels: observed/trend/seasonal/random
# If seasonal swings GROW with the level, use a multiplicative model:
# parts <- decompose(y, type = "multiplicative")3️⃣ Autocorrelation and a First Forecast
The acf() plot reveals how each point relates to earlier ones — your main clue to trend and seasonality. Once you understand the structure, HoltWinters() gives a quick forecast, and the forecast package's auto.arima() is the modern go-to.
# acf() shows AUTOCORRELATION — how each point relates to earlier points.
# Tall bars at lag 12 (for monthly data) signal a yearly seasonal cycle.
y <- ts(sales, start = c(2023, 1), frequency = 12)
acf(y, main = "Autocorrelation of Sales")
# Reading the plot:
# - Bars outside the dashed blue band are statistically meaningful.
# - A slowly decaying pattern hints at a trend.
# - A spike every 'frequency' lags hints at seasonality.
# pacf() (partial ACF) helps choose AR terms when modelling.# Two ways to forecast. Base R ships HoltWinters (exponential smoothing).
y <- ts(sales, start = c(2023, 1), frequency = 12)
hw <- HoltWinters(y) # learns level, trend, and seasonal components
p <- predict(hw, n.ahead = 6) # forecast the next 6 months
p
# The 'forecast' package is the modern standard (install.packages("forecast")):
# library(forecast)
# fit <- auto.arima(y) # picks a good ARIMA model automatically
# fc <- forecast(fit, h = 6) # 6-step-ahead forecast WITH prediction intervals
# plot(fc) # history + forecast + uncertainty bandsYour turn. Fill in the ___ blank and run it.
# 🎯 YOUR TURN — replace each ___ then run it.
# Quarterly revenue for 3 years (12 numbers), season of 4 quarters
rev <- c(50, 65, 70, 90, 55, 70, 78, 100, 60, 76, 85, 112)
# 1) Make a quarterly ts starting in Q1 of 2021 (frequency = 4)
y <- ts(rev, start = c(2021, 1), frequency = ___) # 👉 replace ___ with 4
# 2) Confirm the season length
frequency(y) # should print 4
# 3) Split it into trend / seasonal / random
parts <- decompose(y)
# 4) Plot the four-panel decomposition
plot(parts)
# 👉 Look at the 'seasonal' panel: revenue peaks every 4th quarter.Mini-Challenge: AirPassengers
Use R's classic built-in dataset to practice the whole workflow: inspect, plot, decompose multiplicatively, and forecast. No data setup needed — AirPassengers is always available.
# 🎯 MINI-CHALLENGE: analyse a built-in seasonal dataset.
#
# R ships 'AirPassengers' — monthly airline passengers, 1949-1960.
#
# 1. Confirm it's already a ts: class(AirPassengers) and frequency(...)
# 2. Plot it: plot(AirPassengers) — notice the rising trend AND the
# seasonal swings that grow over time.
# 3. Decompose it. Because the swings grow, try:
# decompose(AirPassengers, type = "multiplicative")
# and plot the result.
# 4. Forecast 12 months ahead with HoltWinters + predict(..., n.ahead = 12).
#
# Write one sentence describing the trend and the seasonal pattern.
# your code hereCommon Errors (and the fix)
- Wrong frequency — ❌ frequency = 1 on monthly data hides the season. ✅ Use 12 (monthly), 4 (quarterly).
- decompose() on a non-seasonal series — ❌ errors with frequency 1. ✅ It needs at least two full cycles of seasonal data.
- Additive model on growing swings — ❌ leaves a pattern in the residuals. ✅ Switch to type = "multiplicative".
- Plotting a plain vector as time — ❌ x-axis is just an index. ✅ Convert with ts() first.
Pro Tips
- 💡 Always plot first: the human eye spots trend and seasonality faster than any test.
- 💡 Hold out data: fit on early years, forecast the last year you kept hidden, then check accuracy.
- 💡 Modern stack: the tidyverts packages (tsibble, fable) bring tidy, pipe-friendly forecasting.
📋 Quick Reference — Time Series
| Task | R Syntax |
|---|---|
| Create monthly ts | ts(x, start = c(2023,1), frequency = 12) |
| Plot over time | plot(y) |
| Split components | decompose(y) |
| Autocorrelation | acf(y) |
| Smoothing forecast | HoltWinters(y) |
| Auto ARIMA forecast | forecast(auto.arima(y), h = 6) |
🎉 Lesson Complete!
- ✅ ts() stores values with their start and frequency
- ✅ Plotting a ts puts time on the x-axis automatically
- ✅ decompose() splits trend, seasonal, and random parts
- ✅ Choose additive for steady swings, multiplicative for growing ones
- ✅ acf() diagnoses structure; HoltWinters() / forecast predict ahead
Practice quiz
What does the ts() function create?
- A plot
- A matrix
- A list of dates
- A time series object that stores values with their time structure
Answer: A time series object that stores values with their time structure. ts() wraps a numeric vector with start time and frequency.
In ts(), what does the frequency argument mean?
- Observations in one full seasonal cycle
- How often you measured per second
- The number of years
- The sampling error
Answer: Observations in one full seasonal cycle. Frequency is observations per cycle: 12 monthly, 4 quarterly, 7 daily-weekly.
What frequency would you use for monthly data with a yearly season?
- 1
- 12
- 4
- 365
Answer: 12. Monthly data with a yearly cycle uses frequency = 12.
What does decompose() split a series into?
- Mean and variance
- Rows and columns
- Trend, seasonal, and random components
- Past and future
Answer: Trend, seasonal, and random components. decompose() separates trend, seasonal, and random parts.
When should you use a multiplicative decomposition?
- When the series is constant
- When there is no season
- Always by default
- When seasonal swings grow with the level
Answer: When seasonal swings grow with the level. Use multiplicative when swings get bigger as the series climbs (e.g. AirPassengers).
What is the default type of decompose()?
- Additive
- Multiplicative
- Logarithmic
- Random
Answer: Additive. decompose() is additive (trend + seasonal + random) by default.
What does the acf() plot show?
- The forecast
- How each point correlates with earlier points (autocorrelation)
- The mean per year
- The residual variance only
Answer: How each point correlates with earlier points (autocorrelation). acf() shows autocorrelation across lags, revealing trend and seasonality.
On a monthly series, a tall ACF spike at lag 12 suggests what?
- A data error
- No pattern
- A yearly seasonal cycle
- Random noise
Answer: A yearly seasonal cycle. A spike at the seasonal lag (12 for monthly) signals seasonality.
Which base R function does exponential-smoothing forecasting?
- HoltWinters()
- lm()
- table()
- summary()
Answer: HoltWinters(). HoltWinters() ships with base R for level/trend/seasonal smoothing.
Why prefer the forecast package's auto.arima() over a single point line?
- It is base R
- It plots faster
- It needs no data
- It returns prediction intervals showing uncertainty
Answer: It returns prediction intervals showing uncertainty. auto.arima()/forecast() give intervals, not just one point estimate.
Continue this course
- Previous: ANOVA & Model Comparison
- Next: Fast Data with data.table — Filter, aggregate, and update huge datasets fast with dt[i, j, by]
- Quick reference: R cheat sheet
Frequently asked questions
What does the 'frequency' argument in ts() actually mean?
Frequency is the number of observations in one full seasonal cycle, not how often you measured. For monthly data with a yearly season it's 12; for quarterly data it's 4; for daily data with a weekly pattern it's 7. R uses this to know where each season begins and ends, which is what makes decompose(), seasonal plots, and Holt-Winters work. Set it wrong and the seasonal component will be meaningless.
When should I use an additive versus a multiplicative decomposition?
Use additive (the default) when the size of the seasonal swings stays roughly constant regardless of the overall level — the ups and downs look the same height across the whole plot. Use multiplicative when the swings grow as the series climbs, so a busy month is a fixed percentage above average rather than a fixed number. AirPassengers is the textbook multiplicative example: the yearly fluctuations get visibly bigger as traffic grows.
What is the ACF plot telling me?
The autocorrelation function measures how strongly each value relates to the value a certain number of steps (lags) earlier. Bars that stick out beyond the dashed significance band indicate real, repeating structure. A slow, gradual decline across many lags usually signals a trend; a sharp spike at the seasonal lag (12 for monthly data) signals seasonality. The ACF is your first diagnostic for choosing a model.
HoltWinters or the forecast package — which should I use?
HoltWinters() ships with base R and is great for learning exponential smoothing: it tracks level, trend, and season and projects them forward. For serious work, install the forecast package and use auto.arima() or ets(), which automatically select a good model and, crucially, return prediction intervals so you can show the uncertainty of your forecast. The modern fable/tsibble ecosystem takes this further with tidy syntax, but forecast is the proven workhorse.