Comprendre exactement quand une data.table est une référence à (par rapport à une copie) d'une autre data.table

194

J'ai un peu de mal à comprendre les propriétés de passage par référence de data.table. Certaines opérations semblent «casser» la référence, et j'aimerais comprendre exactement ce qui se passe.

Lors de la création d'un à data.tablepartir d'un autre data.table(via <-, puis de la mise à jour de la nouvelle table par :=, la table d'origine est également modifiée. Ceci est attendu, selon:

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

Voici un exemple:

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

Cependant, si j'insère une :=modification non basée entre l' <-affectation et les :=lignes ci-dessus, DTelle n'est plus modifiée:

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

Il semble donc que le newDT$b[2] <- 200 ligne «casse» en quelque sorte la référence. Je suppose que cela invoque une copie d'une manière ou d'une autre, mais j'aimerais bien comprendre comment R traite ces opérations, pour m'assurer de ne pas introduire de bogues potentiels dans mon code.

J'apprécierais beaucoup si quelqu'un pouvait m'expliquer cela.

Peter Fine
la source
1
Je viens de découvrir cette "fonctionnalité", et c'est horrible. Il est largement recommandé sur les internets de l'utiliser à la <-place de =l'affectation de base dans R (par exemple par Google: google.github.io/styleguide/Rguide.xml#assignment ). Mais cela signifie que la manipulation de data.table ne fonctionnera pas de la même manière que la manipulation de trame de données et est donc loin d'être un remplacement instantané de la trame de données.
cmo

Réponses:

141

Oui, c'est la sous-affectation dans R en utilisant <-(ou =ou ->) qui fait une copie de l' objet entier . Vous pouvez tracer cela en utilisant tracemem(DT)et .Internal(inspect(DT)), comme ci-dessous. ledata.table fonctionnalités :=et les set()attribuer par référence à tout objet auquel elles sont transmises. Donc, si cet objet a été précédemment copié (par une sous-attribution <-ou un explicite copy(DT)), c'est la copie qui est modifiée par référence.

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

Remarquez comment même le a vecteur a été copié (une valeur hexadécimale différente indique une nouvelle copie du vecteur), même s'il an'a pas été modifié. Même le tout a bété copié, plutôt que de simplement changer les éléments qui doivent être modifiés. C'est important à éviter pour les données volumineuses, et pourquoi :=et set()ont été introduits data.table.

Maintenant, avec notre copié, newDTnous pouvons le modifier par référence:

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

Notez que les 3 valeurs hexadécimales (le vecteur des points de colonne et chacune des 2 colonnes) restent inchangées. Il a donc été vraiment modifié par référence sans aucune copie.

Ou, nous pouvons modifier l'original DT par référence:

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

Ces valeurs hexadécimales sont les mêmes que les valeurs d'origine que nous avons vues DTci-dessus. Tapez example(copy)pour plus d'exemples d'utilisation tracememet de comparaison avec data.frame.

Btw, si vous voyez tracemem(DT)alors DT[2,b:=600]une copie rapportée. C'est une copie des 10 premières lignes que fait la printméthode. Lorsqu'elle est encapsulée avec invisible()ou lorsqu'elle est appelée dans une fonction ou un script, la printméthode n'est pas appelée.

Tout cela s'applique également à l'intérieur des fonctions; c'est-à-dire, :=et set()ne copiez pas en écriture, même dans les fonctions. Si vous devez modifier une copie locale, appelez x=copy(x)au début de la fonction. Mais rappelez data.table- vous que c'est pour les données volumineuses (ainsi que des avantages de programmation plus rapide pour les petites données). Nous ne voulons délibérément pas copier de gros objets (jamais). Par conséquent, nous n'avons pas besoin de tenir compte de la règle empirique habituelle du facteur de mémoire de travail 3 *. Nous essayons de n'avoir besoin que d'une mémoire de travail aussi grande qu'une colonne (c'est-à-dire un facteur de mémoire de travail de 1 / ncol au lieu de 3).

Matt Dowle
la source
1
Quand ce comportement est-il souhaitable?
colin
Fait intéressant, le comportement de copie de l'objet entier ne se produit pas pour un objet data.frame. Dans un data.frame copié, seul le vecteur qui a été modifié directement via l' ->affectation change l'emplacement de la mémoire. Les vecteurs inchangés conservent l'emplacement mémoire des vecteurs du data.frame d'origine. Le comportement de data.tables décrit ici est le comportement actuel à partir du 1.12.2.
lmo
105

Juste un bref résumé.

<-avec data.tableest juste comme la base; c'est-à-dire qu'aucune copie n'est effectuée jusqu'à ce qu'une sous-attribution soit effectuée par la suite avec <-(comme changer les noms de colonne ou changer un élément tel que DT[i,j]<-v). Ensuite, il prend une copie de l'objet entier tout comme la base. C'est ce qu'on appelle la copie sur écriture. Serait mieux connu sous le nom de copie sur sous-attribution, je pense! Il NE copie PAS lorsque vous utilisez l' :=opérateur spécial ou les set*fonctions fournies par data.table. Si vous disposez de données volumineuses, vous souhaiterez probablement les utiliser à la place. :=et set*NE COPIE PAS data.table, MÊME DANS LES FONCTIONS.

Compte tenu de cet exemple de données:

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

Ce qui suit "lie" simplement un autre nom DT2au même objet de données lié actuellement au nom DT:

DT2 <- DT

Cela ne copie jamais, et jamais non plus dans la base. Il marque simplement l'objet de données afin que R sache que deux noms différents ( DT2et DT) pointent vers le même objet. Et donc R devra copier l'objet si l'un ou l'autre est sous- affecté par la suite.

C'est parfait pour data.tableaussi. Le :=n'est pas pour faire ça. Donc, ce qui suit est une erreur délibérée, :=pas seulement pour la liaison des noms d'objets:

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

:=est pour la sous- attribution par référence. Mais vous ne l'utilisez pas comme vous le feriez dans la base:

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

vous l'utilisez comme ceci:

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

Cela a changé DTpar référence. Supposons que vous ajoutiez une nouvelle colonne newpar référence à l'objet de données, il n'est pas nécessaire de le faire:

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

parce que le RHS a déjà changé DTpar référence. Le plus DT <-est de mal comprendre ce que :=fait. Vous pouvez l'écrire là-bas, mais c'est superflu.

DTest modifié par référence :=, MÊME DANS LES FONCTIONS:

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.tableest pour les grands ensembles de données, rappelez-vous. Si vous avez 20 Go data.tablede mémoire, vous avez besoin d'un moyen de le faire. C'est une décision de conception très délibérée de data.table.

Des copies peuvent être faites, bien sûr. Il vous suffit de dire à data.table que vous êtes sûr de vouloir copier votre ensemble de données de 20 Go, en utilisant la copy()fonction:

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

Pour éviter les copies, n'utilisez pas l'attribution ou la mise à jour du type de base:

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

Si vous voulez être sûr que vous mettez à jour par référence, utilisez .Internal(inspect(x))et regardez les valeurs d'adresse mémoire des constituants (voir la réponse de Matthew Dowle).

L' écriture :=dans ce jgenre vous permet subassign par référence par groupe . Vous pouvez ajouter une nouvelle colonne par référence par groupe. C'est pourquoi :=on fait de cette façon à l'intérieur [...]:

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