【问题标题】:R data.table. If column x then row count, else sumR 数据表。如果列 x 则行数,否则求和
【发布时间】:2021-09-19 01:30:37
【问题描述】:

下午

假设我有这张桌子:

df <- data.table(date = rep(c(1,2), each = 2)
                 , user = rep(c(1,2), 2)
                 , turnover = 2:5
                 , profit = 1:4
                 ); df

date user turnover profit
 1    1        2      1
 1    2        3      2
 2    1        4      3
 2    2        5      4

如果我想对多列求和,我会:

# metrics
x <- c('user', 'turnover', 'profit')

# apply
df[, lapply(.SD, function(x) sum(x)), .SDcols=x, by=date]

给出:

date user turnover profit
 1    3        5      3
 2    3        9      7

但是,请注意汇总用户没有意义,相反我想要列“用户”的行数,即

date user turnover profit
 1    2        5      3
 2    2        9      7

假设我不想做一个 1 的虚拟列并将其相加,而是坚持使用 apply data.table。我该怎么做?

谢谢。

【问题讨论】:

    标签: r data.table conditional-statements apply lapply


    【解决方案1】:

    这是一种可能性。您可以将clapply 函数组合在一起,如下所示(注意.N 是每个组的行数):

    df[, c(.(user=.N), lapply(.SD, sum)), by=date, .SDcols=c("turnover", "profit")]
    
    #    date user turnover profit
    # 1:    1    2        5      3
    # 2:    2    2        9      7
    

    【讨论】:

    • 虽然这行得通(谢谢),但如果我有许多列要对其进行另一项操作,则效率将不高。有没有办法解决这个问题?
    • 这真的取决于你想做什么。请记住,j 中的列表会变成列(列表的每个元素都变成一列),并且可以使用 c 函数将多个列表变成单个列表。因此,您可以将其他几个操作与我使用此技巧所做的结合起来。例如:df[, c(.(user=.N), lapply(.SD, sum), lapply(.SD, mean)), ...]。在这里,我一起计算行数、总和和均值。您可以添加其他操作。 继续(见下一条评论)
    • 您所做的是否有效取决于具体操作。检查 datatable.optimize: Optimisations in data.table 的优化级别 '>= 1'(第 3 项)。因此,即使在使用这种技术组合了多个操作之后,您仍然可以从对具有相似结构的表达式的内部优化中受益。如果我确切地知道您还想做什么其他操作,我可以为您提供更好的帮助。
    • 嗨,克里斯蒂安。你确实回答了我的问题。从@akrun 的基准测试来看,你的速度是第二快的,仅次于collapse,这是我刚接触的一个包。我收回我所说的效率低下。虽然我不是指运行时间(我是指从语法的角度来看),但我想如果我有 10 列来计算 na 并且还按日期分组怎么办?如何编码?我仍在解析所有建议。谢谢大家
    【解决方案2】:

    根据this comment 中的OP 要求,以下是将不同 函数应用于不同 列的方法。

    1。使用c() 并分别调用.SD.SDcols

    df[, c(.SD[, lapply(.SD, length), .SDcols = c("user")], 
           .SD[, lapply(.SD, sum), .SDcols = c("turnover", "profit")]), by = date]
    
       date user turnover profit
    1:    1    2        5      3
    2:    2    2        9      7
    

    这不是很优雅,很冗长,并且可能会降低性能,但确实可以完成工作 - 并且保留了列名。

    2。使用purrr::map2()

    df[, purrr::map2(list(length, sum, sum), .SD, \(fn, args) purrr::exec(fn, args)), by = date]
    
       date V1 V2 V3
    1:    1  2  5  3
    2:    2  2  9  7
    

    这不那么罗嗦,但不幸的是,列名丢失了。

    3。使用purrr::map2() 并适当地命名列

    df[, {
      fct <- c("length", "sum", "sum")
      res <- setDT(purrr::map2(fct, .SD, \(fn, args) purrr::exec(fn, args)))
      setnames(res, paste(names(.SD), fct, sep = "_"))
    }, by = date]
    
       date user_length turnover_sum profit_sum
    1:    1           2            5          3
    2:    2           2            9          7
    

    如果使用.SDcols 选择列,这也将起作用:

    df[, {
      fct <- c("length", "mean")
      res <- setDT(purrr::map2(fct, .SD, \(fn, args) purrr::exec(fn, args)))
      setnames(res, paste(names(.SD), fct, sep = "_"))
    }, .SDcols = 2:3, by = date]
    
       date user_length turnover_mean
    1:    1           2           2.5
    2:    2           2           4.5
    

    4。使用purrr::map2() 和灵活的列命名

    如果fct 是一个命名向量并且一个函数已经命名,那么给定的名称将用于相应的列。否则,将使用创建的名称:

    df[, {
      fct <- c(N = "length", "mean")
      res <- setDT(purrr::map2(fct, .SD, \(fn, args) purrr::exec(fn, args)))
      given_names <- names(fct)
      created_names <- paste(names(.SD), fct, sep = "_")
      setnames(res, 
               if (is.null(given_names)) 
                 created_names 
               else 
                 fifelse(given_names == "", created_names, given_names))
    }, .SDcols = 2:3, by = date]
    
       date N turnover_mean
    1:    1 2           2.5
    2:    2 2           4.5
    

    【讨论】:

      【解决方案3】:

      dplyracross一起使用,做这些操作更灵活

      library(dplyr)
      df %>%
           group_by(date) %>%
           summarise(user = n(), across(c(turnover, profit), sum))
      

      -输出

      # A tibble: 2 x 4
         date  user turnover profit
        <dbl> <int>    <int>  <int>
      1     1     2        5      3
      2     2     2        9      7
      

      或者collapse 中的另一个选项来自构建data.table 的同一团队,其唯一目的是提高效率。

      library(collapse)
      collap(df, ~ date, custom = list(fsum = c("turnover", "profit"), 
                 fNobs = "turnover"))
         date fsum.turnover fNobs.turnover fsum.profit
      1:    1             5              2           3
      2:    2             9              2           7
      

      基准测试

      在更大的数据集上测试

      library(data.table)
      library(dplyr)
      library(collapse)
      library(purrr)
      
      # input data
      set.seed(24)
      df1 <- data.table(date = rep(1:1e6, each = 20),
                        user = rep(1:1e6, 20),
                        turnover = rnorm(1e6 * 20),
                        profit = rnorm(1e6 * 20))
      
      # benchmarks
      # - B. Christian Kamgang
      system.time({
        df1[, c(.(user=.N), lapply(.SD, sum)), by=date, .SDcols=c("turnover", "profit")]
        
      })
      #user  system elapsed 
      #0.558   0.110   0.670 
      
      # - Uwe
      #   - first
      system.time({
        df1[, c(.SD[, lapply(.SD, length), .SDcols = c("user")], 
                .SD[, lapply(.SD, sum), .SDcols = c("turnover", "profit")]), by = date]
        
      })
      #Timing stopped at: 245.9 3.336 249.4  0 stopped as it was taking time
      #   - second
      system.time({
        df1[, purrr::map2(list(length, sum, sum), .SD, \(fn, args) purrr::exec(fn, args)), by = date]
        
        
      })
      #user  system elapsed 
      #37.816   0.138  38.016 
      #   - third
      system.time({
        
        df1[, {
          fct <- c("length", "sum", "sum")
          res <- setDT(purrr::map2(fct, .SD, \(fn, args) purrr::exec(fn, args)))
          setnames(res, paste(names(.SD), fct, sep = "_"))
        }, by = date]
        
        
      })
      #user  system elapsed 
      #134.966   1.530 136.620 
      #  - fourth
      system.time({
          df1[, {
            fct <- c("length", "mean")
            res <- setDT(purrr::map2(fct, .SD, \(fn, args) purrr::exec(fn, args)))
            setnames(res, paste(names(.SD), fct, sep = "_"))
          }, .SDcols = 2:3, by = date]
      })
      #user  system elapsed 
      #128.036   1.426 129.610 
      
      
      #   - fifth
      system.time({
          df1[, {
            fct <- c(N = "length", "mean")
            res <- setDT(purrr::map2(fct, .SD, \(fn, args) purrr::exec(fn, args)))
            given_names <- names(fct)
            created_names <- paste(names(.SD), fct, sep = "_")
            setnames(res, 
                     if (is.null(given_names)) 
                       created_names 
                     else 
                       fifelse(given_names == "", created_names, given_names))
          }, .SDcols = 2:3, by = date]
        
      })
      #user  system elapsed 
      #131.960   1.552 133.595 
      

      -这篇文章的解决方案时间

      # - akrun
      #    - first
      system.time({
        df1 %>%
          group_by(date) %>%
          summarise(user = n(), across(c(turnover, profit), sum))
      })
      #user  system elapsed 
      #15.920   0.372  16.322 
      #   - second
      system.time({
        collap(df1, ~ date, custom = list(fsum = c("turnover", "profit"), 
                                         fNobs = "turnover"))
      })
      
      #user  system elapsed 
      #0.311   0.005   0.316 
      

      【讨论】:

        猜你喜欢
        • 2014-05-05
        • 2011-12-29
        • 1970-01-01
        • 1970-01-01
        • 2016-08-17
        • 2013-08-05
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多