Entendendo exatamente quando uma tabela de dados é uma referência a (contra uma cópia de) outra tabela de dados

193

Estou com problemas para entender as propriedades de passagem por referência de data.table. Algumas operações parecem 'quebrar' a referência e eu gostaria de entender exatamente o que está acontecendo.

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

?data.table::copy e stackoverflow: passa-por-referência-o-operador-na-tabela-de-dados-pacote

Aqui está um exemplo:

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

No entanto, se eu inserir uma :=modificação não baseada entre a <-atribuição e as :=linhas acima, DTagora não será mais modificada:

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

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

Eu apreciaria muito se alguém pudesse me explicar isso.

Peter Fine
fonte
1
Acabei de descobrir esse "recurso", e é horrível. É amplamente recomendado que os Internets utilizem, em <-vez de =atribuições básicas em R (por exemplo: Google: google.github.io/styleguide/Rguide.xml#assignment ). Mas isso significa que a manipulação da tabela de dados não funcionará da mesma maneira que a manipulação do quadro de dados e, portanto, está longe de ser uma substituição imediata do quadro de dados.
cmo

Respostas:

140

Sim, é a sub-atribuição em R usando <-(ou =ou ->) que faz uma cópia de todo o objeto. Você pode rastrear isso usando tracemem(DT)e .Internal(inspect(DT)), como abaixo. Os data.tablerecursos :=e set()atribuem por referência a qualquer objeto que eles são transmitidos. Portanto, se esse objeto foi copiado anteriormente (por uma sub-atribuição <-ou explícita copy(DT)), é 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é o avetor foi copiado (valor hexadecimal diferente indica nova cópia do vetor), mesmo que anão tenha sido alterado. Até o todo bfoi 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 data.table.

Agora, com nossa cópia newDT, podemos modificá-la por referência:

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

Ou podemos modificar o original DTpor referência:

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

Esses valores hexadecimais são iguais aos valores originais que vimos DTacima. Digite example(copy)para obter mais exemplos usando tracememe comparando com data.frame.

Btw, se você tracemem(DT), em seguida, DT[2,b:=600]você verá uma cópia relatou. Essa é uma cópia das 10 primeiras linhas que o printmétodo faz. Quando envolvido com invisible()ou quando chamado em uma função ou script, o printmétodo não é chamado.

Tudo isso se aplica também a funções internas; ou seja, :=e set()não copie na gravação, mesmo dentro das funções. Se você precisar modificar uma cópia local, ligue x=copy(x)no início da função. Mas lembre- data.tablese de dados grandes (além de vantagens de programação mais rápidas para dados pequenos). Deliberadamente, não queremos copiar objetos grandes (nunca). Como resultado, não precisamos permitir a regra prática usual do fator de memória de 3 *. Tentamos precisar apenas de memória de trabalho do tamanho de uma coluna (ou seja, um fator de memória de 1 / ncol em vez de 3).

Matt Dowle
fonte
1
Quando esse comportamento é desejável?
colin
Curiosamente, o comportamento de copiar o objeto inteiro não ocorre para um objeto data.frame. Em um data.frame copiado, apenas o vetor que foi alterado diretamente via ->atribuição altera o local da memória. Os vetores inalterados mantêm a localização da memória dos vetores do data.frame original. O comportamento de data.tables descrito aqui é o comportamento atual a partir de 1.12.2.
Lmo
105

Apenas uma rápida soma.

<-com data.tableé 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). Em seguida, é necessária uma cópia de todo o objeto como a base. Isso é conhecido como cópia na gravação. Seria mais conhecido como copiar na subassignação, eu acho! NÃO copia quando você usa o :=operador especial ou as set*funções fornecidas por data.table. Se você possui dados grandes, provavelmente deseja usá-los. :=e set*NÃO COPIARÁ data.table, MESMO DENTRO DAS FUNÇÕES.

Dados esses dados de exemplo:

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

O seguinte apenas "vincula" outro nome DT2ao mesmo objeto de dados vinculado atualmente vinculado ao nome DT:

DT2 <- DT

Isso nunca copia e também nunca copia na base. Apenas marca o objeto de dados para que R saiba que dois nomes diferentes ( DT2e DT) apontam para o mesmo objeto. E, portanto, R precisará copiar o objeto se um deles for subassociado posteriormente.

Isso é perfeito data.tabletambém. O :=não é para fazer isso. Portanto, o seguinte é um erro deliberado, pois :=não é apenas para vincular nomes de objetos:

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

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

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

você usa assim:

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

Isso mudou DTpor referência. Digamos que você adicione uma nova coluna newpor referência ao objeto de dados, não há necessidade de fazer isso:

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

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

DTé alterado por referência, por :=, MESMO DENTRO DAS 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 20 GB data.tablede memória, precisa de uma maneira de fazer isso. É uma decisão de design muito deliberada data.table.

Cópias podem ser feitas, é claro. Você só precisa informar ao data.table que tem certeza de que deseja copiar seu conjunto de dados de 20 GB usando a copy()função:

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 a atribuição ou atualização do tipo base:

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

Se quiser ter certeza de que está atualizando por referência, use .Internal(inspect(x))os valores de endereço de memória dos constituintes (consulte a resposta de Matthew Dowle).

Escrevendo :=em jcomo que permite subassign por referência pelo grupo . Você pode adicionar uma nova coluna por referência por grupo. É por isso que :=é feito dessa maneira por dentro [...]:

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