The following vignette is a **DRAFT** for explaining the `geom_lineribbon()`

family of stats and geoms in `ggdist`

. Currently it just has an example of the experimental `curve_interval()`

function for calculating curvewise (joint) intervals for lineribbon plots.

The experimental `curve_interval()`

function works similarly to `point_interval()`

, but calculates intervals jointly across entire curves. **It is currently available only on the Github version of ggdist.**

Where `point_interval()`

calculates *pointwise* intervals, or intervals *conditional* on each group, `curve_interval()`

calculates *joint* or *curvewise* intervals. In the literature these are also called *curve boxplots* (Mirzargar *et al.* 2014, Juul *et al.* 2020).

An example will help illustrate the difference between the two types of intervals. Consider the following set of curves, where each curve is assumed to be a “draw” from some distribution of curves, \(\mathbf{y} = f(\mathbf{x})\), where \(\mathbf{x}\) and \(\mathbf{y}\) are vectors:

k = 11 # number of curves n = 501 df = tibble( .draw = 1:k, mean = seq(-5,5, length.out = k), x = list(seq(-15,15,length.out = n)), ) %>% unnest(x) %>% mutate(y = dnorm(x, mean, 3)/max(dnorm(x, mean, 3))) df %>% ggplot(aes(x = x, y = y)) + geom_line(aes(group = .draw), alpha=0.2)

If one used one of the `point_interval()`

functions to summarize this curve (such as `median_qi()`

, `mean_qi()`

, etc), it would calculate *pointwise* intervals:

df %>% group_by(x) %>% median_qi(y, .width = c(.5)) %>% ggplot(aes(x = x, y = y)) + geom_lineribbon(aes(ymin = .lower, ymax = .upper)) + geom_line(aes(group = .draw), alpha=0.15, data = df) + scale_fill_brewer() + ggtitle("50% pointwise intervals with point_interval()")

The 50% *pointwise* interval calculated at (say) \(x = 1\) would contain 50% of the draws from \(y|x=1\). At a different value of \(x\), say \(x = 2\), the 50% pointwise interval would also contain 50% of the draws from \(y|x = 2\). However, the specific draws contained in the interval for \(y|x=2\) might be *different* draws from those contained in the interval for \(x|y=1\): if you trace any of the underlying curves, you will notice that each curve is included in some intervals and not included in others. Thus, the set of intervals—the ribbon—may not fully contain 50% of curves. Indeed, inspecting the above plot, the 50% ribbon contains **none** of the curves!

Depending on what type of inference we care about, this might be sufficient for our purposes: maybe we are interested just in what the outcome is likely to be at a given x value (a conditional inference), but we are not interested in joint inferences (e.g., what is the shape of the curve likely to look like?). However, if we *are* interested in such joint inferences, pointwise intervals can be misleading. The shape of the median curve, for example, looks nothing like any of the possible outcomes. The interval also does not include the maximum value of *any* of the underlying curves, which might cause us to conclude (incorrectly) that a value close to 1 is unlikely, when the exact opposite is the case (every curve touches 1).

One solution I like for such situations is to show spaghetti plots: just plot the underlying curves. This is a so-called *frequency framing* uncertainty visualization, and it tends to work fairly well. However, in some cases you may want a visual summary using intervals, in which case curvewise intervals could help. Using `curve_interval()`

instead of `point_interval()`

or `median_qi()`

calculates these:

df %>% group_by(x) %>% curve_interval(y, .width = c(.5)) %>% ggplot(aes(x = x, y = y)) + geom_lineribbon(aes(ymin = .lower, ymax = .upper)) + geom_line(aes(group = .draw), alpha=0.15, data = df) + scale_fill_brewer() + ggtitle("50% curvewise intervals with curve_interval()")

Note how the 50% *curvewise* interval now contains half of the underlying curves, and the median curve *is* one of the underlying curves (so it is more representative of the curve shape we should expect). These intervals also cover the peaks of the curves, where the pointwise intervals did not.

An X% *curvewise* interval is calculated across all the curves by taking the top X% closest curves to the central curve, for some definition of “close” and “central”. The `curve_interval()`

function currently orders curves by mean halfspace depth, which is basically how close each curve is to the pointwise median in percentiles, on average.

Given the above, let’s see what more realistic curvewise intervals of the above example might look like by using a larger number of draws:

k = 1000 # number of curves large_df = tibble( .draw = 1:k, mean = seq(-5,5, length.out = k), x = list(seq(-15,15,length.out = n)), ) %>% unnest(x) %>% mutate(y = dnorm(x, mean, 3)/max(dnorm(x, mean, 3))) pointwise_plot = large_df %>% group_by(x) %>% median_qi(y, .width = c(.5, .8, .95)) %>% ggplot(aes(x = x, y = y)) + geom_hline(yintercept = 1, color = "gray75", linetype = "dashed") + geom_lineribbon(aes(ymin = .lower, ymax = .upper)) + scale_fill_brewer() + ggtitle("point_interval()") curvewise_plot = large_df %>% group_by(x) %>% curve_interval(y, .width = c(.5, .8, .95)) %>% ggplot(aes(x = x, y = y)) + geom_hline(yintercept = 1, color = "gray75", linetype = "dashed") + geom_lineribbon(aes(ymin = .lower, ymax = .upper)) + scale_fill_brewer() + ggtitle("curve_interval()") plot_grid(nrow = 2, pointwise_plot, curvewise_plot )

Notice how the pointwise intervals miss out on the peaks of this distribution of curves. Even the 95% ribbon, which appears to reach up to the peaks, in fact falls slightly short. While this is a bit of a pathological example, it does demonstrate the potential shortcomings of pointwise intervals.