Multivariate Plots

Boxplots and Violinplots

Tutorial Goals

In our first ggplot2 tutorial, we primarily focused on visualizing one variable at a time.

However, we made one exception with boxplots: we compared a numeric variable across multiple groups of a catgorical variable.

In this tutorial, we'll focus more on representing multiple variables together in one plot. This could be comparing two categorical variables, a categorical against a numeric, two numeric, or even 3+ variables in the same plot!

The Palmer Penguins Data

Let's revisit the palmerpenguins package again to compare penguin species.

Something we'd like to try examine more is how the 3 penguins species in this dataset may compare across different factors.

Comparing Species across Flipper Length

One numeric variable we can compare is the flipper length of penguins in each species. Let's do that again with side by side boxplots.

ggplot(data = penguins, aes(x = species, y = flipper_length_mm, fill = species)) +
  geom_boxplot() +
  stat_boxplot(geom = "errorbar")

Orientation Review

Do you remember how to make these appear horizontally instead of vertically?
ggplot(data = penguins, aes(x = ________, y = ________, fill = ________)) +
  geom_boxplot() +
  stat_boxplot(geom = "errorbar")
ggplot(data = penguins, aes(x = flipper_length_mm, y = species, fill = species)) +
  geom_boxplot() +
  stat_boxplot(geom = "errorbar")


Violinplots serve a very similar purpose as boxplots. They are best for comparing multiple groups across a numeric variable. Violinplots, however, show more of the distribution by size, while boxplots only show the positions of the 5-numer summary.

ggplot(data = penguins, aes(x = species, y = flipper_length_mm, fill = species)) +

When we add color, they kind of look like vases, or even Christmas tree ornaments. :)

Does it matter which we use?

Boxplots are cleaner and make efficient comaprisons of median and interquartile range, while violinplots provide more information about the group disributions. It just depends which of those things you value more for a particular plot!

Clustering plots

A third variable we could add to this plot would be the sex of the penguin. In this dataset, sex can be listed as "male" "female" or "NA" (NA likely meaning the information was not collected)

Rather than use fill color as a redundant representation, we can instead represent sex by fill color. Let's try this with the boxplot representation.

ggplot(data = penguins, aes(x = species, y = flipper_length_mm, fill = sex)) +
  geom_boxplot() +
  stat_boxplot(geom = "errorbar")

Subsetting sex

The NA category might be unnecessary clutter here, so let's make a subset to only include the penguins identified as "male" and "female."

penguins_sex = subset(penguins, sex == "male" | sex == "female")

ggplot(data = penguins_sex, aes(x = species, y = flipper_length_mm, fill = sex)) +
  geom_boxplot() +
  stat_boxplot(geom = "errorbar")

Try one?

In a previous tutorial, we briefly looked at the anaesthetic data.


Can you make violinplots to compare the number of breaths it takes under each anaesthetic before the patient can breathe unassisted? Color each violin a different color.

ggplot(data = anaesthetic, aes(x = _________, y = ________, fill = ________)) +
ggplot(data = anaesthetic, aes(x = tgrp, y = breath, fill = tgrp)) +

Strip Charts

What is a Strip Chart?

Strip charts are much like side by side boxplots in that they can compare the distributions of multiple groups, but with all of the data revealed.

Strip charts are often a good choice when there are not a lot of data points and it makes sense to see the individual points. Boxplots and violinplots are often better representations for larger datasets.

An Example

The Strip Chart below again compares the time in minutes for unassisted breathing for participants on one of four different anaesthetics.

And just like before, we can color each group differently. BUT...points are activated by the color argument rather than the fill argument. Think of points as border circles, rather than open plot spaces to be filled.

ggplot(data = anaesthetic, aes(x = tgrp, y = breath, color = tgrp)) +


In this particular data case, you'll notice that our breathing data is rounded to the minute, which results in a lot of identical, overlapping measurements. A great tool for improving a strip chart is to "jitter" the points.

geom_jitter is an alternative to geom_point that jitters points a random distance, but within a maximum range that you can decide.

Notice that geom_jitter will go in place of geom_point

If doing a vertical strip chart, use the width argument to specify the jitter range. You can always experiment with the amount, but generally 0.05 to 0.3 is a good range to consider.

ggplot(data = anaesthetic, aes(x = tgrp, y = breath, color = tgrp)) +
  geom_jitter(width = 0.1)

Horizontal Orientation

Can you plot the same data, but use a horizontal orientation? Since this will be a horizontal plot, you'll need to change the argument to height = for this plot, not width =.

Add an appropriate title!

A suggested solution is available for reference.

ggplot(data = anaesthetic, aes(x = ______, y = ______, color = tgrp)) +
  geom_jitter(_____________) +
ggplot(data = anaesthetic, aes(x = ______, y = ______, color = tgrp)) +
  geom_jitter(height = ___) +
  labs(title = "Breathing until Unassisted by Anaesthetic")
ggplot(data = anaesthetic, aes(x = breath, y = tgrp, color = tgrp)) +
  geom_jitter(height = 0.1) +
  labs(title = "Breathing until Unassisted by Anaesthetic")

Caution with Boxplots and Violinplots

As you might notice now, violinplots and boxplots can't directly communicate how many data points are represented. Unless you have that info available in a label or caption, be cautious of using these representations for relatively small datasets. Representing all of the data points with a strip chart is a great choice in these small data situations!

Densities and Ridge plots

Overlapping Density Curves

While violinplots represent distribution shapes in separate columns, overlapping density curves can represent distributions all in the same plane.

As we mentioned before, the anaesthetic data is not a big dataset, so let's not use this one for density curves alone. Let's use a bigger dataset--the diamonds data housed in the ggplot2 package.


Visualizing Diamond Prices

You might remember the geom_density option. We will again assign a numeric variable to the x axis (like histogram, the y axis is used to compare how many units are relatively in each x axis zone).

ggplot(data = diamonds, aes(x = price)) + 
  geom_density(fill = "orchid")

Looking at Price by Diamond Cut

But now, let's take advantage of the fill argument as a representation for another variable. Let's assign the diamond cut as a fill color so we can compare the price distributions of each diamond cut.

ggplot(data = diamonds, aes(x = price, fill = cut)) + 

Adding Transparency with Alpha

You'll notice that when we add overlap, it's difficult to see the whole story. We should add some transparency to this graph using alpha. Remember that alpha set to 0 is fully transparent, and alpha set to 1 is fully opaque. I plugged in 0.3, but experiment with different values!

ggplot(data = diamonds, aes(x = price, fill = cut)) + 
  geom_density(alpha = 0.3)

Ridgeplots as Alternative

While the transparency helps, it's still a little difficult to compare all of these distributions. Another cool option is a ridgeplot, available in the ggridges package.


A ridgeplot allows us to lift each distribution a bit so that we can better see what's going on. Now that we have loaded the package, we can use the geom_density_ridges geom. We'll also need to assign cut to the y axis now since we are separating out the distributions vertically by cut.

ggplot(data = diamonds, aes(x = price, y = cut, fill = cut)) + 
  geom_density_ridges(alpha = 0.3)

Making sense of the data

If you look at the previous plot more carefully, you might notice that the results seem unintuitive. Why would diamonds of lower cut quality be priced higher on average?

We won't answer that here, but we may return to this in a future tutorial! Keep in mind this is observational study data--what other diamond characteristics contribute to its price? Maybe this is an example of Simpson's Paradox--perhaps "Fair" diamonds are more likely to have some characteristic that would give them higher value.

Practice: Diamond Carat by Cut

Now create a plot to look at the distribution of diamond carat. Let's create separate distribution curves based on the diamond's cut. Try creating a ridge plot to do this (though you may also experiment with overlappint density curves).

Use an appropriate title.

ggplot(__________________________)) +
  geom___________() +
ggplot(data = diamonds, aes(x = carat, y = _____, fill = _____)) +
  geom___________() +
  labs(title = "Carat Value by Cut")
ggplot(data = diamonds, aes(x = carat, y = cut, fill = cut)) +
  geom_density_ridges() +
  labs(title = "Carat Value by Cut")

Now try it as a Violinplot

Let's do the same comparison, but this time create violinplots. You'll notice that both geometries reveal similar information, just in slightly different aesthetics. Also, we typically plot violins vertically (they can go horizontal, but I think it just looks better this way).

ggplot(__________, aes(_________)) +
  geom____________ +
ggplot(data = diamonds, aes(x = cut, y = _______, fill = _________)) +
  geom_______() +
  labs(title = "Carat Distribution by Cut")
ggplot(data = diamonds, aes(x = cut, y = carat, fill = cut)) +
  geom_violin() +
  labs(title = "Carat Distribution by Cut")

Barplots Revisited

Barplots Review

In a previous tutorial, we looked at barplots as a way to visualize one categorical variable. For example, if we continue with the diamonds dataset, we can see how many diamonds we have of each cut category.

ggplot(data = diamonds, aes(x = cut, fill = cut)) +

This is a fairly massive dataset (almost 60,000 diamonds!), though relatively, we have much fewer "Fair" cut diamonds (around 2,000), whereas there are many more diamonds of higher cut qualities.

Representing Multiple categories

An additional categorical variable in this data is the diamond's color. Unfortunately, the color names are just letters, but the documentation for the diamonds data does mention that "D" is considered by most as the "best" color, and the higher the letter, the less valued the color.

How are the different diamond colors distributed throughout each diamond cut? Are they equally distributed, or are some colors more represented in certain cuts?

One option is to check this visually is through a "Stacked" Barplot. To do that, we just need to assing the color variable as a fill color, and then each bar will break up into different colors to represent each diamond's color.

ggplot(data = diamonds, aes(x = cut, fill = color)) +

And yes, you could make these horizontally as well!

100% Stacked

It might be hard to make this comparison on the previous plot since there are not as many Fair diamonds. What we can do is change the y axis to represent within category percentages.

Now with a 100 percent stacked barplot, we add position = "fill" to communicate to R that we want to make each bar the same height. Now we can compare within group percentages of each color.

ggplot(data = diamonds, aes(x = cut, fill = color)) + 
  geom_bar(position = "fill")

This is an easy way to compare across diamond cuts.

Clustered Barplots (also called "Dodged" Barplots)

If we want to more directly compare within a bar, we can break up the bars into a cluster (like we did with boxplots). Let's try that by now doing position = "dodge" rather than position = "fill"

ggplot(data = diamonds, aes(x = cut, fill = color)) + 
  geom_bar(position = "dodge")

We can now see that there are more of the low letter (higher valued) colors in general, with most diamonds being "G" and the fewest definitely being "J" and "I" colors.

Try your own Bar Plot

Let's create a stacked barplot involving the diabetes data. Take a look at the data first.

Let's make a bivariate bar plot to see how many participants we have from each location, and if we have relatively similar proportions of male and female participants in this dataset. Set location to the x axis.

Also, go ahead and add color = "black" into the geom_bar function to build border colors around your bars--it will look cleaner for this one!

The solution key has the code for creating a stacked (But not 100% stacked) barplot. However, you can try 100% stacked OR dodged for practice!

ggplot(data = ____________, aes(_______________)) +
ggplot(data = diabetes, aes(x = ___________, fill = _________)) +
  geom_bar(color = ___________)
ggplot(data = diabetes, aes(x = location, fill = gender)) +
  geom_bar(color = "black")


Scatterplots are helpful for directly comparing two numeric variables.

Let's take a look at the prostate data from the package faraway. This data records information about 97 men who have been diagnosed with prostate cancer.



Is there a relationship between the prostate weight and the volume of cancer in this group? The variable names here are lweight and lcavol

We will set up a very similar code structure, but we will use geom_point again (the same geometry we saw with strip charts). But rather than get clear columns of data, we'll now see points spread out across the cartesian coordinate plane.

ggplot(data = prostate, aes(x = lweight, y = lcavol)) +

The plot makes sense. Men with heavier prostate tend to have more cancer volume on average, but the relationship is not very strong (I can't predict lcavol with much accuracy knowing only lweight).

Add a block color

If we don't want to go with the generic black, we can also add a block color option in geom_point(). Notice again that with points, it needs to be color = rather than fill =. It's easy to mix that up, so try to remember that with points!

ggplot(data = prostate, aes(x = lweight, y = lcavol)) +
  geom_point(color = "coral")

If you want to jump ahead: You can see a full list of all color possibilities by searching for Colors in R. What's your favorite color?

Alpha (transparency)

You can also change the level of transparency within your scatterplot which is denoted by alpha

Alpha spans from 0 (fully transparent) to 1 (fully opaque), and changing this value can help us reveal more of the density of the data when it is heavily clustered. Not necessarily the case for this data, but helpful in other situations!

Feel free to adjust alpha to different values to see what happens.

ggplot(data = prostate, aes(x = lweight, y = lcavol)) +
  geom_point(alpha = 0.5, color = "coral")

Representing a Variable with Color

As with other plots, we can also use color as a way to represent another variable.

One way to do that is by mapping a variable to color. In the prostate data, we don't have a categorical variable exactly, though we do have a numeric variable with a discrete scale. gleason represents the "grade" of cancer, where the higher the gleason score, the more serious the cancer. Typically 8-10 is considered serious.

ggplot(data = prostate, aes(x = lweight, y = lcavol, color = gleason)) +

But since gleason is coded as a numeric variable, it colors in a continuous scale, and this is not the easiest to compare. I'm going to change this to a "factor" variable through some quick code, and then we'll take another look!

prostate$gleason = as.factor(prostate$gleason)

ggplot(data = prostate, aes(x = lweight, y = lcavol, color = gleason)) +

While the prostate weight does not seem very clearly associated with gleason score, it does seem as if higher cancer volume might be more linked to higher gleason scores. We can also check with an easier representation, like a strip chart.

prostate$gleason = as.factor(prostate$gleason)

ggplot(data = prostate, aes(x = gleason, y = lcavol, color = gleason)) +
  geom_jitter(width = 0.1)

Time to Practice!

Let's return to the diabetes data.


Create a scatterplot that compares an individual's weight on the x axis to their cholesterol ratio on the y axis. Then color the plot by gender to see if there are any patterns depending on whether the individual is male or female.

Also add some transparency to the dots to make it easier to see everything.

Add a title the plot: "Weight and Ratio"

ggplot(data = _______, aes(_______________)) +
  geom_point(__________) +
ggplot(data = diabetes, aes(x = ______, y = ______, color = ________)) +
  geom_point(alpha = _____) +
ggplot(data = diabetes, aes(x = weight, y = ______, color = gender)) +
  geom_point(alpha = 0.3) +
    labs(title = "_________")
ggplot(data = diabetes, aes(x = weight, y = ratio, color = gender)) +
  geom_point(alpha = 0.3) +
    labs(title = "Weight and Ratio")

Practice with another representation?

You might notice that on average, males tend to be a little heavier than females. Not very surprising. What's harder to tell is if males in this sample have higher ratios. This represents cholesterol/HDL. So the higher this value, the worse their cholesterol balance is since HDL would be good and LDL would be bad.

Try making the comparison plot of your choice to compare gender across ratio. There are several options we have learned! The built in solution offers one possibility.

ggplot(data = diabetes, aes(____________________))
ggplot(data = diabetes, aes(x = ratio, y = gender, fill = gender)) +
  geom_density_ridges(alpha = 0.5,
                      scale = 0.8) +
  geom_boxplot(width= 0.1,
               alpha = 0.2)


This tutorial was initially created by Kelly Findley, with some assistance on certain sections by Brandon Pazmino (UIUC '21). We hope this experience was helpful for you!