data.table 按行求和,平均值,最小值,最大值,如 dplyr?

Posted

技术标签:

【中文标题】data.table 按行求和,平均值,最小值,最大值,如 dplyr?【英文标题】:data.table row-wise sum, mean, min, max like dplyr? 【发布时间】:2015-09-24 08:23:18 【问题描述】:

还有其他关于数据表上的按行运算符的帖子。他们要么是too simple,要么解决了specific scenario

我的问题更笼统。有一个使用 dplyr 的解决方案。我玩过但未能找到使用 data.table 语法的等效解决方案。您能否建议一个优雅的 data.table 解决方案,它可以重现与 dplyr 版本相同的结果?

编辑 1:在真实数据集(10MB,73000 行,24 个数字列上进行的统计)的建议解决方案的基准总结。基准测试结果是主观的。但是,经过的时间始终可以重现。

| Solution By | Speed compared to dplyr     |
|-------------|-----------------------------|
| Metrics v1  |  4.3 times SLOWER (use .SD) |
| Metrics v2  |  5.6 times FASTER           |
| ExperimenteR| 15   times FASTER           |
| Arun v1     |  3   times FASTER (Map func)|
| Arun v2     |  3   times FASTER (foo func)|
| Ista        |  4.5 times FASTER           |

编辑 2:我在一天后添加了 NACount 列。这就是为什么在各种贡献者建议的解决方案中找不到此专栏的原因。

数据设置

library(data.table)
dt <- data.table(ProductName = c("Lettuce", "Beetroot", "Spinach", "Kale", "Carrot"),
    Country = c("CA", "FR", "FR", "CA", "CA"),
    Q1 = c(NA, 61, 40, 54, NA), Q2 = c(22,  8, NA,  5, NA),
    Q3 = c(51, NA, NA, 16, NA), Q4 = c(79, 10, 49, NA, NA))

#    ProductName Country Q1 Q2 Q3 Q4
# 1:     Lettuce      CA NA 22 51 79
# 2:    Beetroot      FR 61  8 NA 10
# 3:     Spinach      FR 40 NA NA 49
# 4:        Kale      CA 54  5 16 NA
# 5:      Carrot      CA NA NA NA NA

使用 dplyr + rowwise() 的解决方案

library(dplyr) ; library(magrittr)
dt %>% rowwise() %>% 
    transmute(ProductName, Country, Q1, Q2, Q3, Q4,
     AVG = mean(c(Q1, Q2, Q3, Q4), na.rm=TRUE),
     MIN = min (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
     MAX = max (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
     SUM = sum (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
     NAcnt= sum(is.na(c(Q1, Q2, Q3, Q4))))

#   ProductName Country Q1 Q2 Q3 Q4      AVG MIN  MAX SUM NAcnt
# 1     Lettuce      CA NA 22 51 79 50.66667  22   79 152     1
# 2    Beetroot      FR 61  8 NA 10 26.33333   8   61  79     1
# 3     Spinach      FR 40 NA NA 49 44.50000  40   49  89     2
# 4        Kale      CA 54  5 16 NA 25.00000   5   54  75     1
# 5      Carrot      CA NA NA NA NA      NaN Inf -Inf   0     4

data.table 出现错误(计算整列而不是每行)

dt[, .(ProductName, Country, Q1, Q2, Q3, Q4,
    AVG = mean(c(Q1, Q2, Q3, Q4), na.rm=TRUE),
    MIN = min (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
    MAX = max (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
    SUM = sum (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
    NAcnt= sum(is.na(c(Q1, Q2, Q3, Q4))))]

#    ProductName Country Q1 Q2 Q3 Q4      AVG MIN MAX SUM NAcnt
# 1:     Lettuce      CA NA 22 51 79 35.90909   5  79 395     9
# 2:    Beetroot      FR 61  8 NA 10 35.90909   5  79 395     9
# 3:     Spinach      FR 40 NA NA 49 35.90909   5  79 395     9
# 4:        Kale      CA 54  5 16 NA 35.90909   5  79 395     9
# 5:      Carrot      CA NA NA NA NA 35.90909   5  79 395     9

几乎解决方案,但更复杂且缺少 Q1、Q2、Q3、Q4 输出列

dtmelt <- reshape2::melt(dt, id=c("ProductName", "Country"),
            variable.name="Quarter", value.name="Qty")

dtmelt[, .(AVG = mean(Qty, na.rm=TRUE),
    MIN = min (Qty, na.rm=TRUE),
    MAX = max (Qty, na.rm=TRUE),
    SUM = sum (Qty, na.rm=TRUE),
    NAcnt= sum(is.na(Qty))), by = list(ProductName, Country)]

#    ProductName Country      AVG MIN  MAX SUM NAcnt
# 1:     Lettuce      CA 50.66667  22   79 152     1
# 2:    Beetroot      FR 26.33333   8   61  79     1
# 3:     Spinach      FR 44.50000  40   49  89     2
# 4:        Kale      CA 25.00000   5   54  75     1
# 5:      Carrot      CA      NaN Inf -Inf   0     4

【问题讨论】:

dt[, AVG := rowMeans(.SD, na.rm=T),.SDcols=c(Q1, Q2,Q3,Q4)] @ExperimenteR 谢谢(SDcols 应该是一个字符向量吗?)我试过这个dt[, .(Q1, Q2, Q3, Q4, AVG = rowMeans(.SD, na.rm=T), MIN = pmin(Q1,Q2,Q3,Q4, na.rm=T), MAX = pmax(Q1,Q2,Q3,Q4, na.rm=T) ), .SDcols=c("Q1","Q2","Q3","Q4")] 但仍然错过了 SUM 并且没有 ProductName、Country 列 @Metrics 没有评估错误的输出 b/c:dt[, `:=` (AVG = rowMeans(.SD, na.rm=TRUE), MIN = min(.SD, na.rm=TRUE), MAX = max(.SD, na.rm=TRUE), SUM = sum(.SD, na.rm=TRUE)), .SDcols = c("Q1","Q2","Q3","Q4"), by=1:nrow(dt)] Warning messages: 1: In min(c(NA_real_, NA_real_, NA_real_, NA_real_), na.rm = TRUE) : no non-missing arguments to min; returning Inf 2: In max(c(NA_real_, NA_real_, NA_real_, NA_real_), na.rm = TRUE) : no non-missing arguments to max; returning -Inf 看我的回答。我已更新代码并从 cmets 中删除。 Dplyr 和 data.table 都针对 NaN 和 -Inf 发出警告。 data.table 尽可能使用基本 R 函数,以免强加“围墙花园”方法。但是基本 R 没有执行此操作的好函数 :-(。所以我们'将必须实现colwise()rowwise() 下提交的#1063 函数...我已将其标记为下一个版本。 【参考方案1】:

您可以使用matrixStats 包中的高效逐行函数。

library(matrixStats)
dt[, `:=`(MIN = rowMins(as.matrix(.SD), na.rm=T),
          MAX = rowMaxs(as.matrix(.SD), na.rm=T),
          AVG = rowMeans(.SD, na.rm=T),
          SUM = rowSums(.SD, na.rm=T)), .SDcols=c(Q1, Q2,Q3,Q4)]

dt
#    ProductName Country Q1 Q2 Q3 Q4 MIN  MAX      AVG SUM
# 1:     Lettuce      CA NA 22 51 79  22   79 50.66667 152
# 2:    Beetroot      FR 61  8 NA 10   8   61 26.33333  79
# 3:     Spinach      FR 40 NA 79 49  40   79 56.00000 168
# 4:        Kale      CA 54  5 16 NA   5   54 25.00000  75
# 5:      Carrot      CA NA NA NA NA Inf -Inf      NaN   0

对于 500000 行的数据集(使用来自 CRAN 的 data.table

dt <- rbindlist(lapply(1:100000, function(i)dt))
system.time(dt[, `:=`(MIN = rowMins(as.matrix(.SD), na.rm=T),
                      MAX = rowMaxs(as.matrix(.SD), na.rm=T),
                      AVG = rowMeans(.SD, na.rm=T),
                      SUM = rowSums(.SD, na.rm=T)), .SDcols=c("Q1", "Q2","Q3","Q4")])
#  user  system elapsed 
# 0.089   0.004   0.093

rowwise(或by=1:nrow(dt))是for loop的“委婉说法”,例如

library(dplyr) ; library(magrittr)
system.time(dt %>% rowwise() %>% 
  transmute(ProductName, Country, Q1, Q2, Q3, Q4,
            MIN = min (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
            MAX = max (c(Q1, Q2, Q3, Q4), na.rm=TRUE),
            AVG = mean(c(Q1, Q2, Q3, Q4), na.rm=TRUE),
            SUM = sum (c(Q1, Q2, Q3, Q4), na.rm=TRUE)))
#   user  system elapsed 
# 80.832   0.111  80.974 

system.time(dt[, `:=`(AVG= mean(as.numeric(.SD),na.rm=TRUE),MIN = min(.SD, na.rm=TRUE),MAX = max(.SD, na.rm=TRUE),SUM = sum(.SD, na.rm=TRUE)),.SDcols=c("Q1", "Q2","Q3","Q4"),by=1:nrow(dt)] )
#    user  system elapsed 
# 141.492   0.196 141.757

【讨论】:

您的解决方案是最快的! (请参阅原始问题中的基准)感谢您介绍 matrixStats 包。我想知道与 Arun 和 Metrics 的第二个解决方案相比,您的解决方案对内存资源的影响。【参考方案2】:

使用by=1:nrow(dt),在data.table中执行逐行操作

 library(data.table)
dt[, `:=`(AVG= mean(as.numeric(.SD),na.rm=TRUE),MIN = min(.SD, na.rm=TRUE),MAX = max(.SD, na.rm=TRUE),SUM = sum(.SD, na.rm=TRUE)),.SDcols=c(Q1, Q2,Q3,Q4),by=1:nrow(dt)] 
   ProductName Country Q1 Q2 Q3 Q4      AVG MIN  MAX SUM
1:     Lettuce      CA NA 22 51 79 50.66667  22   79 152
2:    Beetroot      FR 61  8 NA 10 26.33333   8   61  79
3:     Spinach      FR 40 NA 79 49 56.00000  40   79 168
4:        Kale      CA 54  5 16 NA 25.00000   5   54  75
5:      Carrot      CA NA NA NA NA      NaN Inf -Inf   0

Warning messages:
1: In min(c(NA_real_, NA_real_, NA_real_, NA_real_), na.rm = TRUE) :
  no non-missing arguments to min; returning Inf
2: In max(c(NA_real_, NA_real_, NA_real_, NA_real_), na.rm = TRUE) :
  no non-missing arguments to max; returning -Inf

您收到警告消息,因为在第 5 行,您正在计算最大值、总和、最小值和最大值。例如,见下图:

min(c(NA,NA,NA,NA),na.rm=TRUE)
[1] Inf
Warning message:
In min(c(NA, NA, NA, NA), na.rm = TRUE) :
  no non-missing arguments to min; returning Inf

【讨论】:

同样的错误,这可能是 b/c 我使用的是最新的 data.table 1.9.4(R 版本 3.2.0 (2015-04-16))吗?此外,我必须将 SDcols 放在引号 .SDcols=c("Q1","Q2","Q3","Q4") 中以避免“找不到对象 'Q1'”。这是我运行您的代码时的错误:1: In min(c(NA_real_, NA_real_, NA_real_, NA_real_), na.rm = TRUE) : no non-missing arguments to min; returning Inf 2: In max(c(NA_real_, NA_real_, NA_real_, NA_real_), na.rm = TRUE) : no non-missing arguments to max; returning -Inf 那些是警告而不是错误(我也明白了)。您收到警告是因为您的输出返回无限值 -InfInfNaN(因为您正在取平均值、总和、最小值和最大值)。如果您运行自己的 dplyr 代码,它也会发出相同的警告。我正在使用开发版本 1.9.5+(您可以从 github 获取)。我不确定你为什么需要加上引号。它对我来说没有引号。在答案中查看我的更新。 哦,那是真的。我忘了打印(dt)。对不起!顺便说一句,如果我不在.SDcols=c(Q1,Q2,Q3,Q4) (data.table 1.9.4, R v3.2.0) 中的列名周围加上引号,你知道为什么我得到object 'Q1' not found 刚刚将您的解决方案应用于 10MB 数据集,73000 行。 dplyr 版本比您建议的实现快 4 倍。会不会是计算 AVG 中的 as.numeric(.SD)? 你不能对这么小的数据集进行基准测试,这毫无意义。【参考方案3】:

只是另一种方式(虽然效率不高,因为每次都会调用na.omit(),而且还会分配许多内存):

require(data.table)
new_cols = c("MIN", "MAX", "SUM", "AVG")
dt[, (new_cols) := Map(function(x, f) f(x), 
                       list(na.omit(c(Q1,Q2,Q3,Q4))), 
                       list(min, max, sum, mean)),
   by = 1:nrow(dt)]

#    ProductName Country Q1 Q2 Q3 Q4 MIN  MAX SUM      AVG
# 1:     Lettuce      CA NA 22 51 79  22   79 152 50.66667
# 2:    Beetroot      FR 61  8 NA 10   8   61  79 26.33333
# 3:     Spinach      FR 40 NA 79 49  40   79 168 56.00000
# 4:        Kale      CA 54  5 16 NA   5   54  75 25.00000
# 5:      Carrot      CA NA NA NA NA Inf -Inf   0      NaN

但正如我所提到的,一旦实现了colwise()rowwise(),这将变得更加简单。这种情况下的语法可能类似于:

dt[, rowwise(.SD, list(MIN=min, MAX=max, SUM=sum, AVG=mean), na.rm=TRUE), by = 1:nrow(dt)]
# `by = ` is really not necessary in this case.

对于这种情况,甚至更直接:

rowwise(dt, list(...), na.rm=TRUE)

编辑:

另一种变化:

myNACount <- function(x, ...) length(attributes(x)$na.action)
foo <- function(x, ...) 
    funs = c(min, max, mean, sum, myNACount)
    lapply(funs, function(f) f(x, ...))


dt[, (new_cols) := foo(na.omit(c(Q1, Q2, Q3, Q4)), na.rm=TRUE), by=1:nrow(dt)]
#    ProductName Country Q1 Q2 Q3 Q4 MIN  MAX      SUM AVG NAs
# 1:     Lettuce      CA NA 22 51 79  22   79 50.66667 152   1
# 2:    Beetroot      FR 61  8 NA 10   8   61 26.33333  79   1
# 3:     Spinach      FR 40 NA NA 49  40   49 44.50000  89   2
# 4:        Kale      CA 54  5 16 NA   5   54 25.00000  75   1
# 5:      Carrot      CA NA NA NA NA Inf -Inf      NaN   0   4

【讨论】:

是的,您为什么在rowwise 潜在解决方案中添加by 可能会有像dt[, if (TRUE) do_bla else rowwise(...), by=some_cols]这样的复杂场景(就像我说的,在这种情况下,没有必要)。 @Arun myNACount &lt;- function(x) length(attributes(x)$na.action) 功能非常出色。谢谢。我希望我能理解优化的机制。您建议的第二个变体非常快。 @Arun Ahem ...对不起,我在基准测试中犯了一个错误。您制作的第二个变体比第一个版本稍快。最快的执行时间来自 ExperimenteR 的解决方案。 @Polymerase,不用担心。我想我们都在这里学到了很多:-)。大问。【参考方案4】:

apply 函数可用于执行逐行计算。单独定义函数使事情更整洁:

dstats <- function(x)
    c(mean(x,na.rm=TRUE),
      min(x, na.rm=TRUE),
      max(x, na.rm=TRUE),
      sum(x, na.rm=TRUE))

该函数现在可以应用于 data.table 的行。

(dt[,
   c("AVG", "MIN", "MAX", "SUM") := data.frame(t(apply(.SD, 1, dstats))),
   .SDcols=c("Q1", "Q2","Q3","Q4"),
])

请注意,使用 [.data.table 执行此操作的唯一优点是它允许使用 := 通过引用快速添加。

这比matrixStats 解决方案更慢但更灵活,并且比@ExperimenteR 的dplyr 解决方案更快,计时为36 秒(我对其他方法的计时与@ExperimenteR 的答案中的类似)。

【讨论】:

1. apply().SD 转换为矩阵 = mem alloc。 2. t() 转置结果 = 另一个副本。 3. data.frame() = 另一个内存分配。不确定这里是否需要with = FALSE。通过避免所有这些副本,我们当然可以做得更好。 @Arun 也许,但它已经相当快了,如果我们需要更快的速度,我们可以使用matrixStats。我有with = FALSE,因为help(":=") 暗示当RHS 返回一个列表时需要这样做。 相当快是不够的,真的,特别是当提高效率是微不足道的时候。我已经在 github 项目页面上回复了您的回复,详细说明了原因。在with=FALSE 上,这不是它的意思,但我理解这种困惑。会修复的。 @Ista 您的解决方案是第二快的,请参阅原始问题中的基准测试结果。【参考方案5】:

我希望其他人在遇到同样的问题时,他们可能会有所帮助。

第一种方法:结合基础R

dt[,`:=`(MIN = apply(dt[, Q1:Q4], 1, FUN = min, na.rm=TRUE),
       MAX = apply(dt[, Q1:Q4], 1, FUN = max, na.rm = TRUE),
       AVG = rowMeans(dt[, Q1:Q4], na.rm = TRUE),
       SUM = rowSums(dt[, Q1:Q4], na.rm = TRUE))][]
# ProductName Country Q1 Q2 Q3 Q4 MIN  MAX      AVG SUM
# 1:     Lettuce      CA NA 22 51 79  22   79 50.66667 152
# 2:    Beetroot      FR 61  8 NA 10   8   61 26.33333  79
# 3:     Spinach      FR 40 NA NA 49  40   49 44.50000  89
# 4:        Kale      CA 54  5 16 NA   5   54 25.00000  75
# 5:      Carrot      CA NA NA NA NA Inf -Inf      NaN   0

第二种方法:基于@ExperimenteR的想法,使用matrixStats包

dt1 <- dt[,`:=`(MIN = rowMins(as.matrix(dt[, Q1:Q4]), na.rm=TRUE),
                MAX = rowMaxs(as.matrix(dt[, Q1:Q4]), na.rm = TRUE),
                AVG = rowMeans(dt[, Q1:Q4], na.rm = TRUE),
                SUM = rowSums(dt[, Q1:Q4], na.rm = TRUE))][]
# ProductName Country Q1 Q2 Q3 Q4 MIN  MAX      AVG SUM
# 1:     Lettuce      CA NA 22 51 79  22   79 50.66667 152
# 2:    Beetroot      FR 61  8 NA 10   8   61 26.33333  79
# 3:     Spinach      FR 40 NA NA 49  40   49 44.50000  89
# 4:        Kale      CA 54  5 16 NA   5   54 25.00000  75
# 5:      Carrot      CA NA NA NA NA Inf -Inf      NaN   0

【讨论】:

以上是关于data.table 按行求和,平均值,最小值,最大值,如 dplyr?的主要内容,如果未能解决你的问题,请参考以下文章

NSArray 快速求和平均值最大值最小值

AWK文本求和求平均最大值最小值

JAVA8 List最大值最小值求和平均值以及排序

iOS 中通过kvc 获取数组的均值求和最大最小值等

Java8 Stream针对List先分组再求和最大值最小值平均值等

利用stream对list集合中的bigdecimal进行分组求和,均值,最大值,最小值