按组提取变量最小值对应的行

118

我希望能够按照一个变量 (State) 进行数据分组,然后在每个组内找到另一个变量 (Employees) 的最小值所在的行,并提取整行数据。

虽然 (1) 和 (2) 可以用一行代码轻松实现,但是我觉得 (3) 也应该可以,但我做不到。

这是一个数据集样例:

> data
  State Company Employees
1    AK       A        82
2    AK       B       104
3    AK       C        37
4    AK       D        24
5    RI       E        19
6    RI       F       118
7    RI       G        88
8    RI       H        42

data <- structure(list(State = structure(c(1L, 1L, 1L, 1L, 2L, 2L, 2L, 
        2L), .Label = c("AK", "RI"), class = "factor"), Company = structure(1:8, .Label = c("A", 
        "B", "C", "D", "E", "F", "G", "H"), class = "factor"), Employees = c(82L, 
        104L, 37L, 24L, 19L, 118L, 88L, 42L)), .Names = c("State", "Company", 
        "Employees"), class = "data.frame", row.names = c(NA, -8L))

使用 aggregate 按组计算 min 很容易:

> aggregate(Employees ~ State, data, function(x) min(x))
  State Employees
1    AK        24
2    RI        19

... 或者 data.table

> library(data.table)
> DT <- data.table(data)
> DT[ , list(Employees = min(Employees)), by = State]
   State Employees
1:    AK        24
2:    RI        19

但是我该如何提取与这些 min 值对应的整行数据,也就是包括 Company 在内的结果?

8个回答

95
稍微更优雅一些:
library(data.table)
DT[ , .SD[which.min(Employees)], by = State]

   State Company Employees
1:    AK       D        24
2:    RI       E        19

相对于使用.SD略显不够优美,但对于有许多组的数据而言更快:

DT[DT[ , .I[which.min(Employees)], by = State]$V1]

如果你的数据集中有多个相同的最小值,且想要将其全部子集化,只需将表达式which.min(Employees)替换为Employees == min(Employees).

参见使用data.table按组子集化与最大值相对应的行


如何获取一个数字,使其返回最小值,但基于该数字已被使用的频率? - Abhishek Singh

90

以下是一个 dplyr 的解决方案(请注意,我不是经常使用者):

library(dplyr)    
data %>% 
    group_by(State) %>% 
    slice(which.min(Employees))

正如jazzurro在评论中提到的那样,自dplyr版本1.0.0起,现在也有一个内置函数slice_min:

data %>% 
   group_by(State) %>% 
   slice_min(order_by = Employees)

16
使用dplyr 1.0.0,您可以通过以下方式实现相同的效果:group_by(data, State) %>% slice_min(order_by = Employees) - jazzurro
dplyr 1.1.0 新增了实验性参数 by: data |> slice_min(Employees, by = State) - LMc

48

作为谷歌最受欢迎的搜索结果,我想补充一些我认为有用的其他选项。基本思路是先按Employees排序,然后只获取每个State的唯一值。

可以使用data.table来实现。

library(data.table)
unique(setDT(data)[order(Employees)], by = "State")
#    State Company Employees
# 1:    RI       E        19
# 2:    AK       D        24

另外,我们还可以先排序再对.SD进行子集操作。这两个操作在最新的data.table版本中都进行了优化,order似乎会触发data.table:::forderv,而.SD[1L]则会触发Gforce

setDT(data)[order(Employees), .SD[1L], by = State, verbose = TRUE] # <- Added verbose
# order optimisation is on, i changed from 'order(...)' to 'forder(DT, ...)'.
# i clause present and columns used in by detected, only these subset: State 
# Finding groups using forderv ... 0 sec
# Finding group sizes from the positions (can be avoided to save RAM) ... 0 sec
# Getting back original order ... 0 sec
# lapply optimization changed j from '.SD[1L]' to 'list(Company[1L], Employees[1L])'
# GForce optimized j to 'list(`g[`(Company, 1L), `g[`(Employees, 1L))'
# Making each group and running j (GForce TRUE) ... 0 secs
#    State Company Employees
# 1:    RI       E        19
# 2:    AK       D        24
或者 dplyr
library(dplyr)
data %>% 
  arrange(Employees) %>% 
  distinct(State, .keep_all = TRUE)
#   State Company Employees
# 1    RI       E        19
# 2    AK       D        24

另一个有趣的思路借鉴自@Khashaas精彩回答(稍作修改,使用mult = "first"以处理多个匹配项)是首先按组找到最小值,然后再进行二进制合并。这样做的优点在于利用了data.tables的gmin函数(它跳过了评估开销),以及二进制合并功能。

tmp <- setDT(data)[, .(Employees = min(Employees)), by = State]
data[tmp, on = .(State, Employees), mult = "first"]
#    State Company Employees
# 1:    AK       D        24
# 2:    RI       E        19

一些基准测试

library(data.table)
library(dplyr)
library(plyr)
library(stringi)
library(microbenchmark)

set.seed(123)
N <- 1e6
data <- data.frame(State = stri_rand_strings(N, 2, '[A-Z]'),
                   Employees = sample(N*10, N, replace = TRUE))
DT <- copy(data)
setDT(DT)
DT2 <- copy(DT)
str(DT)
str(DT2)

microbenchmark("(data.table) .SD[which.min]: " = DT[ , .SD[which.min(Employees)], by = State],
               "(data.table) .I[which.min]: " = DT[DT[ , .I[which.min(Employees)], by = State]$V1],
               "(data.table) order/unique: " = unique(DT[order(Employees)], by = "State"),
               "(data.table) order/.SD[1L]: " = DT[order(Employees), .SD[1L], by = State],
               "(data.table) self join (on):" = {
                 tmp <- DT[, .(Employees = min(Employees)), by = State]
                 DT[tmp, on = .(State, Employees), mult = "first"]},
               "(data.table) self join (setkey):" = {
                 tmp <- DT2[, .(Employees = min(Employees)), by = State] 
                 setkey(tmp, State, Employees)
                 setkey(DT2, State, Employees)
                 DT2[tmp, mult = "first"]},
               "(dplyr) slice(which.min): " = data %>% group_by(State) %>% slice(which.min(Employees)),
               "(dplyr) arrange/distinct: " = data %>% arrange(Employees) %>% distinct(State, .keep_all = TRUE),
               "(dplyr) arrange/group_by/slice: " = data %>% arrange(Employees) %>% group_by(State) %>% slice(1),
               "(plyr) ddply/which.min: " = ddply(data, .(State), function(x) x[which.min(x$Employees),]),
               "(base) by: " = do.call(rbind, by(data, data$State, function(x) x[which.min(x$Employees), ])))


# Unit: milliseconds
#                             expr        min         lq       mean     median         uq       max neval      cld
#    (data.table) .SD[which.min]:   119.66086  125.49202  145.57369  129.61172  152.02872  267.5713   100    d    
#     (data.table) .I[which.min]:    12.84948   13.66673   19.51432   13.97584   15.17900  109.5438   100 a       
#      (data.table) order/unique:    52.91915   54.63989   64.39212   59.15254   61.71133  177.1248   100  b      
#     (data.table) order/.SD[1L]:    51.41872   53.22794   58.17123   55.00228   59.00966  145.0341   100  b      
#     (data.table) self join (on):   44.37256   45.67364   50.32378   46.24578   50.69411  137.4724   100  b      
# (data.table) self join (setkey):   14.30543   15.28924   18.63739   15.58667   16.01017  106.0069   100 a       
#       (dplyr) slice(which.min):    82.60453   83.64146   94.06307   84.82078   90.09772  186.0848   100   c     
#       (dplyr) arrange/distinct:   344.81603  360.09167  385.52661  379.55676  395.29463  491.3893   100     e   
# (dplyr) arrange/group_by/slice:   367.95924  383.52719  414.99081  397.93646  425.92478  557.9553   100      f  
#         (plyr) ddply/which.min:   506.55354  530.22569  568.99493  552.65068  601.04582  727.9248   100       g 
#                      (base) by:  1220.38286 1291.70601 1340.56985 1344.86291 1382.38067 1512.5377   100        h

1
干得好。只是为了完整性/好奇心,我提供了一个正确的plyr解决方案。随意包含在基准测试中...虽然我不指望它能与data.table相媲美。 - C8H10N4O2
1
@C8H10N4O2 更新。 - David Arenburg

9

基本函数by在处理数据框中的分块数据时非常有用。例如:

by(data, data$State, function(x) x[which.min(x$Employees), ] )

它确实会返回一个列表,但是你可以使用 "collapse" 来折叠它。
do.call(rbind, by(data, data$State, function(x) x[which.min(x$Employees), ] ))

4
base中,您可以使用ave按组获取每个组的最小值,并将其与Employees进行比较并获取逻辑向量以对data.frame进行子集化。
data[data$Employees == ave(data$Employees, data$State, FUN=min),]
#  State Company Employees
#4    AK       D        24
#5    RI       E        19

或者在函数中进行比较。

data[as.logical(ave(data$Employees, data$State, FUN=function(x) x==min(x))),]
#data[ave(data$Employees, data$State, FUN=function(x) x==min(x))==1,] #Variant
#  State Company Employees
#4    AK       D        24
#5    RI       E        19

3

修正后的 plyr 解决方案:

ddply(df, .(State), function(x) x[which.min(x$Employees),])
#   State Company Employees
# 1    AK       D        24
# 2    RI       E        19

感谢 @joel.wilson


2
使用 collapse
library(collapse)
library(magrittr)
data %>% 
  fgroup_by(State) %>% 
  fsummarise(Employees = fmin(Employees))

0
另一个 `data.table` 的解决方案:
DT[, E_min := min(Employees), by = State][Employees == E_min]

非常直接且速度最快之一。以下是我重新运行David Arenburg的基准测试,使用此方法和其他最快的data.table解决方案。

library(data.table)
library(microbenchmark)

set.seed(123)
N <- 1e6
data <- data.frame(State = stri_rand_strings(N, 2, '[A-Z]'),
                   Employees = sample(N * 10, N, replace = TRUE))
DT <- copy(data)
setDT(DT)
DT2 <- copy(DT)
DT3 <- copy(DT)

microbenchmark(
    "(data.table) min column: " = DT3[, E_min := min(Employees), by = State][Employees == E_min],
    "(data.table) .I[which.min]: " = DT[DT[, .I[which.min(Employees)], by = State]$V1],
    "(data.table) order/unique: " = unique(DT[order(Employees)], by = "State"),
    "(data.table) self join (setkey):" = {
        tmp <- DT2[, .(Employees = min(Employees)), by = State]
        setkey(tmp, State, Employees)
        setkey(DT2, State, Employees)
        DT2[tmp, mult = "first"]
    }
)

                             expr      min       lq      mean   median        uq      max neval
        (data.table) min column:  44.30078 52.17932  68.31826 58.65887  76.89786 184.0207   100
     (data.table) .I[which.min]:  20.34116 26.31244  39.36874 34.01958  42.65439 124.9204   100
      (data.table) order/unique:  70.07820 80.20577 109.71235 95.25586 114.87695 514.4456   100
 (data.table) self join (setkey): 13.48105 16.06614  22.58310 17.35083  22.31206 161.9103   100

如果你只想返回其中几列,而不是全部列,那么这个解决方案非常有用,例如[Employees == E_min, ..columns_to_keep],在这种情况下,它甚至可以更快。


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