按组计算平均值

100

我有一个类似于这样的大型数据框:

df <- data.frame(dive = factor(sample(c("dive1","dive2"), 10, replace=TRUE)),
                 speed = runif(10)
                 )
> df
    dive      speed
1  dive1 0.80668490
2  dive1 0.53349584
3  dive2 0.07571784
4  dive2 0.39518628
5  dive1 0.84557955
6  dive1 0.69121443
7  dive1 0.38124950
8  dive2 0.22536126
9  dive1 0.04704750
10 dive2 0.93561651
我的目标是在另一列等于特定值时获取一个列的值的平均数,并为所有值重复此操作。例如,在上面的示例中,我想要返回每个唯一值的列 div 的平均速度。因此,当dive==dive1 时,speed 的平均值为这个值,以此类推到 dive 的每个值。

关于如何拆分-应用-合并但保留结果在原始框架中的相关问题:https://dev59.com/F2_Xa4cB1Zd3GeqP48c8#_ZLjnYgBc1ULPQZFtwyr - Ari B. Friedman
10个回答

177
有许多方法可以在R中完成此操作。 具体来说,使用byaggregatesplitplyrcasttapplydata.tabledplyr等等。
广义上说,这些问题的形式是拆分-应用-合并。 Hadley Wickham撰写了一篇精美的文章,将为您提供更深入的了解整个问题类别,并且值得阅读。他的plyr包实现了一般数据结构的策略,而dplyr是针对数据框架进行性能调优的较新实现。它们允许解决与此类似但更加复杂的问题。学习它们作为解决数据操作问题的通用工具非常值得。

性能是处理大型数据集时的一个问题,对于这种情况,很难超越基于data.table的解决方案。但如果你只处理中等大小或更小的数据集,学习data.table可能不值得花费时间和精力。dplyr也可以快速处理,因此如果您想加速处理速度但又不需要data.table的可伸缩性,则是一个不错的选择。

下面许多其他解决方案不需要任何额外的包。其中一些在中等到大型数据集上甚至相当快速。它们的主要缺点要么是隐喻,要么是灵活性。所谓隐喻,就是指将设计用于其他事物的工具强制用于以“聪明”的方式解决特定类型的问题。而灵活性则意味着它们缺乏解决更广泛类似问题的能力或轻松生成整洁的输出。


例子

base 函数

tapply:

tapply(df$speed, df$dive, mean)
#     dive1     dive2 
# 0.5419921 0.5103974

aggregate:

aggregate 接受数据框作为输入,输出数据框,并使用公式界面。

aggregate( speed ~ dive, df, mean )
#    dive     speed
# 1 dive1 0.5790946
# 2 dive2 0.4864489

by:

在最用户友好的形式中,它接收向量并对它们应用函数。然而,其输出不是非常易于操作的形式。

res.by <- by(df$speed, df$dive, mean)
res.by
# df$dive: dive1
# [1] 0.5790946
# ---------------------------------------
# df$dive: dive2
# [1] 0.4864489

为了解决这个问题,在使用by时,可以使用taRifx库中的as.data.frame方法进行简单操作:
library(taRifx)
as.data.frame(res.by)
#    IDX1     value
# 1 dive1 0.6736807
# 2 dive2 0.4051447

split

顾名思义,它只执行分离操作,而不是整个分离-应用-合并策略。为了使其余部分正常工作,我将编写一个使用 sapply 进行应用-合并的小函数。sapply 会自动尽可能简化结果。在我们的情况下,这意味着一个向量而不是数据框,因为我们只有一个维度的结果。

splitmean <- function(df) {
  s <- split( df, df$dive)
  sapply( s, function(x) mean(x$speed) )
}
splitmean(df)
#     dive1     dive2 
# 0.5790946 0.4864489 

外部包

data.table:

library(data.table)
setDT(df)[ , .(mean_speed = mean(speed)), by = dive]
#    dive mean_speed
# 1: dive1  0.5419921
# 2: dive2  0.5103974

dplyr:

library(dplyr)
group_by(df, dive) %>% summarize(m = mean(speed))

plyrdplyr的前身)

以下是官方页面plyr的介绍:

使用base R函数(如splitapply函数系列)已经可以实现此功能,但plyr通过以下方式使所有操作都更加轻松:

  • 完全一致的名称、参数和输出
  • 通过foreach包方便地进行并行处理
  • 支持从数据框、矩阵和列表输入和输出
  • 进度条可用于跟踪长时间运行的操作
  • 内置错误恢复和信息丰富的错误消息
  • 标签在所有转换中得以保留

换句话说,如果您只想学习一种分割-应用-合并操作工具,那就应该是plyr

library(plyr)
res.plyr <- ddply( df, .(dive), function(x) mean(x$speed) )
res.plyr
#    dive        V1
# 1 dive1 0.5790946
# 2 dive2 0.4864489

reshape2

reshape2库的主要目的不是拆分-应用-合并。相反,它使用两部分的融合/重塑策略来执行各种数据重塑任务。然而,由于它允许聚合函数,因此可以用于这个问题。对于拆分-应用-合并操作,这可能不是我的首选,但它的重塑功能非常强大,因此您也应该学习这个包。

library(reshape2)
dcast( melt(df), variable ~ dive, mean)
# Using dive as id variables
#   variable     dive1     dive2
# 1    speed 0.5790946 0.4864489

基准测试

10行,2个组

library(microbenchmark)
m1 <- microbenchmark(
  by( df$speed, df$dive, mean),
  aggregate( speed ~ dive, df, mean ),
  splitmean(df),
  ddply( df, .(dive), function(x) mean(x$speed) ),
  dcast( melt(df), variable ~ dive, mean),
  dt[, mean(speed), by = dive],
  summarize( group_by(df, dive), m = mean(speed) ),
  summarize( group_by(dt, dive), m = mean(speed) )
)

> print(m1, signif = 3)
Unit: microseconds
                                           expr  min   lq   mean median   uq  max neval      cld
                    by(df$speed, df$dive, mean)  302  325  343.9    342  362  396   100  b      
              aggregate(speed ~ dive, df, mean)  904  966 1012.1   1020 1060 1130   100     e   
                                  splitmean(df)  191  206  249.9    220  232 1670   100 a       
  ddply(df, .(dive), function(x) mean(x$speed)) 1220 1310 1358.1   1340 1380 2740   100      f  
         dcast(melt(df), variable ~ dive, mean) 2150 2330 2440.7   2430 2490 4010   100        h
                   dt[, mean(speed), by = dive]  599  629  667.1    659  704  771   100   c     
 summarize(group_by(df, dive), m = mean(speed))  663  710  774.6    744  782 2140   100    d    
 summarize(group_by(dt, dive), m = mean(speed)) 1860 1960 2051.0   2020 2090 3430   100       g 

autoplot(m1)

benchmark 10 rows

通常情况下,data.table 的开销略微高一些,因此对于小数据集的平均表现也差不多。尽管如此,这些差异微不足道。这里的任何方法都可以很好地处理,你应该根据以下原则进行选择:

  • 你已经熟悉或想熟悉哪种方法(plyr 值得学习,因为它非常灵活;如果你计划分析大型数据集,则值得学习 data.tablebyaggregatesplit 都是基本 R 函数,因此普遍可用)
  • 它返回的输出(数值、数据框或数据表——后者继承自数据框)

1000万行,10个组

但是如果我们有一个大数据集呢?让我们尝试将 10^7 行数据分成十组。

df <- data.frame(dive=factor(sample(letters[1:10],10^7,replace=TRUE)),speed=runif(10^7))
dt <- data.table(df)
setkey(dt,dive)

m2 <- microbenchmark(
  by( df$speed, df$dive, mean),
  aggregate( speed ~ dive, df, mean ),
  splitmean(df),
  ddply( df, .(dive), function(x) mean(x$speed) ),
  dcast( melt(df), variable ~ dive, mean),
  dt[,mean(speed),by=dive],
  times=2
)

> print(m2, signif = 3)
Unit: milliseconds
                                           expr   min    lq    mean median    uq   max neval      cld
                    by(df$speed, df$dive, mean)   720   770   799.1    791   816   958   100    d    
              aggregate(speed ~ dive, df, mean) 10900 11000 11027.0  11000 11100 11300   100        h
                                  splitmean(df)   974  1040  1074.1   1060  1100  1280   100     e   
  ddply(df, .(dive), function(x) mean(x$speed))  1050  1080  1110.4   1100  1130  1260   100      f  
         dcast(melt(df), variable ~ dive, mean)  2360  2450  2492.8   2490  2520  2620   100       g 
                   dt[, mean(speed), by = dive]   119   120   126.2    120   122   212   100 a       
 summarize(group_by(df, dive), m = mean(speed))   517   521   531.0    522   532   620   100   c     
 summarize(group_by(dt, dive), m = mean(speed))   154   155   174.0    156   189   321   100  b      

autoplot(m2)

benchmark 1e7 rows, 10 groups

如果涉及到使用data.tabledplyrdata.table进行操作,这显然是正确的方法。某些方法(如aggregatedcast)开始变得非常缓慢。

1000万行,1000个组

如果有更多的组,则差异将变得更加明显。对于相同的1000万行和1000个组

df <- data.frame(dive=factor(sample(seq(1000),10^7,replace=TRUE)),speed=runif(10^7))
dt <- data.table(df)
setkey(dt,dive)

# then run the same microbenchmark as above
print(m3, signif = 3)
Unit: milliseconds
                                           expr   min    lq    mean median    uq   max neval    cld
                    by(df$speed, df$dive, mean)   776   791   816.2    810   828   925   100  b    
              aggregate(speed ~ dive, df, mean) 11200 11400 11460.2  11400 11500 12000   100      f
                                  splitmean(df)  5940  6450  7562.4   7470  8370 11200   100     e 
  ddply(df, .(dive), function(x) mean(x$speed))  1220  1250  1279.1   1280  1300  1440   100   c   
         dcast(melt(df), variable ~ dive, mean)  2110  2190  2267.8   2250  2290  2750   100    d  
                   dt[, mean(speed), by = dive]   110   111   113.5    111   113   143   100 a     
 summarize(group_by(df, dive), m = mean(speed))   625   630   637.1    633   644   701   100  b    
 summarize(group_by(dt, dive), m = mean(speed))   129   130   137.3    131   142   213   100 a     

autoplot(m3)

enter image description here

所以,data.table 仍然很好地扩展,而在 data.table 上操作的 dplyr 也很好用,而在 data.frame 上使用 dplyr 则慢了一个数量级。 split/sapply 策略似乎在组数上扩展性较差(这意味着 split() 可能很慢,而 sapply 很快)。 by 仍然相对高效 - 在5秒内,对于这么大的数据集来说,对用户来说肯定是明显的,但仍不算过分。但是,如果你经常处理这样大小的数据集,data.table 显然是最佳选择 - 100% 使用 data.table 可以获得最佳性能,或者使用 dplyr 并将 data.table 作为可行的替代方案。

1
此外,microbenchmark 软件包可对事物进行基准测试。实际上是 ggplot2 制作了那个图表(它是一个很棒的软件包)。 - Ari B. Friedman
1
好的,太棒了。有1000个组更符合要求。非常感谢您添加这个功能。我接下来的两周会度假,所以您可以从我的烦恼中得到一个愉快的休息,您会松了一口气的 :-) - Matt Dowle
@AriB.Friedman 我重新运行了所有内容,包括在基准测试和图表中加入了 dplyr。我还添加了一些 dplyr 的评论,试图保持你回答的精神,但请进行任何调整 - 毕竟,这是 你的 回答 :) - Gregor Thomas
1
@Gregor 很好!非常感谢。看起来很漂亮,而且dplyr更新已经过期了。双倍赞,因为展示了有/没有data.tabledplyr - Ari B. Friedman
3
哇塞,多么优秀、精准、全面的回答啊!赞一个! - Sander W. van der Laan
显示剩余11条评论

11

2015年使用dplyr进行更新:

df %>% group_by(dive) %>% summarise(percentage = mean(speed))
Source: local data frame [2 x 2]

   dive percentage
1 dive1  0.4777462
2 dive2  0.6726483

10
aggregate(speed~dive,data=df,FUN=mean)
   dive     speed
1 dive1 0.7059729
2 dive2 0.5473777

4
添加基于 R 的替代方法,在各种情况下仍然保持快速。
rowsummean <- function(df) {
  rowsum(df$speed, df$dive) / tabulate(df$dive)
}

借鉴@Ari的基准:

10行,2组

res1

1000万行,10组

res2

1000万行,1000组

res3


2
使用新函数 across
df %>% 
  group_by(dive) %>% 
  summarise(across(speed, mean, na.rm = TRUE))

0

使用dplyr 1.1.0(及以上版本),我们可以使用.by参数进行临时分组。

这使得代码更短(因为我们避免了group_byungroup语句),并且.by始终返回未分组的数据框。

library(dplyr)

df %>% summarise(speed = mean(speed), .by = dive)

#   dive     speed
#1 dive1 0.5788479
#2 dive2 0.4401514

0

使用 collapse

library(collapse)
library(magrittr)
df %>% 
   fgroup_by(dive) %>%
   fsummarise(speed = fmean(speed))
#   dive     speed
#1 dive1 0.5788479
#2 dive2 0.4401514

数据

set.seed(123)
df <- data.frame(dive=factor(sample(c("dive1","dive2"),10,replace=TRUE)),
             speed=runif(10))

0

在RCchelsie提供的答案基础上进行扩展 - 如果有人想要计算数据框中所有列按组计算的平均值:

df %>% 
  group_by(dive) %>% 
  summarise(across(.cols=everything(), mean, na.rm=TRUE))

0

使用 timeplyr,你可以兼得两全其美。

当我说“蛋糕”,我指的是整洁的语法;而当我说“吃掉它”,我意味着速度极快。

stat_summarise() 结合了 collapsedata.tabledplyr 的优点,既能保持最佳性能又不损失整洁的语法。

比较使用 data.table 和相应的 timeplyr 方法,我们可以看到相似的速度。

# remotes::install_github("NicChr/timeplyr")
library(plyr)
library(dplyr)
library(ggplot2)
library(timeplyr)
library(data.table)

dt <- data.table(dive = factor(sample.int(10^6, size = 10^7, replace=TRUE)),
                 speed = runif(10^7))
setkey(dt, dive)

m2 <- microbenchmark::microbenchmark(
  dt[,mean(speed),by=dive],
  stat_summarise(dt, .cols = "speed", 
                 .by = dive, stat = "mean", sort = F),
  times = 15
)

print(m2, signif = 3)
#> Unit: milliseconds
#>                                                                           expr
#>                                                   dt[, mean(speed), by = dive]
#>  stat_summarise(dt, .cols = "speed", .by = dive, stat = "mean",      sort = F)
#>  min  lq mean median  uq max neval cld
#>  148 184  272    261 344 499    15   a
#>  139 197  283    221 328 540    15   a
autoplot(m2)
#> Coordinate system already present. Adding new coordinate system, which will
#> replace the existing one.

1000万行,~100万组

在比较内存使用时,stat_summarise()实际上比data.table更高效。

# Memory comparison
bench::mark(
  DT = dt[, list(speed = mean(speed)), by = dive],
  TP = stat_summarise(dt, .cols = "speed", 
                 .by = dive, stat = "mean", sort = F),
  check = FALSE
)
#> Warning: Some expressions had a GC in every iteration; so filtering is disabled.
#> # A tibble: 2 x 6
#>   expression      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 DT            194ms    308ms      3.25     248MB     4.87
#> 2 TP            133ms    277ms      3.61    68.7MB     1.81

使用reprex v2.0.2于2023年5月19日创建


0

我们已经有了很多按组计算平均值的选项,现在从mosaic软件包中添加一个。

mosaic::mean(speed~dive, data = df)
#dive1 dive2 
#0.579 0.440 

如果需要一个数据框,我们可以将其包装在stack中,这将返回一个命名的数字向量。

stack(mosaic::mean(speed~dive, data = df))

#  values   ind
#1  0.579 dive1
#2  0.440 dive2

数据

set.seed(123)
df <- data.frame(dive=factor(sample(c("dive1","dive2"),10,replace=TRUE)),
                 speed=runif(10))

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