Powered by AppSignal & Oban Pro

Smooths: geom_smooth

notebooks/geom_smooth.livemd

Smooths: geom_smooth

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

alias Giggly.Plot

mpg = Giggly.Datasets.mpg()

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_smooth(), which covers stat_smooth() 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_smooth() draws a smoothed line through the data, with a band for its confidence interval, to help the eye see a pattern among overlapping points. stat_smooth() takes the same arguments, for drawing the fit with another geom. Left to choose, it fits a local regression, R's loess(), when the largest group has fewer than 1,000 rows, and otherwise a generalised additive model, mgcv::gam(); method names any other modelling function, such as lm(). Giggly has both, Giggly.Plot.geom_smooth/3 and Giggly.Plot.stat_smooth/3, which fit a straight line by least squares, a loess or a gam (method: :lm, :loess or :gam), choosing as ggplot2 does when no method is given, and take se:, level:, n:, span:, fullrange: and orientation:. It has no formula option, no method.args option, no other modelling function, such as glm(), and no function as a method.

Giggly's loess fits the curve at each of its points directly, as R's loess(surface = "direct") does; R's default fits at the corners of cells it divides the data into and joins those fits with cubic pieces, so a loess curve here sits a point or two from R's, more where the span is small, and each example says how far. Giggly's gam, from 1,000 rows, fixes a choice mgcv leaves to its linear algebra's rounding, so its curve can sit up to about a thousandth of the y axis's range from R's; no example here fits one. 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. It binds Giggly.Datasets.mpg/0, ggplot2's mpg, to mpg, ggplot2's name for it.

A smooth through the points

ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  geom_smooth()

mpg has fewer than 1,000 rows, so the smooth is a loess() fit, with a band for its 95% confidence interval. Giggly's curve sits up to 1.7 points from R's on this drawing, and its band's edges up to 2.9.

mpg
|> Plot.new(%{x: :displ, y: :hwy})
|> Plot.geom_point()
|> Plot.geom_smooth()
|> render.()

Fitting along the y axis

# If you need the fitting to be done along the y-axis set the orientation
ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  geom_smooth(orientation = "y")

The curve fits displ to hwy. Giggly's sits up to 1.7 points from R's, and its band's edges up to 5.7.

mpg
|> Plot.new(%{x: :displ, y: :hwy})
|> Plot.geom_point()
|> Plot.geom_smooth(orientation: :y)
|> render.()

How wiggly the smooth is

# Use span to control the "wiggliness" of the default loess smoother.
# The span is the fraction of points used to fit each local regression:
# small numbers make a wigglier curve, larger numbers make a smoother curve.
ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  geom_smooth(span = 0.3)

With a span this small, R's cells are small too, and its curve bends where Giggly's direct fit does not: Giggly's sits up to 15 points from R's, among the smallest engines, and its band's edges up to 18.

mpg
|> Plot.new(%{x: :displ, y: :hwy})
|> Plot.geom_point()
|> Plot.geom_smooth(span: 0.3)
|> render.()

Other models

# Instead of a loess smooth, you can use any other modelling function:
ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  geom_smooth(method = lm, se = FALSE)

ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  geom_smooth(method = lm, formula = y ~ splines::bs(x, 3), se = FALSE)

se = FALSE leaves out the confidence band. The second fits a linear model to a B-spline of x from the splines package.

Giggly draws the first plot, where R's method = lm is method: :lm.

mpg
|> Plot.new(%{x: :displ, y: :hwy})
|> Plot.geom_point()
|> Plot.geom_smooth(method: :lm, se: false)
|> render.()

Not in Giggly yet: geom_smooth()'s formula option, here a B-spline from the splines package.

A smooth for each group

# Smooths are automatically fit to each group (defined by categorical
# aesthetics or the group aesthetic) and for each facet.

ggplot(mpg, aes(displ, hwy, colour = class)) +
  geom_point() +
  geom_smooth(se = FALSE, method = lm)

Mapping colour to class makes a group of each class, and a line is fitted to each.

mpg
|> Plot.new(%{x: :displ, y: :hwy, colour: :class})
|> Plot.geom_point()
|> Plot.geom_smooth(se: false, method: :lm)
|> render.()

A smooth in each panel

ggplot(mpg, aes(displ, hwy)) +
  geom_point() +
  geom_smooth(span = 0.8) +
  facet_wrap(~drv)

facet_wrap(~drv) is Giggly.Plot.facet_wrap(plot, :drv). Giggly's curves sit up to 7.8 points from R's, in the panel of rear-wheel-drive cars, the fewest, and their bands' edges up to 13.

mpg
|> Plot.new(%{x: :displ, y: :hwy})
|> Plot.geom_point()
|> Plot.geom_smooth(span: 0.8)
|> Plot.facet_wrap(:drv)
|> render.()

Logistic regression

binomial_smooth() is a plain R function returning a geom_smooth() layer that fits a logistic regression with glm(). The rpart package's kyphosis records children who had spinal surgery: their Age, in months, and whether a kyphosis was present afterwards, Kyphosis, a factor of absent and present.

binomial_smooth <- function(...) {
  geom_smooth(method = "glm", method.args = list(family = "binomial"), ...)
}
# To fit a logistic regression, you need to coerce the values to
# a numeric vector lying between 0 and 1.
ggplot(rpart::kyphosis, aes(Age, Kyphosis)) +
  geom_jitter(height = 0.05) +
  binomial_smooth()

Not in Giggly yet: geom_jitter() and its height option, geom_smooth() with method = "glm", the logistic regression R's glm() fits, and its method.args option, and the kyphosis dataset from the rpart package.

Values between 0 and 1

ggplot(rpart::kyphosis, aes(Age, as.numeric(Kyphosis) - 1)) +
  geom_jitter(height = 0.05) +
  binomial_smooth()

ggplot(rpart::kyphosis, aes(Age, as.numeric(Kyphosis) - 1)) +
  geom_jitter(height = 0.05) +
  binomial_smooth(formula = y ~ splines::ns(x, 2))

# But in this case, it's probably better to fit the model yourself
# so you can exercise more control and see whether or not it's a good model.

as.numeric(Kyphosis) - 1 makes absent 0 and present 1. The second fits the logistic regression to a natural spline of x from the splines package.

Not in Giggly yet: a whole-column function in a mapping, here as.numeric(Kyphosis), a factor's level codes, geom_jitter() and its height option, geom_smooth() with method = "glm", the logistic regression R's glm() fits, and its method.args and formula options, and the kyphosis dataset from the rpart package.