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()
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)
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))
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)