Displaying geom_smooth() trend line from a specified x value

Viewed 721

Suppose a dataset containing count data per multiple time periods and per multiple groups in the following format:

set.seed(123)
df <- data.frame(group = as.factor(rep(1:3, each = 50)),
                 week = rep(1:50, 3),
                 rate = c(round(700 - rnorm(50, 100, 10) - 1:50 * 2, 0),
                          round(1000 - rnorm(50, 200, 10) - 1:50 * 2, 0),
                          round(1000 - rnorm(50, 200, 10) - 1:50 * 2, 0)))

    group week rate
1       1    1  604
2       1    2  598
3       1    3  578
4       1    4  591
5       1    5  589
6       1    6  571
7       1    7  581
8       1    8  597
9       1    9  589
10      1   10  584

I'm interested in fitting a model-based trend line per groups, however, I want this trend line to be displayed only from a certain x value. To visualize the trend line using all data points (requires ggplot2):

df %>%
 ggplot(aes(x = week,
            y = rate,
            group = group,
            lty = group)) + 
 geom_line() +
 geom_point() +
 geom_smooth(method = "glm", 
             method.args = list(family = "quasipoisson"),
             se = FALSE) 

Plot 1

Or to fit a model based on a specific range of values (requires ggplot2 and dplyr):

df %>%
 group_by(group) %>%
 mutate(rate2 = ifelse(week < 35, NA, rate)) %>%
 ggplot(aes(x = week,
            y = rate,
            group = group,
            lty = group)) + 
 geom_line() +
 geom_point() +
 geom_smooth(aes(y = rate2),
             method = "glm", 
             method.args = list(family = "quasipoisson"),
             se = FALSE)

Plot 2

However, I cannot find a way to fit the models using all data, but display the trend line only from a specific x value (let's say 35+). Thus, I essentially want the trend line as computed for plot one, but displaying it according the second plot, using ggplot2 and ideally only one pipeline.

5 Answers

I went to look at the after_stat function mentioned by @tjebo. See if the following works for you?

df %>%
  ggplot(aes(x = week,
             y = rate,
             lty = group)) + 
  geom_line() +
  geom_point() +
  geom_smooth(method = "glm", 
              aes(group = after_stat(interaction(group, x > 35)),
                  colour = after_scale(alpha(colour, as.numeric(x > 35)))),
              method.args = list(family = "quasipoisson"),
              se = F)

result

This works by splitting the points associated with each line into two groups, those in the x <=35 region and those in the x >35 region, since a line's colour shouldn't vary, and defining a separate colour transparency for each new group. As a result, only the lines in the x > 35 region are visible.

When used, the code triggers a warning that the after_scale modification isn't applied to the legend. I don't think that's a problem though, since we don't need it to appear in the legend anyway.

If you can tolerate a warning, you can solve this with 1 line difference from the example code using stage().

library(tidyverse)

set.seed(123)
df <- data.frame(group = as.factor(rep(1:3, each = 50)),
                 week = rep(1:50, 3),
                 rate = c(round(700 - rnorm(50, 100, 10) - 1:50 * 2, 0),
                          round(1000 - rnorm(50, 200, 10) - 1:50 * 2, 0),
                          round(1000 - rnorm(50, 200, 10) - 1:50 * 2, 0)))

df %>%
  ggplot(aes(x = week,
             y = rate,
             group = group,
             lty = group)) + 
  geom_line() +
  geom_point() +
  geom_smooth(method = "glm", 
              method.args = list(family = "quasipoisson"),
              aes(x = stage(week, after_stat = ifelse(x > 35, x, NA))),
              se = FALSE) 
#> `geom_smooth()` using formula 'y ~ x'
#> Warning: Removed 165 rows containing missing values (geom_smooth).

One way to do this is to construct the fitted values outside of ggplot so you have control over them:

df$fit <- glm(rate ~ week + group, data = df, family = "quasipoisson")$fitted.values

library(dplyr)
library(ggplot2)

ggplot(df, aes(x = week, group = group, lty = group)) + 
  geom_line(aes(y = rate)) +
  geom_point(aes(y = rate)) +
  geom_line(data = df %>% filter(week >= 35), aes(y = fit), color = "blue", size = 1.25)

enter image description here

  1. I am not sure if it is generally correct to use a linear model in time series. The whole point about time series is that they require specific statistics because of their expected autocorrelation. You might want something like average rolling models instead.

  2. I am not sure if your visualisation might not be quite confusing and, more dangerously, misleading.

Besides, an interesting problem. I thought the new after_stat might somehow help, but I couldn't get it working.

So, here a quick hack. Change the order of your geom-s and draw a rectangle in-between. I am cheekily using a different theme, but if you really want to use theme_grey(), you can fake the axis lines as well.

library(tidyverse)
set.seed(123)
df <- data.frame(group = as.factor(rep(1:3, each = 50)),
                 week = rep(1:50, 3),
                 rate = c(round(700 - rnorm(50, 100, 10) - 1:50 * 2, 0),
                          round(1000 - rnorm(50, 200, 10) - 1:50 * 2, 0),
                          round(1000 - rnorm(50, 200, 10) - 1:50 * 2, 0)))
df %>%
  ggplot(aes(x = week, y = rate, group = group, lty = group)) + 
  stat_smooth(se = FALSE) +
  geom_rect(xmin = -Inf, xmax = 35, ymin = -Inf, ymax = Inf, 
            fill = "white") +
  geom_line() +
  geom_point() +
  theme_classic()
#> `geom_smooth()` using method = 'loess' and formula 'y ~ x'

Created on 2021-02-09 by the reprex package (v1.0.0)

P.S. I've removed a few of the unnecessary bits in the code to reproduce this, like the model specs.

You could use ggplot_build to get the structure of the plot :

p <- ggplot(df, aes(x = week,
                        y = rate,
                        group = group,
                        lty = group)) + 
                  geom_line() +
                  geom_point() +
                  geom_smooth(method = "glm", 
                              method.args = list(family = "quasipoisson"),
                              se = FALSE) 
p_build <- ggplot_build(p)

enter image description here

You could then modify the internal data, here the third element of the data list (geom_smooth):

p_build$data[[3]]$x <- sapply(p_build$data[[3]]$x,function(x) {ifelse(x<35,NA,x)})

and use ggplot_gtable to regenerate the plot (the lm calculations still apply to the whole dataset):

plot(ggplot_gtable(p_build))

enter image description here

Related