使用dplyr::mutate添加多个值

37

这个问题在dplyr的Github repo上已经有了几个讨论,还有至少一个相关的Stack Overflow问题,但它们都没能完全回答我的问题——我想。

  • 在dplyr的mutate中添加多列大致是我想要的,但那里只提供了一个特例的答案(tidyr::separate),似乎对我不适用。
  • 这个问题(“使用返回多个值/列的函数进行总结或变异”)建议使用“do()”。

我的使用场景是:我想计算精确二项置信区间。

dd <- data.frame(x=c(3,4),n=c(10,11))
get_binCI <- function(x,n) {
    rbind(setNames(c(binom.test(x,n)$conf.int),c("lwr","upr")))
}
with(dd[1,],get_binCI(x,n))
##             lwr       upr
## [1,] 0.06673951 0.6524529

我可以用do()完成这个任务,但我想知道是否有更具表达力的方式来完成它(感觉mutate() 可能会有一个像summarise()正在讨论的.n参数...)

library("dplyr")
dd %>% group_by(x,n) %>%
    do(cbind(.,get_binCI(.$x,.$n)))

## Source: local data frame [2 x 4]
## Groups: x, n
## 
##   x  n        lwr       upr
## 1 3 10 0.06673951 0.6524529
## 2 4 11 0.10926344 0.6920953

2
你确定要使用 dplyr 来完成这个任务吗?如果使用 data.table,你可以很快地执行 setDT(dd)[, as.list(get_binCI(x, n)), by = .(x, n)]。虽然我的心灵感应能力无法确定你所说的“表达方式”具体是什么意思... - David Arenburg
4
这确实很好。我*曾希望得到一个dplyr的答案(尽管如果我上面的解决方案是目前最好的,我也不会感到惊讶)。我并不反对data.table,但我更喜欢dplyr,而且——主要是——我仍然在花费大量的脑力来理解它,目前不想添加一整套新的语法(也不想将其强加给我的学生和同事)。但如果你那样回答我,我会点赞的,这很有用。 - Ben Bolker
1
大家好,希望能够提高这个问题的关注度;现在有没有更好的嵌套方法来解决这个问题?我正在尝试,但还没有成功。 - Aaron left Stack Overflow
@Aaron,我尝试使用unnestmap2,你可能会感兴趣。 - markdly
7个回答

19

另外一种变体,虽然我认为我们都在纠结琐事。

> dd <- data.frame(x=c(3,4),n=c(10,11))
> get_binCI <- function(x,n) {
+   as_data_frame(setNames(as.list(binom.test(x,n)$conf.int),c("lwr","upr")))
+ }
> 
> dd %>% 
+   group_by(x,n) %>%
+   do(get_binCI(.$x,.$n))
Source: local data frame [2 x 4]
Groups: x, n

  x  n        lwr       upr
1 3 10 0.06673951 0.6524529
2 4 11 0.10926344 0.6920953

就可读性而言,我个人觉得这更可取:

foo  <- function(x,n){
    bi <- binom.test(x,n)$conf.int
    data_frame(lwr = bi[1],
               upr = bi[2])
}

dd %>% 
    group_by(x,n) %>%
    do(foo(.$x,.$n))

... 但现在我们正在真的过于苛求。


事实证明,我并不需要data.frame()(请参阅编辑)。 - Ben Bolker
在我的实际使用情况中,我需要按照除了 xn 之外的其他内容进行分组...但是我可能可以使用这个。 - Ben Bolker
我认为你的第二个解决方案更胜一筹,但我会暂时不接受。 - Ben Bolker
1
@weymouth 请尝试阅读magrittr文档 - joran

18

另一个选择是使用purrr::map函数系列。

如果您在get_binCI函数中用dplyr::bind_rows替换rbind

library(tidyverse)

dd <- data.frame(x = c(3, 4), n = c(10, 11))
get_binCI <- function(x, n) {
  bind_rows(setNames(c(binom.test(x, n)$conf.int), c("lwr", "upr")))
}

您可以使用purrr::map2tidyr::unnest来操作:


dd %>% mutate(result = map2(x, n, get_binCI)) %>% unnest()

#>   x  n        lwr       upr
#> 1 3 10 0.06673951 0.6524529
#> 2 4 11 0.10926344 0.6920953

或者使用 purrr::map2_dfrdplyr::bind_cols:

dd %>% bind_cols(map2_dfr(.$x, .$n, get_binCI))

#>   x  n        lwr       upr
#> 1 3 10 0.06673951 0.6524529
#> 2 4 11 0.10926344 0.6920953

1
在dplyr 0.8.5中,这将需要是dd %>% mutate(result = map2(x, n, get_binCI)) %>% unnest(result)。此外,unnest的帮助文档表明它主要用于数据框列表。帮助文件中提供了替代方法。 - Tony Ladson

7

这里有一个使用data.table包的快速解决方案。

首先,对函数进行一些小改动。

get_binCI <- function(x,n) as.list(setNames(binom.test(x,n)$conf.int, c("lwr", "upr")))

然后,只需简单地
library(data.table)
setDT(dd)[, get_binCI(x, n), by = .(x, n)]
#    x  n        lwr       upr
# 1: 3 10 0.06673951 0.6524529
# 2: 4 11 0.10926344 0.6920953

这是一个基本解决方案,@David Arenburg!! dd[, c('lwr','upr')] <- t(mapply(get_binCI, dd[, 1], dd[, 2])) - rawr
7
不确定你为什么要在我的回答下发布这个评论 :) 我建议您将其发布为您自己的解决方案(我保证会点赞)。 - David Arenburg
@rawr,Map()更安全(没有简化)吗? - Ben Bolker
@BenBolker 但我猜你也必须使用 do.call,对吗? - rawr
我一直以为 Map()mapply() 基本上是相同的:‘Map’ 是一个简单的包装器,用于调用 ‘mapply’,它不尝试简化结果,类似于 Common Lisp 中的 ‘mapcar’(但参数会被循环利用)。 - Ben Bolker
@BenBolker 唯一的区别是 mapply 的默认值为 SIMPLIFY = TRUE 而 map 是 false,而且你显然不能更改 map 的默认值。 - rawr

7
以下是使用 rowwisenesting 的一些可能性。
library("dplyr")
library("tidyr")

为了好玩,这是一个包含重复x/n组合的数据框。

dd <- data.frame(x=c(3, 4, 3), n=c(10, 11, 10))

一种返回数据框的CI函数版本,类似于@Joran的实现
get_binCI_df <- function(x,n) {
  binom.test(x, n)$conf.int %>% 
    setNames(c("lwr", "upr")) %>% 
    as.list() %>% as.data.frame()
}

与之前一样,按照xn分组可以消除重复。

dd %>% group_by(x,n) %>% do(get_binCI_df(.$x,.$n))
# # A tibble: 2 x 4
# # Groups:   x, n [2]
#       x     n       lwr       upr
#   <dbl> <dbl>     <dbl>     <dbl>
# 1     3    10 0.1181172 0.8818828
# 2     4    11 0.1092634 0.6920953

使用rowwise可以保留所有行,但是除非您使用cbind(.将它们放回(就像Ben在他的OP中所做的那样),否则会删除xn
dd %>% rowwise() %>% do(cbind(., get_binCI_df(.$x,.$n)))
# Source: local data frame [3 x 4]
# Groups: <by row>
#   
# # A tibble: 3 x 4
#       x     n        lwr       upr
# * <dbl> <dbl>      <dbl>     <dbl>
# 1     3    10 0.06673951 0.6524529
# 2     4    11 0.10926344 0.6920953
# 3     3    10 0.06673951 0.6524529

感觉嵌套可能会更加清晰,但这是我能达到的最好结果。使用 mutate 意味着我可以直接使用 xn 而不是 .$x.$n,但是 mutate 期望一个单一的值,所以它需要被包装在 list 中。

dd %>% rowwise() %>% mutate(ci=list(get_binCI_df(x, n))) %>% unnest()
# # A tibble: 3 x 4
#       x     n        lwr       upr
#   <dbl> <dbl>      <dbl>     <dbl>
# 1     3    10 0.06673951 0.6524529
# 2     4    11 0.10926344 0.6920953
# 3     3    10 0.06673951 0.6524529

最后,看起来像这样的问题是dplyr的一个未解决问题(截至2017年10月5日);请参见https://github.com/tidyverse/dplyr/issues/2326。如果类似的功能被实现,那将是最简单的方法!

5

这个使用了“标准”的dplyr工作流程,但正如@BenBolker在评论中指出的那样,它需要调用get_binCI两次:

dd %>% group_by(x,n) %>%
  mutate(lwr=get_binCI(x,n)[1],
         upr=get_binCI(x,n)[2])

  x  n        lwr       upr
1 3 10 0.06673951 0.6524529
2 4 11 0.10926344 0.6920953

是的,这是一个解决方案,但这种方法的丑陋之处在于需要两次调用 get_binCI()。这种方式是否比 do(cbind(.,data.frame(get_binCI(.$x,.$n))) 更好或更差,这取决于观察者的眼光。(我可以通过将其放入 get_binCI 中来摆脱 data.frame())。 - Ben Bolker
我同意。我只是试图找到一些使用dplyr而不调用“do”的方法。 - eipi10

3

虽然这是一个旧问题(有很多好的答案),但这是使用tidyverse的broom包的一个很好的用例,该包处理测试和建模对象的输出整理(如binom.testlm等)。

它比其他方法更冗长,但我认为它符合您对更表达式方法的期望。

流程如下:

  1. 定义你将在其上运行binom.test的组(在此情况下,这些组由xn定义)并将它们进行nest,从而为每个组创建单独的数据框(在完整的数据框内)
  2. map binom.test调用到来自每个组的xn
  3. 清理每个组的binom.test输出(这是broom发挥作用的地方)
  4. 将清理过的测试输出数据框unnest到完整的数据框中

现在,您剩下的是一个数据框,其中每行都包含xn值,结合了相应binom.test的所有输出,整洁地格式化为每个输出信息(点估计,上/下置信区间,p-值等)的单独列。

library(tidyverse)
library(broom)
dd <- data.frame(x=c(3,4),n=c(10,11))
dd %>%
  group_by(x, n) %>%
  nest() %>%
  mutate(test = map(data, ~tidy(binom.test(x, n)))) %>%
  unnest(test)
#> # A tibble: 2 x 11
#> # Groups:   x, n [2]
#>       x     n data  estimate statistic p.value parameter conf.low conf.high
#>   <dbl> <dbl> <lis>    <dbl>     <dbl>   <dbl>     <dbl>    <dbl>     <dbl>
#> 1     3    10 <tib…    0.3           3   0.344        10   0.0667     0.652
#> 2     4    11 <tib…    0.364         4   0.549        11   0.109      0.692
#> # … with 2 more variables: method <chr>, alternative <chr>

从这里开始,只需要稍微进行一些操作,选择所需的输出变量并将它们重命名,就可以得到您想要的精确格式:

dd %>%
  group_by(x, n) %>%
  nest() %>%
  mutate(test = map(data, ~tidy(binom.test(x, n)))) %>%
  unnest(test) %>%
  rename(lwr = conf.low, upr = conf.high) %>%
  select(x, n, lwr, upr)
#> # A tibble: 2 x 4
#> # Groups:   x, n [2]
#>       x     n    lwr   upr
#>   <dbl> <dbl>  <dbl> <dbl>
#> 1     3    10 0.0667 0.652
#> 2     4    11 0.109  0.692

如前所述,它很冗长。比如 @joran 精炼简洁的写作风格要多得多。

dd %>% 
    group_by(x,n) %>%
    do(foo(.$x,.$n))

然而,使用“扫帚”方法的好处是您无需定义函数foo(或get_binCI)。它是完全自包含的,并且在我看来更加表达和灵活。

非常好且最新的答案。如果与原帖作者选择的答案相比,哪一个会更快(假设我们追求的是原始速度)? - venrey

1

这里有另一种选项,它依赖于使用mutate和summarise自动解压命名的tibble结果参考

dd <- data.frame(x=c(3,4),n=c(10,11))

get_binCI <- function(x,n) {
  s1 <- binom.test(x,n)$conf.int
  names(s1) <- c("lwr", "upr")
  as_tibble(as.list(s1))
}

dd %>% 
  group_by(x,n) %>%
  summarise(get_binCI(x, n))

# A tibble: 2 × 4
# Groups:   x [2]
      x     n    lwr   upr
  <dbl> <dbl>  <dbl> <dbl>
1     3    10 0.0667 0.652
2     4    11 0.109  0.692

当使用像quantile这样的函数时,as_tibble(as.list())部分可以移动到summarise内:

mtcars %>% 
  group_by(cyl) %>% 
  summarise(as_tibble(as.list(quantile(mpg)))) 

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