总结具有不同功能的不同列的简洁方法

时间:2019-04-11 20:46:32

标签: r dplyr

我的问题建立在similar one的基础上,另外施加了一个约束,即每个变量的名称应只出现一次。

考虑数据框

library( tidyverse )
df <- tibble( potentially_long_name_i_dont_want_to_type_twice = 1:10,
              another_annoyingly_long_name = 21:30 )

我想将mean应用于第一列,并将sum应用于第二列,而不必每次都键入两次列名。

正如我在上面链接的问题所示,summarize允许您执行此操作,但要求每列的名称出现两次。另一方面,summarize_at允许您简洁地将多个函数应用于多个列,但这可以通过在 all 指定列上调用 all 指定函数来实现以一对一的方式进行操作。有没有办法结合summarizesummarize_at的这些独特功能?

我可以用rlang对其进行破解,但是我不确定它是否比每次输入两次变量都更干净:

v <- c("potentially_long_name_i_dont_want_to_type_twice",
       "another_annoyingly_long_name")
f <- list(mean,sum)

## Desired output
smrz <- set_names(v) %>% map(sym) %>% map2( f, ~rlang::call2(.y,.x) )
df %>% summarize( !!!smrz )
# # A tibble: 1 x 2
#   potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                             <dbl>                        <int>
# 1                                             5.5                          255

编辑以解决一些哲学问题

我认为不想避免使用x=f(x)这个惯用法是不合理的。键入长名称可能使我有些过分热衷,但是真正的问题实际上是(相对)长名称彼此非常相似。实例包括核苷酸序列(例如AGCCAGCGGAAACAGTAAGG)和TCGA barcodes。在这种情况下,不仅自动完成功能有限,而且编写诸如AGCCAGCGGAAACAGTAAGG = sum( AGCCAGCGGAAACAGTAAGG )之类的代码会引入不必要的耦合,并增加了在开发和维护代码时分配的两边可能意外不同步的风险。

我完全同意@MrFlick的观点,dplyr提高了代码的可读性,但是我不认为可读性应该以正确性为代价。像summarize_atmutate_at这样的函数之所以出色,是因为它们在将运算符放在其操作数旁边(清晰度)与确保将结果写入正确的列(正确性)之间达到了完美的平衡。

出于同样的原因,我认为所提出的删除变量提及的解决方案在另一个方向上的摆动太大。尽管本质上很聪明-我当然很欣赏它们所节省的额外键入-我认为,通过消除函数和变量名之间的关联,这样的解决方案现在依赖于正确的变量排序,从而产生了偶然错误的风险。 / p>

简而言之,我相信自变量/自总结操作应该只对每个变量名称提及一次。

4 个答案:

答案 0 :(得分:2)

我提出了2个技巧来解决此问题,请参阅底部的两种解决方案的代码和一些详细信息:

函数.at返回一组变量的结果(这里只有一组变量),然后我们可以将其取消拼接,因此我们可以从summarizesummarize_at这两个世界中受益:

df %>% summarize(
  !!!.at(vars(potentially_long_name_i_dont_want_to_type_twice), mean),
  !!!.at(vars(another_annoyingly_long_name), sum))

# # A tibble: 1 x 2
#     potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                               <dbl>                        <dbl>
#   1                                             5.5                          255

summarize的副词,用美元符号表示。

df %>%
  ..flx$summarize(potentially_long_name_i_dont_want_to_type_twice = ~mean(.),
                  another_annoyingly_long_name = ~sum(.))

# # A tibble: 1 x 2
#     potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
#                                               <dbl>                        <int>
#   1                                             5.5                          255

.at的代码

它必须在管道中使用,因为它在父环境中使用.,虽然杂乱无章,但它可以工作。

.at <- function(.vars, .funs, ...) {
  in_a_piped_fun <- exists(".",parent.frame()) &&
    length(ls(envir=parent.frame(), all.names = TRUE)) == 1
  if (!in_a_piped_fun)
    stop(".at() must be called as an argument to a piped function")
  .tbl <- try(eval.parent(quote(.)))
  dplyr:::manip_at(
    .tbl, .vars, .funs, rlang::enquo(.funs), rlang:::caller_env(),
    .include_group_vars = TRUE, ...)
}

我设计了它来结合summarizesummarize_at

df %>% summarize(
  !!!.at(vars(potentially_long_name_i_dont_want_to_type_twice), list(foo=min, bar = max)),
  !!!.at(vars(another_annoyingly_long_name), median))

# # A tibble: 1 x 3
#       foo   bar another_annoyingly_long_name
#     <dbl> <dbl>                        <dbl>
#   1     1    10                         25.5

..flx的代码

..flx输出一个函数,该函数在运行前通过调用a = ~mean(.)来替换其公式参数,例如a = purrr::as_mapper(~mean(.))(a)。方便使用summarizemutate,因为列不能是公式,所以不会有任何冲突。

我喜欢使用美元符号作为简写,并以..开头,所以我可以命名这些“标签”(并给它们一个类"tag")并将它们视为不同的对象(仍在尝试)。 ..flx(summarize)(...)也可以。

..flx <- function(fun){
  function(...){
    mc <- match.call()
    mc[[1]] <- tail(mc[[1]],1)[[1]]
    mc[] <- imap(mc,~if(is.call(.) && identical(.[[1]],quote(`~`))) {
      rlang::expr(purrr::as_mapper(!!.)(!!sym(.y))) 
    } else .)
    eval.parent(mc)
  }
}

class(..flx) <- "tag"

`$.tag` <- function(e1, e2){
  # change original call so x$y, which is `$.tag`(tag=x, data=y), becomes x(y)
  mc <- match.call()
  mc[[1]] <- mc[[2]]
  mc[[2]] <- NULL
  names(mc) <- NULL
  # evaluate it in parent env
  eval.parent(mc)
}

答案 1 :(得分:2)

使用.[[i]]!!names(.)[i]:=来引用第ith列及其名称。

library(tibble)
library(dplyr)
library(rlang)

df %>% summarize(!!names(.)[1] := mean(.[[1]]), !!names(.)[2] := sum(.[[2]])) 

给予:

# A tibble: 1 x 2
  potentially_long_name_i_dont_want_to_type_twice another_annoyingly_long_name
                                            <dbl>                        <int>
1                                             5.5                          255

更新

如果将df分组(这不是问题,因此不需要这样做),则用summarize包围do,如下所示:

library(dplyr)
library(rlang)
library(tibble)

df2 <- tibble(a = 1:10, b = 11:20, g = rep(1:2, each = 5))

df2 %>%
  group_by(g) %>%
  do(summarize(., !!names(.)[1] := mean(.[[1]]), !!names(.)[2] := sum(.[[2]]))) %>%
  ungroup

给予:

# A tibble: 2 x 3
      g     a     b
  <int> <dbl> <int>
1     1     3    65
2     2     8    90

答案 2 :(得分:1)

这是一个骇人听闻的函数,它使用dplyr中未导出的函数,因此不能作为未来的证明,但是您可以为每列指定不同的摘要。

summarise_with <- function(.tbl, .funs) {
  funs <- enquo(.funs)
  syms <- syms(tbl_vars(.tbl))
  calls <- dplyr:::as_fun_list(.funs, funs, caller_env())
  stopifnot(length(syms)==length(calls))
  cols <- purrr::map2(calls, syms, ~dplyr:::expr_substitute(.x, quote(.), .y))
  cols <- purrr::set_names(cols, purrr::map_chr(syms, rlang::as_string))
  summarize(.tbl, !!!cols)
}

那么你可以做

df %>% summarise_with(list(mean, sum))

,而不必完全键入列名。

答案 3 :(得分:1)

为了完整起见,我想提供以下答案,该答案最初是由@IceCreamToucan提出的(我昨晚看到了它,但是在删除之前没有时间回复)。

map2_dfc( df[v], f, ~.y(.x) )

这是一个非常简单,优雅的解决方案,它提醒我们数据帧是列表,可以照此遍历。

与@G一样。 Grothendieck的解决方案不能自动处理分组的小动作,但这一点do都解决不了。