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:.