Zrozumienie, kiedy data.table jest odniesieniem do (zamiast kopii) innego data.table

194

Mam mały problem ze zrozumieniem właściwości przejścia przez odniesienie data.table. Niektóre operacje wydają się „łamać” odniesienie i chciałbym dokładnie zrozumieć, co się dzieje.

Po utworzeniu data.tablez innego data.table(poprzez <-, a następnie aktualizację nowej tabeli o :=, oryginalna tabela również ulega zmianie. Jest to oczekiwane, zgodnie z:

?data.table::copy i stackoverflow: pass-by-reference-the-operator-in-the-data-table-package

Oto przykład:

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

Jeśli jednak wstawię nieopartą :=na modyfikacji modyfikację między <-przypisaniem a :=powyższymi liniami, DTnie będzie ona już modyfikowana:

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

Wygląda więc na to, że newDT$b[2] <- 200linia w jakiś sposób „łamie” odniesienie. Domyślam się, że to w jakiś sposób wywołuje kopię, ale chciałbym w pełni zrozumieć, jak R traktuje te operacje, aby upewnić się, że nie wprowadzam potencjalnych błędów w moim kodzie.

Byłbym bardzo wdzięczny, gdyby ktoś mi to wyjaśnił.

Peter Fine
źródło
1
Właśnie odkryłem tę „funkcję” i jest to przerażające. Jest powszechnie zalecane w Internecie, aby używać <-zamiast =podstawowego zadania w R (np. Przez Google: google.github.io/styleguide/Rguide.xml#assignment ). Oznacza to jednak, że manipulowanie tabelą danych nie będzie działało w taki sam sposób, jak manipulowanie ramką danych, a zatem dalekie jest od zastąpienia ramki danymi.
cmo

Odpowiedzi:

141

Tak, to poddział w R za pomocą <-(lub =lub ->), który tworzy kopię całego obiektu. Możesz to prześledzić za pomocą tracemem(DT)i .Internal(inspect(DT)), jak poniżej. Te data.tablecechy :=i set()przypisanie przez referencję do obiektu bez względu są one przekazywane. Więc jeśli ten obiekt został wcześniej skopiowany (przez podzadanie <-lub jawnie copy(DT)), to kopia jest modyfikowana przez odwołanie.

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

Zauważ, że nawet awektor został skopiowany (inna wartość szesnastkowa oznacza nową kopię wektora), nawet jeśli anie został zmieniony. Nawet całość bzostała skopiowana, a nie tylko zmiana elementów, które należy zmienić. Jest to ważne, aby unikać dużych danych oraz dlaczego :=i set()zostały wprowadzone data.table.

Teraz za pomocą naszych skopiowanych newDTmożemy zmodyfikować go przez odniesienie:

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

Zauważ, że wszystkie 3 wartości szesnastkowe (wektor punktów kolumny i każda z 2 kolumn) pozostają niezmienione. Więc został naprawdę zmodyfikowany przez odniesienie bez żadnych kopii.

Lub możemy zmodyfikować oryginał DTprzez odniesienie:

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

Te wartości szesnastkowe są takie same, jak wartości pierwotne, które widzieliśmy DTpowyżej. Wpisz example(copy)więcej przykładów, używając tracememi porównując z data.frame.

Btw, jeśli tracemem(DT)to DT[2,b:=600]zobaczysz jeden raport zgłoszony. To jest kopia pierwszych 10 wierszy, które printrobi metoda. Kiedy owinięte invisible()lub po nazwie wewnątrz funkcji lub skrypcie printmetoda nie jest tzw.

Wszystko to dotyczy również funkcji wewnętrznych; tzn. :=i set()nie kopiuj podczas zapisu, nawet w obrębie funkcji. Jeśli musisz zmodyfikować kopię lokalną, zadzwoń x=copy(x)na początku funkcji. Pamiętaj jednak, że dotyczy data.tableto dużych danych (a także szybszych korzyści programowania dla małych danych). Celowo nie chcemy kopiować dużych obiektów (nigdy). W rezultacie nie musimy dopuszczać zwykłej reguły 3 * współczynnika pamięci operacyjnej. Staramy się potrzebować pamięci roboczej tak dużej jak jedna kolumna (tj. Współczynnik pamięci roboczej 1 / ncol zamiast 3).

Matt Dowle
źródło
1
Kiedy to zachowanie jest pożądane?
colin,
Co ciekawe, zachowanie kopiowania całego obiektu nie występuje w przypadku obiektu data.frame. W skopiowanej ramce data.frame tylko wektor, który został zmieniony bezpośrednio przez ->przypisanie, zmienia lokalizację pamięci. Niezmienione wektory utrzymują lokalizację pamięci wektorów pierwotnej ramki danych. data.tableOpisane tutaj zachowanie s jest bieżącym zachowaniem od 1.12.2.
lmo
105

Krótkie podsumowanie.

<-z data.tablejest jak baza; tzn. nie jest pobierana żadna kopia, dopóki nie zostanie wykonana podpozycja <-(np. zmiana nazw kolumn lub zmiana elementu, np. DT[i,j]<-v). Następnie pobiera kopię całego obiektu, podobnie jak baza. To się nazywa kopiowanie przy zapisie. Myślę, że byłby lepiej znany jako kopiowanie na poddziały! NIE kopiuje, gdy używasz specjalnego :=operatora lub set*funkcji udostępnianych przez data.table. Jeśli masz duże dane, prawdopodobnie zechcesz ich użyć. :=i set*NIE KOPIUJE data.table, NAWET W RAMACH FUNKCJI.

Biorąc pod uwagę przykładowe dane:

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

Poniższe po prostu „wiąże” inną nazwę DT2z tym samym obiektem danych powiązanym obecnie z tą nazwą DT:

DT2 <- DT

To nigdy nie kopiuje i nigdy też nie jest kopiowane w bazie. Po prostu zaznacza obiekt danych, aby R wiedział, że dwie różne nazwy ( DT2i DT) wskazują na ten sam obiekt. I tak R będzie musiał skopiować obiekt, jeśli któryś z nich zostanie później przypisany .

To też jest idealne dla data.table. To :=nie do tego. Poniżej znajduje się celowy błąd, który :=nie dotyczy tylko wiązania nazw obiektów:

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

:=służy do przypisywania przez odniesienie. Ale nie używasz go tak, jak w bazie:

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

używasz go w ten sposób:

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

To zmieniło się DTprzez odniesienie. Załóżmy, że dodajesz nową kolumnę newprzez odniesienie do obiektu danych, nie musisz tego robić:

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

ponieważ RHS już zmieniono DTprzez odniesienie. Dodatkową korzyścią DT <-jest niezrozumienie, co się :=dzieje. Możesz to tam napisać, ale jest to zbyteczne.

DTzmienia się przez odniesienie :=, NAWET W RAMACH FUNKCJI:

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.tablejest dla dużych zestawów danych, pamiętaj. Jeśli masz 20 GB data.tablepamięci, musisz to zrobić. To bardzo przemyślana decyzja projektowa data.table.

Oczywiście można wykonać kopie. Musisz tylko powiedzieć data.table, że na pewno chcesz skopiować zestaw danych 20 GB, korzystając z copy()funkcji:

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

Aby uniknąć kopiowania, nie używaj przypisania typu podstawowego ani aktualizacji:

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

Jeśli chcesz mieć pewność, że aktualizujesz przez referencję, skorzystaj .Internal(inspect(x))z wartości adresowych pamięci składników (patrz odpowiedź Matthew Dowle'a).

Pisząc :=w jten sposób, możesz dokonać podpozycji przez odniesienie według grupy . Możesz dodać nową kolumnę przez odniesienie według grupy. Właśnie dlatego :=odbywa się to w ten sposób [...]:

DT[, newcol:=mean(x), by=group]
statquant
źródło