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



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

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

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.


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]

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

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


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.


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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.