Powered by AppSignal & Oban Pro

Histograms: geom_histogram

notebooks/geom_histogram.livemd

Histograms: geom_histogram

Mix.install([
  {:giggly, path: Path.join(__DIR__, "..")},
  {:kino, "~> 0.19"}
  ])

alias Giggly.Plot

diamonds = Giggly.Datasets.diamonds()

render = fn plot ->
  plot
  |> Giggly.to_svg()
  |> IO.iodata_to_binary()
  |> Kino.Image.new(:svg)
end

About this notebook

This notebook follows the examples on ggplot2's reference page for geom_histogram(), which covers geom_freqpoly() and stat_bin() too, in the order the page gives them. Each example shows ggplot2's code first and then Giggly's. An example Giggly cannot draw yet keeps its ggplot2 code and names what it is waiting for, so the page is also a list of what remains.

A histogram shows how one continuous column's values are spread. stat_bin() divides the x axis into bins and counts the rows in each; geom_histogram() draws the counts as bars and geom_freqpoly(), a frequency polygon, as a line through them, which is easier to compare across the values of a categorical column. The bins are set by their number, their width or their boundaries, and a bar's height can be a density or a sum of weights instead of a count. Giggly has all three, Giggly.Plot.geom_histogram/3, Giggly.Plot.geom_freqpoly/3 and Giggly.Plot.stat_bin/3, which take ggplot2's binning options as keyword options (bins:, binwidth:, breaks:, boundary: and the rest), sum a mapped weight, and map what the binning computes, such as the density, with {:after_stat, [:density]}. It has no scale_x_binned(), no function as a binwidth, no coord_transform() and not the movies dataset. A plot is described with Giggly.Plot and drawn with Giggly.to_svg/2, which returns an SVG document. The setup cell aliases Giggly.Plot, so the cells write Plot.new/2, and defines render, which draws a plot and shows it below the cell with Kino.Image. The examples use ggplot2's diamonds, Giggly.Datasets.diamonds/0, which the setup cell binds to diamonds, ggplot2's name for it.

A histogram

ggplot(diamonds, aes(carat)) +
  geom_histogram()

With neither bins nor binwidth, ggplot2 makes 30 bins and suggests choosing a better value. Giggly makes 30 bins too, without the suggestion.

Plot.new(diamonds, %{x: :carat})
|> Plot.geom_histogram()
|> render.()

The width and number of bins

ggplot(diamonds, aes(carat)) +
  geom_histogram(binwidth = 0.01)
ggplot(diamonds, aes(carat)) +
  geom_histogram(bins = 200)
Plot.new(diamonds, %{x: :carat})
|> Plot.geom_histogram(binwidth: 0.01)
|> render.()
Plot.new(diamonds, %{x: :carat})
|> Plot.geom_histogram(bins: 200)
|> render.()

A horizontal histogram

# Map values to y to flip the orientation
ggplot(diamonds, aes(y = carat)) +
  geom_histogram()

With y mapped rather than x, the bins run up the panel and the bars across it.

Plot.new(diamonds, %{y: :carat})
|> Plot.geom_histogram()
|> render.()

Binning the axis instead

# For histograms with tick marks between each bin, use `geom_bar()` with
# `scale_x_binned()`.
ggplot(diamonds, aes(carat)) +
  geom_bar() +
  scale_x_binned()

scale_x_binned() bins the x axis itself, so geom_bar() counts the rows in each bin and the ticks fall between the bars.

Not in Giggly yet: scale_x_binned(). Giggly has geom_bar(), which without a binned scale counts the rows at each distinct x.

Stacked histograms and frequency polygons

# Rather than stacking histograms, it's easier to compare frequency
# polygons
ggplot(diamonds, aes(price, fill = cut)) +
  geom_histogram(binwidth = 500)
ggplot(diamonds, aes(price, colour = cut)) +
  geom_freqpoly(binwidth = 500)

cut is an ordered factor, so the bars stack and the lines are coloured in the order of its levels, from Fair to Ideal, with viridis, ggplot2's default fill and colour scales for one.

diamonds
|> Plot.new(%{x: :price, fill: :cut})
|> Plot.geom_histogram(binwidth: 500)
|> render.()
diamonds
|> Plot.new(%{x: :price, colour: :cut})
|> Plot.geom_freqpoly(binwidth: 500)
|> render.()

Density instead of count

# To make it easier to compare distributions with very different counts,
# put density on the y axis instead of the default count
ggplot(diamonds, aes(price, after_stat(density), colour = cut)) +
  geom_freqpoly(binwidth = 500)

Giggly maps the density as {:after_stat, [:density]}. The y axis reads 0.0000 to 0.0005 where R writes 0e+00 to 5e-04, since Giggly writes numbers as Elixir does.

diamonds
|> Plot.new(%{x: :price, y: {:after_stat, [:density]}, colour: :cut})
|> Plot.geom_freqpoly(binwidth: 500)
|> render.()

Bins of unequal width

# When using the non-equal-width bins, we should set the area of the bars to
# represent the counts (not the height).
# Here we're using 10 equi-probable bins:
price_bins <- quantile(diamonds$price, probs = seq(0, 1, length = 11))

ggplot(diamonds, aes(price)) +
  geom_histogram(breaks = price_bins, color = "black") # misleading (height = count)

ggplot(diamonds, aes(price, after_stat(count / width))) +
  geom_histogram(breaks = price_bins, color = "black") # area = count

quantile() finds the prices that split the diamonds into bins holding equal numbers of them. With the count as its height, every bar is about as tall as the next and the widest look the biggest; dividing each count by its bin's width makes each bar's area its count instead.

Giggly has no quantile a user can call, so the cell writes out the eleven prices R's quantile() gives. {:after_stat, [{&Kernel.//2, [:count, :width]}]} divides each bin's count by its width, and titles the y axis count / width, where R writes count/width.

price_bins = [326, 646, 837, 1087, 1698, 2401, 3465, 4662, 6301.2, 9821, 18823]

Plot.new(diamonds, %{x: :price})
|> Plot.geom_histogram(breaks: price_bins, color: "black")
|> render.()
Plot.new(diamonds, %{x: :price, y: {:after_stat, [{&Kernel.//2, [:count, :width]}]}})
|> Plot.geom_histogram(breaks: price_bins, color: "black")
|> render.()

Movies by rating

The movies dataset comes from the ggplot2movies package, so these examples run only where it is installed.

if (require("ggplot2movies")) {
# Often we don't want the height of the bar to represent the
# count of observations, but the sum of some other variable.
# For example, the following plot shows the number of movies
# in each rating.
m <- ggplot(movies, aes(rating))
m + geom_histogram(binwidth = 0.1)

Not in Giggly yet: the movies dataset.

Summing a weight

# If, however, we want to see the number of votes cast in each
# category, we need to weight by the votes variable
m +
  geom_histogram(aes(weight = votes), binwidth = 0.1) +
  ylab("votes")

Mapping weight makes each bar the sum of its rows' votes rather than their number, as it does for Giggly.Plot.geom_histogram/3 and Giggly.Plot.geom_bar/3.

Not in Giggly yet: ylab() and the movies dataset.

Bins on a transformed scale

# For transformed scales, binwidth applies to the transformed data.
# The bins have constant width on the transformed scale.
m +
 geom_histogram() +
 scale_x_log10()
m +
  geom_histogram(binwidth = 0.05) +
  scale_x_log10()

Not in Giggly yet: the movies dataset.

Bins in a transformed coordinate system

# For transformed coordinate systems, the binwidth applies to the
# raw data. The bins have constant width on the original scale.

# Using log scales does not work here, because the first
# bar is anchored at zero, and so when transformed becomes negative
# infinity. This is not a problem when transforming the scales, because
# no observations have 0 ratings.
m +
  geom_histogram(boundary = 0) +
  coord_transform(x = "log10")
# Use boundary = 0, to make sure we don't take sqrt of negative values
m +
  geom_histogram(boundary = 0) +
  coord_transform(x = "sqrt")

Not in Giggly yet: coord_transform() and the movies dataset.

Transforming the y axis

# You can also transform the y axis.  Remember that the base of the bars
# has value 0, so log transformations are not appropriate
m <- ggplot(movies, aes(x = rating))
m +
  geom_histogram(binwidth = 0.5) +
  scale_y_sqrt()
}

Not in Giggly yet: the movies dataset.

A bin width for each panel

# You can specify a function for calculating binwidth, which is
# particularly useful when faceting along variables with
# different ranges because the function will be called once per facet
ggplot(economics_long, aes(value)) +
  facet_wrap(~variable, scales = 'free_x') +
  geom_histogram(binwidth = \(x) 2 * IQR(x) / (length(x)^(1/3)))

The function is the Freedman–Diaconis rule, computed from each panel's own values.

Not in Giggly yet: a function as geom_histogram()'s binwidth. The facets are in Giggly already, Giggly.Plot.facet_wrap/3 with scales: :free_x, and Giggly.Plot.geom_histogram/3 takes a number as its binwidth:.