Comprendere esattamente quando un data.table è un riferimento a (contro una copia di) un altro data.table


194

Ho qualche problema a capire le proprietà pass-by-reference di data.table. Alcune operazioni sembrano "rompere" il riferimento e mi piacerebbe capire esattamente cosa sta succedendo.

Quando si crea un data.tableda un altro data.table(tramite <-, quindi si aggiorna la nuova tabella con :=, anche la tabella originale viene modificata. Questo è previsto, come da:

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

Ecco un esempio:

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

Tuttavia, se inserisco una :=modifica non basata tra l' <-assegnazione e le :=righe sopra, DTora non viene più modificata:

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

Quindi sembra che la newDT$b[2] <- 200linea in qualche modo "spezzi" il riferimento. Immagino che questo invochi una copia in qualche modo, ma vorrei capire appieno come R sta trattando queste operazioni, per assicurarmi di non introdurre potenziali bug nel mio codice.

Gradirei molto se qualcuno potesse spiegarmelo.


1
Ho appena scoperto questa "caratteristica", ed è orribile. È ampiamente raccomandato su Internet da utilizzare <-anziché =per un compito di base in R (ad es. Da Google: google.github.io/styleguide/Rguide.xml#assignment ). Ciò significa che la manipolazione di data.table non funzionerà allo stesso modo della manipolazione del frame di dati ed è quindi tutt'altro che una sostituzione drop-in del frame di dati.
cmo

Risposte:


141

Sì, è il sottoassegnazione in R che utilizza <-(o =o ->) a creare una copia dell'intero oggetto. Puoi rintracciarlo usando tracemem(DT)e .Internal(inspect(DT)), come sotto. Le data.tablecaratteristiche :=e set()assegnate in riferimento a qualunque oggetto vengano passate. Quindi se quell'oggetto è stato precedentemente copiato (da una sottoassegnazione <-o da un esplicito copy(DT)), allora è la copia che viene modificata per riferimento.

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

Notare come anche il avettore è stato copiato (valore esadecimale diverso indica una nuova copia del vettore), anche se anon è stato modificato. Anche l'intero è bstato copiato, piuttosto che cambiare semplicemente gli elementi che devono essere cambiati. È importante evitare dati di grandi dimensioni e perché :=e perché set()sono stati introdotti data.table.

Ora, con il nostro copiato newDT, possiamo modificarlo come riferimento:

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

Si noti che tutti e 3 i valori esadecimali (il vettore dei punti di colonna e ciascuna delle 2 colonne) rimangono invariati. Quindi è stato veramente modificato per riferimento senza alcuna copia.

Oppure, possiamo modificare l'originale DTper riferimento:

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

Quei valori esadecimali sono gli stessi dei valori originali che abbiamo visto DTsopra. Digitare example(copy)per ulteriori esempi utilizzando tracememe confronto a data.frame.

A proposito, se tracemem(DT)poi DT[2,b:=600]vedrai una copia segnalata. Questa è una copia delle prime 10 righe eseguite dal printmetodo. Se racchiuso in invisible()o quando viene chiamato all'interno di una funzione o di uno script, il printmetodo non viene chiamato.

Tutto ciò vale anche per le funzioni interne; cioè, :=e set()non copiare su scrittura, anche all'interno delle funzioni. Se è necessario modificare una copia locale, chiamare x=copy(x)all'inizio della funzione. Tuttavia, ricorda che data.tableè per dati di grandi dimensioni (oltre a vantaggi di programmazione più rapidi per dati di piccole dimensioni). Non vogliamo deliberatamente copiare oggetti di grandi dimensioni (mai). Di conseguenza non è necessario consentire la consueta regola pratica del fattore di memoria di lavoro 3 *. Cerchiamo di avere bisogno solo di una memoria di lavoro grande quanto una colonna (cioè un fattore di memoria di lavoro di 1 / ncol anziché 3).


1
Quando è desiderabile questo comportamento?
colin,

È interessante notare che il comportamento della copia dell'intero oggetto non si verifica per un oggetto data.frame. In un data.frame copiato, solo il vettore che è stato modificato direttamente tramite l' ->assegnazione cambia la posizione della memoria. I vettori invariati mantengono la posizione di memoria dei vettori del data.frame originale. Il comportamento di data.tables qui descritto è il comportamento attuale di 1.12.2.
lmo,

105

Solo una breve sintesi.

<-con data.tableè proprio come base; vale a dire, non viene eseguita alcuna copia fino a quando non viene eseguita successivamente una sottoassegnazione con <-(come la modifica dei nomi delle colonne o la modifica di un elemento come DT[i,j]<-v). Quindi prende una copia dell'intero oggetto proprio come base. Questo è noto come copia su scrittura. Penso che sarebbe meglio conosciuto come copia su subassegna! NON copia quando si utilizza l' :=operatore speciale o le set*funzioni fornite da data.table. Se disponi di dati di grandi dimensioni, probabilmente vorrai utilizzarli. :=e set*NON COPIERA ' data.table, ANCHE ENTRO LE FUNZIONI.

Dati questi dati di esempio:

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

Quanto segue "lega" un altro nome DT2allo stesso oggetto dati associato attualmente al nome DT:

DT2 <- DT

Questo non copia mai e nemmeno copia nella base. Contrassegna semplicemente l'oggetto dati in modo che R sappia che due nomi diversi ( DT2e DT) puntano allo stesso oggetto. Quindi R dovrà copiare l'oggetto se uno dei due viene successivamente assegnato .

È perfetto anche per data.tablequesto. Non :=è per quello. Quindi il seguente è un errore deliberato poiché :=non riguarda solo i nomi di oggetti vincolanti:

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

:=è per la sottoassegnazione per riferimento. Ma non lo usi come faresti nella base:

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

lo usi in questo modo:

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

Ciò è cambiato DTper riferimento. Supponi di aggiungere una nuova colonna newfacendo riferimento all'oggetto dati, non è necessario farlo:

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

perché l'RHS è già cambiato DTper riferimento. Il extra DT <-è di fraintendere cosa :=fa. Puoi scriverlo lì, ma è superfluo.

DTè cambiato per riferimento, da :=, ANCHE ENTRO LE FUNZIONI:

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.tableè per grandi set di dati, ricorda. Se hai 20 GB data.tabledi memoria, hai bisogno di un modo per farlo. È una decisione progettuale molto deliberata di data.table.

Le copie possono essere fatte, ovviamente. Devi solo dire a data.table che sei sicuro di voler copiare il tuo set di dati da 20 GB, usando la copy()funzione:

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

Per evitare copie, non utilizzare l'assegnazione o l'aggiornamento del tipo di base:

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

Se vuoi essere sicuro di aggiornare per riferimento, usa .Internal(inspect(x))i valori dell'indirizzo di memoria dei componenti (vedi la risposta di Matthew Dowle).

Scrivere :=in jquesto modo ti consente di sottoassegnare per riferimento per gruppo . È possibile aggiungere una nuova colonna per riferimento per gruppo. Ecco perché :=è fatto così dentro [...]:

DT[, newcol:=mean(x), by=group]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.