Powered by AppSignal & Oban Pro

Density estimates: geom_density

notebooks/geom_density.livemd

Density estimates: geom_density

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_density(), which covers stat_density() 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.

geom_density() draws a kernel density estimate, a smoothed version of the histogram, which suits continuous data drawn from a smooth distribution; stat_density() computes it. Besides the density, the stat computes counts, the density times the number of rows, for stacking densities. Giggly has both, Giggly.Plot.geom_density/3 and Giggly.Plot.stat_density/3, which take ggplot2's options as keyword options (bw:, adjust:, kernel:, n:, trim:, bounds: and orientation:), stack curves with position: :stack, and map what the stat computes, such as the count, with {:after_stat, [:count]}. It has no position_fill(), and of the bandwidth rules bw names, only R's "nrd0" and "nrd", as :nrd0 and :nrd, not "ucv", "bcv" or "SJ". 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. Every example uses ggplot2's diamonds, Giggly.Datasets.diamonds/0, which the setup cell binds to diamonds, ggplot2's name for it.

A density curve

ggplot(diamonds, aes(carat)) +
  geom_density()
diamonds
|> Plot.new(%{x: :carat})
|> Plot.geom_density()
|> render.()

Horizontal densities

# Map the values to y to flip the orientation
ggplot(diamonds, aes(y = carat)) +
  geom_density()
diamonds
|> Plot.new(%{y: :carat})
|> Plot.geom_density()
|> render.()

Rougher and smoother curves

ggplot(diamonds, aes(carat)) +
  geom_density(adjust = 1/5)
ggplot(diamonds, aes(carat)) +
  geom_density(adjust = 5)

adjust multiplies the bandwidth the density is smoothed over.

diamonds
|> Plot.new(%{x: :carat})
|> Plot.geom_density(adjust: 1 / 5)
|> render.()
diamonds
|> Plot.new(%{x: :carat})
|> Plot.geom_density(adjust: 5)
|> render.()

A curve for each cut

ggplot(diamonds, aes(depth, colour = cut)) +
  geom_density() +
  xlim(55, 70)
ggplot(diamonds, aes(depth, fill = cut, colour = cut)) +
  geom_density(alpha = 0.1) +
  xlim(55, 70)

The limits drop the diamonds whose depth lies outside them before each curve is estimated.

diamonds
|> Plot.new(%{x: :depth, colour: :cut})
|> Plot.geom_density()
|> Plot.xlim(55, 70)
|> render.()
diamonds
|> Plot.new(%{x: :depth, fill: :cut, colour: :cut})
|> Plot.geom_density(alpha: 0.1)
|> Plot.xlim(55, 70)
|> render.()

Bounded data

# Use `bounds` to adjust computation for known data limits
big_diamonds <- diamonds[diamonds$carat >= 1, ]
ggplot(big_diamonds, aes(carat)) +
  geom_density(color = 'red') +
  geom_density(bounds = c(1, Inf), color = 'blue')

The diamonds of a carat or more have no values below 1, which the red curve, knowing no bounds, smooths past. The cell keeps the rows R's indexing keeps, each column's values where carat is 1 or more, and writes R's Inf as :infinity.

kept = Enum.map(Keyword.fetch!(diamonds, :carat), fn carat -> carat >= 1 end)

big_diamonds =
  Enum.map(diamonds, fn {name, values} ->
    rows =
      values
      |> Enum.zip(kept)
      |> Enum.filter(fn {_value, keep} -> keep end)
      |> Enum.map(fn {value, _keep} -> value end)

    {name, rows}
  end)

big_diamonds
|> Plot.new(%{x: :carat})
|> Plot.geom_density(colour: "red")
|> Plot.geom_density(bounds: {1, :infinity}, colour: "blue")
|> render.()

Stacked densities

# Stacked density plots: if you want to create a stacked density plot, you
# probably want to 'count' (density * n) variable instead of the default
# density
# Loses marginal densities
ggplot(diamonds, aes(carat, fill = cut)) +
  geom_density(position = "stack")
# Preserves marginal densities
ggplot(diamonds, aes(carat, after_stat(count), fill = cut)) +
  geom_density(position = "stack")
diamonds
|> Plot.new(%{x: :carat, fill: :cut})
|> Plot.geom_density(position: :stack)
|> render.()
diamonds
|> Plot.new(%{x: :carat, y: {:after_stat, [:count]}, fill: :cut})
|> Plot.geom_density(position: :stack)
|> render.()

Conditional densities

# You can use position="fill" to produce a conditional density estimate
ggplot(diamonds, aes(carat, after_stat(count), fill = cut)) +
  geom_density(position = "fill")

Not in Giggly yet: position_fill().