26  Grouped violin plots

Graphs in this chapter use fmggp2::theme_aim(), modeled on the AOL Instant Messenger sign on window: a gray window, an AOL blue frame and title, a white chat panel, blue, sky, running man yellow, and slate marks, and an Arial Rounded title.


Requirements:

✅ a categorical variable

✅ a numeric (continuous) variable

26.1 Description

A ‘violin plot’ is a variation of a density or ridgeline plot, where the distribution is plotted symmetrically, creating a two-sided, smoothed distribution.

26.2 Set up

PACKAGES:

Install packages.

show/hide
library(ggplot2)
library(dplyr) # for tibble() and data manipulation

DATA:

In the late 90s, your AOL Instant Messenger (AIM) buddy list was a status symbol. We’ll simulate buddy list sizes for 150 users at each level of school, and each level gets a different kind of distribution. Middle schoolers’ lists are counts from rpois(). High schoolers’ lists come from rlnorm(), so a few popular kids have much longer lists. College students are a mix of two groups (runif() decides which): most have about 60 buddies, and about 30% are heavy users with about 150. Setting the seed with set.seed() means you’ll get the same 450 users every time you run the code below.

show/hide
set.seed(1997) # same 450 users every time the code runs
aim_buddies <- tibble(
  user_id = 1:450, # 450 different AIM users, one row each
  school = rep(c("Middle school", "High school", "College"), each = 150) # 150 users per level
  ) |>
  mutate(
    school = factor(school,
      levels = c("Middle school", "High school", "College")), # youngest to oldest
    buddies = case_when( # size of each buddy list
      school == "Middle school" ~ rpois(n(), lambda = 20), # short, tight lists
      school == "High school" ~ round(rlnorm(n(), meanlog = log(45),
                                             sdlog = 0.45)), # longer lists with a right tail
      TRUE ~ round(if_else(runif(n()) < 0.7, # college has two kinds of users
                           rnorm(n(), mean = 60, sd = 10), # most keep about 60 buddies
                           rnorm(n(), mean = 150, sd = 20))) # some keep about 150
      )
    )
glimpse(aim_buddies)
#> Rows: 450
#> Columns: 3
#> $ user_id <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 1…
#> $ school  <fct> Middle school, Middle school, Mi…
#> $ buddies <dbl> 16, 23, 21, 25, 18, 18, 11, 23, …

26.3 Distribution

Violin plots are at their best when groups differ in shape, not just in center and spread. Summary numbers (and box plots built from them) can make three very different distributions look alike. Below we’ll move from the users, to a picture, to the formula.

COUNT IT:

Start with the first eight users (all middle schoolers).

show/hide
head(aim_buddies$buddies, 8)
#> [1] 16 23 21 25 18 18 11 23

These middle schoolers have between 11 and 25 buddies. The summary below shows each group’s mean, median, and standard deviation. For middle schoolers the mean and median are close, for high schoolers the mean is a little higher, and for college students the mean is almost 20 buddies higher than the median. The numbers hint that something is different about the college group, but they don’t tell us what.

show/hide
aim_buddies |> 
  group_by(school) |> 
  summarise(mean = mean(buddies), median = median(buddies), 
            sd = sd(buddies))

DRAW IT:

The graph below draws a narrow box plot inside each violin. The middle school violin is a short, symmetric diamond. The high school violin has a wide base and a tail that stretches up. The college violin has two bulges, one near 60 buddies and one near 150, with almost nobody in between. The college box plot hides that gap completely: it looks like one long, lopsided box.

show/hide
ggplot(aim_buddies, aes(x = school, y = buddies, fill = school)) + 
  geom_violin(alpha = 1/2, show.legend = FALSE) + 
  geom_boxplot(width = 0.12, fill = "#FFFFFF", outlier.shape = NA) + 
  labs(x = NULL, y = "Buddies")

NAME IT:

Each level of school has its own distribution:

\[ \begin{aligned} \text{Middle school} &\sim \text{Poisson}(\lambda = 20) \\ \log(\text{High school}) &\sim \text{Normal}(\log(45), \; 0.45) \\ \text{College} &\sim 0.7 \cdot \text{Normal}(60, 10) + 0.3 \cdot \text{Normal}(150, 20) \end{aligned} \]

The Poisson gives a narrow, nearly symmetric violin because its standard deviation is only \(\sqrt{20} \approx 4.5\). The log normal gives the long upper tail. The mixture gives two peaks, and its mean (\(0.7 \times 60 + 0.3 \times 150 = 87\)) lands in the empty space between them. A box plot only knows about quartiles, so it can’t show a gap like that. Try moving the two college means closer together in rnorm() and the two bulges merge into one.

26.4 Grammar

CODE:

  • Create labels with labs()

  • Initialize the graph with ggplot() and provide data

  • Map school to the x, buddies to the y, and school to fill

  • Set alpha to 2/3

  • Remove the legend with show.legend = FALSE

show/hide
labs_grp_violin <- labs(
  title = "AOL Instant Messenger",
  caption = "Simulated data",
  subtitle = "Buddy list sizes for 150 users at each level of school",
  x = "School", fill = "School",
  y = "Buddies")
ggp2_grp_violin <- ggplot(data = aim_buddies,
       aes(x = school,
           y = buddies,
        fill = school)) +
  geom_violin(alpha = 2/3,
      show.legend = FALSE)
ggp2_grp_violin +
  labs_grp_violin

GRAPH:

Violin plots can allow us to compare the ‘center’ and ‘spread’ of continuous variables across categorical groups.

show/hide
labs_grp_violin <- labs(
  title = "AOL Instant Messenger",
  caption = "Simulated data",
  subtitle = "Buddy list sizes for 150 users at each level of school",
  x = "School", fill = "School",
  y = "Buddies")
ggp2_grp_violin <- ggplot(data = aim_buddies,
       aes(x = school,
           y = buddies,
        fill = school)) +
  geom_violin(alpha = 2/3,
      show.legend = FALSE)
ggp2_grp_violin +
  labs_grp_violin

26.5 More info

26.5.1 Quartiles

Change the shape of the line with linetype and linewidth.

We can include lines for the 25th, 50th, and 75th percentiles (the three quartiles) using the quantiles argument, and draw them with quantile.linetype = 1.

show/hide
ggplot(data = aim_buddies,
       aes(x = school,
           y = buddies,
        fill = school)) +
  geom_violin(
      quantiles = c(0.25, 0.5, 0.75),
      quantile.linetype = 1,
      alpha = 1/2,
      linewidth = 0.5,
      show.legend = FALSE)

26.5.2 Kernel

The kernel argument lets us change the “kernel density estimate” used to create the violin shape. The possible kernel density estimates are "gaussian", "epanechnikov", "rectangular", "triangular", "biweight", "cosine", and "optcosine".

show/hide
ggplot(data = aim_buddies,
       aes(x = school,
           y = buddies,
        fill = school)) +
  geom_violin(alpha = 1/2,
      linewidth = 0.5,
      kernel = "rectangular",
      show.legend = FALSE) +
  labs_grp_violin +
  labs(caption = "kernel = rectangular")

26.5.3 Bandwidth

We can directly adjust the shape of the violin with the bw argument, which is the standard deviation of the smoothing kernel. The trim argument trim(s) the tails of the violins to the range of the data.

show/hide
# bw of 2
grp_violin_bw2 <- ggplot(data = aim_buddies,
       aes(x = school,
           y = buddies,
        fill = school)) +
  geom_violin(bw = 2,
      alpha = 2/3,
      trim = TRUE,
      show.legend = FALSE)
grp_violin_bw2 +
    labs_grp_violin +
    labs(caption = "bw = 2")
# bw of 20
grp_violin_bw20 <- ggplot(data = aim_buddies,
       aes(x = school,
           y = buddies,
        fill = school)) +
  geom_violin(bw = 20,
      alpha = 2/3,
      trim = TRUE,
      show.legend = FALSE)
grp_violin_bw20 +
    labs_grp_violin +
    labs(caption = "bw = 20")