web-dev-qa-db-fra.com

Sélectionnez la première ligne par groupe

À partir d'un cadre de données comme celui-ci

test <- data.frame('id'= rep(1:5,2), 'string'= LETTERS[1:10])
test <- test[order(test$id), ]
rownames(test) <- 1:10

> test
    id string
 1   1      A
 2   1      F
 3   2      B
 4   2      G
 5   3      C
 6   3      H
 7   4      D
 8   4      I
 9   5      E
 10  5      J

Je veux en créer un nouveau avec la première ligne de chaque paire id/string. Si sqldf accepte le code R qu'il contient, la requête pourrait ressembler à ceci:

res <- sqldf("select id, min(rownames(test)), string 
              from test 
              group by id, string")

> res
    id string
 1   1      A
 3   2      B
 5   3      C
 7   4      D
 9   5      E

Y at-il une solution à court de créer une nouvelle colonne comme

test$row <- rownames(test)

et en exécutant la même requête sqldf avec min (ligne)?

63
dmvianna

Vous pouvez utiliser duplicated pour le faire très rapidement.

test[!duplicated(test$id),]

Benchmarks, pour les freaks de vitesse:

ju <- function() test[!duplicated(test$id),]
gs1 <- function() do.call(rbind, lapply(split(test, test$id), head, 1))
gs2 <- function() do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
jply <- function() ddply(test,.(id),function(x) head(x,1))
jdt <- function() {
  testd <- as.data.table(test)
  setkey(testd,id)
  # Initial solution (slow)
  # testd[,lapply(.SD,function(x) head(x,1)),by = key(testd)]
  # Faster options :
  testd[!duplicated(id)]               # (1)
  # testd[, .SD[1L], by=key(testd)]    # (2)
  # testd[J(unique(id)),mult="first"]  # (3)
  # testd[ testd[,.I[1L],by=id] ]      # (4) needs v1.8.3. Allows 2nd, 3rd etc
}

library(plyr)
library(data.table)
library(rbenchmark)

# sample data
set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]

benchmark(ju(), gs1(), gs2(), jply(), jdt(),
    replications=5, order="relative")[,1:6]
#     test replications elapsed relative user.self sys.self
# 1   ju()            5    0.03    1.000      0.03     0.00
# 5  jdt()            5    0.03    1.000      0.03     0.00
# 3  gs2()            5    3.49  116.333      2.87     0.58
# 2  gs1()            5    3.58  119.333      3.00     0.58
# 4 jply()            5    3.69  123.000      3.11     0.51

Essayons encore une fois, mais avec seulement les prétendants de la première manche, avec plus de données et plus de réplications.

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
benchmark(ju(), jdt(), order="relative")[,1:6]
#    test replications elapsed relative user.self sys.self
# 1  ju()          100    5.48    1.000      4.44     1.00
# 2 jdt()          100    6.92    1.263      5.70     1.15
100
Joshua Ulrich

Qu'en est-il de

DT <- data.table(test)
setkey(DT, id)

DT[J(unique(id)), mult = "first"]

Modifier

Il existe également une méthode unique pour data.tables qui retournera la première ligne par clé

jdtu <- function() unique(DT)

Je pense que si vous commandez test en dehors du point de référence, vous pouvez supprimer les setkey et data.table conversion du repère également (comme setkey trie essentiellement par id, comme order).

set.seed(21)
test <- data.frame(id=sample(1e3, 1e5, TRUE), string=sample(LETTERS, 1e5, TRUE))
test <- test[order(test$id), ]
DT <- data.table(DT, key = 'id')
ju <- function() test[!duplicated(test$id),]

jdt <- function() DT[J(unique(id)),mult = 'first']


 library(rbenchmark)
benchmark(ju(), jdt(), replications = 5)
##    test replications elapsed relative user.self sys.self 
## 2 jdt()            5    0.01        1      0.02        0        
## 1  ju()            5    0.05        5      0.05        0         

et avec plus de données

** Modifier avec une méthode unique **

set.seed(21)
test <- data.frame(id=sample(1e4, 1e6, TRUE), string=sample(LETTERS, 1e6, TRUE))
test <- test[order(test$id), ]
DT <- data.table(test, key = 'id')
       test replications elapsed relative user.self sys.self 
2  jdt()            5    0.09     2.25      0.09     0.00    
3 jdtu()            5    0.04     1.00      0.05     0.00      
1   ju()            5    0.22     5.50      0.19     0.03        

La méthode unique est la plus rapide ici.

15
mnel

Je privilégie l'approche Dplyr.

group_by(id) suivi de l'un ou l'autre

  • filter(row_number()==1) ou
  • slice(1) ou
  • top_n(n = -1)
    • top_n() utilise en interne la fonction rank. Negative sélectionne depuis le bas du rang.

Dans certains cas, il peut être nécessaire de disposer les identifiants après group_by.

library(dplyr)

# using filter(), top_n() or slice()

m1 <-
test %>% 
  group_by(id) %>% 
  filter(row_number()==1)

m2 <-
test %>% 
  group_by(id) %>% 
  slice(1)

m3 <-
test %>% 
  group_by(id) %>% 
  top_n(n = -1)

Les trois méthodes donnent le même résultat

# A tibble: 5 x 2
# Groups:   id [5]
     id string
  <int> <fct> 
1     1 A     
2     2 B     
3     3 C     
4     4 D     
5     5 E
12
Kresten

Une simple option ddply:

ddply(test,.(id),function(x) head(x,1))

Si la rapidité est un problème, une approche similaire pourrait être adoptée avec data.table:

testd <- data.table(test)
setkey(testd,id)
testd[,.SD[1],by = key(testd)]

ou cela pourrait être considérablement plus rapide:

testd[testd[, .I[1], by = key(testd]$V1]
11
joran

maintenant, pour dplyr, ajouter un compteur distinct.

df %>%
    group_by(aa, bb) %>%
    summarise(first=head(value,1), count=n_distinct(value))

Vous créez des groupes, résumez-les au sein de groupes.

Si les données sont numériques, vous pouvez utiliser:
first(value) [il y a aussi last(value)] à la place de head(value, 1)

voir: http://cran.rstudio.com/web/packages/dplyr/vignettes/introduction.html

Plein:

> df
Source: local data frame [16 x 3]

   aa bb value
1   1  1   GUT
2   1  1   PER
3   1  2   SUT
4   1  2   GUT
5   1  3   SUT
6   1  3   GUT
7   1  3   PER
8   2  1   221
9   2  1   224
10  2  1   239
11  2  2   217
12  2  2   221
13  2  2   224
14  3  1   GUT
15  3  1   HUL
16  3  1   GUT

> library(dplyr)
> df %>%
>   group_by(aa, bb) %>%
>   summarise(first=head(value,1), count=n_distinct(value))

Source: local data frame [6 x 4]
Groups: aa

  aa bb first count
1  1  1   GUT     2
2  1  2   SUT     2
3  1  3   SUT     3
4  2  1   221     3
5  2  2   217     3
6  3  1   GUT     2
7
Paul

(1) SQLite a une pseudo-colonne rowid intégrée pour que cela fonctionne:

sqldf("select min(rowid) rowid, id, string 
               from test 
               group by id")

donnant:

  rowid id string
1     1  1      A
2     3  2      B
3     5  3      C
4     7  4      D
5     9  5      E

(2) Aussi sqldf lui-même a un row.names= argument:

sqldf("select min(cast(row_names as real)) row_names, id, string 
              from test 
              group by id", row.names = TRUE)

donnant:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

(3) Une troisième alternative qui mélange les éléments des deux précédents pourrait être encore meilleure:

sqldf("select min(rowid) row_names, id, string 
               from test 
               group by id", row.names = TRUE)

donnant:

  id string
1  1      A
3  2      B
5  3      C
7  4      D
9  5      E

Notez que tous les trois reposent sur une extension SQLite à SQL dans laquelle l'utilisation de min ou max a pour résultat que les autres colonnes sont choisies dans la même ligne. (Dans d'autres bases de données SQL, il peut ne pas être garanti.)

7
G. Grothendieck

Une option de base R est la split()-lapply()-do.call() idiome:

> do.call(rbind, lapply(split(test, test$id), head, 1))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

Une option plus directe consiste à lapply() la fonction [:

> do.call(rbind, lapply(split(test, test$id), `[`, 1, ))
  id string
1  1      A
2  2      B
3  3      C
4  4      D
5  5      E

La virgule 1, ) À la fin de l'appel lapply() est essentiel, car cela équivaut à l'appel de [1, ] Pour sélectionner la première ligne et tous colonnes.

4
Gavin Simpson