boxplot() in R: How to Make BoxPlots in RStudio

โšก Smart Summary

Boxplot in R summarises a numeric distribution through its median, quartiles, whiskers, and outliers using geom_boxplot() from ggplot2. This walkthrough builds a box plot on the airquality dataset and layers on colour, dots, jitter, notches, and group comparisons.

  • ๐Ÿ“ Five-Number Summary: The box spans the first to third quartile, the line marks the median, and the whiskers reach 1.5 times the interquartile range.
  • ๐Ÿงฑ Base Syntax: ggplot(df, aes(x = group, y = value)) + geom_boxplot() produces one box per level of the grouping factor.
  • ๐Ÿ”ด Outlier Styling: outlier.colour, outlier.shape, and outlier.size control how points beyond the whiskers are drawn.
  • ๐Ÿ’  Showing Observations: geom_jitter() displaces overlapping points sideways, revealing the sample size behind every box.
  • ๐Ÿ“‰ Notched Boxes: notch = TRUE draws the confidence interval of the median, and non-overlapping notches indicate a real difference.
  • ๐ŸŽจ Grouping: Mapping fill inside geom_boxplot() splits each category into several side-by-side boxes.

Boxplot in R

boxplot() in R

boxplot() in R helps to visualize the distribution of the data by quartile and detect the presence of outliers. You can use the geometric object geom_boxplot() from ggplot2 library to draw a boxplot() in R.

We will use the airquality dataset to introduce boxplot() in R with ggplot. The dataset records daily air quality measurements in New York from May to September 1973 and contains 153 observations. We will use the following variables:

  • Ozone: Numerical variable
  • Wind: Numerical variable
  • Month: May to September. Numerical variable

Before drawing one, it is worth knowing exactly what each part of the box represents.

How to Read a Box Plot: Quartiles, IQR, and Outliers

Every element of a box plot encodes one number from the five-number summary. Knowing which is which turns the chart from decoration into analysis.

  • Lower hinge: the first quartile, Q1. Twenty-five percent of observations sit below it.
  • Median line: the second quartile. Its position inside the box reveals skew: a line pushed towards the bottom means the data are right-skewed.
  • Upper hinge: the third quartile, Q3. Seventy-five percent of observations sit below it.
  • Box height: the interquartile range, IQR = Q3 – Q1, which holds the middle half of the data and is the standard robust measure of spread.
  • Whiskers: they extend to the most extreme observation still within 1.5 times the IQR of the nearest hinge. They are not the minimum and maximum.
  • Points beyond the whiskers: observations flagged as outliers by that 1.5 IQR rule.

Two cautions. First, an “outlier” here is a statistical flag, not an error: in a skewed distribution such as ozone concentration, high values are expected and should not be deleted. Second, a box plot hides the shape of the distribution, so two groups with identical boxes can have very different underlying data. Adding jittered points, as shown below, guards against that.

Box Plot vs Histogram vs Violin Plot in R

All three charts describe a numeric distribution, but each reveals something the others hide.

Criteria Box Plot Histogram Violin Plot
Shows Median, quartiles, outliers Frequency in each bin Full density curve
Reveals multiple peaks No Yes Yes
Flags outliers Yes, explicitly Only visually Not directly
Comparing many groups Excellent Awkward Good
Needs a tuning choice No Yes, bin count Yes, bandwidth
ggplot2 object geom_boxplot() geom_histogram() geom_violin()

A common compromise is to draw a violin plot with a narrow box plot inside it, which keeps the density shape and the quartile summary in a single chart. See the histogram tutorial for the binning side of the comparison.

Create Box Plot

Before you start to create your first boxplot() in R, you need to manipulate the data as follow:

  • Step 1: Import the data
  • Step 2: Drop unnecessary variables
  • Step 3: Convert Month into an ordered factor
  • Step 4: Create a new categorical variable splitting each month into three parts: Begin, Middle and End
  • Step 5: Remove missing observations

All these steps are done with dplyr and the pipeline operator %>%.

library(dplyr)
library(ggplot2)
# Step 1
data_air <- airquality %>%

#Step 2
select(-c(Solar.R, Temp)) %>%

#Step 3
mutate(Month = factor(Month, order = TRUE, labels = c("May", "June", "July", "August", "September")), 
       
#Step 4 
day_cat = factor(ifelse(Day < 10, "Begin", ifelse(Day < 20, "Middle", "End"))))

A good practice is to check the structure of the data with the function glimpse().

glimpse(data_air)

Output:

## Observations: 153
## Variables: 5
## $ Ozone   <int> 41, 36, 12, 18, NA, 28, 23, 19, 8, NA, 7, 16, 11, 14, ...
## $ Wind    <dbl> 7.4, 8.0, 12.6, 11.5, 14.3, 14.9, 8.6, 13.8, 20.1, 8.6...
## $ Month   <ord> May, May, May, May, May, May, May, May, May, May, May,...
## $ Day     <int> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,...
## $ day_cat <fctr> Begin, Begin, Begin, Begin, Begin, Begin, Begin, Begi...

Both Ozone and Solar.R contain NA values. Because geom_boxplot() would silently drop them and print a warning, it is cleaner to remove them explicitly.

# Step 5
data_air_nona <-data_air %>% na.omit()	

Basic box plot

Now plot the distribution of ozone by month.

# Store the graph
box_plot <- ggplot(data_air_nona, aes(x = Month, y = Ozone))
# Add the geometric object box plot
box_plot +
    geom_boxplot()

Code Explanation

  • Store the graph for further use
    • box_plot: the base plot is stored in the object box_plot, which lets you add layers later without repeating the whole call
  • Add the geometric object
    • You pass the dataset data_air_nona to ggplot boxplot.
    • Inside the aes() argument, you add the x-axis and y-axis.
    • The + sign means you want R to keep reading the code. It makes the code more readable by breaking it.
    • Use geom_boxplot() to create a box plot

Output:

Basic Box Plot

Change side of the graph

You can flip the side of the graph.

box_plot +
  geom_boxplot()+
  coord_flip()

Code Explanation

  • box_plot: You use the graph you stored. It avoids rewriting all the codes each time you add new information to the graph.
  • geom_boxplot(): draw the boxes and whiskers
  • coord_flip(): Flip the side of the graph

Output:

Change Side of the Graph

Change color of outlier

You can change the color, shape and size of the outliers.

box_plot +
    geom_boxplot(outlier.colour = "red",
        outlier.shape = 2,
        outlier.size = 3) +
    theme_classic()

Code Explanation

  • outlier.colour=”red”: Control the color of the outliers
  • outlier.shape=2: Change the shape of the outlier. 2 refers to triangle
  • outlier.size=3: Change the size of the triangle. Larger numbers draw larger markers.

Output:

Change Color of Outlier

Add a summary statistic

You can overlay a summary statistic such as the group mean, which the box plot itself does not show.

box_plot +
    geom_boxplot() +
    stat_summary(fun.y = mean,
        geom = "point",
        size = 3,
        color = "steelblue") +
    theme_classic()

Code Explanation

  • stat_summary() adds a computed statistic on top of the box plot
  • The argument fun controls which statistic is returned. Here it is the mean. Note that older code uses fun.y, which ggplot2 deprecated in version 3.3.0.
  • Note: Other statistics are available such as min and max. More than one statistics can be exhibited in the same graph
  • geom = “point”: Plot the average with a point
  • size=3: Size of the point
  • color =”steelblue”: Color of the points

Output:

Add a Summary Statistic

Box Plot with Dots

Next, add a dot plot layer on top of the boxes. Each dot represents a single observation, which makes the sample size behind every box visible.

box_plot +
    geom_boxplot() +
    geom_dotplot(binaxis = 'y',
        dotsize = 1,
        stackdir = 'center') +
    theme_classic()

Code Explanation

  • geom_dotplot() draws one dot per observation, stacked within each bin
  • binaxis=’y’: Change the position of the dots along the y-axis. By default, x-axis
  • dotsize=1: Size of the dots
  • stackdir=’center’: Way to stack the dots: Four values:
    • “up” (default),
    • “down”
    • “center”
    • “centerwhole”

Output:

Box Plot with Dots

Control Aesthetic of the Box Plot

Change the color of the box

You can change the colors of the group.

ggplot(data_air_nona, aes(x = Month, y = Ozone, color = Month)) +
    geom_boxplot() +
    theme_classic()

Code Explanation

  • The colors of the groups are controlled in the aes() mapping. You can use color= Month to change the color of the box and whisker plot according to the months

Output:

Change the Color of the Box

Box plot with multiple groups

It is also possible to add multiple groups. You can visualize the difference in the air quality according to the day of the measure.

ggplot(data_air_nona, aes(Month, Ozone)) +
    geom_boxplot(aes(fill = day_cat)) +
    theme_classic()

Code Explanation

  • The aes() mapping of the geometric object controls the groups to display (this variable has to be a factor)
  • aes(fill= day_cat) allows creating three boxes for each month in the x-axis

Output:

Box Plot with Multiple Groups

Box Plot with Jittered Dots

Another way to show individual observations is with jittered points. Jittering is the usual choice when a categorical x-axis causes many points to land on the same position.

This method avoids the overlapping of the discrete data.

box_plot +
    geom_boxplot() +
    geom_jitter(shape = 15,
        color = "steelblue",
        position = position_jitter(width = 0.21)) +
    theme_classic()

Code Explanation

  • geom_jitter() adds a small random displacement to each point so overlapping values become visible.
  • shape=15 changes the shape of the points. 15 represents the squares
  • color = “steelblue”: Change the color of the point
  • position = position_jitter(width = 0.21): controls how far points are displaced sideways, measured in x-axis units. The default is 40 percent of the spacing between categories.

Output:

Box Plot with Jittered Dots

You can see the difference between the first graph with the jitter method and the second with the point method.

box_plot +
    geom_boxplot() +
    geom_point(shape = 5,
        color = "steelblue") +
    theme_classic()

Box Plot with Jittered Dots

Notched Box Plot

An interesting feature of geom_boxplot(), is a notched boxplot function in R. The notch plot narrows the box around the median. The main purpose of a notched box plot is to compare the significance of the median between groups. There is strong evidence two groups have different medians when the notches do not overlap. A notch is computed as follow:

Notched Box Plot

Here IQR is the interquartile range and n is the number of observations in the group. The notch spans roughly the 95 percent confidence interval of the median.

box_plot +
    geom_boxplot(notch = TRUE) +
    theme_classic()

Code Explanation

  • geom_boxplot(notch = TRUE): draw the box plot with notches around the median

Output:

Notched Box Plot

How to Add Titles, Labels, and Custom Colors to a Box Plot in R

The plots above use ggplot2 defaults, which take variable names straight from the data frame. A publishable chart needs readable labels and a deliberate palette.

Titles and axis labels. One labs() call sets every text element:

box_plot +
    geom_boxplot(fill = "coral", alpha = 0.7) +
    labs(title = "Ozone concentration by month",
        subtitle = "New York, May to September 1973",
        x = "Month",
        y = "Ozone (parts per billion)",
        caption = "Source: airquality dataset") +
    theme_classic()

Choosing your own colours. Use scale_fill_manual() when fill is mapped inside aes(), and scale_colour_manual() when colour is:

ggplot(data_air_nona, aes(x = Month, y = Ozone, fill = Month)) +
    geom_boxplot() +
    scale_fill_manual(values = c("#0e9cd1", "coral", "#7dc27d", "#c9a227", "grey60")) +
    theme_classic() +
    theme(legend.position = "none")

The legend is switched off here because the x-axis already names each month, so repeating it would waste space.

Reordering the boxes. Factor levels drive the order on the axis. Because Month was created as an ordered factor, it already reads chronologically. For an unordered factor, sort by the median instead:

ggplot(data_air_nona, aes(x = reorder(Month, Ozone, FUN = median), y = Ozone)) +
    geom_boxplot() +
    theme_classic()

Saving the chart. ggsave() writes the last plot to disk at a resolution you control:

ggsave("ozone_boxplot.png", width = 8, height = 5, dpi = 300)

Box Plot in R: Code Reference

The table below lists the ggplot2 call for each box plot variant covered above:

Objective Code
Basic box plot
ggplot(df, aes(x = x1, y = y)) + geom_boxplot()
Flip the orientation
ggplot(df, aes(x = x1, y = y)) + geom_boxplot() + coord_flip()
Notched box plot
ggplot(df, aes(x = x1, y = y)) + geom_boxplot(notch = TRUE)
Box plot with jittered dots
ggplot(df, aes(x = x1, y = y)) + geom_boxplot() + geom_jitter(position = position_jitter(0.21))
Colour by group
ggplot(df, aes(x = x1, y = y, color = x1)) + geom_boxplot()
Multiple groups per category
ggplot(df, aes(x = x1, y = y)) + geom_boxplot(aes(fill = x2))
Add the group mean
ggplot(df, aes(x = x1, y = y)) + geom_boxplot() + stat_summary(fun = mean, geom = "point")

Also Check:- R Tutorial for Beginners: Learn R Programming Language

FAQs

The whiskers reach the most extreme observation lying within 1.5 times the interquartile range of the nearest quartile. They are not the minimum and maximum, and anything beyond them is drawn as an outlier point.

Not automatically. The 1.5 IQR rule is a convention, not proof of error. Investigate each point first: skewed data such as ozone readings naturally produce high values that are genuine measurements.

Overlapping notches suggest there is no strong evidence that the two medians differ. Non-overlapping notches indicate a likely real difference, roughly equivalent to a 95 percent confidence comparison.

Box plots are a standard exploratory step before training: they expose skew, outliers, and differing feature scales across classes. Teams also use them to compare model accuracy across cross-validation folds.

Yes. AI assistants can explain errors such as a continuous x-axis producing a single box, suggest the right aes() mapping, and flag deprecated arguments like fun.y. Always rerun the corrected code on your own data.

Summarize this post with: