Compreender exatamente quando um data.table é uma referência a (vs uma cópia de) outro data.table

Eu estou tendo um pouco de dificuldade em entender as propriedades de data.table de data.table do data.table . Algumas operações parecem “quebrar” a referência e eu gostaria de entender exatamente o que está acontecendo.

Ao criar um data.table partir de outro data.table (via <- , então atualizando a nova tabela por := , a tabela original também é alterada. Isso é esperado, conforme:

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

Aqui está um exemplo:

 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 

No entanto, se eu inserir uma modificação não baseada em := entre as linhas <- assignment e := acima, o DT não será mais modificado:

 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 

Assim, parece que a newDT$b[2] <- 200 , de alguma forma, ‘quebra’ a referência. Eu acho que isso invoca uma cópia de alguma forma, mas eu gostaria de entender completamente como R está tratando essas operações, para garantir que eu não introduza erros potenciais no meu código.

Eu apreciaria muito se alguém pudesse explicar isso para mim.

Sim, é subassignment em R usando < - (ou = ou -> ) que faz uma cópia do object inteiro . Você pode rastrear isso usando tracemem(DT) e .Internal(inspect(DT)) , como abaixo. Os resources do data.table := e set() atribuem por referência a qualquer object que são passados. Então, se esse object foi copiado anteriormente (por uma subtransferência < - ou uma copy(DT) explícita copy(DT) ), então é a cópia que é modificada por referência.

 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 como até mesmo o vetor a foi copiado (valor hexadecimal diferente indica uma nova cópia do vetor), mesmo que não tenha sido alterado. Até mesmo o conjunto de b foi copiado, em vez de apenas alterar os elementos que precisam ser alterados. Isso é importante para evitar dados grandes e por que := e set() foram introduzidos em data.table .

Agora, com o nosso novo newDT copiado, podemos modificá-lo por referência:

 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 todos os 3 valores hexadecimais (o vetor de pontos de coluna e cada uma das duas colunas) permanecem inalterados. Por isso, foi verdadeiramente modificado por referência, sem cópias.

Ou podemos modificar o DT original por referência:

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

Esses valores hexadecimais são os mesmos que os valores originais que vimos para o DT acima. Digite example(copy) para mais exemplos usando tracemem e compare com data.frame .

Btw, se você tracemem(DT) seguida, DT[2,b:=600] você verá uma cópia relatada. Essa é uma cópia das primeiras 10 linhas que o método de print faz. Quando empacotado com invisible() ou quando chamado dentro de uma function ou script, o método de print não é chamado.

Tudo isso se aplica também às funções internas; ou seja,: := e set() não copiam em gravação, mesmo dentro de funções. Se você precisar modificar uma cópia local, chame x=copy(x) no início da function. Mas, lembre-se de que data.table é para dados grandes (assim como vantagens de programação mais rápidas para dados pequenos). Nós deliberadamente não queremos copiar objects grandes (nunca). Como resultado, não precisamos permitir a regra usual do fator de memory de trabalho 3 *. Tentamos apenas precisar de memory de trabalho tão grande quanto uma coluna (ou seja, um fator de memory de trabalho de 1 / ncol em vez de 3).

Apenas uma rápida sum.

< - com data.table é apenas como base; ou seja, nenhuma cópia é feita até que uma sub-atribuição seja feita posteriormente com < - (como alterar os nomes das colunas ou alterar um elemento como DT[i,j]< -v ). Então, é necessária uma cópia de todo o object como se fosse uma base. Isso é conhecido como copy-on-write. Seria mais conhecido como copy-on-subassign, eu acho! NÃO copia quando você usa o operador especial := , ou as funções set* fornecidas pelo data.table . Se você tiver dados grandes, provavelmente desejará usá-los. := e set* NÃO data.table o data.table , MESMO DENTRO DAS FUNÇÕES.

Dado este exemplo de dados:

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

O seguinte apenas "liga" outro nome DT2 ao mesmo object de dados ligado atualmente vinculado ao nome DT :

 DT2 < - DT 

Isso nunca copia e nunca copia na base. Ele apenas marca o object de dados para que R saiba que dois nomes diferentes ( DT2 e DT ) apontam para o mesmo object. E assim, R precisará copiar o object se ambos forem sub - atribuídos posteriormente.

Isso é perfeito para data.table . O := não é para fazer isso. Então, o seguinte é um erro deliberado como := não é apenas para vincular nomes de objects:

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

:= é para subassigning por referência. Mas você não usa como você faria na base:

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

você usa assim:

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

Isso mudou o DT por referência. Digamos que você adicione uma nova coluna new por referência ao object de dados, não há necessidade de fazer isso:

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

porque o RHS já mudou o DT por referência. O DT < - extra DT < - é entender mal o que := faz. Você pode escrever lá, mas é supérfluo.

DT é alterado por referência, por := , ATÉ DENTRO DE FUNÇÕES:

 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 é para grandes conjuntos de dados, lembre-se. Se você tem um data de 20GB na memory, então você precisa de uma maneira de fazer isso. É uma decisão de design muito deliberada de data.table .

Cópias podem ser feitas, é claro. Você só precisa informar o data.table que você tem certeza que deseja copiar seu dataset de 20GB, usando a function 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 cópias, não use atribuição de tipo base ou atualização:

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

Se você quiser ter certeza de que está atualizando por referência use .Internal(inspect(x)) e observe os valores de endereço de memory dos constituintes (veja a resposta de Matthew Dowle).

Escrita := em j assim permite sub-atribuir por referência por grupo . Você pode adicionar uma nova coluna por referência por grupo. Então é por isso que := é feito assim dentro [...] :

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