# Line plot for two-way designs using ggplot2

Want to use R to plot the means and compare differences between groups, but don’t know where to start? This post is for you.

As usual, let’s start with a finished example:

```
library(dplyr)
library(ggplot2)
pd <- position_dodge(width = 0.2)
mtcars %>%
mutate(cyl = factor(cyl), am = factor(am, labels = c("automatic", "manual"))) %>%
group_by(cyl, am) %>%
summarise(hp_mean = mean(hp),
hp_ci = 1.96 * sd(hp)/sqrt(n())) %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_line(aes(linetype = am), position = pd) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1, position = pd, linetype = 1) +
geom_point(size = 4, position = pd) +
geom_point(size = 3, position = pd, color = "white") +
guides(linetype = guide_legend("Transmission")) +
labs(title = paste("Mean horsepower depending on",
"number of cylinders and transmission type.",
"Error bars represent 95% Confidence Intervals",
sep = "\n"),
x = "Number of cylinders",
y = "Gross horsepower") +
theme(
panel.background = element_rect(fill = "white"),
legend.key = element_rect(fill = "white"),
axis.line.x = element_line(colour = "black", size = 1),
axis.line.y = element_line(colour = "black", size = 1)
)
```

Let’s break this down.

## Summarising the data

The first challenge is the data. When attempting to make a plot like this in R, I’ve noticed that many people (myself included) start by searching for how to make line plots, etc. in R. This is natural. However, for those who are relatively new to R and are more comfortable with the likes of SPSS, being able to produce the plot isn’t necessarily the place to start. Rather, the first thing you should think about is transforming your data into the points that are going to be plotted.

Imagine the plot you’re about to produce. In our example, each point represents the mean horsepower of some group (based on the number of cylinders and transmission), and error bars represent the 95% confidence intervals. We’re not plotting every point in our data set; we’re plotting very specific summary statistics. So, although programmes like SPSS do this summary behind the scenes for us (and there are ways to make this happen automatically in R), I find that it’s best to explicitly calculate these values as a sanity check and to better understand our data. Let’s get to it.

We’ll be working with the `mtcars`

dataset which comes with R and contains various information about 32 cars from 1974. Run `?mtcars`

from your console to learn more. Here, we’re interested in plotting the mean gross horsepower (`hp`

) as a function of two categorical variables: the number of cylinders (`cyl`

) and the transmission type (`am`

). As a quick aside, note that this example will, therefore, be directly applicable to plotting the mean of a continuous variable grouped by two categorical variables (e.g., from a two-way experimental design).

Let’s focus on the relevant columns and convert the grouping variables to factors. To do this, we’ll use `select()`

and `mutate()`

from the `dplyr`

package. We won’t bother giving `cyl`

labels (as they’re already the numbers we want), but we’ll label `am`

so we don’t need to worry about converting 0s and 1s in our head.

```
library(dplyr)
d <- mtcars %>%
select(cyl, am, hp) %>% # select relevant variables
mutate(cyl = factor(cyl), # Convert grouping variables to factors
am = factor(am, labels = c("automatic", "manual")))
head(d)
#> cyl am hp
#> 1 6 manual 110
#> 2 6 manual 110
#> 3 4 manual 93
#> 4 6 automatic 110
#> 5 8 automatic 175
#> 6 6 automatic 105
```

We need the mean horsepower for each cylinder-transmission combination. This is best achieved with a couple more functions from `dplyr`

: group the data by the factors with `group_by()`

, and `summarise()`

to compute the means:

```
d %>%
group_by(cyl, am) %>%
summarise(hp_mean = mean(hp))
#> Source: local data frame [6 x 3]
#> Groups: cyl [?]
#>
#> cyl am hp_mean
#> <fctr> <fctr> <dbl>
#> 1 4 automatic 84.66667
#> 2 4 manual 81.87500
#> 3 6 automatic 115.25000
#> 4 6 manual 131.66667
#> 5 8 automatic 194.16667
#> 6 8 manual 299.50000
```

We now have a mean column which represents where the points will go on in the plot. Let’s also compute the distance from the mean to the 95% confidence interval (`hp_ci`

) in anticipation of the error bars and save the resulting data frame:

```
sum_d <- d %>%
group_by(cyl, am) %>%
summarise(hp_mean = mean(hp),
hp_ci = 1.96 * sd(hp)/sqrt(n()))
sum_d
#> Source: local data frame [6 x 4]
#> Groups: cyl [?]
#>
#> cyl am hp_mean hp_ci
#> <fctr> <fctr> <dbl> <dbl>
#> 1 4 automatic 84.66667 22.242138
#> 2 4 manual 81.87500 15.699402
#> 3 6 automatic 115.25000 8.995204
#> 4 6 manual 131.66667 42.466667
#> 5 8 automatic 194.16667 18.875105
#> 6 8 manual 299.50000 69.580000
```

For those a little rusty with statistics, `sd(hp)/sqrt(n())`

gives us the standard error of the mean, and multiplying this by 1.96 gives us the distance from the mean to the 95% interval boundary (which we then add or subtract from the mean).

Great, we’ve now got a data frame of the information we need to produce the plot.

## Creating the plot

To create the plot, we’ll use the `ggplot2`

package. When you create a plot with `ggplot2`

, you build up layers of graphics. It’s important to keep this idea of layering in mind as we gradually build the plot.

### Setup

To start, we’ll set up a blank plot canvas with relevant x and y-axes using `ggplot()`

:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean))
```

### Adding points

Next, we want to represent our data in our plot. `ggplot2`

uses various geoms to do this, which are layered into the plot using `+`

. Let’s start with the points themselves by using `geom_point()`

:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean)) +
geom_point()
```

### Adding lines

Next we’ll add the lines with `geom_line()`

:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean)) +
geom_point() +
geom_line()
```

Hmm, that doesn’t seem right! What’s going on here? The problem is that we haven’t specified that the lines should be grouped by transmission, so it’s just using the already provided number of cylinders. To handle this, we assign the `group`

and `linetype`

aesthetics to our second categorical variable, `am`

. Note that `group`

is handled in `ggplot`

, but `linetype`

is in `geom_line()`

. These can be moved around, but having `group`

in `ggplot`

is important for the position adjustment discussed later.

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_point() +
geom_line(aes(linetype = am))
```

### Adding error bars

The last visual element to add is the error bars using `geom_errorbar()`

, which requires us to specify `ymin`

and `ymax`

for each error bar. I like to do the calculation within this function as follows:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_point() +
geom_line(aes(linetype = am)) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci))
```

OK, not looking perfect yet, but let’s quickly discuss how the error bars are working. By declaring `ymin = hp_mean - hp_ci`

, we’re saying that the minimum level of the error bar for each point will be `hp_mean`

(which is the position of the point itself) minus the distance to the confidence interval boundary (one standard error multiplied by 1.96). For `ymax`

, we add rather than subtract, thus giving us the upper bound for the 95% confidence interval. A quick note, you can change these error bar values to whatever suits you (e.g., could drop `* 1.96`

from the original calculation for a single standard error).

## Make it pretty

We’ve got all the visual elements we need. Now, it’s all about making the plot look nice.

### Error bars

Let’s start by making those error bars a little less intense by reducing their `width`

:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_point() +
geom_line(aes(linetype = am)) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1) # Reduce the width of the error bars
```

### Points

Next, let’s get the points to be big white circles with black outlines. We’ll start by adjusting the `size`

and `color`

of the points:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_point(size = 3, color = "white") + # Increase point size and change to white.
geom_line(aes(linetype = am)) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1)
```

What’s gone wrong here? Well, a couple of things. Firstly, the points are being obscured by the lines and error bars. Secondly, the points don’t have a black outline. Handling the first problem is easier. Recall that we’re building the plot up with layers. So `ggplot2`

places each layer on top of previous ones. Thus, the order in which we create the geoms will determine which layers fall on top of the others. We created the `geom_point()`

layer before `geom_line()`

and `geom_errorbar()`

, so the points appear behind them. Therefore, the fix is easy – add the points AFTER the other layers as such:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_line(aes(linetype = am)) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1) +
geom_point(size = 3, color = "white")
```

Now, what about those black outlines? Thanks to the layering, we can put black points, which are slightly larger in `size`

than the white points, in the plot before, and thus behind, the white points. Doing this creates the effect that the points have an outline. Admittedly, this is a little bit of trickery for which a better approach might exist, but this is how I do it:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_line(aes(linetype = am)) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1) +
geom_point(size = 4) + # Add black points first; larger size than white points
geom_point(size = 3, color = "white")
```

If you were to comment out the last line of the above code, you’ll see that it’s just large black points that are being plotted, and over which we’re putting smaller white ones.

### Position

Next, we’ll adjust the `position`

of the points so that they, and their error bars, don’t overlap. First, remember that there are multiple geom layers: the lines, the error bars, and two point layers. Each of these has to be adjusted separately. We can do this inside our pipe, but to save the effort when tweaking the value, let’s set a single adjustment variable as follows:

```
pd <- position_dodge(width = 0.2)
```

Now, we add `position = pd`

into all the geom layers:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_line(aes(linetype = am), position = pd) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1, position = pd) +
geom_point(size = 4, position = pd) +
geom_point(size = 3, color = "white", position = pd)
```

### Labels

Almost there. Now, we need to fix the labels of the axes and the legend and add a title. The axes and title can be handled together using `labs()`

. The legend, however, needs to be managed separately using `guides()`

. To create a multi-line title, I use `paste()`

, and separate each section (`sep =`

) of text with `"\n"`

, which represents a new line.

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_line(aes(linetype = am), position = pd) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1, position = pd) +
geom_point(size = 4, position = pd) +
geom_point(size = 3, color = "white", position = pd) +
guides(linetype = guide_legend("Transmission")) + # Change the legend title
labs(title = paste("Mean horsepower depending on", # Add a multi-line title
"number of cylinders and transmission type.",
"Error bars represent 95% Confidence Intervals",
sep = "\n"),
x = "Number of cylinders", # Change x-axis label
y = "Gross horsepower") # Change y-axis label
```

### Theme

The final piece of the puzzle is to add a theme that removes the background grid and adds some solid lines to the x and y-axes:

```
sum_d %>%
ggplot(aes(x = cyl, y = hp_mean, group = am)) +
geom_line(aes(linetype = am), position = pd) +
geom_errorbar(aes(ymin = hp_mean - hp_ci, ymax = hp_mean + hp_ci),
width = .1, position = pd) +
geom_point(size = 4, position = pd) +
geom_point(size = 3, color = "white", position = pd) +
guides(linetype = guide_legend("Transmission")) +
labs(title = paste("Mean horsepower depending on",
"number of cylinders and transmission type",
"Error bars represent 95% Confidence Intervals",
sep = "\n"),
x = "Number of cylinders",
y = "Gross horsepower") +
theme(
panel.background = element_rect(fill = "white"), # Set plot background to white
legend.key = element_rect(fill = "white"), # Set legend item backgrounds to white
axis.line.x = element_line(colour = "black", size = 1), # Add line to x axis
axis.line.y = element_line(colour = "black", size = 1) # Add line to y axis
)
```

The only difference between this and the example at the beginning is that the data preparation (computing mean and confidence interval distance) is handled within a single pipe.

## Sign off

Thanks for reading and I hope this was useful for you.

For updates of recent blog posts, follow @drsimonj on Twitter, or email me at drsimonjackson@gmail.com to get in touch.

If you’d like the code that produced this blog, check out my GitHub repository, blogR.