Genau verstehen, wann eine data.table auf eine andere data.table verweist (im Vergleich zu einer Kopie davon)

193

Ich habe ein kleines Problem damit, die Pass-by-Reference-Eigenschaften von zu verstehen data.table . Einige Operationen scheinen die Referenz zu "brechen", und ich möchte genau verstehen, was passiert.

Beim Erstellen eines data.tablevon einem anderen data.table(via<- und anschließendes Aktualisieren der neuen Tabelle durch :=wird auch die ursprüngliche Tabelle geändert. Dies wird erwartet wie folgt:

?data.table::copy und Stackoverflow: Referenzübergabe des Operators im Datentabellenpaket

Hier ist ein Beispiel:

library(data.table)

DT <- data.table(a=c(1,2), b=c(11,12))
print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

newDT <- DT        # reference, not copy
newDT[1, a := 100] # modify new DT

print(DT)          # DT is modified too.
#        a  b
# [1,] 100 11
# [2,]   2 12

Wenn ich jedoch eine nicht :=basierte Änderung zwischen der <-Zuordnung und den :=obigen Zeilen einfüge, DTwird diese jetzt nicht mehr geändert:

DT = data.table(a=c(1,2), b=c(11,12))
newDT <- DT        
newDT$b[2] <- 200  # new operation
newDT[1, a := 100]

print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

So scheint es, dass die newDT$b[2] <- 200 Zeile die Referenz irgendwie "bricht". Ich würde vermuten, dass dies irgendwie eine Kopie aufruft, aber ich möchte vollständig verstehen, wie R diese Operationen behandelt, um sicherzustellen, dass ich keine potenziellen Fehler in meinen Code einführe.

Ich würde mich sehr freuen, wenn mir jemand das erklären könnte.

Peter Fine
quelle
1
Ich habe gerade dieses "Feature" entdeckt und es ist schrecklich. Im Internet wird allgemein empfohlen,<- anstelle der =grundlegenden Zuweisung in R zu verwenden (z. B. von Google: google.github.io/styleguide/Rguide.xml#assignment ). Dies bedeutet jedoch, dass die Manipulation von data.table nicht wie die Manipulation von Datenrahmen funktioniert und daher weit davon entfernt ist, den Datenrahmen durch einen Drop-In zu ersetzen.
cmo

Antworten:

140

Ja, es ist die Unterzuweisung in R mit <-(oder =oder ->), die eine Kopie des gesamten Objekts erstellt. Sie können dies mit tracemem(DT)und .Internal(inspect(DT))wie unten verfolgen . Die data.tableFunktionen :=und set()Zuweisung unter Bezugnahme auf das Objekt, an das sie übergeben werden. Wenn dieses Objekt zuvor kopiert wurde (durch eine Unterzuweisung <-oder eine explizite Zuweisung copy(DT)), wird die Kopie durch Referenz geändert.

DT <- data.table(a = c(1, 2), b = c(11, 12)) 
newDT <- DT 

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

.Internal(inspect(newDT))   # precisely the same object at this point
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

tracemem(newDT)
# [1] "<0x0000000003b7e2a0"

newDT$b[2] <- 200
# tracemem[0000000003B7E2A0 -> 00000000040ED948]: 
# tracemem[00000000040ED948 -> 00000000040ED830]: .Call copy $<-.data.table $<- 

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),TR,ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,12
# ATTRIB:  # ..snip..

.Internal(inspect(newDT))
# @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,200
# ATTRIB:  # ..snip..

Beachten Sie, wie sogar der aVektor kopiert wurde (ein anderer Hex-Wert zeigt eine neue Kopie des Vektors an), obwohl er anicht geändert wurde. Sogar das Ganze bwurde kopiert, anstatt nur die Elemente zu ändern, die geändert werden müssen. Dies ist wichtig, um große Datenmengen zu vermeiden, und warum :=und set()wurden eingeführtdata.table .

Jetzt newDTkönnen wir mit unserer Kopie es durch Bezugnahme ändern:

newDT
#      a   b
# [1,] 1  11
# [2,] 2 200

newDT[2, b := 400]
#      a   b        # See FAQ 2.21 for why this prints newDT
# [1,] 1  11
# [2,] 2 400

.Internal(inspect(newDT))
# @0000000003D97A58 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040ED7F8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040ED8D8 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,400
# ATTRIB:  # ..snip ..

Beachten Sie, dass alle 3 Hex-Werte (der Vektor der Spaltenpunkte und jede der 2 Spalten) unverändert bleiben. Es wurde also wirklich durch Referenz ohne Kopien modifiziert.

Oder wir können das Original DTdurch Bezugnahme ändern :

DT[2, b := 600]
#      a   b
# [1,] 1  11
# [2,] 2 600

.Internal(inspect(DT))
# @0000000003B7E2A0 19 VECSXP g0c7 [OBJ,NAM(2),ATT] (len=2, tl=100)
#   @00000000040C2288 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 1,2
#   @00000000040C2250 14 REALSXP g0c2 [NAM(2)] (len=2, tl=0) 11,600
#   ATTRIB:  # ..snip..

Diese Hex-Werte sind die gleichen wie die ursprünglichen Werte, die wir DToben gesehen haben. Geben Sie example(copy)für weitere Beispiele ein tracememund vergleichen Sie mit data.frame.

Übrigens, wenn Sie tracemem(DT)dann sehen DT[2,b:=600]Sie eine Kopie gemeldet. Dies ist eine Kopie der ersten 10 Zeilen, die die printMethode ausführt. Beim Umschließen mit invisible()oder beim Aufrufen innerhalb einer Funktion oder eines Skripts wird dieprint Methode nicht aufgerufen.

All dies gilt auch für Funktionen; dh :=und set()nicht beim Schreiben kopieren, auch nicht innerhalb von Funktionen. Wenn Sie eine lokale Kopie ändern müssen, rufen Sie x=copy(x)zu Beginn der Funktion auf. Denken Sie jedoch daran, dass dies data.tablefür große Datenmengen gilt (sowie für schnellere Programmiervorteile für kleine Datenmengen). Wir wollen absichtlich (nie) keine großen Objekte kopieren. Infolgedessen müssen wir die übliche Faustregel für den 3 * Arbeitsspeicherfaktor nicht berücksichtigen. Wir versuchen, nur Arbeitsspeicher zu benötigen, der so groß wie eine Spalte ist (dh einen Arbeitsspeicherfaktor von 1 / ncol anstelle von 3).

Matt Dowle
quelle
1
Wann ist dieses Verhalten wünschenswert?
Colin
Interessanterweise tritt das Verhalten beim Kopieren des gesamten Objekts für ein data.frame-Objekt nicht auf. In einem kopierten data.frame ändert nur der Vektor, der direkt über die ->Zuweisung geändert wurde, den Speicherort. Die unveränderten Vektoren behalten den Speicherort der Vektoren des ursprünglichen Datenrahmens bei. Das data.tablehier beschriebene Verhalten von s ist das aktuelle Verhalten ab 1.12.2.
lmo
105

Nur eine kurze Zusammenfassung.

<-mit data.tableist wie Basis; Das heißt, es wird keine Kopie erstellt, bis anschließend eine Unterzuweisung durchgeführt wird <-(z. B. Ändern der Spaltennamen oder Ändern eines Elements wie DT[i,j]<-v). Dann wird eine Kopie des gesamten Objekts wie bei base erstellt. Das ist als Copy-on-Write bekannt. Wäre besser bekannt als Copy-on-Subassign, denke ich! Es wird NICHT kopiert, wenn Sie den speziellen :=Operator oder die von set*bereitgestellten Funktionen verwenden data.table. Wenn Sie große Datenmengen haben, möchten Sie diese wahrscheinlich stattdessen verwenden. :=und set*wird die data.tablenicht kopieren, auch nicht innerhalb von Funktionen.

Angesichts dieser Beispieldaten:

DT <- data.table(a=c(1,2), b=c(11,12))

Im Folgenden wird nur ein anderer Name DT2an dasselbe Datenobjekt "gebunden", das derzeit an den Namen gebunden ist DT:

DT2 <- DT

Dies kopiert niemals und auch niemals in der Basis. Es markiert nur das Datenobjekt, sodass R weiß, dass zwei verschiedene Namen ( DT2und DT) auf dasselbe Objekt verweisen. Und so muss R das Objekt kopieren, wenn beide später untergeordnet werden.

Das ist auch perfekt für data.table. Das :=ist nicht dafür. Das Folgende ist also ein absichtlicher Fehler, da :=nicht nur Objektnamen gebunden werden:

DT2 := DT    # not what := is for, not defined, gives a nice error

:=dient zur Unterzuweisung als Referenz. Aber Sie verwenden es nicht wie in der Basis:

DT[3,"foo"] := newvalue    # not like this

Sie verwenden es wie folgt:

DT[3,foo:=newvalue]    # like this

Das hat sich DTdurch Bezugnahme geändert . Angenommen, Sie fügen eine neue Spalte newunter Bezugnahme auf das Datenobjekt hinzu. Dies ist nicht erforderlich:

DT <- DT[,new:=1L]

weil sich die RHS bereits DTdurch Bezugnahme geändert hat . Das Extra DT <-ist, was falsch zu verstehen:= tut. Sie können es dort schreiben, aber es ist überflüssig.

DTwird durch Bezugnahme geändert, durch :=, AUCH IN FUNKTIONEN:

f <- function(X){
    X[,new2:=2L]
    return("something else")
}
f(DT)   # will change DT

DT2 <- DT
f(DT)   # will change both DT and DT2 (they're the same data object)

data.tableist für große Datenmengen gedacht. Wenn Sie über 20 GB data.tableSpeicher verfügen , benötigen Sie eine Möglichkeit, dies zu tun. Es ist eine sehr bewusste Designentscheidung von data.table.

Kopien können natürlich angefertigt werden. Sie müssen data.table lediglich mitteilen, dass Sie sicher sind, dass Sie Ihren 20-GB-Datensatz kopieren möchten, indem Sie die folgende copy()Funktion verwenden:

DT3 <- copy(DT)   # rather than DT3 <- DT
DT3[,new3:=3L]     # now, this just changes DT3 because it's a copy, not DT too.

Verwenden Sie keine Zuordnung oder Aktualisierung des Basistyps, um Kopien zu vermeiden:

DT$new4 <- 1L                 # will make a copy so use :=
attr(DT,"sorted") <- "a"      # will make a copy use setattr() 

Wenn Sie sicher sein möchten, dass Sie durch Referenz aktualisieren, verwenden Sie .Internal(inspect(x))die Speicheradresswerte der Bestandteile (siehe Antwort von Matthew Dowle).

Das Schreiben :=in jso können Sie durch Verweis subassign durch Gruppe . Sie können eine neue Spalte nach Referenz nach Gruppe hinzufügen. Deshalb :=wird es im Inneren so gemacht [...]:

DT[, newcol:=mean(x), by=group]
statquant
quelle