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

194

Tengo un pequeño problema para comprender las propiedades de paso por referencia de data.table. Algunas operaciones parecen "romper" la referencia, y me gustaría entender exactamente lo que está sucediendo.

Al crear un archivo data.tabledesde otro data.table(vía <-, y luego actualizar la nueva tabla :=, la tabla original también se modifica. Esto se espera, según:

?data.table::copy y stackoverflow: pase por referencia del operador en el paquete de tabla de datos

Aquí hay un ejemplo:

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

Sin embargo, si inserto una :=modificación no basada entre la <-asignación y las :=líneas anteriores, DTahora 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)
#      a  b
# [1,] 1 11
# [2,] 2 12

Entonces parece que la newDT$b[2] <- 200línea 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 asegurarme de no introducir errores potenciales en mi código.

Apreciaría mucho si alguien me pudiera explicar esto.

Peter bien
fuente
1
Acabo de descubrir esta "característica", y es horrible. Se recomienda ampliamente el uso de Internet en <-lugar de =la asignación básica en R (por ejemplo, Google: google.github.io/styleguide/Rguide.xml#assignment ). Pero esto significa que la manipulación de data.table no funcionará de la misma manera que la manipulación del marco de datos y, por lo tanto, está lejos de ser un reemplazo directo al marco de datos.
cmo

Respuestas:

141

Sí, es la subasignación en R usando <-(o =o ->) lo que hace una copia de todo el objeto. Puede rastrear eso usando tracemem(DT)y .Internal(inspect(DT)), como a continuación. Las data.tablecaracterísticas :=y set()asignar por referencia a cualquier objeto que se pasan. Entonces, si ese objeto fue copiado previamente (mediante una subasignación <-o un explícito 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 ase copió incluso el vector (un valor hexadecimal diferente indica una nueva copia del vector), aunque ano se modificó. Incluso bse copió todo, en lugar de simplemente cambiar los elementos que deben cambiarse. Eso es importante para evitar grandes datos, y por qué :=y se set()les presentó data.table.

Ahora, con nuestra copia newDTpodemos modificarla por referencia:

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

Observe que los 3 valores hexadecimales (el vector de los puntos de columna y cada una de las 2 columnas) permanecen sin cambios. Por lo tanto, fue realmente modificado por referencia sin copias en absoluto.

O podemos modificar el original DTpor referencia:

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

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

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

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

Matt Dowle
fuente
1
¿Cuándo es deseable este comportamiento?
colin
Curiosamente, el comportamiento de copiar todo el objeto no ocurre para un objeto data.frame. En un data.frame copiado, solo el vector que se cambió directamente a través de la ->asignación cambia la ubicación de la memoria. Los vectores sin cambios mantienen la ubicación de la memoria de los vectores del marco de datos original. El comportamiento de data.tables descrito aquí es el comportamiento actual a partir de 1.12.2.
lmo
105

Solo un resumen rápido.

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

Dado este ejemplo de datos:

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

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

DT2 <- DT

Esto nunca copia, y nunca copia en base tampoco. Simplemente marca el objeto de datos para que R sepa que dos nombres diferentes ( DT2y DT) apuntan al mismo objeto. Y entonces R necesitará copiar el objeto si cualquiera de los dos se asigna a continuación.

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

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

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

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

lo usas así:

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

Eso cambió DTpor referencia. Supongamos que agrega una nueva columna newpor referencia al objeto de datos, no es necesario hacer esto:

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

porque el RHS ya cambió DTpor referencia. El extra DT <-es entender mal lo que :=hace. Puedes escribirlo allí, pero es superfluo.

DTse cambia por referencia, por :=, INCLUSO EN 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.tablees para grandes conjuntos de datos, recuerda. Si tiene 20 GB data.tablede memoria, 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, utilizando la copy()función:

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 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 uso de referencia .Internal(inspect(x))y observe los valores de dirección de memoria de los componentes (consulte la respuesta de Matthew Dowle).

Escribiendo :=en jel estilo le permite subassign por referencia por grupo . Puede agregar una nueva columna por referencia por grupo. Así que por eso :=se hace así dentro [...]:

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