13  Visualizing Trends

Class Objectives:
  1. Trend of a single variable
  2. Smoothed trends
  3. Trend of multiple groups
Packages for this chapter

If you haven’t installed the following packages, run this code:

pak::pak(c("tidyverse", "janitor", "mgcv", "gghighlight", "plotly", "ggalt", "zoo", "ggstream", "ggh4x", "ggrepel", "MetBrewer", "hrbrthemes"))

Trend plots show how a measured quantity changes along an ordered variable, usually time. Start with the observed values, then decide whether a line, a summary, or a smoother helps answer your question. Connecting observations implies an order; it is not appropriate for unrelated categories.

As in Chapter 12, load tidyverse to use its functions directly, and use package::function() for other packages, including janitor::clean_names() to clean column names. We use a different theme this time, with a generic font so the examples do not require extra font installation.

library(tidyverse)
theme_set(hrbrthemes::theme_ipsum(base_family = "sans"))
1
Set the theme for the plots.

13.1 A Basic Trend Plot

We use economics, a historical monthly dataset. Its unemploy column counts unemployed people in thousands; it is not an unemployment rate. First, clean the names and put the observations in chronological order. The names are already tidy here, but we keep the same preparation workflow as in the data-import chapter.

economics_monthly <- economics |>
    janitor::clean_names() |>
    arrange(date)

economics_recent <- economics_monthly |>
    filter(year(date) > 2000)

For the first examples, select January from each year to make the individual observations easy to see. These are January snapshots, not annual averages.

economics_recent |>
    filter(month(date) == 1) |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_point(color = "white", fill = "#0072B2", shape = 21, size = 2)

The date stays on a continuous time axis, so the distance between points represents elapsed time.

The points show individual measurements. Connecting them makes changes between successive observations easier to follow.

Connect the dots with a line to reveal the trend. This is easy to do with geom_line().

economics_recent |>
    filter(month(date) == 1) |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_line(linewidth = 1, color = "#0072B2") +
    geom_point(color = "white", fill = "#0072B2", shape = 21, size = 4)

A white border helps the points stand out against the line. geom_line() connects points in x order; geom_path() follows the row order instead. Neither one establishes what happened between observations. If a scheduled observation is missing, keep its date with an NA value to show a gap rather than silently connecting across it.

An interpolating spline draws a curved connection between observations. ggalt::geom_xspline() provides this option. The curve is a drawing choice, not a fitted statistical model: it can suggest peaks or dips between dates where no measurement was taken. Prefer straight segments when those extra shapes could mislead.

economics_recent |>
    filter(month(date) == 1) |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    ggalt::geom_xspline(color = "#0072B2") +
    geom_point(color = "white", fill = "#0072B2", shape = 21, size = 4)

An area plot emphasizes the magnitude of a nonnegative quantity by filling the space between the line and zero. Use it when zero is a meaningful baseline, and keep that baseline visible. Draw the area first so it sits behind the line and points.

economics_recent |>
    filter(month(date) == 1) |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_area(fill = "#0072B2", alpha = 0.2) +
    geom_line(linewidth = 1, color = "#0072B2") +
    geom_point(color = "white", fill = "#0072B2", shape = 21, size = 4)

13.2 Dealing with Many Points

With monthly observations, large point symbols can crowd the line. Smaller points or a line alone may be enough; smoothing is useful when the question concerns changes over a longer timescale.

economics_recent |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_line(color = "#0072B2") +
    geom_point(color = "white", fill = "#0072B2", shape = 21, size = 3)

Below are some alternatives to consider, from the least to the most modelling.

The simplest fix is to remove clutter before adding anything: drop the points and let the line carry the trend.

economics_recent |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_line(color = "#0072B2")

If month-to-month wiggles still distract from the question, summarize the data with one of the next approaches.

Aggregating replaces many observations with one summary per coarser time step, such as one mean per year. No model is involved, so the result is easy to explain: “the average monthly unemployment in each year.”

economics_recent |>
    mutate(year = year(date)) |>
    filter(n() == 12, .by = year) |>
    summarise(unemploy = mean(unemploy), .by = year) |>
    ggplot(aes(x = year, y = unemploy)) +
    labs(x = "Year", y = "Mean unemployed people (thousands)") +
    geom_line(color = "#0072B2") +
    geom_point(color = "white", fill = "#0072B2", shape = 21, size = 3)
1
Extract the year from each date.
2
Keep only complete years. The data end in April 2015, and a mean of four months is not comparable with a mean of twelve.
3
Collapse each year to its mean. Use median() instead if a few extreme months should not dominate.

Aggregation also hides change within each period: a recession that starts in October looks like a small rise in that year’s mean.

A moving average replaces each observation with the mean of a nearby window. A longer window removes more short-term variation but can hide brief events and delay turning points.

Use zoo::rollmean() on the chronologically ordered monthly data. Here, 12 rows represent 12 consecutive months. With irregular sampling or missing months, a 12-row window is not necessarily a year: regularize the dates and decide how to handle missing measurements first. Compute the averages before selecting the display period so earlier observations remain available to its first windows.

economics_averages <- economics_monthly |>
    mutate(unemploy_avg_year = zoo::rollmean(unemploy, k = 12, fill = NA, align = "right")) |>
    mutate(unemploy_avg_2years = zoo::rollmean(unemploy, k = 24, fill = NA, align = "right"))

economics_averages |>
    filter(year(date) > 2000) |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_line(aes(color = 'line')) +
    geom_line(aes(y = unemploy_avg_year, color = 'year'), linewidth = 1) +
    geom_line(aes(y = unemploy_avg_2years, color = '2 years'), linewidth = 1) +
    scale_color_manual(
      values = c(`line` = "grey60", `year` = "#d55e00", `2 years` = "#009E73"),
      breaks = c("year", "2 years", "line"),
      labels = c("Trailing 12 months", "Trailing 24 months", "Monthly observations"),
      name = NULL
    ) +
    theme(legend.position = 'top')
1
Calculate the moving average of the current and previous 11 months (align = "right"; the default align = "center" would center the window on each month instead).
2
Calculate the moving average of the current and previous 23 months.
3
Add the moving average for the last 12 months to the plot.
4
Add the moving average for the last 24 months to the plot.
5
Customize the color legend.

The first 11 (or 23) observations in the full series have no complete window, so their averages are NA. They precede the displayed period here. A trailing average uses the current and previous observations; a centered average also uses future observations relative to its plotted date.

Alternatively, you can add a regression smoother to highlight the trend. We have already used the linear regression smoother with geom_smooth(method = "lm") in Chapter 6. It is easy to use other nonlinear methods.

economics_recent |>
    ggplot(aes(x = date, y = unemploy)) +
    labs(x = "Year", y = "Unemployed people (thousands)") +
    geom_point(color = "white", fill = "grey60", shape = 21, size = 3) +
    geom_smooth(method = "loess", span = 0.3, se = FALSE)
1
Fit a LOESS (local regression) smoother. span is the fraction of the data used for each local fit: smaller values follow the data more closely, larger values give a smoother curve. Try 0.1 and 0.75 to see the difference.

Specify the smoothing method and keep the observations visible. We set se = FALSE because this is a descriptive trend: a reliable uncertainty interval for time-series data needs to account for dependence between nearby observations. The default smoother ribbon is not a prediction interval or a forecast.

Additionally, when handling a lot of data, you can fit the model yourself once (for example with mgcv::gam()) and plot its predictions, instead of letting ggplot2 refit the smoother every time you draw the plot.

13.4 Choosing a Trend Plot

Question Useful starting point
What changed between observations? Points connected by straight lines
What is the longer-term pattern? Observations plus a clearly specified moving average or smoother
How do group trajectories compare? Facets with shared axes, or a few labeled lines
How do groups contribute to a total? Stacked areas with a zero baseline
How did the ordering change? A bump chart with an explicit tie rule