Comprender exactamente cuando un data.table es una referencia a (frente a una copia de) otro data.table

Tengo problemas para entender las propiedades de data.table de data.table . Algunas operaciones parecen ‘romper’ la referencia, y me gustaría entender exactamente lo que está sucediendo.

Al crear una data.table de data.table de otra data.table (a través de <- , y luego actualizar la nueva tabla por := , la tabla original también se modifica. Esto es esperado, de acuerdo con:

?data.table::copy y stackoverflow: pass-by-reference-the-operator-en-the-data-table-package

Aquí hay un ejemplo:

 library(data.table) DT <- data.table(a=c(1,2), b=c(11,12)) print(DT) # ab # [1,] 1 11 # [2,] 2 12 newDT <- DT # reference, not copy newDT[1, a := 100] # modify new DT print(DT) # DT is modified too. # ab # [1,] 100 11 # [2,] 2 12 

Sin embargo, si inserto una modificación que no está := entre las líneas <- asignación y := arriba, DT ya no se modifica:

 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) # ab # [1,] 1 11 # [2,] 2 12 

Entonces parece que la línea newDT$b[2] <- 200 de alguna manera ‘rompe’ la referencia. Supongo que esto invoca una copia de alguna manera, pero me gustaría entender completamente cómo R está tratando estas operaciones, para garantizar que no introduzca errores potenciales en mi código.

Apreciaría mucho si alguien pudiera explicarme esto.

Sí, es una subasignación en R usando <- (o = o -> ) que hace una copia del objeto completo . Puede rastrear eso usando tracemem(DT) y .Internal(inspect(DT)) , como se muestra a continuación. Las características de data.table := y set() asignan por referencia a cualquier objeto que se pasen. Entonces, si ese objeto fue previamente copiado (por una subasignación <- o una copy(DT) explícita copy(DT) ), entonces es la copia la que se modifica por referencia.

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

Observe cómo incluso el vector fue copiado (diferentes valores hexadecimales indican una nueva copia del vector), aunque a no se modificó. Incluso la totalidad de b se copió, en lugar de simplemente cambiar los elementos que deben cambiarse. Es importante evitar esto para datos grandes, y por qué := y set() se introdujeron en data.table .

Ahora, con nuestro newDT copiado, podemos modificarlo por referencia:

 newDT # ab # [1,] 1 11 # [2,] 2 200 newDT[2, b := 400] # ab # 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 .. 

Observe que los 3 valores hexadecimales (el vector de puntos de columna y cada una de las 2 columnas) permanecen sin cambios. Entonces fue modificado por referencia sin ninguna copia.

O bien, podemos modificar el DT original por referencia:

 DT[2, b := 600] # ab # [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.. 

Esos valores hexadecimales son los mismos que los valores originales que vimos para DT arriba. Escriba example(copy) para obtener más ejemplos utilizando tracemem y comparación con data.frame .

Por cierto, si tracemem(DT) entonces DT[2,b:=600] verá una copia informada. Esa es una copia de las primeras 10 filas que hace el método de print . Cuando se envuelve con invisible() o cuando se llama dentro de una función o script, no se llama al método de print .

Todo esto aplica dentro de las funciones también; es decir := y set() no copian en escritura, incluso dentro de funciones. Si necesita modificar una copia local, llame a x=copy(x) al inicio de la función. Pero recuerde que data.table es para datos grandes (así como también ventajas de progtwigción más rápidas para datos pequeños). Deliberadamente no queremos copiar objetos grandes (nunca). Como resultado, no es necesario que tengamos en cuenta la regla general de 3 * del factor de memoria de trabajo. Intentamos solo necesitar una memoria de trabajo tan grande como una columna (es decir, un factor de memoria de trabajo de 1 / ncol en lugar de 3).

Solo un resumen rápido.

<- con data.table es como base; es decir, no se realiza ninguna copia hasta que se realice una subasignación con <- (como cambiar los nombres de las columnas o cambiar un elemento como DT[i,j]<-v ). Luego toma una copia del objeto completo como base. Eso se conoce como copy-on-write. ¡Sería mejor conocido como copy-on-subassign, creo! NO se copia cuando utiliza el operador especial := o las funciones set* proporcionadas por data.table . Si tiene datos grandes, probablemente quiera usarlos en su lugar. := y set* NO COPIA la data.table , INCLUSO DENTRO DE FUNCIONES.

Dado este ejemplo de datos:

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

Lo siguiente simplemente "vincula" otro nombre DT2 al mismo objeto de datos vinculado actualmente vinculado al nombre DT :

 DT2 <- DT 

Esto nunca se copia, y tampoco se copia en la base tampoco. Simplemente marca el objeto de datos para que R sepa que dos nombres diferentes ( DT2 y DT ) apuntan al mismo objeto. Y entonces R tendrá que copiar el objeto si alguno de ellos se asigna posteriormente.

Eso es perfecto para data.table , también. The := no es para hacer eso. Entonces, el siguiente es un error deliberado, ya que := no es solo para los nombres de objetos vinculantes:

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

:= es para subasignar por referencia. Pero no lo usa como lo haría en la base:

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

lo usas así:

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

Eso cambió DT por referencia. Digamos que agrega una nueva columna new por referencia al objeto de datos, no es necesario hacer esto:

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

porque el RHS ya cambió DT por referencia. El DT <- adicional DT <- es malinterpretar qué := hace. Puedes escribirlo allí, pero es superfluo.

DT se cambia por referencia, por := , INCLUSO DENTRO DE FUNCIONES:

 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 es para grandes conjuntos de datos, recuerda. Si tiene una data.table de data.table 20 data.table en la memoria, entonces necesita una forma de hacerlo. Es una decisión de diseño muy deliberada de data.table .

Se pueden hacer copias, por supuesto. Solo necesita decirle a data.table que está seguro de que desea copiar su conjunto de datos de 20GB, mediante el uso de la función copy() :

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

Para evitar copias, no use la asignación o actualización del tipo de base:

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

Si desea estar seguro de que está actualizando por referencia, utilice .Internal(inspect(x)) y observe los valores de la dirección de memoria de los componentes (consulte la respuesta de Matthew Dowle).

Escritura := en j así que le permite subasignar por referencia por grupo . Puede agregar una nueva columna por referencia por grupo. Entonces esa es la razón por la cual := se hace de esa manera dentro de [...] :

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