31  Slope graphs

Graphs in this chapter use fmggp2::theme_savedbythebell(), built from the Saved by the Bell logo: a black background with a royal blue frame, a yellow title, a white classroom panel, sky blue, pink, yellow, orange, royal blue, and navy marks, and a SignPainter title.


Requirements:

✅ a categorical variable

✅ a numeric (continuous) variable

✅ a categorical variable with two ordered time points

31.1 Description

A slope graph shows how a number changes from one time to the next, most often across two times.1 The number goes up the y axis, and the times go across the x axis. A line joins the two numbers for each group,2 so how far a line goes up or down shows how much that group changed (which is where the name comes from).3

We can make slope graphs in ggplot2 with geom_line() and geom_point().

31.2 Set up

PACKAGES:

Bring in the code we’ll use.4

show/hide
library(ggplot2)
library(dplyr) # for data manipulation
library(tidyr) # for expand_grid()

DATA:

Bayside High is a school from a show, run by a man named Belding. His school gave us six students who feel very different ways about school work they take home:5 Zack finds ways to get out of it, Screech and Jessie do well on everything, and Slater and Kelly are in the middle. A quiz is a short test, and a score is the number of points a student gets on a quiz. A semester is half of a school year. We’ll make up6 ten biology quizzes and ten history quizzes for each student in the fall of 1990 and the spring of 1991. Each student gets an average score for each semester (avg_score). Zack and Slater do better in the spring, Lisa does a little worse, and Kelly and Jessie stay just the same. rnorm() picks each quiz score close to those averages,7 and pmin() keeps every score at 100 or under.8 Starting with set.seed()9 means you’ll get the same 240 quizzes every time you run the code below.

show/hide
set.seed(1989) # same quiz scores every time the code runs
bayside_quizzes <- expand_grid(
  student = c("Zack", "Kelly", "Slater", "Jessie", "Lisa", "Screech"), # six Bayside students
  semester = c("Fall 1990", "Spring 1991"), # two semesters
  subject = c("Biology", "History"), # two subjects
  quiz = 1:10 # ten quizzes in each subject
  ) |>
  mutate(
    student = factor(student, levels = unique(student)), # keep the order above
    avg_score = case_when( # each student's average score
      student == "Zack" ~ if_else(semester == "Fall 1990", 70, 80), # improves
      student == "Kelly" ~ 82, # holds steady
      student == "Slater" ~ if_else(semester == "Fall 1990", 72, 78), # improves
      student == "Jessie" ~ 91, # holds steady
      student == "Lisa" ~ if_else(semester == "Fall 1990", 84, 81), # slips a little
      student == "Screech" ~ if_else(semester == "Fall 1990", 93, 95) # improves a little
      ),
    score = round(pmin(100, rnorm(n(), mean = avg_score, sd = 8))) # no score above 100
    )
glimpse(bayside_quizzes)
#> Rows: 240
#> Columns: 6
#> $ student   <fct> Zack, Zack, Zack, Zack, Zack, …
#> $ semester  <chr> "Fall 1990", "Fall 1990", "Fal…
#> $ subject   <chr> "Biology", "Biology", "Biology…
#> $ quiz      <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,…
#> $ avg_score <dbl> 70, 70, 70, 70, 70, 70, 70, 70…
#> $ score     <dbl> 79, 79, 55, 68, 65, 67, 72, 74…

A slope graph needs one number for each student in each semester, so we’ll find the average of the 20 quizzes in each semester and keep the results in bayside_slope.

show/hide
bayside_slope <- bayside_quizzes |>
  group_by(semester, student) |> # one group per student per semester
  summarise(avg_score = mean(score)) |> # average across both subjects
  ungroup()
glimpse(bayside_slope)
#> Rows: 12
#> Columns: 3
#> $ semester  <chr> "Fall 1990", "Fall 1990", "Fal…
#> $ student   <fct> Zack, Kelly, Slater, Jessie, L…
#> $ avg_score <dbl> 70.00, 83.70, 72.55, 88.30, 83…

31.3 Distribution

How far each line goes up or down10 is the difference between two averages. Averages move around much less than single quiz scores, but they still move a little, so a slope graph can show a small change up or down even when nothing changed. Below we’ll move from the quizzes, to a graph, to the rule behind it,11 and work out how big a change needs to be before we take it seriously.

COUNT IT:

Start with Zack’s first five biology quizzes from the fall.

show/hide
bayside_quizzes |>
  filter(student == "Zack", semester == "Fall 1990") |>
  select(subject, quiz, score) |>
  head(5)

A single quiz score usually lands about 6 points above or below the student’s average, and a few land more than 14 points away. Below we compare the change in each student’s average from fall to spring (change) with the change we built into the data we made up (true_change).

show/hide
slope_changes <- bayside_quizzes |>
  group_by(student, semester) |>
  summarise(avg = mean(score), truth = first(avg_score),
            .groups = "drop") |>
  pivot_wider(names_from = semester, values_from = c(avg, truth)) |>
  mutate(change = `avg_Spring 1991` - `avg_Fall 1990`,
         true_change = `truth_Spring 1991` - `truth_Fall 1990`) |>
  select(student, change, true_change)
slope_changes

Zack’s and Slater’s averages went up, just as they should. But Kelly’s average went down by about 4.5 points, even though the average we gave her didn’t change at all.12

DRAW IT:

The graph below shows every quiz for Zack and Kelly as a point we can see through,13 with the slope graph lines going through their averages for each semester. Zack’s group of points moves up in the spring. Kelly’s two groups of points are in about the same place, and her line goes down only because her fall quizzes happened to land a little high (about 84) and her spring quizzes a little low (about 79), even though we gave her 82 in both.

show/hide
zack_kelly <- bayside_quizzes |>
  filter(student %in% c("Zack", "Kelly"))
ggplot(zack_kelly, aes(x = semester, y = score, color = student)) +
  geom_jitter(width = 0.08, height = 0, alpha = 1 / 3, size = 2) +
  stat_summary(aes(group = student), fun = mean, geom = "line",
               linewidth = 2) +
  stat_summary(fun = mean, geom = "point", size = 4) +
  labs(x = NULL, y = "Quiz score", color = "Student")

NAME IT:

A hill is a line that is high in the middle and low on both sides. Each quiz score is picked from a hill14 with its middle at the student’s average for that semester (and kept at 100 or under):

\[ \text{score} \sim \text{Normal}(\mu_{\text{student, semester}}, \; \sigma = 8) \]

An average of \(n = 20\) quizzes usually falls about \(\sigma / \sqrt{n} = 8 / \sqrt{20} \approx 1.8\) points from the true average.15 A slope is the difference between two of these averages, so it usually falls about \(\sqrt{1.8^2 + 1.8^2} \approx 2.5\) points from the true change.16 About 95% of slopes land within two times that (about 5 points) of the true change. Kelly’s drop is inside that range, while Zack and Slater went up by enough to stand out. Try changing quiz = 1:10 to quiz = 1:40, and every slope will move around half as much.17

31.4 Grammar

CODE:

  • Make the words around the graph18 with labs()

  • Start the graph with ggplot() and give it data

  • Put semester on the x axis and avg_score on the y axis, and join student to group so each student gets one line19

  • Add geom_line() on top,20 join student to color,21 and set linewidth to 2

  • Add geom_point(), join student to color, and set size to 4

  • We’ll move the x axis to the "top" of the graph with scale_x_discrete()

show/hide
labs_slope <- labs(
        title = "Bayside High quizzes",
        caption = "Simulated data",
        subtitle = "Fall 1990 & Spring 1991",
        x = NULL, y = "Average quiz score",
        color = "Student")

ggp2_slope <- ggplot(data = bayside_slope,
    mapping = aes(x = semester,
                y = avg_score,
                group = student)) +
  geom_line(aes(color = student),
                linewidth = 2, show.legend = FALSE) +
  geom_point(aes(color = student),
                size = 4) +
  scale_x_discrete(position = "top")

ggp2_slope +
    labs_slope

GRAPH:

31.5 More info

We can also cut a slope graph into small graphs22 to show a third column of groups.23

DATA:

We’ll use the bayside_quizzes data again, but this time we put it into groups by semester, student, and subject.

show/hide
bayside_grp_slope <- bayside_quizzes |>
    group_by(semester, student, subject) |> # one group per student, semester, and subject
    summarise(avg_score = mean(score)) |> # average across the ten quizzes
    ungroup()
glimpse(bayside_grp_slope)
#> Rows: 24
#> Columns: 4
#> $ semester  <chr> "Fall 1990", "Fall 1990", "Fal…
#> $ student   <fct> Zack, Zack, Kelly, Kelly, Slat…
#> $ subject   <chr> "Biology", "History", "Biology…
#> $ avg_score <dbl> 69.4, 70.6, 86.6, 80.8, 70.9, …

GRAPH:

  • Make the words around the graph with labs()

  • Start the graph with ggplot() and give it data

  • Put semester on the x axis and avg_score on the y axis, and join student to group so each student gets one line

  • Add geom_line() on top, join student to color, and set linewidth to 2

  • Add geom_point(), join student to color, and set size to 4

  • We’ll move the x axis to the "top" of the graph with scale_x_discrete()

  • Last, we make one small graph for each subject,24 set the size of the words, and move the key25 to the "bottom" of the graph.

show/hide
labs_grp_slope <- labs(
    title = "Bayside High quizzes",
    caption = "Simulated data",
    subtitle = "Fall 1990 & Spring 1991, by subject",
    x = NULL, y = "Average quiz score",
    color = "Student")

ggp2_grp_slope <- ggplot(data = bayside_grp_slope,
    mapping = aes(x = semester,
        y = avg_score,
        group = student)) +
    geom_line(aes(color = student),
        linewidth = 2, show.legend = FALSE) +
    geom_point(aes(color = student),
        size = 4) +
    scale_x_discrete(position = "top") +
    facet_wrap(. ~ subject,
        ncol = 2) +
    theme(
        legend.position = "bottom",
        axis.text.x = element_text(size = 9),
        axis.text.y = element_text(size = 9),
        strip.text = element_text(size = 10))

ggp2_grp_slope +
    labs_grp_slope

Each average for a subject comes from only ten quizzes, so these slopes move around more than the ones above (they usually fall about \(\sqrt{2} \times 8 / \sqrt{10} \approx 3.6\) points from the true change).26 That’s why Jessie’s history line goes up while her biology line goes down, even though the average we gave her is the same in both semesters.


  1. Slope graphs show changes in a numeric value, typically over two points in time.↩︎

  2. Each group or unit of measurement.↩︎

  3. Differences between the two time points are represented by the slope of the lines (hence the name).↩︎

  4. Install and load the packages.↩︎

  5. Very different relationships to homework.↩︎

  6. Simulate.↩︎

  7. rnorm() draws each quiz score from a normal distribution around those averages, with sd = 8.↩︎

  8. pmin() caps the scores at 100.↩︎

  9. Setting the random seed.↩︎

  10. The slope of each line.↩︎

  11. The formula.↩︎

  12. Her true average.↩︎

  13. A faded (partially transparent) point, set with alpha.↩︎

  14. A normal distribution centered on the student’s average.↩︎

  15. Its standard deviation, the standard error.↩︎

  16. The standard deviation of a slope.↩︎

  17. The noise in every slope is cut in half.↩︎

  18. Labels.↩︎

  19. Map semester to x, avg_score to y, and student to group.↩︎

  20. A layer.↩︎

  21. Map student to the color aesthetic.↩︎

  22. Facets.↩︎

  23. A third categorical variable.↩︎

  24. Facet by subject with facet_wrap().↩︎

  25. The legend.↩︎

  26. Their standard deviation.↩︎