Search code examples
rggplot2errorbaraesthetics

Wrong position of dodged error bars when aes(group = ...) but not aes(fill/shape = ...)


Plotting error bars with position = "dodge" has caused me many headaches lately... Curiously, dodging them with the aesthetics shape or fill (which should not apply for error bars) seem to work well. However, dodging with aesthetics group places the bars in unexpected positions. I was wondering if this might be a ggplot2 bug.

I like placing custom error bars behind bar plots or boxplots. Sometimes I give special colors to different elements of my plots. For this reason, I often include aes() not in the ggplot() function, but in the geoms or stats.

Here is an example of "well placed" error bars:

library(ggplot2)
library(dplyr)

ToothGrowth %>% 
  mutate(dose = factor(dose)) %>% 
  ggplot(aes(dose, len)) +
  stat_boxplot(aes(fill = supp), geom = "errorbar", position = "dodge") + 
  geom_boxplot(aes(fill = supp), position = "dodge", coef = 0) 

Plot1

This produces the warning Warning: Ignoring unknown aesthetics: fill. Using aes(shape = supp) prints the same plot.

I would expect that same plot, but no warnings by exchanging fill/shape with "group" (aes(group = supp)). This produces no warnings, but a very unexpected result:

ToothGrowth %>% 
  mutate(dose = factor(dose)) %>% 
  ggplot(aes(dose, len)) +
  stat_boxplot(aes(group = supp), geom = "errorbar", position = "dodge") + 
  geom_boxplot(aes(fill = supp), position = "dodge", coef = 0)  

Plot2

Would someone have an explanation for this behavior? Shouldn't grouping with aes(group = ...) and aes(fill = ...) behave similarly on the dodge position?


Solution

  • From ?aes_group_order (emphasis added):

    By default, the group is set to the interaction of all discrete variables in the plot. This often partitions the data correctly, but when it does not, or when no discrete variable is used in the plot, you will need to explicitly define the grouping structure, by mapping group to a variable that has a different value for each group.

    With

    ToothGrowth %>% 
      mutate(dose = factor(dose)) %>% 
      ggplot(aes(dose, len)) +
      stat_boxplot(aes(fill = supp), geom = "errorbar", position = "dodge")
    

    The group for error bars is automatically set to the interaction of dose (which has been converted into a factor, i.e. discrete variable) and supp (which is already a factor in the ToothGrowth dataset). In other words, every combination of dose c(0.5, 1, 1.5) and supp c("OJ", "VJ") is treated as a separate group for the purpose of calculating boxplot summary statistics. As a result, the displayed error bars match the boxplot layer perfectly, even though fill isn't a relevant aesthetic for geom_errorbar.

    With

    ToothGrowth %>% 
      mutate(dose = factor(dose)) %>% 
      ggplot(aes(dose, len)) +
      stat_boxplot(aes(group = supp), geom = "errorbar", position = "dodge")
    

    The group for error bars is explicitly set to be supp and only supp. This overrides the default behaviour, so instead of 6 groups as above, we only have two (one for "OJ" and one for "VJ"). This results in the mismatch between the error bars layer and the boxplot layer.

    You can explicitly set the group mapping to mimic the default behaviour:

    p1 <- ToothGrowth %>%
      mutate(dose = factor(dose)) %>%
      ggplot(aes(dose, len)) +
      stat_boxplot(aes(group = interaction(dose, supp)), geom = "errorbar", position = "dodge") +
      geom_boxplot(aes(fill = supp), position = "dodge", coef = 0)
    p1
    layer_data(p1, 1L) # view data associated with error bar layer
    layer_data(p1, 2L) # view data associated with boxplot layer
    
    p2 <- ToothGrowth %>%
      mutate(dose = factor(dose)) %>%
      ggplot(aes(dose, len)) +
      stat_boxplot(aes(group = interaction(supp, dose)), geom = "errorbar", position = "dodge")+
      geom_boxplot(aes(fill = supp), position = "dodge", coef = 0)
    p2
    layer_data(p2, 1L) # view data associated with error bar layer
    layer_data(p2, 2L) # view data associated with boxplot layer
    

    Note: interaction(dose, supp) and interaction(supp, dose) will result in the same plot, appearance-wise, though if you want to compare the underlying data associated with each layer, interaction(dose, supp) generates groups in the same order as the default, while interaction(supp, dose) does not.