R - ggplot2 'dodge' geom_step() 重叠 geom_bar()

R - ggplot2 'dodge' geom_step() to overlap geom_bar()

使用 ggplot2 的 geom_bar(stat="identity") 绘制计数是可视化计数的有效方法。我想使用这种方法来显示我观察到的计数并将它们与预期计数进行比较我想通过使用 geom_step 在条形图上覆盖一个阶梯图层来做到这一点。

然而,当我这样做时,我 运行 遇到了默认情况下条形图的位置被躲避但 geom_step 没有的问题。例如同时使用连续和离散因变量:

library(tidyverse)

test <- data_frame(a = 1:10, b = runif(10, 1, 10))

test_plot <- ggplot(test, aes(a, b)) + 
  geom_bar(stat="identity") + 
  geom_step(color = 'red')

test2 <- data_frame(a = letters[1:10], b = runif(10, 1, 10))

test2_plot <- ggplot(test2, aes(a, b, group = 1)) + 
  geom_bar(stat="identity") + 
  geom_step(color = 'red'))

gridExtra::grid.arrange(test_plot, test2_plot, ncol = 2)

如您所见,两层发生偏移,这是不希望发生的。

阅读文档我看到 geom_path 有一个 position = 选项,但是尝试 geom_step(color = 'red', position = position_dodge(width = 0.5)) 之类的东西并没有达到我想要的效果,而是压缩了条形图和阶梯线朝向中心。另一种选择是像这样直接调整数据 geom_step(aes(a-0.5, b), color = 'red') ,这对于具有连续因变量的数据产生接近可接受的结果。您还可以将阶梯线计算为函数并使用 stat_function().

绘制它

但是这些方法不适用于具有离散因变量的数据,而我的实际数据具有离散因变量,因此我需要另一个答案。

此外,如上图所示,当移动阶梯线时,不会覆盖最后一根柱子。有没有一种简单优雅的方法可以将它扩展到覆盖最后一个小节?

如果 geom_step() 是错误的方法,而我想要得到的东西可以通过另一种方式实现,我也对此很感兴趣。

这是一个相当粗略的解决方案,但在这种情况下应该有效。

创建一个备用数据框,扩展每行以将 x 轴扩展 -0.5 和 0.5:

test2 <- data.frame(a = lapply(1:nrow(test), function(x) c(test[x,"a"]-.5, test[x,"a"], test[x, "a"]+0.5)) %>% unlist, 
                b = lapply(1:nrow(test), function(x) rep(test[x,"b"], 3)) %>% unlist)

使用 geom_line 参数绘制轮廓:

ggplot(test, aes(a,b)) + geom_bar(stat="identity", alpha=.7) + geom_line(data=test2, colour="red")

如果将 geom_bar 宽度设置为 1,这将看起来更整洁:

ggplot(test, aes(a,b)) + geom_bar(width=1, stat="identity", alpha=.7) + geom_line(data=test2, colour="red")

我认为解决这个问题最有效的方法是按以下方式定义自定义 geom:

library(tidyverse)

geom_step_extend <- function(data, extend = 1, nudge = -0.5,
                             ...) {
  # Function for computing the last segment data
  get_step_extend_data <- function(data, extend = 1, nudge = -0.5) {
    data_out <- as.data.frame(data[order(data[[1]]), ])
    n <- nrow(data)
    max_x_y <- data_out[n, 2]
    if (is.numeric(data_out[[1]])) {
      max_x <- data_out[n, 1] + nudge
    } else {
      max_x <- n + nudge
    }

    data.frame(x = max_x,
               y = max_x_y,
               xend = max_x + extend,
               yend = max_x_y)
  }

  # The resulting geom
  list(
    geom_step(position = position_nudge(x = nudge), ...),
    geom_segment(
      data = get_step_extend_data(data, extend = extend, nudge = nudge),
      mapping = aes(x = x, y = y,
                    xend = xend, yend = yend),
      ...
    )
  )
}

set.seed(111)
test <- data_frame(a = 1:10, b = runif(10, 1, 10))
test2 <- data_frame(a = letters[1:10], b = runif(10, 1, 10))

test_plot <- ggplot(test, aes(a, b, group = 1)) + 
  geom_bar(stat = "identity") + 
  geom_step_extend(data = test, colour = "red")

test2_plot <- ggplot(test2, aes(a, b, group = 1)) + 
  geom_bar(stat = "identity") + 
  geom_step_extend(data = test2, colour = "red")

gridExtra::grid.arrange(test_plot, test2_plot, ncol = 2)

这个解决方案基本上由三部分组成:

  1. position_nudge 步长曲线向左微移所需值(在本例中为 -0.5);
  2. 用函数get_step_extend_data计算缺失的(右边那个)段数据。它的行为受到 ggplot2:::stairstep 的启发,这是 geom_step;
  3. 的基础功能
  4. geom_stepgeom_segment 在单独的 geom 中与 list 组合。

因为 ggplot2 version 3.3.0 这个选项现在被 geom_step 使用 direction = "mid":

支持
library(tidyverse)

test <- data_frame(a = 1:10, b = runif(10, 1, 10))

test_plot <- ggplot(test, aes(a, b)) + 
  geom_bar(stat="identity") + 
  geom_step(color = 'red', direction = "mid", size = 2)

test_plot

我喜欢 molx 在 ggplot2 版本 3.3.0 中使用 direction = 'mid' 作为 geom_step() 的回答。但是,对于时间序列,我建议移动用于 geom_bar()geom_col() 图的 x 轴的数据:

data.frame(time = seq(as.POSIXct('2020-10-01 05:00'), 
                      as.POSIXct('2020-10-01 14:00'), by = 'hour'), 
                 value = runif(10, 0, 100)) %>%
  mutate(time_shift_bars = times + 30*60) %>% 
  ggplot(df, mapping = aes(y = value)) + 
  geom_step(color  = 'red', mapping = aes(x = time)) +
  geom_col(width = 60*60, mapping = aes(x = time_shift_bars))

![resulting plot](https://i.stack.imgur.com/fJBac.png)

The reason I prefer this is because for example 09:00 occurs at a specific instance, and the data represents the average for the following hour. If your time-series data is not averaged like this, use the `direction` method.