【问题标题】:Multiple functions on multiple columns by group, and create informative column names按组在多个列上执行多个函数,并创建信息丰富的列名
【发布时间】:2018-12-21 11:52:17
【问题描述】:

如何调整数据表操作,以便除了 sum 每个类别的几列之外, 它还会同时计算其他函数,例如 mean 和计数 (.N) 并自动创建列名:“sum c1”、“sum c2”、“sum c4”、“mean c1”、“mean c2”,“mean c4”,最好还有 1 列“counts”?

我的老办法是写出来

mean col1 = ....
mean col2 = ....

等等,data.table命令里面

这可行,但我认为效率极低,如果在新的应用程序版本中,计算取决于用户在 R Shiny 应用程序中选择要计算哪些列的内容,则预编码将不再有效。

我已经阅读了很多帖子和博客文章,但还没有完全弄清楚如何最好地做到这一点。我读到在某些情况下,根据您使用的方法(.sdcols、get、lapply 和 or by =),对大型数据表的操作可能会变得非常慢。因此我添加了一个“相当大”的虚拟数据集

我的真实数据大约是 100k 行乘 100 列和 1-100 个组。

library(data.table)
n = 100000
dt  = data.table(index=1:100000,
                 category = sample(letters[1:25], n, replace = T),
                 c1=rnorm(n,10000),
                 c2=rnorm(n,1000),
                 c3=rnorm(n,100),
                 c4 = rnorm(n,10)
)

# add more columns to test for big data tables 
lapply(c(paste('c', 5:100, sep ='')),
       function(addcol) dt[[addcol]] <<- rnorm(n,1000) )

# Simulate columns selected by shiny app user 

Colchoice <- c("c1", "c4")
FunChoice <- c(".N", "mean", "sum")

# attempt which now does just one function and doesn't add names
dt[, lapply(.SD, sum, na.rm=TRUE), by=category, .SDcols=Colchoice ]

预期输出是每个组的一行,每个选定列的每个函数的一列。

Category  Mean c1 Sum c1 Mean c4 ...
A
B
C
D
E
......

可能是重复的,但我没有找到我需要的确切答案

【问题讨论】:

  • 伙计们,感谢所有出色的答案和研究。我现在不在过圣诞节,会在那些日子之后立即试用并接受一个并给予一堆支持

标签: r data.table


【解决方案1】:

如果我理解正确的话,这个问题由两部分组成:

  1. 如何使用多个函数对列列表进行分组和聚合,并自动生成新的列名。
  2. 如何将函数名称作为字符向量传递。

对于第 1 部分,这几乎Apply multiple functions to multiple columns in data.table 重复,但附加要求应使用 by = 对结果进行分组。

因此,eddi's answer 必须通过在对unlist() 的调用中添加参数recursive = FALSE 来修改:

my.summary = function(x) list(N = length(x), mean = mean(x), median = median(x))
dt[, unlist(lapply(.SD, my.summary), recursive = FALSE), 
   .SDcols = ColChoice, by = category]
    category c1.N   c1.mean c1.median c4.N   c4.mean c4.median
 1:        f 3974  9999.987  9999.989 3974  9.994220  9.974125
 2:        w 4033 10000.008  9999.991 4033 10.004261  9.986771
 3:        n 4025  9999.981 10000.000 4025 10.003686  9.998259
 4:        x 3975 10000.035 10000.019 3975 10.010448  9.995268
 5:        k 3957 10000.019 10000.017 3957  9.991886 10.007873
 6:        j 4027 10000.026 10000.023 4027 10.015663  9.998103
...

对于第 2 部分,我们需要从函数名称的字符向量创建 my.summary()。这可以通过“在语言上编程”来实现,即将表达式组装为字符串,最后对其进行解析和评估:

my.summary <- 
  sapply(FunChoice, function(f) paste0(f, "(x)")) %>% 
  paste(collapse = ", ") %>% 
  sprintf("function(x) setNames(list(%s), FunChoice)", .) %>% 
  parse(text = .) %>% 
  eval()

my.summary
function(x) setNames(list(length(x), mean(x), sum(x)), FunChoice)
<environment: 0xe376640>

或者,我们可以遍历类别并rbind() 之后的结果:

library(magrittr)   # used only to improve readability
lapply(dt[, unique(category)],
       function(x) dt[category == x, 
                      c(.(category = x), unlist(lapply(.SD, my.summary))), 
                      .SDcols = ColChoice]) %>% 
  rbindlist()

基准测试

到目前为止,已经发布了 4 个data.table 和一个dplyr 解决方案。至少有一个答案声称“超快”。因此,我想通过具有不同行数的基准进行验证:

library(data.table)
library(magrittr)
bm <- bench::press(
  n = 10L^(2:6),
  {
    set.seed(12212018)
    dt <- data.table(
      index = 1:n,
      category = sample(letters[1:25], n, replace = T),
      c1 = rnorm(n, 10000),
      c2 = rnorm(n, 1000),
      c3 = rnorm(n, 100),
      c4 = rnorm(n, 10)
    )
    # use set() instead of <<- for appending additional columns
    for (i in 5:100) set(dt, , paste0("c", i), rnorm(n, 1000))
    tables()

    ColChoice <- c("c1", "c4")
    FunChoice <- c("length", "mean", "sum")
    my.summary <- function(x) list(length = length(x), mean = mean(x), sum = sum(x))
    
    bench::mark(
      unlist = {
        dt[, unlist(lapply(.SD, my.summary), recursive = FALSE),
           .SDcols = ColChoice, by = category]
      },
      loop_category = {
        lapply(dt[, unique(category)],
               function(x) dt[category == x, 
                              c(.(category = x), unlist(lapply(.SD, my.summary))), 
                              .SDcols = ColChoice]) %>% 
          rbindlist()
        },
      dcast = {
        dcast(dt, category ~ 1, fun = list(length, mean, sum), value.var = ColChoice)
        },
      loop_col = {
        lapply(ColChoice, function(col)
          dt[, setNames(lapply(FunChoice, function(f) get(f)(get(col))), 
                        paste0(col, "_", FunChoice)), 
             by=category]
        ) %>% 
          Reduce(function(x, y) merge(x, y, by="category"), .)
      },
      dplyr = {
        dt %>% 
          dplyr::group_by(category) %>% 
          dplyr::summarise_at(dplyr::vars(ColChoice), .funs = setNames(FunChoice, FunChoice))
      },
      check = function(x, y) 
        all.equal(setDT(x)[order(category)], 
                  setDT(y)[order(category)] %>%  
                    setnames(stringr::str_replace(names(.), "_", ".")),
                  ignore.col.order = TRUE,
                  check.attributes = FALSE
                  )
    )  
  }
)

绘制时结果更容易比较:

library(ggplot2)
autoplot(bm)

请注意对数时间刻度。

对于这个测试用例,unlist 方法总是最快的方法,其次是 dcastdplyr 正在追赶更大的问题规模n。两种 lapply/loop 方法的性能都较差。特别是,Parfait's approach 循环遍历列并随后合并子结果似乎对n 的问题大小相当敏感。

编辑:第二个基准

正如jangorecki 所建议的那样,我已经用更多的行和不同数量的组重复了基准测试。 由于内存限制,最大问题大小为 10 M 行乘以 102 列,占用 7.7 GB 内存。

所以,基准代码的第一部分修改为

bm <- bench::press(
  n_grp = 10^(1:3),
  n_row = 10L^seq(3, 7, by = 2),
  {
    set.seed(12212018)
    dt <- data.table(
      index = 1:n_row,
      category = sample(n_grp, n_row, replace = TRUE),
      c1 = rnorm(n_row),
      c2 = rnorm(n_row),
      c3 = rnorm(n_row),
      c4 = rnorm(n_row, 10)
    )
    for (i in 5:100) set(dt, , paste0("c", i), rnorm(n_row, 1000))
    tables()
    ...

正如jangorecki 所期望的那样,某些解决方案对组数的敏感性高于其他解决方案。特别是,loop_category 的性能随着组数的增加而下降,而 dcast 似乎受到的影响较小。对于较少的组,unlist 方法总是比 dcast 快,而对于许多组,dcast 更快。然而,对于较大的问题规模,unlist 似乎领先于 dcast

编辑 2019-03-12:计算语言,第 3 次基准测试

this follow-up question 的启发,我添加了一种语言计算 方法,其中整个表达式被创建为字符串、解析和评估。

表达式的创建者

library(magrittr)
ColChoice <- c("c1", "c4")
FunChoice <- c("length", "mean", "sum")
my.expression <- CJ(ColChoice, FunChoice, sorted = FALSE)[
  , sprintf("%s.%s = %s(%s)", V1, V2, V2, V1)] %>% 
  paste(collapse = ", ") %>% 
  sprintf("dt[, .(%s), by = category]", .) %>% 
  parse(text = .)
my.expression
expression(dt[, .(c1.length = length(c1), c1.mean = mean(c1), c1.sum = sum(c1), 
                  c4.length = length(c4), c4.mean = mean(c4), c4.sum = sum(c4)), by = category])

然后由它评估

eval(my.expression)

产生

    category c1.length   c1.mean   c1.sum c4.length   c4.mean   c4.sum
 1:        f      3974  9999.987 39739947      3974  9.994220 39717.03
 2:        w      4033 10000.008 40330032      4033 10.004261 40347.19
 3:        n      4025  9999.981 40249924      4025 10.003686 40264.84
 4:        x      3975 10000.035 39750141      3975 10.010448 39791.53
 5:        k      3957 10000.019 39570074      3957  9.991886 39537.89
 6:        j      4027 10000.026 40270106      4027 10.015663 40333.07
 ...

我已经修改了第二个基准测试的代码以包含这种方法,但必须将额外的列从 100 减少到 25 以应对更小的 PC 的内存限制。图表显示“eval”方法几乎总是最快或第二:

【讨论】:

  • 在所有显示的图中,只有一个测量时间超过一秒,您可以考虑将其放大一点,例如10^(1:4*2)。您还可以扩展数据基数,因为它会产生巨大的差异,请参阅此 groupby 基准上的 query1 和 query5:h2oai.github.io/db-benchmark
  • .Ndata.table包的特殊符号,只有在这个包已经正确加载的情况下才可用。
  • 发现了 .N 的问题所在。没有看到您将函数列表更改为“长度”,现在可以使用了。我可能最终会使用 dcast 解决方案。感谢 Uwe 的所有测试和研究!肯定很有教育意义的答案
  • Uwe,我已经发布了另一个复杂程度的跟进。可以在这里找到:stackoverflow.com/questions/55098961/…
【解决方案2】:

这是一个 data.table 答案:

funs_list <- lapply(FunChoice, as.symbol)
dcast(dt, category~1, fun=eval(funs_list), value.var = Colchoice)

它超级快,可以做你想做的事。

【讨论】:

  • 这真是优雅的 data.table 解决方法,不知道你为什么说差不多。如果你想像在 Shiny 中一样传递字符串,你可以这样做:FunChoice &lt;- c("sum", "mean") funs_list &lt;- lapply(FunChoice, as.symbol) dcast(dt, category~1, fun=eval(funs_list), value.var = Colchoice)
【解决方案3】:

考虑构建一个数据表列表,在其中迭代每个 ColChoice 并应用 FuncChoice 的每个函数(相应地设置名称)。然后,要将所有数据表合并在一起,请在 Reduce 调用中运行 merge。此外,使用get 检索环境对象(函数/列)。

注意ColChoice 被重命名为驼峰式,length 函数替换了 .N 的计数函数形式:

set.seed(12212018)  # RUN BEFORE data.table() BUILD TO REPRODUCE OUTPUT
...

ColChoice <- c("c1", "c4")
FunChoice <- c("length", "mean", "sum")

output <- lapply(ColChoice, function(col)
                   dt[, setNames(lapply(FunChoice, function(f) get(f)(get(col))), 
                                 paste0(col, "_", FunChoice)), 
                      by=category]
          )

final_dt <- Reduce(function(x, y) merge(x, y, by="category"), output)

head(final_dt)

#    category c1_length   c1_mean   c1_sum c4_length   c4_mean   c4_sum
# 1:        a      3893 10000.001 38930003      3893  9.990517 38893.08
# 2:        b      4021 10000.028 40210113      4021  9.977178 40118.23
# 3:        c      3931 10000.008 39310030      3931  9.996538 39296.39
# 4:        d      3954 10000.010 39540038      3954 10.004578 39558.10
# 5:        e      4016  9999.998 40159992      4016 10.002131 40168.56
# 6:        f      3974  9999.987 39739947      3974  9.994220 39717.03

【讨论】:

    【解决方案4】:

    使用 data.table 似乎没有一个简单的答案,因为还没有人回答这个问题。因此,我将提出一个基于 dplyr 的答案,该答案应该可以满足您的要求。我以内置的 iris 数据集为例:

    library(dplyr)
    iris %>% 
       group_by(Species) %>% 
      summarise_at(vars(Sepal.Length, Sepal.Width), .funs = c(sum=sum,mean= mean), na.rm=TRUE)
    
    ## A tibble: 3 x 5
    #  Species    Sepal.Length_sum Sepal.Width_sum Sepal.Length_mean Sepal.Width_mean
    #  <fct>                 <dbl>           <dbl>             <dbl>            <dbl>
    #1 setosa                 245.            171.              5.00             3.43
    #2 versicolor             297.            138.              5.94             2.77
    #3 virginica              323.            149.              6.60             2.97
    

    或对列和函数使用字符向量输入:

    Colchoice <- c("Sepal.Length", "Sepal.Width")
    FunChoice <- c("mean", "sum")
    iris %>% 
      group_by(Species) %>% 
      summarise_at(vars(Colchoice), .funs = setNames(FunChoice, FunChoice), na.rm=TRUE)
    ## A tibble: 3 x 5
    #  Species    Sepal.Length_mean Sepal.Width_mean Sepal.Length_sum Sepal.Width_sum
    #  <fct>                  <dbl>            <dbl>            <dbl>           <dbl>
    #1 setosa                  5.00             3.43             245.            171.
    #2 versicolor              5.94             2.77             297.            138.
    #3 virginica               6.60             2.97             323.            149.
    

    【讨论】:

    • 很好,只是不知道如何在其中计算“长度”或“.N”。 'length' 给出错误'2 个参数传递给'length' 需要一个'
    【解决方案5】:

    如果您需要计算的汇总统计信息是 mean.N 和(也许)mediandata.table 会在整个过程中优化为 c 代码,如果转换,您可能会获得更快的性能将表转换为长格式,以便您可以以数据表可以优化它们的方式进行计算:

    > library(data.table)
    > n = 100000
    > dt  = data.table(index=1:100000,
                       category = sample(letters[1:25], n, replace = T),
                       c1=rnorm(n,10000),
                       c2=rnorm(n,1000),
                       c3=rnorm(n,100),
                       c4 = rnorm(n,10)
      )
    > {lapply(c(paste('c', 5:100, sep ='')), function(addcol) dt[[addcol]] <<- rnorm(n,1000) ); dt}
    
    > Colchoice <- c("c1", "c4")
    
    > dt[, .SD
         ][, c('index', 'category', Colchoice), with=F
         ][, melt(.SD, id.vars=c('index', 'category'))
         ][, mean := mean(value), .(category, variable)
         ][, median := median(value), .(category, variable)
         ][, N := .N, .(category, variable)
         ][, value := NULL
         ][, index := NULL
         ][, unique(.SD)
         ][, dcast(.SD, category ~ variable, value.var=c('mean', 'median', 'N') 
         ]
    
        category mean_c1 mean_c4 median_c1 median_c4 N_c1 N_c4
     1:        a   10000  10.021     10000    10.041 4128 4128
     2:        b   10000  10.012     10000    10.003 3942 3942
     3:        c   10000  10.005     10000     9.999 3926 3926
     4:        d   10000  10.002     10000    10.007 4046 4046
     5:        e   10000   9.974     10000     9.993 4037 4037
     6:        f   10000  10.025     10000    10.015 4009 4009
     7:        g   10000   9.994     10000     9.998 4012 4012
     8:        h   10000  10.007     10000     9.986 3950 3950
    ...
    

    【讨论】:

    • 嗯,我真的不明白这如何满足 OP 的要求,因为他们想提供两个向量,一个带有列名,另一个带有函数名。在您的示例中,您只需手动计算均值、中位数等。另外,我不确定为什么您的第一步是 dt[, .SD](这似乎没有必要),然后是 dt[, ..., with=FALSE],这也不是必需的,并且可能会进行深层复制。不过,您将整形为长格式的一般想法可能很好。
    • 是的,这不能满足将函数向量作为文本提供的要求。我没有看到一个干净的方法来做这件事,并认为这可能是需要编程的 2 个向量中最不可能的。 dt[, SD] 作为管道中的第一行,..., with=FALSE 是为了提高可读性的选择。
    • 代码有效(除了缺少最后的右括号),但它似乎比我已经拥有的代码过于复杂,而且确实如前所述并不能满足我对这两个功能的需求和来自文本字符串的列
    猜你喜欢
    • 2017-09-29
    • 2011-10-26
    • 1970-01-01
    • 2011-07-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-18
    • 2013-02-21
    相关资源
    最近更新 更多