Comprender exactamente cuándo una tabla de datos es una referencia a (frente a una copia de) otra tabla de datos.
Tengo algunos problemas para comprender las propiedades de paso por referencia de data.table
. Algunas operaciones parecen "romper" la referencia y me gustaría entender exactamente qué está sucediendo.
Al crear una data.table
a partir de otra data.table
(a través de <-
y luego actualizar la nueva tabla mediante :=
, la tabla original también se modifica. Esto es lo esperado, según:
?data.table::copy
y stackoverflow: pasar-por-referencia-el-operador-en-el-paquete-de-tabla-de-datos
He aquí 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 <-
tarea y las :=
líneas anteriores, DT
ahora 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] <- 200
lí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 trata estas operaciones, para asegurarme de no introducir errores potenciales en mi código.
Agradecería mucho si alguien pudiera explicarme esto.
Sí, es una subasignación en R usando <-
(o =
o ->
) que hace una copia de todo el objeto. Puede rastrear eso usando tracemem(DT)
y .Internal(inspect(DT))
, como se muestra a continuación. Las data.table
características :=
y set()
se asignan por referencia a cualquier objeto que se les pase. Entonces, si ese objeto se copió 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 incluso a
se copió el vector (un valor hexadecimal diferente indica una nueva copia del vector), aunque a
no se cambió. Incluso se copió todo b
, en lugar de simplemente cambiar los elementos que deben cambiarse. Es importante evitarlo en el caso de datos de gran tamaño, y por qué :=
nos set()
presentaron data.table
.
Ahora, con nuestra copia newDT
podemos 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 puntos de columna y cada una de las 2 columnas) permanecen sin cambios. Así que fue realmente modificado por referencia sin ninguna copia.
O podemos modificar el original DT
por 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 DT
arriba. Escriba example(copy)
para obtener más ejemplos de uso tracemem
y comparación con data.frame
.
Por cierto, si lo hace, tracemem(DT)
verá DT[2,b:=600]
una copia informada. Esa es una copia de las primeras 10 filas que print
hace el método. Cuando se incluye invisible()
o se llama dentro de una función o secuencia de comandos, print
no se llama al método.
Todo esto también se aplica dentro de las funciones; es decir, :=
y set()
no copiar al escribir, ni siquiera dentro de funciones. Si necesita modificar una copia local, llame x=copy(x)
al inicio de la función. Pero recuerde data.table
que es para datos grandes (así como ventajas de programación más rápida para datos pequeños). Deliberadamente no queremos copiar objetos grandes (nunca). Como resultado, no necesitamos permitir la regla general habitual del factor de memoria de trabajo 3*. Intentamos necesitar sólo una memoria de trabajo del tamaño de una columna (es decir, un factor de memoria de trabajo de 1/ncol en lugar de 3).
Sólo un breve resumen.
<-
with data.table
es como la base; es decir, no se realiza ninguna copia hasta que se realiza una subasignación posterior <-
(como cambiar los nombres de las columnas o cambiar un elemento como DT[i,j]<-v
). Luego toma una copia de todo el objeto como si fuera la base. Esto se conoce como copia en escritura. ¡Creo que sería mejor conocido como copiar en subasignación! NO copia cuando usa el :=
operador especial o las set*
funciones proporcionadas por data.table
. Si tiene una gran cantidad de datos, probablemente desee utilizarlos en su lugar. :=
y set*
NO COPIARÁ el data.table
, INCLUSO DENTRO DE LAS FUNCIONES.
Dados estos datos de ejemplo:
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 al nombre DT
:
DT2 <- DT
Esto nunca copia, y tampoco copia nunca en la base. Simplemente marca el objeto de datos para que R sepa que dos nombres diferentes ( DT2
y DT
) apuntan al mismo objeto. Y entonces R necesitará copiar el objeto si alguno de ellos se subasigna posteriormente.
data.table
Eso también es perfecto para él . No :=
es para hacer eso. Entonces, lo siguiente es un error deliberado y :=
no sirve 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 base:
DT[3,"foo"] := newvalue # not like this
lo usas así:
DT[3,foo:=newvalue] # like this
Eso cambió DT
por referencia. Supongamos 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 extra DT <-
es entender mal lo que :=
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, recuerde. Si tiene 20 GB data.table
de memoria, entonces necesita una forma de hacerlo. Es una decisión de diseño muy deliberada data.table
.
Por supuesto, se pueden hacer copias. Solo necesita decirle a data.table que está seguro de que desea copiar su conjunto de datos de 20 GB, usando 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 utilice la asignación de tipo base ni actualice:
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 dirección de memoria de los componentes (consulte la respuesta de Matthew Dowle).
Escribir :=
así j
le permite subasignar por referencia por grupo . Puede agregar una nueva columna por referencia por grupo. Por eso :=
se hace así por dentro [...]
:
DT[, newcol:=mean(x), by=group]