如何使geom_smooth不那么动态

4
当在ggplot中使用分面生成平滑曲线时,如果数据的范围从一个分面到另一个分面发生变化,则对于数据较少的分面而言,平滑度可能会获得过多的自由度。
例如:
library(dplyr)
library(ggplot2) # ggplot2_2.2.1

set.seed(1234)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
  mutate(y = dnorm(x) + 0.4*runif(n())) %>% 
  filter(z <= x) %>%
  ggplot(aes(x,y)) + 
  geom_line() +
  geom_smooth(method = 'loess', span = 0.3) +
  facet_wrap(~ z)

生成如下内容:faceted plot 当移动到后续面时,z=-5面是好的,但平滑似乎“过度拟合”;事实上,z=-1已经受到影响,在最后一个面,z=2,平滑线完美地适配了数据。理想情况下,我希望有一种不太动态的平滑方法,例如始终平滑大约4个点(或使用固定内核的内核平滑)。 以下SO问题相关,但也许更为雄心勃勃(因为它想要更多关于span的控制);在这里,我想要一种更简单的平滑形式。

1
你可以根据数据点数量等因素,使用不同的函数来适应这个问题。可以参考这个答案,它使用了自定义平滑函数。 - Dan
3个回答

2
我建议删除 span 选项(因为0.3似乎太细致),或者使用 lm 方法进行多项式拟合。
library(dplyr)
library(ggplot2) # ggplot2_2.2.1

set.seed(1234)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
  mutate(y = dnorm(x) + 0.4*runif(n())) %>% 
  filter(z <= x) %>%
  ggplot(aes(x,y)) + 
  geom_line() +
  geom_smooth(method = 'lm', formula = y ~ poly(x, 4)) +
  #geom_smooth(method = 'loess') +
  #geom_smooth(method = 'loess', span = 0.3) +
  facet_wrap(~ z)

去掉 span 参数(相当于将其增加到0.75)的问题在于它会减少所有分面中平滑度的数量。实际上,使用 method='lm',formula=y~poly(x,4) 也可以看到这种情况;例如将度数从4增加到9,您会得到与我原始帖子中类似的图形。重新表述的一种方式是,poly(x,4)(以及更多或更少的loess)固定了平滑曲线可以具有的“摆动”数量。虽然摆动的数量可能适合第一个分面(具有大量数据和广泛范围),但它过度拟合了最后一个分面。 - banbh
我明白了。所以你想为每个面使用不同的平滑参数。据我所知,geom_smooth没有这个选项。method.args参数可能有机会,但是loess函数似乎没有根据观测数量自动调整参数的功能。我能想到两种方法:在geom_smooth调用之外进行预测建模,并手动绘制线条和带状物(如你的链接所示),或编写自己的平滑函数(如评论所建议)。 - Kota Mori
你可能是对的;你提到的方法可能是继续进行的最佳方式。我提出这个问题的原因是因为我不想武断地在各个方面上改变“跨度”,而是想要一种在某种意义上更简单的平滑方法;例如(笼统地说“摆动”),我可能希望“每1个x坐标单位最多只有1个摆动”。 - banbh

2

我在你的代码中做了一些调整以使其正常工作。我不确定这是否是最好的方法,但它是一种简单的方法。

首先,我们按照你的 z 变量进行分组,然后生成一个数字 span,对于大量观测值来说很小,但对于小量观测值来说很大。我猜测是 10/length(x)。也许还有一些更具统计学意义的方法来看待它。或者也许应该是 2/diff(range(x))。由于这是为了你自己的可视化平滑,你需要自己微调该参数。

  expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%    
    filter(z <= x) %>%
    group_by(z) %>% 
    mutate(y = dnorm(x) + 0.4*runif(length(x)),
           span = 10/length(x)) %>% 
    distinct(z, span)
# A tibble: 8 x 2
# Groups:   z [8]
      z      span
  <int>     <dbl>
1    -5 0.2000000
2    -4 0.2222222
3    -3 0.2500000
4    -2 0.2857143
5    -1 0.3333333
6     0 0.4000000
7     1 0.5000000
8     2 0.6666667

更新

我之前提供的方法并不能正常工作。最好的方法是预先计算,这也是进行模型拟合的最灵活方式。

因此,我们需要使用已经计算好的跨度来对分组的数据框进行建模拟合,然后使用broom::augment将其转换回数据框。

  library(broom)

  expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%    
    filter(z <= x) %>%
    group_by(z) %>% 
    mutate(y = dnorm(x) + 0.4*runif(length(x)),
           span = 10/length(x)) %>% 
    do(fit = list(augment(loess(y~x, data = ., span = unique(.$span)), newdata = .))) %>%
    unnest()
# A tibble: 260 x 7
       z    z1         x           y  span    .fitted    .se.fit
   <int> <int>     <dbl>       <dbl> <dbl>      <dbl>      <dbl>
 1    -5    -5 -5.000000 0.045482851   0.2 0.07700057 0.08151451
 2    -5    -5 -4.795918 0.248923802   0.2 0.18835244 0.05101045
 3    -5    -5 -4.591837 0.243720422   0.2 0.25458037 0.04571323
 4    -5    -5 -4.387755 0.249378098   0.2 0.28132026 0.04947480
 5    -5    -5 -4.183673 0.344429272   0.2 0.24619206 0.04861535
 6    -5    -5 -3.979592 0.256269425   0.2 0.19213489 0.05135924
 7    -5    -5 -3.775510 0.004118627   0.2 0.14574901 0.05135924
 8    -5    -5 -3.571429 0.093698117   0.2 0.15185599 0.04750935
 9    -5    -5 -3.367347 0.267809673   0.2 0.17593182 0.05135924
10    -5    -5 -3.163265 0.208380125   0.2 0.22919335 0.05135924
# ... with 250 more rows
这会导致分组列z重复,但它会智能地重命名以避免名称冲突,因此我们可以忽略它。您可以看到行数与原始数据相同,原始的x,yz仍然存在,以及我们计算出的span
如果您想证明它确实将每个组与正确的跨度配对,可以尝试以下操作:
  ... mutate(...) %>% 
    do(fit = (loess(y~x, data = ., span = unique(.$span)))) %>% 
    pull(fit) %>% purrr::map(summary)

这将打印包含span的模型摘要。

现在只需要绘制我们刚刚创建的增强数据框,并手动重构平滑线和置信区间即可。

  ... %>%
    ggplot(aes(x,y)) + 
    geom_line() +
    geom_ribbon(aes(x, ymin = .fitted - 1.96*.se.fit, 
                    ymax = .fitted + 1.96*.se.fit), 
                alpha = 0.2) +
    geom_line(aes(x, .fitted), color = "blue", size = 1) +
    facet_wrap(~ z)

enter image description here


我很想让这样的东西工作起来,但在我看来,你的技术实际上只是省略了 span(这与将其设置为0.75相同)。例如,省略 span 后重新运行我的代码,您应该会得到与上面的图相同的图。 - banbh
@banbh,您似乎是正确的。stat_smooth实际上忽略了我放在method.args中的所有参数。我需要进行调查。 - Brian
我认为我同意这篇帖子(以及我原始帖子中的链接)的观点,最直接的方法可能是按照这篇帖子所说的那样进行操作(预计算和绘图)。@Lyngbakr的评论是我能想到的唯一其他方法,尽管它可能会使代码更难理解。 - banbh

1

自从我问了如何进行核平滑,我想为此提供一个答案。

我将从将其作为额外数据添加到数据框中并绘制它开始,就像接受的答案一样。

首先,这是我将要使用的数据和软件包(与我的帖子中相同):

library(dplyr)
library(ggplot2) # ggplot2_2.2.1

set.seed(1234)
expand.grid(z = -5:2, x = seq(-5,5, len = 50)) %>%
  mutate(y = dnorm(x) + 0.4*runif(n())) %>% 
  filter(z <= x) ->
  Z

下面是情节:

接下来是情节:

Z %>%
  group_by(z) %>%
  do(data.frame(ksmooth(.$x, .$y, 'normal', bandwidth = 2))) %>%
  ggplot(aes(x,y)) + 
  geom_line(data = Z) +
  geom_line(color = 'blue', size = 1) +
  facet_wrap(~ z)

这只是简单地使用了基本R中的ksmooth。请注意,避免动态平滑非常简单(使带宽恒定即可解决此问题)。实际上,可以通过以下方式恢复动态样式平滑(例如geom_smooth):

Z %>%
  group_by(z) %>%
  do(data.frame(ksmooth(.$x, .$y, 'normal', bandwidth = diff(range(.$x))/5))) %>%
  ggplot(aes(x,y)) + 
  geom_line(data = Z) +
  geom_line(color = 'blue', size = 1) +
  facet_wrap(~ z)

我也参考了https://github.com/hrbrmstr/ggalt/blob/master/R/geom_xspline.r中的示例,将这个想法转化为实际的stat_geom_,具体如下:

geom_ksmooth <- function(mapping = NULL, data = NULL, stat = "ksmooth",
                         position = "identity", na.rm = TRUE, show.legend = NA,
                         inherit.aes = TRUE,
                         bandwidth = 0.5, ...) {
  layer(
    geom = GeomKsmooth,
    mapping = mapping,
    data = data,
    stat = stat,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(bandwidth = bandwidth,
                  ...)
  )
}

GeomKsmooth <- ggproto("GeomKsmooth", GeomLine,
                       required_aes = c("x", "y"),
                       default_aes = aes(colour = "blue", size = 1, linetype = 1, alpha = NA)
)

stat_ksmooth <- function(mapping = NULL, data = NULL, geom = "line",
                         position = "identity", na.rm = TRUE, show.legend = NA, inherit.aes = TRUE,
                         bandwidth = 0.5, ...) {
  layer(
    stat = StatKsmooth,
    data = data,
    mapping = mapping,
    geom = geom,
    position = position,
    show.legend = show.legend,
    inherit.aes = inherit.aes,
    params = list(bandwidth = bandwidth,
                  ...
    )
  )
}

StatKsmooth <- ggproto("StatKsmooth", Stat,
                       required_aes = c("x", "y"),
                       compute_group = function(self, data, scales, params,
                                                bandwidth = 0.5) {
                         data.frame(ksmooth(data$x, data$y, kernel = 'normal', bandwidth = bandwidth))
                       }
)

(请注意,我对上面的代码理解很差。)但现在我们可以做到:

Z %>%
  ggplot(aes(x,y)) + 
  geom_line() +
  geom_ksmooth(bandwidth = 2) +
  facet_wrap(~ z)

"而且平滑处理不是我最初想要的动态的。虽然我在想是否有更简单的方法。"

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接