Wybierz pierwszy wiersz według grupy

85

Z takiej ramki danych

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

Chcę utworzyć nowy z pierwszym wierszem każdej pary identyfikator / ciąg. Jeśli sqldf zaakceptowałby w nim kod R, zapytanie mogłoby wyglądać następująco:

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

Czy istnieje rozwiązanie, które nie pozwala na utworzenie nowej kolumny, takiej jak

test$row <- rownames(test)

i uruchamiając to samo zapytanie sqldf z min (wiersz)?

dmvianna
źródło
1
@Matthew, moje pytanie jest starsze.
dmvianna
2
Twoje pytanie ma 1 rok, a drugie pytanie ma 4 lata, nie? Jest tak wiele powtórzeń tego pytania
Matthew
@Matthew Przepraszamy, chyba źle odczytałem daty.
dmvianna

Odpowiedzi:

119

Możesz duplicatedto zrobić bardzo szybko.

test[!duplicated(test$id),]

Benchmarki dla maniaków prędkości:

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

Spróbujmy jeszcze raz, ale tylko z rywalami z pierwszego biegu, z większą ilością danych i większą liczbą replikacji.

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
Joshua Ulrich
źródło
Zwycięzca: system.time (dat3 [! Duplicated (dat3 $ id),]) system użytkownika upłynął 0,07 0,00 0,07
dmvianna
2
@dmvianna: Nie mam go zainstalowanego i nie chciałem się tym przejmować. :)
Joshua Ulrich
Czy mamy pewność, że mój kod data.table jest tak wydajny, jak to tylko możliwe? Nie jestem przekonany, czy uda mi się uzyskać najlepszą wydajność tego narzędzia.
joran
2
Uważam również, że jeśli masz zamiar porównać dane.table, kluczowanie powinno zawierać porządkowanie według identyfikatora w wywołaniach bazy.
mnel
1
@JoshuaUlrich Jeszcze jedno pytanie: dlaczego potrzebne jest pierwsze zdanie, czyli założenie, że dane są już posortowane. !duplicated(x)znajduje pierwszą z każdej grupy, nawet jeśli nie jest posortowana, iiuc.
Matt Dowle
37

Preferuję podejście dplyr.

group_by(id) po którym następuje albo

  • filter(row_number()==1) lub
  • slice(1) lub
  • slice_head(1) # (dplyr => 1,0)
  • top_n(n = -1)
    • top_n()wewnętrznie używa funkcji rangi. Negatywne selekcje z dołu rangi.

W niektórych przypadkach może być konieczne umieszczenie identyfikatorów po parametrze 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)

Wszystkie trzy metody zwracają ten sam wynik

# 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
Kresten
źródło
2
Warto sliceteż to powiedzieć. slice(x)jest skrótem do filter(row_number() %in% x).
Gregor Thomas
Bardzo elegancko. Czy wiesz, dlaczego muszę zamienić mój data.tablena a, data.frameaby to zadziałało?
James Hirschorn
@JamesHirschorn Nie jestem ekspertem od wszystkich różnic. Ale data.tabledziedziczy po tym, data.framewięc w wielu przypadkach możesz użyć poleceń dplyr na pliku data.table. Przykład powyżej np. Działa również, jeśli testjest data.table. Patrz np stackoverflow.com/questions/13618488/... głębszego explanantion
Kresten
Jest to prosty sposób na zrobienie tego i, jak widzisz, data.frame jest tutaj tak naprawdę tibble. Osobiście radzę ci zawsze pracować z tibbles również dlatego, że ggplot2 jest zbudowany w podobny sposób.
Garini
17

Co powiesz na

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

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

Edytować

Istnieje również unikalna metoda, data.tablesktóra zwraca pierwszy wiersz według klucza

jdtu <- function() unique(DT)

Myślę, że jeśli zamawiasz testpoza benchmarkiem, możesz również usunąć setkeyi data.tablekonwersję z benchmarku (ponieważ setkey zasadniczo sortuje według identyfikatora, tak samo jak 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         

i więcej danych

** Edytuj unikalną metodą **

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        

Unikalna metoda jest tutaj najszybsza.

mnel
źródło
4
Nie musisz nawet ustawiać klucza. unique(DT,by="id")działa bezpośrednio
Matthew
FYI od data.tablewersji> = 1.9.8, domyślny byargument dla uniqueto by = seq_along(x)(wszystkie kolumny), zamiast poprzedniego domyślnegoby = key(x)
IceCreamToucan
12

Prosta ddplyopcja:

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

Jeśli problemem jest szybkość, podobne podejście można zastosować w przypadku data.table:

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

lub może to być znacznie szybsze:

testd[testd[, .I[1], by = key(testd]$V1]
joran
źródło
Co zaskakujące, sqldf robi to szybciej: 1,77 0,13 1,92 vs 10,53 0,00 10,79 z data.table
dmvianna
3
@dmvianna Nie musiałbym koniecznie liczyć data.table. Nie jestem ekspertem w zakresie tego narzędzia, więc mój kod data.table może nie być najskuteczniejszym sposobem, aby to osiągnąć.
joran
Głosowałem za tym przedwcześnie. Kiedy uruchomiłem go na dużej tabeli danych, był absurdalnie wolny i nie działał: liczba wierszy była taka sama po.
James Hirschorn
@JamesHirachorn Napisałem to dawno temu, pakiet bardzo się zmienił i prawie wcale nie używam data.table. Jeśli znajdziesz właściwy sposób na zrobienie tego z tym pakietem, możesz zasugerować edycję, która poprawi go.
joran
8

teraz, dla dplyrdodania odrębnego licznika.

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

Tworzysz grupy, a one podsumowują w grupach.

Jeśli dane są numeryczne, możesz użyć:
first(value)[istnieje również last(value)] zamiasthead(value, 1)

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

Pełny:

> 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
Paweł
źródło
Ta odpowiedź jest dość przestarzała - istnieją lepsze sposoby na zrobienie tego dplyr, które nie wymagają pisania instrukcji dla każdej kolumny, która ma być uwzględniona (patrz na przykład odpowiedź atommana poniżej) . Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use first (value) `vs head(value)(or just value[1])
Gregor Thomas
7

(1) SQLite ma wbudowaną rowidpseudokolumnę, więc działa to:

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

dający:

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

(2) Również sqldfsama ma row.names=argument:

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

dający:

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

(3) Trzecia alternatywa, która łączy elementy dwóch powyższych, może być jeszcze lepsza:

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

dający:

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

Zauważ, że wszystkie trzy opierają się na rozszerzeniu SQLite do SQL, w którym użycie minlub maxgwarantuje, że inne kolumny zostaną wybrane z tego samego wiersza. (W innych bazach danych opartych na języku SQL, które mogą nie być gwarantowane).

G. Grothendieck
źródło
Dzięki! Jest to znacznie lepsze niż akceptowana odpowiedź IMO, ponieważ można uogólnić przyjmowanie pierwszego / ostatniego elementu w zagregowanym kroku przy użyciu wielu funkcji agregujących (tj. Pobranie pierwszej z tej zmiennej, zsumowanie tej zmiennej itp.).
Bridgeburners
4

Podstawową opcją R jest idiom split()- lapply()- do.call():

> 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

Bardziej bezpośredni opcją jest lapply()do [funkcji:

> 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

Przecinek 1, )na końcu lapply()wywołania jest niezbędny, ponieważ jest to równoważne wywołaniu w [1, ]celu wybrania pierwszego wiersza i wszystkich kolumn.

Gavin Simpson
źródło
To było bardzo powolne, Gavin: system użytkownika upłynął 91,84 6,02 101,10
dmvianna
Wszystko, co dotyczy ramek danych, będzie. Ich użyteczność ma swoją cenę. Stąd na przykład data.table.
Gavin Simpson
w mojej obronie i R, w pytaniu nie wspomniał pan o skuteczności. Często cechą jest łatwość obsługi . Zobacz popularność ply, która też jest „wolna”, przynajmniej do następnej wersji obsługującej dane.table.
Gavin Simpson
1
Zgadzam się. Nie chciałem cię obrazić. Jednak zauważyłem, że metoda @ Joshua-Ulricha była zarówno szybka, jak i łatwa. : 7)
dmvianna
Nie ma potrzeby przepraszać i nie odebrałem tego jako zniewagi. Wskazał tylko, że był oferowany bez żadnych roszczeń dotyczących wydajności. Pamiętaj, że te pytania i odpowiedzi dotyczące przepełnienia stosu są nie tylko dla Ciebie, ale także dla innych użytkowników, którzy natknęli się na Twoje pytanie, ponieważ mają podobny problem.
Gavin Simpson