按组选择最小值的行

30

我有一个让我苦恼了一段时间的问题...希望这里的任何人都能帮助我。

我得到了以下数据框

f <- c('a','a','b','b','b','c','d','d','d','d')
v1 <- c(1.3,10,2,10,10,1.1,10,3.1,10,10)
v2 <- c(1:10)
df <- data.frame(f,v1,v2)

f是一个因子;v1和v2是数值。 对于每个f的水平,我只想保留一行:在这个因子水平中具有最低的v1值的那一行。

f   v1  v2
a   1.3 1
b   2   3
c   1.1 6
d   3.1 8

我尝试过使用aggregate、ddply、by、tapply等方法,但似乎都不起作用。如果有什么建议,我将非常感激。

10个回答

33

使用 DWin 的解决方案,可以使用 ave 避免使用 tapply

df[ df$v1 == ave(df$v1, df$f, FUN=min), ]

这样可以获得另一个加速效果,如下所示。请注意,这也取决于层数。我提供这个是因为我注意到ave经常被遗忘,尽管它是R中更强大的函数之一。

f <- rep(letters[1:20],10000)
v1 <- rnorm(20*10000)
v2 <- 1:(20*10000)
df <- data.frame(f,v1,v2)

> system.time(df[ df$v1 == ave(df$v1, df$f, FUN=min), ])
   user  system elapsed 
   0.05    0.00    0.05 

> system.time(df[ df$v1 %in% tapply(df$v1, df$f, min), ])
   user  system elapsed 
   0.25    0.03    0.29 

> system.time(lapply(split(df, df$f), FUN = function(x) {
+             vec <- which(x[3] == min(x[3]))
+             return(x[vec, ])
+         })
+  .... [TRUNCATED] 
   user  system elapsed 
   0.56    0.00    0.58 

> system.time(df[tapply(1:nrow(df),df$f,function(i) i[which.min(df$v1[i])]),]
+ )
   user  system elapsed 
   0.17    0.00    0.19 

> system.time( ddply(df, .var = "f", .fun = function(x) {
+     return(subset(x, v1 %in% min(v1)))
+     }
+ )
+ )
   user  system elapsed 
   0.28    0.00    0.28 

这显然是最好的答案!很棒你还加上了时间。 - Eduardo Leoni
谢谢你的回答和时间。如果我有多个因素,我该如何调整呢?比如说,我有f1和f2作为因素,并且我想要每个组合的最小值...我尝试了c()和list(),但它们都不起作用。 - donodarazao
@donodarazao:请查看?ave:ave(x,factor1,factor2,factor3,factor4,...,FUN=min) - Joris Meys
@hadley:谢谢,我甚至没有意识到我纠正了一个棘手的错误。 - Joris Meys

16

一个 data.table 的解决方案。

library(data.table)
DT <- as.data.table(df)
DT[,.SD[which.min(v1)], by = f]

##   f  v1 v2
## 1: a 1.3  1
## 2: b 2.0  3
## 3: c 1.1  6
## 4: d 3.1  8

或者更高效的方式

DT[DT[,.I[which.min(v1)],by=f][['V1']]]

一些基准测试

f <- rep(letters[1:20],100000)
v1 <- rnorm(20*100000)
v2 <- 1:(20*100000)
df <- data.frame(f,v1,v2)
DT <- as.data.table(df)
f1<-function(){df2<-df[order(df$f,df$v1),]
               df2[!duplicated(df2$f),]}

f2<-function(){df2<-df[order(df$v1),]
               df2[!duplicated(df2$f),]}

f3<-function(){df[ df$v1 == ave(df$v1, df$f, FUN=min), ]}


f4 <- function(){DT[,.SD[which.min(v1)], by = f]}

f5 <- function(){DT[DT[,.I[which.min(v1)],by=f][['V1']]]}

library(microbenchmark)
microbenchmark(f1(),f2(),f3(),f4(), f5(),times = 5)
# Unit: milliseconds
# expr       min        lq    median        uq       max neval
# f1() 3254.6620 3265.4760 3286.5440 3411.4054 3475.4198     5
# f2() 1630.8572 1639.3472 1651.5422 1721.4670 1738.6684     5
# f3()  172.2639  174.0448  177.4985  179.9604  184.7365     5
# f4()  206.1837  209.8161  209.8584  210.4896  210.7893     5
# f5()  105.5960  106.5006  107.9486  109.7216  111.1286     5

.I方法是获胜者(FR #2330希望在实现时也能像.SD方法一样快速而优雅)。


8

使用 plyr,我会这样做:

ddply(df, .var = "f", .fun = function(x) {
    return(subset(x, v1 %in% min(v1)))
    }
)

试试看,看它是否返回你想要的结果。


9
更简单地说,ddply(df, "f", subset, v1 == min(v1)) 的意思是按照 f 列对数据框 df 进行分组,然后在每个分组中选择 v1 列的最小值所在的行。 - hadley
1
嗯,解决方案很好,但是如果每个级别只有很少的值,那么速度会非常慢... - JelenaČuklina

6

另一个使用tapply的解决方案,无需通过%in%扫描向量:

df[tapply(1:nrow(df),df$f,function(i) i[which.min(df$v1[i])]),]

编辑:在平局的情况下,这将只保留第一行。

编辑2:受到 ave 的印象,我进行了额外的改进:

df[sapply(split(1:nrow(df),df$f),function(x) x[which.min(df$v1[x])]),]

在我的电脑上(使用Joris的基准数据):

> system.time(df[ df$v1 == ave(df$v1, df$f, FUN=min), ])
   user  system elapsed
  0.022   0.000   0.021
> system.time(df[sapply(split(1:nrow(df),df$f),function(x) x[which.min(df$v1[x])]),])
   user  system elapsed
  0.006   0.000   0.007

好的,mbq,我投了你的一票。就像Matt对我的那样,我不得不仔细研究你的代码才能“看到”它的内部运作。 - IRTFM
@DWin 对于这个%in%的评论,我很抱歉,也许我倾向于高估效率:| 我同意所有的解决方案都相当复杂;by的解决方案在我看来相当易读,但结果很糟糕(-; - mbq

6
这是使用dplyr的方式,按照f分组筛选出最小的v1值。
library(dplyr)
df |>
  group_by(f) |>
  slice_min(v1)

请参阅?slice_min帮助页面,了解是否包括并列项(默认包括)以及保留多个最低值的选项(例如,最低的5个或最低的10%)。
您还可以更明确地执行此操作:
df %>%
  group_by(f) %>%
  filter(v1 == min(v1))

#Source: local data frame [4 x 3]
#Groups: f
#
#  f  v1 v2
#1 a 1.3  1
#2 b 2.0  3
#3 c 1.1  6
#4 d 3.1  8

v1中出现平局的情况下,这将导致每个f组中有多行数据。如果你想避免这种情况,可以使用以下方法:
df %>% 
  group_by(f) %>% 
  filter(rank(v1, ties.method= "first") == 1)

这样,如果有多个并列的情况下,你只会得到第一行。你也可以选择使用ties.method = "random"或其他在帮助文件中描述的方法。

3
这里提供一个tapply解决方案;
> df[ df$v1 %in% tapply(df$v1, df$f, min), ]

  f  v1 v2
1 a 1.3  1
3 b 2.0  3
6 c 1.1  6
8 d 3.1  8

在您的示例中,它只挑选出每个组中的一个,但如果存在并列情况,此方法将显示所有并列结果(我认为Parker和Luštrik的方法也是如此)。

关于平局的观点非常好。这个函数也很不错——我读了几遍并查看了“tapply”才弄清楚发生了什么。 - Matt Parker
哇,太棒了!Matt的ddply解决方法可行,但在我的真实数据框架中(约10,000行)需要大约2分钟。这个解决方案提供了相同的结果,但只需要不到1秒钟的时间。非常优雅,谢谢! - donodarazao
4
如果ddply在10,000个观察值上需要2分钟,那么就有问题了。此外,这种方法并不总是能够返回正确的结果——考虑一下某个组中最低值为2,另一个组中第二低的值也为2的情况。它只是在这个例子中运行成功纯属巧合。 - hadley
也许是因为在这10000个观测值中有7700个因子水平?我用了你的ddply解决方案再次尝试,确实需要那么长时间... - donodarazao
根据Hadley的观点,这个解决方案是错误的。对于输入df = data.frame(f = c("a", "a", "b", "b"), v1 = c(2, 3, 1, 2))进行测试 - 正确的结果应该是一个有两行的数据框,但是这段代码却产生了三行。答案真的应该被修正或删除。 - Gregor Thomas

2

抱歉,我的思维能力已经耗尽了,在凌晨1点,这个丑陋的解决方案是我所能想到的全部。

lapply(split(df, df$f), FUN = function(x) {
            vec <- which(x[3] == min(x[3]))
            return(x[vec, ])
        })

2
另一种方法是使用order!duplicated,但是在值相同时只能获取第一个。
df2 <- df[order(df$f,df$v1),]
df2[!duplicated(df2$f),]

  f  v1 v2
1 a 1.3  1
3 b 2.0  3
6 c 1.1  6
8 d 3.1  8

时间

f1<-function(){df2<-df[order(df$f,df$v1),]
df2[!duplicated(df2$f),]}

f2<-function(){df2<-df[order(df$v1),]
df2[!duplicated(df2$f),]}

f3<-function(){df[ df$v1 == ave(df$v1, df$f, FUN=min), ]}

library(rbenchmark)
> benchmark(f1(),f2(),f3())
  test replications elapsed relative user.self sys.self user.child sys.child
1 f1()          100   38.16 7.040590     36.66     1.48         NA        NA
2 f2()          100   20.54 3.789668     19.30     1.23         NA        NA
3 f3()          100    5.42 1.000000      4.96     0.46         NA        NA

v1以上的版本已经足够了。很好的解决方案,时间方面怎么样? - Marek
@Marek 谢谢,我没有想到在 f 上不需要排序。这似乎可以加快大约2倍的速度,但仍然比Joris Meys的 ave 解决方案慢得多。 - James

2

这里有一个使用by的解决方案。

do.call(rbind, unname(by(df, df$f, function(x) x[x$v1 == min(x$v1),])))
##   f  v1 v2
## 1 a 1.3  1
## 3 b 2.0  3
## 6 c 1.1  6
## 8 d 3.1  8

0

使用 tidyverse

df %>%
  arrange(v1) %>% # You can also do arrange(f, v1)
  distinct(f, .keep_all = TRUE)

我也喜欢@talat之前的答案

df %>%
  group_by(f) %>%
  filter(v1 == min(v1))

但第一个避免了分组和取消分组。


1
这是我的个人观点,它看起来非常类似于@talat的答案。 - Guannan Shen

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