Capire esattamente quando un data.table è un riferimento a(vs una copia di) un altro data.table




(2)

Sto avendo un po 'di problemi a capire le proprietà di pass-by-reference di data.table . Alcune operazioni sembrano "interrompere" il riferimento e mi piacerebbe capire esattamente cosa sta succedendo.

Creando un data.table da un altro data.table (tramite <- , quindi aggiornando la nuova tabella per := , anche la tabella originale viene modificata. Ciò è previsto, come da:

?data.table::copy e ?data.table::copy : pass-per-riferimento-l'-operatore-nella-tabella-dati-pacchetto

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 su := tra l'assegnazione <- e le righe := sopra, DT non viene più modificato:

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] <- 200 qualche modo "spezzi" il riferimento. Immagino che questo invochi una copia in qualche modo, ma vorrei capire appieno come R tratti queste operazioni, per assicurarmi di non introdurre potenziali bug nel mio codice.

Sarei molto grato se qualcuno potesse spiegarmelo.


Solo una rapida sintesi.

<- con data.table è come base; cioè, nessuna copia viene presa fino a quando una subassign viene eseguita in seguito con <- (come cambiare i nomi delle colonne o cambiare un elemento come DT[i,j]<-v ). Quindi prende una copia dell'intero oggetto proprio come base. Questo è noto come copy-on-write. Sarebbe meglio conosciuto come copy-on-sub-assegnazione, penso! NON copia quando si utilizza l'operatore speciale := , o le funzioni set* fornite da data.table . Se hai grandi dati, probabilmente preferisci usarli. := e set* NON COPY i data.table , ANCHE ENTRO data.table FUNZIONI.

Dati questi dati di esempio:

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

Il seguente "lega" un altro nome DT2 allo stesso oggetto dati associato attualmente associato al nome DT :

DT2 <- DT

Questo non copia mai, né copia mai in base. Contrassegna solo l'oggetto dati in modo che R sappia che due nomi diversi ( DT2 e DT ) puntano allo stesso oggetto. E così R dovrà copiare l'oggetto se uno dei due viene assegnato in seguito.

È perfetto anche per data.table . Il := non è per farlo. Quindi il seguente è un errore intenzionale come := non è solo per i nomi di oggetti vincolanti:

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

:= è per subassegnare per riferimento. Ma tu non lo usi come faresti in base:

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

lo usi in questo modo:

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

Questo ha cambiato DT per riferimento. Supponiamo che tu aggiunga una nuova colonna in base al riferimento all'oggetto dati, non è necessario eseguire questa operazione:

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

perché l'RHS ha già cambiato DT per riferimento. Il DT <- supplementare DT <- sta fraintendendo cosa := fa. Puoi scriverlo lì, ma è superfluo.

DT è cambiato per riferimento, da := , EVEN WITHIN FUNCTIONS:

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 dataset di grandi dimensioni, ricorda. Se si dispone di un data.table 20 data.table in memoria, è necessario un modo per farlo. È una decisione progettuale molto deliberata di data.table .

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

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 del tipo di base o l'aggiornamento:

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

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

Scrittura := in j come questo ti consente di eseguire l'assegnazione secondaria per riferimento per gruppo . È possibile aggiungere una nuova colonna per riferimento per gruppo. Ecco perché := è fatto in questo modo all'interno [...] :

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

Sì, è la sottoassegnazione in R usando <- (o = o -> ) che crea una copia dell'intero oggetto. Puoi rintracciarlo usando tracemem(DT) e .Internal(inspect(DT)) , come sotto. Le caratteristiche data.table := e set() assegnano per riferimento a qualunque oggetto siano passati. Quindi se quell'oggetto è stato precedentemente copiato (con una subassegnazione <- o una copy(DT) esplicita 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..

Si noti come è stato copiato persino a vettore (un diverso valore esadecimale indica una nuova copia del vettore), anche se non è stato modificato. Anche l'insieme di b stato copiato, piuttosto che semplicemente cambiando gli elementi che devono essere modificati. Questo è importante da evitare per i dati di grandi dimensioni e perché := e set() sono stati introdotti in data.table .

Ora, con il nostro newDT copiato possiamo modificarlo per 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 della colonna e ciascuna delle 2 colonne) rimangono invariati. Quindi è stato veramente modificato per riferimento senza copie.

Oppure, possiamo modificare il DT originale per 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 per DT sopra. Digitare example(copy) per altri esempi usando tracemem e confronto con data.frame .

A proposito, se si tracemem(DT) poi DT[2,b:=600] vedrete una copia riportata. Questa è una copia delle prime 10 righe del metodo di print . Se avvolto con invisible() o quando chiamato all'interno di una funzione o di uno script, il metodo di print non viene chiamato.

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





data.table