Wie lösche ich eine Zeile als Referenz in data.table?


150

Meine Frage bezieht sich auf die Zuweisung durch Referenz oder das Kopieren in data.table. Ich möchte wissen, ob man Zeilen durch Referenz löschen kann, ähnlich wie

DT[ , someCol := NULL]

Ich möchte es wissen

DT[someRow := NULL, ]

Ich denke, es gibt einen guten Grund, warum diese Funktion nicht existiert. Vielleicht können Sie einfach eine gute Alternative zum üblichen Kopieransatz aufzeigen, wie unten. Insbesondere mit meinem Favoriten aus Beispiel (data.table),

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
#      x y v
# [1,] a 1 1
# [2,] a 3 2
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Angenommen, ich möchte die erste Zeile aus dieser Datentabelle löschen. Ich weiß, ich kann dies tun:

DT <- DT[-1, ]

aber oft möchten wir das vermeiden, weil wir das Objekt kopieren (und das erfordert ungefähr 3 * N Speicher, wenn N object.size(DT), wie hier ausgeführt . Jetzt habe ich gefunden set(DT, i, j, value). Ich weiß, wie man bestimmte Werte setzt (wie hier: setze alle Werte in den Zeilen 1 und 2 und den Spalten 2 und 3 bis Null)

set(DT, 1:2, 2:3, 0) 
DT
#      x y v
# [1,] a 0 0
# [2,] a 0 0
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Aber wie kann ich beispielsweise die ersten beiden Zeilen löschen? Tun

set(DT, 1:2, 1:3, NULL)

setzt den gesamten DT auf NULL.

Meine SQL-Kenntnisse sind sehr begrenzt, also sagt ihr mir: Wenn data.table SQL-Technologie verwendet, gibt es eine Entsprechung zum SQL-Befehl

DELETE FROM table_name
WHERE some_column=some_value

in data.table?


17
Ich denke nicht, dass data.table()SQL-Technologie so sehr verwendet wird, als dass man eine Parallele zwischen den verschiedenen Operationen in SQL und den verschiedenen Argumenten zu a ziehen kann data.table. Für mich impliziert der Verweis auf "Technologie" etwas, dass er data.tableirgendwo auf einer SQL-Datenbank sitzt, was AFAIK nicht der Fall ist.
Chase

1
danke Chase. Ja, ich denke, dass SQL-Analogie eine wilde Vermutung war.
Florian Oswald

1
Oft sollte es ausreichen, ein Flag zu definieren, um Zeilen zu behalten, wie DT[ , keep := .I > 1]dann eine Teilmenge für spätere Operationen: DT[(keep), ...]vielleicht sogar setindex(DT, keep)die Geschwindigkeit dieser Teilmenge. Kein Allheilmittel, aber es lohnt sich, es als Design-Wahl in Ihrem Workflow zu betrachten. Möchten Sie wirklich alle diese Zeilen aus dem Speicher löschen oder möchten Sie sie lieber ausschließen? Die Antwort unterscheidet sich je nach Anwendungsfall.
MichaelChirico

Antworten:


125

Gute Frage. data.tableZeilen können noch nicht als Referenz gelöscht werden.

data.tablekann Spalten als Referenz hinzufügen und löschen , da der Vektor der Spaltenzeiger bekanntlich überbelegt wird. Der Plan ist, etwas Ähnliches für Zeilen zu tun und schnell insertund schnell zuzulassen delete. Ein Zeilenlöschvorgang würde memmovein C verwendet, um die Elemente (in jeder einzelnen Spalte) nach den gelöschten Zeilen zu verschieben. Das Löschen einer Zeile in der Mitte der Tabelle wäre im Vergleich zu einer Zeilenspeicherdatenbank wie SQL, die sich besser zum schnellen Einfügen und Löschen von Zeilen eignet, wo immer sich diese Zeilen in der Tabelle befinden, immer noch recht ineffizient. Trotzdem wäre es viel schneller als das Kopieren eines neuen großen Objekts ohne die gelöschten Zeilen.

Da andererseits Spaltenvektoren überbelegt würden, könnten Zeilen am Ende sofort eingefügt (und gelöscht) werden . zB eine wachsende Zeitreihe.


Es wurde als Problem abgelegt: Löschen Sie Zeilen als Referenz .


1
@ Matthew Dowle Gibt es Neuigkeiten dazu?
statquant

15
@statquant Ich denke, ich sollte die 37 Fehler beheben und freadzuerst fertig werden . Danach ist es ziemlich hoch.
Matt Dowle

15
@ MatthewDowle sicher, nochmals vielen Dank für alles, was Sie tun.
statquant

1
@rbatt Richtig. DT[b<8 & a>3]gibt eine neue data.table zurück. Wir möchten hinzufügen delete(DT, b>=8 | a<=3)und DT[b>=8 | a<=8, .ROW:=NULL]. Der Vorteil der letzteren würde mit anderen Merkmalen kombiniert werden , []wie beispielsweise die Zeilennummern in i, verbinden sie in iund rollprofitiert von der [i,j,by]Optimierung.
Matt Dowle

2
@charliealpha Kein Update. Beiträge willkommen. Ich bin bereit zu führen. Es braucht C-Kenntnisse - wieder bin ich bereit zu führen.
Matt Dowle

29

Der Ansatz, den ich gewählt habe, um die Verwendung des Speichers ähnlich wie das Löschen an Ort und Stelle zu machen, besteht darin, jeweils eine Spalte zu unterteilen und zu löschen. Nicht so schnell wie eine richtige C-Memmove-Lösung, aber die Speichernutzung ist alles, was mich hier interessiert. etwas wie das:

DT = data.table(col1 = 1:1e6)
cols = paste0('col', 2:100)
for (col in cols){ DT[, (col) := 1:1e6] }
keep.idxs = sample(1e6, 9e5, FALSE) # keep 90% of entries
DT.subset = data.table(col1 = DT[['col1']][keep.idxs]) # this is the subsetted table
for (col in cols){
  DT.subset[, (col) := DT[[col]][keep.idxs]]
  DT[, (col) := NULL] #delete
}

5
+1 Schöner speichereffizienter Ansatz. Idealerweise müssen wir also eine Reihe von Zeilen als Referenz löschen, nicht wahr? Daran hatte ich nicht gedacht. Es muss eine Reihe von memmoves sein, um die Lücken zu schließen, aber das ist in Ordnung.
Matt Dowle

Würde dies als Funktion funktionieren oder erzwingt die Verwendung in einer Funktion und deren Rückgabe das Erstellen von Speicherkopien?
Russellpierce

1
Es würde in einer Funktion funktionieren, da data.tables immer Referenzen sind.
VC273

1
danke, nett. Um ein wenig zu beschleunigen (besonders bei vielen Spalten), wechseln Sie DT[, col:= NULL, with = F]inset(DT, NULL, col, NULL)
Michele

2
Aktualisierung angesichts sich ändernder Redewendung und Warnung "mit = FALSE zusammen mit: = wurde in Version 1.9.4, veröffentlicht im Oktober 2014, veraltet. Bitte umschließen Sie die LHS von: = mit Klammern, z. B. DT [, (myVar): = sum (b) , by = a], um Spaltennamen zuzuweisen, die in der Variablen myVar enthalten sind. Weitere Beispiele finden Sie unter? ': ='. Wie 2014 gewarnt, ist dies jetzt eine Warnung. "
Frank

6

Hier ist eine Arbeitsfunktion, die auf der Antwort von @ vc273 und dem Feedback von @ Frank basiert.

delete <- function(DT, del.idxs) {           # pls note 'del.idxs' vs. 'keep.idxs'
  keep.idxs <- setdiff(DT[, .I], del.idxs);  # select row indexes to keep
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs]); # this is the subsetted table
  setnames(DT.subset, cols[1]);
  for (col in cols[2:length(cols)]) {
    DT.subset[, (col) := DT[[col]][keep.idxs]];
    DT[, (col) := NULL];  # delete
  }
   return(DT.subset);
}

Und ein Beispiel für seine Verwendung:

dat <- delete(dat,del.idxs)   ## Pls note 'del.idxs' instead of 'keep.idxs'

Wobei "dat" eine Datentabelle ist. Das Entfernen von 14.000 Zeilen aus 1,4 Millionen Zeilen dauert auf meinem Laptop 0,25 Sekunden.

> dim(dat)
[1] 1419393      25
> system.time(dat <- delete(dat,del.idxs))
   user  system elapsed 
   0.23    0.02    0.25 
> dim(dat)
[1] 1404715      25
> 

PS. Da ich neu bei SO bin, konnte ich dem Thread von @ vc273 keinen Kommentar hinzufügen :-(


Ich habe unter der Antwort von vc die geänderte Syntax für (col) kommentiert: =. Es ist seltsam, eine Funktion namens "Löschen" zu haben, aber ein Argument, das sich darauf bezieht, was zu behalten ist. Übrigens ist es im Allgemeinen vorzuziehen, ein reproduzierbares Beispiel zu verwenden, anstatt für Ihre eigenen Daten ein Dimm anzuzeigen. Sie können beispielsweise DT aus der Frage wiederverwenden.
Frank

Ich verstehe nicht, warum Sie es als Referenz tun, aber später eine Aufgabe verwenden dat <-
skan

1
@skan, Diese Zuweisung weist "dat" zu, um auf die geänderte data.table zu verweisen, die selbst erstellt wurde, indem die ursprüngliche data.table untergeordnet wurde. Das <- Assingment kopiert die Rückgabedaten nicht, weist ihr lediglich einen neuen Namen zu. Link
Jarno P.

@Frank, ich habe die Funktion für die Kuriosität aktualisiert, auf die Sie hingewiesen haben.
Jarno P.

OK danke. Ich hinterlasse den Kommentar, da ich immer noch der Meinung bin, dass das Anzeigen der Konsolenausgabe anstelle eines reproduzierbaren Beispiels hier nicht empfohlen wird. Außerdem ist ein einzelner Benchmark nicht so informativ. Wenn Sie auch die Zeit messen würden, die für die Teilmenge benötigt wird, wäre dies informativer (da die meisten von uns nicht intuitiv wissen, wie lange das dauert, geschweige denn, wie lange es für Ihre Komposition dauert). Jedenfalls möchte ich nicht behaupten, dass dies eine schlechte Antwort ist. Ich bin einer seiner Aufsteiger.
Frank

4

Versuchen Sie stattdessen oder versuchen Sie, auf NULL zu setzen, auf NA (passend zum NA-Typ für die erste Spalte).

set(DT,1:2, 1:3 ,NA_character_)

3
Ja, das funktioniert, denke ich. Mein Problem ist, dass ich viele Daten habe und genau diese Zeilen mit NA entfernen möchte, möglicherweise ohne DT kopieren zu müssen, um diese Zeilen zu entfernen. trotzdem danke für deinen kommentar!
Florian Oswald

4

Das Thema ist immer noch interessant für viele Leute (ich eingeschlossen).

Was ist damit? Ich habe assignden glovalenvund den zuvor beschriebenen Code ersetzt. Es wäre besser, die ursprüngliche Umgebung zu erfassen, aber zumindest globalenvist sie speichereffizient und wirkt wie eine Änderung durch ref.

delete <- function(DT, del.idxs) 
{ 
  varname = deparse(substitute(DT))

  keep.idxs <- setdiff(DT[, .I], del.idxs)
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs])
  setnames(DT.subset, cols[1])

  for (col in cols[2:length(cols)]) 
  {
    DT.subset[, (col) := DT[[col]][keep.idxs]]
    DT[, (col) := NULL];  # delete
  }

  assign(varname, DT.subset, envir = globalenv())
  return(invisible())
}

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
delete(DT, 3)

Um ganz klar zu sein, dies wird nicht durch Verweis (basierend auf address(DT); delete(DT, 3); address(DT)) gelöscht , obwohl es in gewissem Sinne effizient sein kann.
Frank

1
Nein, tut es nicht. Es emuliert das Verhalten und ist speichereffizient. Deshalb habe ich gesagt: Es verhält sich wie . Aber genau genommen haben Sie Recht, die Adresse hat sich geändert.
JRR

3

Hier sind einige Strategien, die ich verwendet habe. Ich glaube, dass eine .ROW-Funktion kommen könnte. Keiner dieser folgenden Ansätze ist schnell. Dies sind einige Strategien, die etwas über Teilmengen oder Filterung hinausgehen. Ich habe versucht, wie dba zu denken, nur um Daten zu bereinigen. Wie oben erwähnt, können Sie Zeilen in data.table auswählen oder entfernen:

data(iris)
iris <- data.table(iris)

iris[3] # Select row three

iris[-3] # Remove row three

You can also use .SD to select or remove rows:

iris[,.SD[3]] # Select row three

iris[,.SD[3:6],by=,.(Species)] # Select row 3 - 6 for each Species

iris[,.SD[-3]] # Remove row three

iris[,.SD[-3:-6],by=,.(Species)] # Remove row 3 - 6 for each Species

Hinweis: .SD erstellt eine Teilmenge der Originaldaten und ermöglicht es Ihnen, in j oder nachfolgenden Datentabellen eine Menge Arbeit zu erledigen. Siehe https://stackoverflow.com/a/47406952/305675 . Hier habe ich meine Iris nach Sepal Länge bestellt, mindestens eine bestimmte Sepal.Länge genommen, die drei besten (nach Sepal Länge) aller Arten ausgewählt und alle zugehörigen Daten zurückgegeben:

iris[order(-Sepal.Length)][Sepal.Length > 3,.SD[1:3],by=,.(Species)]

Die Ansätze ordnen vor allem eine Datentabelle beim Entfernen von Zeilen nacheinander neu an. Sie können eine data.table transponieren und die alten Zeilen entfernen oder ersetzen, die jetzt transponierte Spalten sind. Wenn Sie mit ': = NULL' eine transponierte Zeile entfernen, wird auch der nachfolgende Spaltenname entfernt:

m_iris <- data.table(t(iris))[,V3:=NULL] # V3 column removed

d_iris <- data.table(t(iris))[,V3:=V2] # V3 column replaced with V2

Wenn Sie den data.frame zurück in eine data.table transponieren, möchten Sie möglicherweise die ursprüngliche data.table umbenennen und beim Löschen die Klassenattribute wiederherstellen. Durch Anwenden von ": = NULL" auf eine jetzt transponierte Datentabelle werden alle Zeichenklassen erstellt.

m_iris <- data.table(t(d_iris));
setnames(d_iris,names(iris))

d_iris <- data.table(t(m_iris));
setnames(m_iris,names(iris))

Möglicherweise möchten Sie nur doppelte Zeilen entfernen, die Sie mit oder ohne Schlüssel ausführen können:

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]     

d_iris[!duplicated(Key),]

d_iris[!duplicated(paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)),]  

Es ist auch möglich, einen inkrementellen Zähler mit '.I' hinzuzufügen. Sie können dann nach doppelten Schlüsseln oder Feldern suchen und diese entfernen, indem Sie den Datensatz mit dem Zähler entfernen. Dies ist rechenintensiv, hat jedoch einige Vorteile, da Sie die zu entfernenden Zeilen drucken können.

d_iris[,I:=.I,] # add a counter field

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]

for(i in d_iris[duplicated(Key),I]) {print(i)} # See lines with duplicated Key or Field

for(i in d_iris[duplicated(Key),I]) {d_iris <- d_iris[!I == i,]} # Remove lines with duplicated Key or any particular field.

Sie können auch einfach eine Zeile mit Nullen oder NAs füllen und diese dann mit einer i-Abfrage löschen:

 X 
   x v foo
1: c 8   4
2: b 7   2

X[1] <- c(0)

X
   x v foo
1: 0 0   0
2: b 7   2

X[2] <- c(NA)
X
    x  v foo
1:  0  0   0
2: NA NA  NA

X <- X[x != 0,]
X <- X[!is.na(x),]

Dies beantwortet die Frage (zum Entfernen durch Referenz) nicht wirklich und die Verwendung teines data.frame ist normalerweise keine gute Idee. Überprüfen Sie str(m_iris), ob alle Daten zu Zeichenfolgen / Zeichen geworden sind. Übrigens können Sie auch Zeilennummern abrufen, d_iris[duplicated(Key), which = TRUE]indem Sie eine Zählerspalte erstellen.
Frank

1
Ja, du hast recht. Ich beantworte die Frage nicht speziell. Das Entfernen einer Zeile durch Referenz hat jedoch noch keine offizielle Funktionalität oder Dokumentation, und viele Leute werden in diesem Beitrag nach generischen Funktionen suchen, um genau das zu tun. Wir könnten einen Beitrag erstellen, um nur die Frage zu beantworten, wie eine Zeile entfernt werden kann. Stapelüberlauf ist sehr nützlich und ich verstehe wirklich die Notwendigkeit, die Antworten genau auf die Frage zu halten. Manchmal denke ich, dass SO in dieser Hinsicht nur ein kleiner Faschist sein kann ... aber vielleicht gibt es einen guten Grund dafür.
Rferrisx

Ok, danke für die Erklärung. Ich denke, für den Moment ist unsere Diskussion hier ein Wegweiser für jeden, der in diesem Fall verwirrt ist.
Frank
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.