Wählen Sie die erste Zeile nach Gruppe aus

87

Von einem Datenrahmen wie diesem

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

Ich möchte eine neue mit der ersten Zeile jedes ID / String-Paares erstellen. Wenn sqldf R-Code darin akzeptiert, könnte die Abfrage folgendermaßen aussehen:

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

Gibt es eine Lösung, um eine neue Spalte wie zu erstellen?

test$row <- rownames(test)

und dieselbe sqldf-Abfrage mit min (Zeile) ausführen?

dmvianna
quelle
1
@ Matthew, meine Frage ist älter.
dmvianna
2
Ihre Frage ist 1 Jahr alt und die andere Frage ist 4 Jahre alt, nein? Es gibt so viele Duplikate dieser Frage
Matthäus
@ Matthew Entschuldigung, ich muss die Daten falsch gelesen haben.
dmvianna

Antworten:

120

Sie können duplicateddies sehr schnell tun.

test[!duplicated(test$id),]

Benchmarks für die Geschwindigkeitsfreaks:

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

Versuchen wir das noch einmal, aber nur mit den Konkurrenten vom ersten Lauf an und mit mehr Daten und mehr Replikationen.

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
quelle
Der Gewinner: system.time (dat3 [! Dupliziert (dat3 $ id),]) Benutzersystem verstrichen
0.07
2
@dmvianna: Ich habe es nicht installiert und hatte keine Lust, mich damit zu beschäftigen. :)
Joshua Ulrich
Sind wir sicher, dass mein data.table-Code so effizient wie möglich ist? Ich bin nicht sicher, ob ich mit diesem Tool die beste Leistung erzielen kann.
Joran
2
Ich denke auch, wenn Sie die data.table als Benchmark verwenden möchten, sollten Sie die Reihenfolge nach ID in die Basisaufrufe aufnehmen.
mnel
1
@JoshuaUlrich Noch eine Frage: Warum wird der erste Satz benötigt, dh die Annahme, dass die Daten bereits sortiert sind. !duplicated(x)findet den ersten jeder Gruppe, auch wenn er nicht sortiert ist, iiuc.
Matt Dowle
37

Ich bevorzuge den dplyr-Ansatz.

group_by(id) gefolgt von entweder

  • filter(row_number()==1) oder
  • slice(1) oder
  • slice_head(1) # (dplyr => 1.0)
  • top_n(n = -1)
    • top_n()Verwendet intern die Rangfunktion. Negativ wählt am Ende des Ranges aus.

In einigen Fällen kann es erforderlich sein, die IDs nach group_by anzuordnen.

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)

Alle drei Methoden geben das gleiche Ergebnis zurück

# 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
quelle
2
Es lohnt sich auch, einen Gruß zu geben slice. slice(x)ist eine Abkürzung für filter(row_number() %in% x).
Gregor Thomas
Sehr elegant. Wissen Sie, warum ich meine data.tablein eine konvertieren muss , data.framedamit dies funktioniert?
James Hirschorn
@ JamesHirschorn Ich bin kein Experte für all die Unterschiede. Aber data.tableerbt von der data.frameso in vielen Fällen können Sie dplyr Befehle auf a verwenden data.table. Das obige Beispiel funktioniert zB auch wenn a testist data.table. Siehe z. B. stackoverflow.com/questions/13618488/… für eine ausführlichere Erklärung
Kresten
Dies ist ein ordentlicher Weg, und wie Sie sehen, ist der data.frame hier tatsächlich ein tibble. Ich persönlich rate Ihnen, immer mit tibbles zu arbeiten, auch weil ggplot2 auf ähnliche Weise aufgebaut ist.
Garini
17

Wie wäre es mit

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

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

Bearbeiten

Es gibt auch eine eindeutige Methode, bei data.tablesder die erste Zeile per Schlüssel zurückgegeben wird

jdtu <- function() unique(DT)

Ich denke, wenn Sie testaußerhalb des Benchmarks bestellen , können Sie das setkeyund die data.tableKonvertierung auch aus dem Benchmark entfernen (da der Setkey grundsätzlich nach ID sortiert ist, genau wie 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         

und mit mehr Daten

** Mit einzigartiger Methode bearbeiten **

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        

Die einzigartige Methode ist hier am schnellsten.

mnel
quelle
4
Sie müssen nicht einmal den Schlüssel setzen. unique(DT,by="id")arbeitet direkt
Matthew
Zu Ihrer Information ab data.tableVersion> = 1.9.8, die Standard byfür Argument uniqueist by = seq_along(x)(alle Spalten), anstelle der bisherigen Standardby = key(x)
IceCreamToucan
12

Eine einfache ddplyOption:

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

Wenn Geschwindigkeit ein Problem ist, könnte ein ähnlicher Ansatz gewählt werden mit data.table:

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

oder das könnte erheblich schneller sein:

testd[testd[, .I[1], by = key(testd]$V1]
Joran
quelle
Überraschenderweise macht es sqldf schneller: 1,77 0,13 1,92 vs 10,53 0,00 10,79 mit data.table
dmvianna
3
@dmvianna Ich würde data.table nicht unbedingt zählen. Ich bin kein Experte mit diesem Tool, daher ist mein data.table-Code möglicherweise nicht der effizienteste Weg, dies zu tun.
Joran
Ich habe dies vorzeitig bewertet. Als ich es auf einer großen Datentabelle ausführte, war es lächerlich langsam und es funktionierte nicht: Die Anzahl der Zeilen war danach gleich.
James Hirschorn
@JamesHirachorn Ich habe das vor langer Zeit geschrieben, das Paket hat sich sehr verändert und ich benutze data.table kaum. Wenn Sie mit diesem Paket den richtigen Weg finden, können Sie eine Bearbeitung vorschlagen, um es besser zu machen.
Joran
8

jetzt zum dplyrHinzufügen eines deutlichen Zählers.

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

Sie erstellen Gruppen, die in Gruppen zusammengefasst werden.

Wenn die Daten numerisch sind, können Sie Folgendes verwenden:
first(value)[gibt es auch last(value)] anstelle vonhead(value, 1)

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

Voll:

> 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
Paul
quelle
Diese Antwort ist ziemlich veraltet - es gibt bessere Möglichkeiten, dies zu dplyrtun, ohne dass für jede einzelne Spalte eine Erklärung geschrieben werden muss (siehe zum Beispiel die Antwort von atomman unten). . Also I'm not sure what *"if data is numeric"* has anything to do with whether or not one would use Zuerst (Wert) `vs head(value)(oder nur value[1])
Gregor Thomas
7

(1) SQLite hat eine eingebaute rowidPseudospalte, so dass dies funktioniert:

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

Geben:

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

(2) Auch sqldfselbst hat ein row.names=Argument:

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

Geben:

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

(3) Eine dritte Alternative, die die Elemente der beiden oben genannten Elemente mischt, könnte noch besser sein:

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

Geben:

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

Beachten Sie, dass alle drei auf einer SQLite-Erweiterung für SQL basieren, bei der die Verwendung von minoder maxgarantiert dazu führt, dass die anderen Spalten aus derselben Zeile ausgewählt werden. (In anderen SQL-basierten Datenbanken kann dies möglicherweise nicht garantiert werden.)

G. Grothendieck
quelle
Vielen Dank! Dies ist viel besser als die akzeptierte Antwort IMO, da es verallgemeinerbar ist, das erste / letzte Element in einem Aggregatschritt unter Verwendung mehrerer Aggregatfunktionen zu verwenden (dh das erste dieser Variablen zu nehmen, diese Variable zu summieren usw.).
Bridgeburners
4

Eine Basis-R-Option ist das split()- lapply()- do.call()Idiom:

> 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

Eine direktere Option ist lapply()die [Funktion:

> 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

Das Komma 1, )am Ende des lapply()Aufrufs ist wichtig, da dies dem Aufruf [1, ]zur Auswahl der ersten Zeile und aller Spalten entspricht.

Gavin Simpson
quelle
Dies war sehr langsam, Gavin: Benutzersystem abgelaufen 91.84 6.02 101.10
dmvianna
Alles, was Datenrahmen betrifft, wird sein. Ihr Nutzen hat seinen Preis. So zum Beispiel data.table.
Gavin Simpson
Zu meiner Verteidigung und zu Rs haben Sie in der Frage nichts über Effizienz erwähnt. Oft einfache Bedienung ist ein Feature. Erleben Sie die Beliebtheit von Ply, die auch "langsam" ist, zumindest bis zur nächsten Version, die Unterstützung für data.table bietet.
Gavin Simpson
1
Genau. Ich wollte dich nicht beleidigen. Ich fand jedoch, dass die Methode von @ Joshua-Ulrich sowohl schnell als auch einfach war. : 7)
dmvianna
Ich brauche mich nicht zu entschuldigen und habe es nicht als Beleidigung angesehen. Ich habe nur darauf hingewiesen, dass es ohne Anspruch auf Effizienz angeboten wurde. Denken Sie daran, dass diese Fragen und Antworten zum Stapelüberlauf nicht nur zu Ihrem Vorteil sind, sondern auch für andere Benutzer, die auf Ihre Frage stoßen, da sie ein ähnliches Problem haben.
Gavin Simpson