web-dev-qa-db-fra.com

Extraire la ligne correspondant à la valeur minimale d'une variable par groupe

Je souhaite (1) grouper les données par une variable (State), (2) dans chaque groupe, trouver la ligne de valeur minimale d'une autre variable (Employees), et (3) extraire le ligne entière.

(1) et (2) sont simples, et je pense que (3) devrait l'être aussi, mais je ne peux pas l'obtenir.

Voici un exemple de jeu de données:

> 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))

Calculer min par groupe est facile, en utilisant aggregate:

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

...ou data.table:

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

Mais comment puis-je extraire la ligne entière correspondant à ces valeurs min, c’est-à-dire inclure également Company dans le résultat?

55
Ed Swindelles

Un peu plus élégant:

library(data.table)
DT[ , .SD[which.min(Employees)], by = State]

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

Un peu moins élégant que l’utilisation de .SD, Mais un peu plus rapide (pour les données contenant de nombreux groupes):

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

En outre, remplacez simplement l'expression which.min(Employees) par Employees == min(Employees), si votre ensemble de données contient plusieurs valeurs min identiques et que vous souhaitez toutes les sous-définir.

Voir aussi Sous-ensemble par groupe avec data.table .

49
Señor O

Une solution dplyr:

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

Comme il s’agit là du succès de Google, j’ai pensé ajouter quelques options supplémentaires que je trouve utile de connaître. L'idée est fondamentalement d'organiser une fois par Employees et ensuite de prendre les uniques par State

Soit en utilisant data.table

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

Alternativement, nous pourrions aussi d'abord commander, puis sous-ensemble .SD. Ces deux opérations ont été optimisées dans les versions précédentes de data.table et order semble déclencher apparemment data.table:::forderv, tandis que .SD[1L] déclenche 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

Ou dplyr

library(dplyr)
data %>% 
  arrange(Employees) %>% 
  distinct(State, .keep_all = TRUE)
#   State Company Employees
# 1    RI       E        19
# 2    AK       D        24

Une autre idée intéressante empruntée à @ Khashaas géniale réponse (avec une petite modification sous la forme de mult = "first" afin de gérer plusieurs correspondances) consiste à rechercher d’abord un minimum par groupe, puis à effectuer une jointure binaire. L'avantage de ceci est à la fois l'utilisation de la fonction data.tables gmin (qui évite la surcharge de l'évaluation) et la fonctionnalité binary join

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

Quelques repères

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
27
David Arenburg

La fonction de base by est souvent utile pour utiliser des données de bloc dans data.frames. Par exemple

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

Il renvoie les données dans une liste, mais vous pouvez le réduire avec

do.call(rbind, by(data, data$State, function(x) x[which.min(x$Employees), ] ))
8
MrFlick

Solution corrigée plyr:

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

merci à @ joel.wilson

3
C8H10N4O2