使用ddply在R中聚合求和与平均值

5

我的数据框有两列作为分组键,17列需要在每个组中求和,还有一列应该取平均值。让我用一个不同的数据框 diamonds 来说明。

我知道可以像这样做:

ddply(diamonds, ~cut, summarise, x=sum(x), y=sum(y), z=sum(z), price=mean(price))

虽然对于3列而言这是合理的,但对于17列来说就不可接受了。

在研究时,我发现了colwise函数,但我得出的最佳方法如下:

cbind(ddply(diamonds, ~cut, colwise(sum, 7:9)), price=ddply(diamonds, ~cut, summarise, mean(price))[,2])

有没有可能进一步改进这个呢?我想用更简单直接的方式来实现,就像(虚构的指令):

ddply(diamonds, ~cut, colwise(sum, 7:9), price=mean(price))

或者:

ddply(diamonds, ~cut, colwise(sum, 7:9), colwise(mean, ~price))

总之:
  • 我不想像第一个示例中的xyz那样显式地输入所有17列。
  • 理想情况下,我希望能够通过单个调用ddply来完成,而不需要使用cbind(或类似函数),就像第二个示例中一样。

参考结果应为5行5列:

        cut         x         y        z    price
1      Fair  10057.50   9954.07  6412.26 4358.758
2      Good  28645.08  28703.75 17855.42 3928.864
3 Very Good  69359.09  69713.45 43009.52 3981.760
4   Premium  82385.88  81985.82 50297.49 4584.258
5     Ideal 118691.07 118963.24 73304.61 3457.542

2
如果您正在使用 dplyr,也许可以使用以下代码:diamonds %>% group_by(cut) %>% mutate(MeanPrice=mean(price)) %>% mutate_each(funs(sum), 7:9) %>% select(c(2,7:11)) %>% unique() - akrun
7个回答

12

我建议使用data.table来解决这个问题。你可以通过位置或名称很容易地预定义要操作的列,然后重复使用相同的代码来操作任意数量的列。

预定义列名

Sums <- 7:9
Means <- "price"

运行代码

library(data.table)
data.table(diamonds)[, c(lapply(.SD[, Sums, with = FALSE], sum),
                         lapply(.SD[, Means, with = FALSE], mean))
                     , by = cut]

#          cut         x         y        z    price
# 1:     Ideal 118691.07 118963.24 73304.61 3457.542
# 2:   Premium  82385.88  81985.82 50297.49 4584.258
# 3:      Good  28645.08  28703.75 17855.42 3928.864
# 4: Very Good  69359.09  69713.45 43009.52 3981.760
# 5:      Fair  10057.50   9954.07  6412.26 4358.758

对于您的具体示例,这可以简化为仅

data.table(diamonds)[, c(lapply(.SD[, 7:9, with = FALSE], sum), pe = mean(price)), by = cut]
#          cut         x         y        z       pe
# 1:     Ideal 118691.07 118963.24 73304.61 3457.542
# 2:   Premium  82385.88  81985.82 50297.49 4584.258
# 3:      Good  28645.08  28703.75 17855.42 3928.864
# 4: Very Good  69359.09  69713.45 43009.52 3981.760
# 5:      Fair  10057.50   9954.07  6412.26 4358.758

5

对于你的特殊情况(平均值 = 总和/数量!),我认为另一种方法更易读。

nCut <- ddply(diamonds, ~cut, nrow)
res <- ddply(diamonds, ~cut, colwise(sum, 6:9))
res$price <- res$price/nCut$V1

或者更通用的,
do.call(merge, 
    lapply(c(colwise(sum, 7:9), colwise(mean, 6)), 
           function(cw) ddply(diamonds, ~cut, cw)))

5

另一个使用 dplyr 的解决方案。首先,您将两个聚合函数应用于要聚合的每个变量。然后,您仅选择所需的功能/变量组合中的结果变量。

library(dplyr)
library(ggplot2)

diamonds %>%
    group_by(cut) %>%
    summarise_each(funs(sum, mean), x:z, price) %>%
    select(cut, matches("[xyz]_sum"), price_mean)

那么你是否需要构建一个匹配表达式,要求用户显式地输入所有17列...我想你可以通过在范围内从colnames中进行文本解析表达式来快捷处理它...? - russellpierce
@dplyr的select函数提供了许多选择变量的可能性。OP需要找到一种指定需要聚合的变量的方法。但这在所有讨论的解决方案中都是必要的。 - MarkusN
从技术上讲,任何使用范围选择器的东西在满足他们关于不需要直接指定每个列的要求方面似乎更加清晰。无论如何,您的解决方案比其他解决方案快得多! - russellpierce

2

再提供一个解决方案:

library(plyr)
library(ggplot2)
trans <- list(mean = 8:10, sum = 7)

makeList <- function(inL, mdat = diamonds, by = ~cut) {
   colN <- names(mdat)
   args <- unlist(llply(names(inL), function(n) {
      llply(inL[[n]], function(x) {
         ret <- list(call(n, as.symbol(colN[[x]])))
         names(ret) <- paste(n, colN[[x]], sep = ".")
         ret
      })
   }))
   args$.data <- as.symbol(deparse(substitute(mdat)))
   args$.variables <- by
   args$.fun <- as.symbol("summarise")
   args
}

do.call(ddply, makeList(trans))
#         cut   mean.x   mean.y   mean.z sum.price
# 1      Fair 6.246894 6.182652 3.982770   7017600
# 2      Good 5.838785 5.850744 3.639507  19275009
# 3 Very Good 5.740696 5.770026 3.559801  48107623
# 4   Premium 5.973887 5.944879 3.647124  63221498
# 5     Ideal 5.507451 5.520080 3.401448  74513487

这个想法是函数makeListddply创建一个参数列表。通过这种方式,你可以很容易地将术语添加到列表中(如function.name = column.indices),而ddply将按预期工作:

trans <- c(trans, sd = list(9:10))
do.call(ddply, makeList(trans))
#         cut   mean.x   mean.y   mean.z sum.price      sd.y      sd.z
# 1      Fair 6.246894 6.182652 3.982770   7017600 0.9563804 0.6516384
# 2      Good 5.838785 5.850744 3.639507  19275009 1.0515353 0.6548925
# 3 Very Good 5.740696 5.770026 3.559801  48107623 1.1029236 0.7302281
# 4   Premium 5.973887 5.944879 3.647124  63221498 1.2597511 0.7311610
# 5     Ideal 5.507451 5.520080 3.401448  74513487 1.0744953 0.6576481

2

它使用dplyr,但我相信这将完全实现指定的目标,并以相对易于阅读的语法呈现:

diamonds %>%
  group_by(cut) %>%
  select(x:z) %>%
  summarize_each(funs(sum)) %>%
  merge(diamonds %>%
          group_by(cut) %>%
          summarize(price = mean(price))
        ,by = "cut")

唯一的“诀窍”是合并中有一个被管道表达式包裹的表达式,它可以将平均价格的计算与求和的计算分开处理。
我对这个解决方案进行了基准测试,与 @David Arenburg 提供的解决方案(使用data.table)和 @thothal 提供的解决方案(根据问题要求使用plyr)进行了5000次复制。在这里,data.tableplyrdplyr慢。 dplyrplyr快。可以想象,基准测试结果可能会随着列数、分组因子中级数和特定应用函数的变化而改变。例如,MarkusN在我做初步基准测试后提交了一个答案,对于样本数据,它比之前提交的答案快得多。他通过计算许多不需要的摘要统计信息,然后丢弃它们来实现这一目标......肯定有一个点,在这一方法的成本超过优势之前。
       test replications elapsed relative user.self sys.self user.child sys.child
2 dataTable         5000 119.686    2.008   119.611    0.127          0         0
1     dplyr         5000  59.614    1.000    59.676    0.004          0         0
3      plyr         5000  68.505    1.149    68.493    0.064          0         0
?      MarkusN      5000  23.172    ?????    23.926        0          0         0

当然,速度并不是唯一的考虑因素。特别地,dplyr和plyr对它们被加载的顺序很挑剔(plyr在dplyr之前),并且有几个函数会互相掩盖。

我猜这不是关于速度,而是关于方便性。如果你现在需要添加另一个总结函数,你需要再添加一个“合并”语句。 - thothal
相对于 data.table 解决方案,这显然更冗长。然而,它比 plyr 解决方案要少冗长。我对这三种解决方案中哪一种是“最佳”的没有任何利益关系。我猜答案完全取决于某人的舒适程度和他们所重视的内容。我唯一的想法是提供一个符合问题标准的 dplyr 解决方案。 - russellpierce
据我所知,如果您想要使用另一组不相交的变量进行汇总,那么您只需要添加另一个合并语句。在dplyr中,可能有更好的表达这个过程的方法,但那只是我的尝试。 - russellpierce
这是与哪个data.table解决方案进行比较的?第一种还是第二种?你只在此处使用了一次summarize_each,因为您假设只有一列要运行mean,所以应该与第二个解决方案进行比较。此外,数据应足够大。您应该发布整个基准代码,而不仅仅是说哪个更快或不快。 - David Arenburg
遗憾的是基准测试代码是短暂的。我下次会发布它;在我写的时候,它似乎有点啰嗦。我进行基准测试的data.table版本是你的。 - russellpierce
我发布了两个不同的解决方案,请再仔细阅读我上面的评论。 - David Arenburg

1

虽然不是完全符合你的要求,但这可能会给你另一种实现方式的想法。使用data.table,你可以像这样做:

diamonds2[, .(c = sum(c), p = sum(p), ce = sum(ce), pe = mean(pe)), by = cut]

为了缩短代码(就像您尝试使用colwise做的那样),您可能需要编写一些函数来实现您想要的精确功能。


我想使用ggplot2中的diamonds数据集来使其可重复,但被我工作区域中的一个旧数据框所迷惑,非常抱歉。我已经更新了我的问题,并希望这次能够清楚地表达我想要实现的目标。 - arekolek

0

为了完整起见,这里提供了一个基于dplyr另一个问题中Veerendra Gadekar的答案以及MarkusN在此处的答案的解决方案。

在这种特定情况下,可以先对一些列应用sum,然后对所有感兴趣的列应用 mean

diamonds %>%
  group_by(cut) %>%
  mutate_each('sum', 8:10) %>%
  summarise_each('mean', 8:10, price)

这是可能的,因为mean不会改变列8:10的计算总和,并且将计算所需的价格平均值。但是,如果我们想要计算价格的标准差而不是平均值,这种方法就行不通了,因为列8:10都将为0。
一个更一般的方法可能是:
diamonds %>%
   group_by(cut) %>%
   mutate_each('sum', 8:10) %>%
   mutate_each('mean', price) %>%
   summarise_each('first', 8:10, price)

可能有些人不喜欢summarise_each重复之前命名的列规范,但这似乎仍然是一个优雅的解决方案。

它比MarkusN的解决方案更具优势,因为它不需要匹配新创建的列,也不会改变它们的名称。

Veerendra Gadekar的解决方案应该以select(cut, 8:10, price) %>% arrange(cut)结束,以产生预期结果(列的子集,加上按分组键排序的行)。Hong Ooi的建议与此处的第一个建议类似,但假设没有其他列。

最后,它似乎比像David Arenburg提出的那个data.table解决方案更易读和易于理解。


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