web-dev-qa-db-fra.com

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

J'ai un peu de difficulté à comprendre les propriétés de référence par défaut 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.table À partir d'un autre data.table (Via <-, Puis de la mise à jour de la nouvelle table par :=, La table d'origine est également modifiée. selon:

?data.table::copy Et stackoverflow: passe-par-référence-l'opérateur-dans-le-paquet-table-données

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 sur := Entre l'assignation <- Et les lignes := Ci-dessus, DT n'est plus modifié:

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 la ligne newDT$b[2] <- 200 "Rompt" en quelque sorte la référence. J'imagine que cela appelle d'une manière ou d'une autre une copie, mais j'aimerais bien comprendre comment R traite ces opérations, afin de ne pas introduire de bugs potentiels dans mon code.

J'apprécierais beaucoup que quelqu'un puisse m'expliquer cela.

181
Peter Fine

Oui, c'est la sous-affectation dans R qui utilise <- (Ou = Ou ->) Qui crée une copie de l'objet entier. Vous pouvez suivre cela en utilisant tracemem(DT) et .Internal(inspect(DT)), comme ci-dessous. Les fonctions data.table:= Et set() affectent par référence à l'objet qu'ils ont été transmis. Donc, si cet objet a déjà été copié (par une sous-affectation <- Ou une copy(DT) explicite), il s'agit de la copie 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..

Notez que même le vecteur a a été copié (une valeur hexadécimale différente indique une nouvelle copie du vecteur), même si a n'a pas été modifié. Même tout b a été copié, plutôt que de changer les éléments à modifier. Il est important d'éviter les grandes données et pourquoi := Et set() ont été introduits dans data.table.

Maintenant, avec notre copie newDT, nous 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. Donc, il a é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 hex sont identiques à celles que nous avons vues pour DT ci-dessus. Tapez example(copy) pour plus d'exemples utilisant tracemem et comparant à data.frame.

Btw, si vous tracemem(DT), puis DT[2,b:=600], Vous verrez une copie signalée. Il s’agit d’une copie des 10 premières lignes de la méthode print. Lorsqu'elle est encapsulée avec invisible() ou lorsqu'elle est appelée dans une fonction ou un script, la méthode print n'est pas appelée.

Tout cela s’applique aussi à l’intérieur des fonctions; c'est-à-dire que := et set() ne copient pas en écriture, même au sein de fonctions. Si vous devez modifier une copie locale, appelez x=copy(x) au début de la fonction. Mais rappelez-vous que data.table Concerne les grandes données (ainsi que les avantages de la programmation plus rapide pour les petites données). Nous n'avons délibérément pas envie de copier des objets volumineux (jamais). En conséquence, 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 de la taille d'une colonne (c'est-à-dire un facteur de mémoire de travail de 1/ncol au lieu de 3).

134
Matt Dowle

Juste un bref résumé.

<- Avec data.table Est comme base; c'est-à-dire qu'aucune copie n'est prise jusqu'à ce qu'une sous-affectation soit effectuée par la suite avec <- (telle que la modification du nom des colonnes ou la modification d'un élément tel que DT[i,j]<-v). Ensuite, il faut une copie de l'objet entier comme une base. C'est ce qu'on appelle copie sur écriture. Serait mieux connu comme copie sur sous-affectation, je pense! Il ne copie pas lorsque vous utilisez l'opérateur spécial := Ou les fonctions set* Fournies par data.table. Si vous avez des données volumineuses, vous souhaiterez probablement les utiliser à la place. := Et set* NE COPIERONT PAS le data.table, MÊME AVEC DES FONCTIONS.

Étant donné cet exemple de données:

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

Ce qui suit vient "lier" un autre nom DT2 Au même objet de données lié actuellement au nom DT:

DT2 <- DT

Cela ne copie jamais et ne copie jamais en base non plus. Cela marque simplement l'objet de données pour que R sache que deux noms différents (DT2 Et DT) désignent le même objet. Et donc R devra copier l'objet si l'un ou l'autre est sous-assigné à après.

C'est parfait aussi pour data.table. Le := N'est pas pour cela. Donc, ce qui suit est une erreur délibérée, car := Ne concerne pas uniquement les noms d'objet liés:

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

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

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

vous l'utilisez comme ceci:

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

Cela a changé DT par référence. Supposons que vous ajoutiez une nouvelle colonne new par 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é DT par référence. Le supplément DT <- Est une mauvaise compréhension de ce que := Fait. Vous pouvez l'écrire ici, mais c'est superflu.

DT est changé par référence, par :=, 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.table Est pour les grands ensembles de données, rappelez-vous. Si vous avez 20 Go data.table En mémoire, vous avez besoin d'un moyen de le faire. C'est une décision très délibérée de la part de data.table.

Des copies peuvent être faites, bien sûr. Vous devez simplement indiquer à data.table que vous êtes sûr de vouloir copier votre jeu de données de 20 Go, à l'aide de la fonction copy():

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 ni affectation ni mise à jour de 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 de mettre à jour par référence, utilisez .Internal(inspect(x)) et regardez les valeurs d'adresse de mémoire des constituants (voir la réponse de Matthew Dowle).

Écrire := Dans j comme cela vous permet de sous-attribuer par référence par groupe. Vous pouvez ajouter une nouvelle colonne par référence par groupe. C'est pourquoi := Est ainsi réalisé dans [...]:

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