Top Guide: Creating Histograms in R

R Published Updated 4 mins read By Leon Wei
Top Guide: Creating Histograms in R cover image

Quick summary

Summarize this blog with AI

Introduction

A histogram groups numeric values into intervals and shows their frequency or density. For monthly sales, its horizontal axis is sales amount and its vertical axis is number of months or density. A histogram discards month order, so it cannot reveal which month peaked or establish seasonality.

Tested with R 4.5.1 and ggplot2 4.0.0. Run the code blocks in order. Install the named packages before starting; inputs are supplied below.

Use one complete numeric dataset

Work on the plotted variable rather than removing rows with missing values anywhere in a larger table. Here the twelve supplied values are monthly sales in fictional units. Missing and infinite values are counted explicitly; numeric type is checked before calculation.

monthly_sales <- c(200, 450, 300, 500, 600, 700, 550, 400, 650, 300, 450, 500)
 stopifnot(is.numeric(monthly_sales))
 finite <- is.finite(monthly_sales)
 cat("Excluded nonfinite values:", sum(!finite), "
")
 sales <- monthly_sales[finite]
 stopifnot(length(sales) > 0)
 edges <- seq(100, 700, by = 100)
 h <- hist(sales, breaks = edges, right = TRUE, include.lowest = TRUE, plot = FALSE)
 print(h$counts)
 stopifnot(identical(h$counts, c(1L, 2L, 1L, 4L, 2L, 2L)),
           sum(h$counts) == length(sales))

Counts are 1, 2, 1, 4, 2, 2. The first bin is [100,200]; the later bins are (200,300], …, (600,700]. Exact breakpoints make boundary handling clear.

Plot counts with accurate labels

A scalar breaks = 20 is a suggested number of cells, not an exact bin count or a width. A vector of edges fixes the intervals. hist() also supports rules such as Sturges and FD; inspect more than one reasonable choice because apparent shape can change with bin width and origin.

draw_counts <- function() {
   hist(sales, breaks = edges, right = TRUE, include.lowest = TRUE,
        col = "#8fc5d8", border = "white", labels = TRUE,
        main = "Monthly sales amounts", xlab = "Sales (units)",
        ylab = "Number of months", ylim = c(0, 5))
 }
 draw_counts()
Monthly sales frequencyCounts 1,2,1,4,2,2 across six 100-unit bins. Monthly sales amounts Sales (units) Number of months 100 200 300 400 500 600 700 0 1 2 3 4 5 1 2 1 4 2 2
Twelve months grouped by sales amount.

The tallest interval contains four months with sales above 400 and at most 500. The chart identifies an amount range, not the identities of those months. Use a time-series plot with month on x to retain order.

Put the histogram and density curve on the same scale

Set probability = TRUE before overlaying a kernel density estimate. Density has units of inverse sales units; bar area, not height alone, represents relative frequency. This distinction is essential for unequal-width bins. A smoothed curve based on only twelve observations is exploratory and depends on bandwidth.

draw_density <- function() {
   old <- par(mar = c(4.5, 7, 3, 1), las = 1)
   on.exit(par(old))
   hist(sales, breaks = edges, probability = TRUE,
        col = "#d3e8ef", border = "white", main = "Density scale",
        xlab = "Sales (units)", ylab = "")
   mtext("Density (per sales unit)", side = 2, line = 5.5, las = 0)
   lines(density(sales, from = min(edges), to = max(edges)),
         col = "#a63d28", lwd = 2)
 }
 draw_density()
 stopifnot(abs(sum(h$density * diff(h$breaks)) - 1) < 1e-12)
 unequal <- hist(sales, breaks = c(100, 300, 400, 700), plot = FALSE)
 stopifnot(abs(sum(unequal$density * diff(unequal$breaks)) - 1) < 1e-12)
Monthly sales densityArea-normalized histogram with an exploratory kernel density curve. Density scale Sales (units) 100 200 300 400 500 600 700 0.0000 0.0005 0.0010 0.0015 0.0020 0.0025 0.0030 Density (per sales unit)
Histogram and smoothed curve share the density scale.

Do not add density(sales) to the default count histogram: the two vertical scales would describe different quantities. A density estimate can also extend beyond the observed range; that alone is not evidence of impossible observations.

Use the same breaks in ggplot2

The ggplot2 histogram layer can use explicit breaks and right-closed bins. Here its computed counts are checked against base R, so the two displays answer the same question.

library(ggplot2)
 p <- ggplot(data.frame(sales), aes(sales)) +
   geom_histogram(breaks = edges, closed = "right", fill = "#8fc5d8", color = "white") +
   labs(title = "The same sales bins in ggplot2", x = "Sales (units)", y = "Number of months") +
   theme_minimal(base_size = 12)
 print(p)
 counts <- ggplot_build(p)$data[[1]]$count
 stopifnot(identical(as.integer(counts), h$counts))
Monthly sales in ggplot2The same six bins and counts as the base R count histogram. 0 1 2 3 4 200 400 600 Sales (units) Number of months The same sales bins in ggplot2
Explicit common breaks produce the same counts in ggplot2.

Check edge cases before drawing

Choose documented handling for missing and nonfinite values. Empty input has no distribution to display. A constant vector can still be plotted using explicit edges that surround its value; report that it is constant rather than interpreting a smooth shape.

clean_hist_input <- function(x) {
   if (!is.numeric(x) || !is.null(dim(x))) stop("Need a numeric vector")
   out <- x[is.finite(x)]
   if (!length(out)) stop("No finite observations to plot")
   out
 }
 fails <- function(expr) inherits(tryCatch(expr, error = identity), "error")
 stopifnot(identical(clean_hist_input(c(1, NA, Inf, 2)), c(1, 2)),
           fails(clean_hist_input(numeric())), fails(clean_hist_input(c(NA_real_, Inf))),
           fails(clean_hist_input("10")), fails(clean_hist_input(matrix(1:4, 2))),
           sum(hist(rep(5, 3), breaks = c(4, 5, 6), plot = FALSE)$counts) == 3)

Primary references

Interview Prep

Begin Your SQL, Python, and R Journey

Master 230 interview-style coding questions and build the data skills needed for analyst, scientist, and engineering roles.

Related Articles

All Articles