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

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 bands

Your 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 here

Common Errors (and the fix)

Pro Tips

📋 Quick Reference — Time Series

TaskR Syntax
Create monthly tsts(x, start = c(2023,1), frequency = 12)
Plot over timeplot(y)
Split componentsdecompose(y)
Autocorrelationacf(y)
Smoothing forecastHoltWinters(y)
Auto ARIMA forecastforecast(auto.arima(y), h = 6)

🎉 Lesson Complete!

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

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.