210 votes

Comprendre exactement quand un data.table est une référence à (vs une copie de) un autre data.table

Je rencontre quelques difficultés à 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.

En créant une data.table à partir d'une autre data.table (via <-, puis en mettant à jour la nouvelle table par :=, la table d'origine est également modifiée. C'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        # référence, pas une copie
newDT[1, a := 100] # modifier new DT

print(DT)          # DT est aussi modifié.
#        a  b
# [1,] 100 11
# [2,]   2 12

Cependant, si j'insère une modification basée sur un non-:= entre l'affectation <- 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  # nouvelle opération
newDT[1, a := 100]

print(DT)
#      a  b
# [1,] 1 11
# [2,] 2 12

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

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

2 votes

Je viens de découvrir cette "fonctionnalité", et c'est horrifiant. Il est largement recommandé sur Internet d'utiliser <- au lieu de = pour l'assignation de base en R (par exemple, par Google: google.github.io/styleguide/Rguide.xml#assignment). Mais cela signifie que la manipulation des data.table ne fonctionnera pas de la même manière que la manipulation des data frame et n'est donc pas un remplacement standard des data frame.

147voto

Matt Dowle Points 20936

Oui, c'est une sous-attribution en R utilisant <- (ou = ou ->) qui fait une copie de l'objet entier. Vous pouvez le suivre en utilisant tracemem(DT) et .Internal(inspect(DT)), comme ci-dessous. Les fonctionnalités de data.table := et set() assignent par référence à tout objet auquel ils sont passés. Donc si cet objet a été précédemment copié (par une sous-attribution <- ou un explicite copy(DT)) alors 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))   # précisément le même objet à ce stade
# @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 vecteur a a été copié (différente valeur hexadécimale indique une nouvelle copie du vecteur), même si a n'a pas été changé. Même la totalité de b a été copiée, plutôt que de changer simplement les éléments qui nécessitent d'être changés. C'est important de l'éviter pour les grands ensembles de données, c'est pourquoi := et set() ont été introduits dans data.table.

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

newDT
#      a   b
# [1,] 1  11
# [2,] 2 200

newDT[2, b := 400]
#      a   b        # Voir la FAQ 2.21 pour savoir pourquoi cela affiche 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 ..

Remarquez que les 3 valeurs hexadécimales (le vecteur des points de colonne, et chacune des 2 colonnes) restent inchangées. Donc il a été réellement 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 originales que nous avons vues pour DT ci-dessus. Tapez example(copy) pour plus d'exemples utilisant tracemem et comparaison à data.frame.

En passant, si vous tracemem(DT) ensuite DT[2,b:=600] vous verrez une copie signalée. C'est une copie des 10 premières lignes que la méthode print fait. Lorsqu'elle est enveloppé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 ceci s'applique également à l'intérieur des fonctions ; c'est-à-dire, := et set() ne copient pas à l'écriture, même à l'intérieur des fonctions. Si vous devez modifier une copie locale, alors appelez x=copy(x) au début de la fonction. Mais, rappelez-vous que data.table est pour les grands ensembles de données (ainsi que les avantages de programmation plus rapides pour les petits ensembles de données). Nous ne voulons délibérément pas copier de grands objets (jamais). En conséquence nous n'avons pas besoin de permettre la règle d'or habituelle du facteur de mémoire de travail 3*. Nous essayons de n'avoir besoin que de la 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).

2 votes

Quand ce comportement est-il souhaitable ?

0 votes

De manière intéressante, le comportement de copier l'ensemble de l'objet ne se produit pas pour un objet data.frame. Dans un data.frame copié, seul le vecteur qui a été modifié directement via l'assignation -> change d'emplacement mémoire. Les vecteurs non modifiés conservent l'emplacement mémoire des vecteurs du data.frame original. Le comportement des data.tables décrit ici est le comportement actuel à partir de la version 1.12.2.

111voto

statquant Points 3200

Juste un petit résumé rapide.

<- avec data.table est comme base; c'est-à-dire, aucune copie n'est faite jusqu'à ce qu'une sous-assignation soit effectuée par la suite avec <- (comme changer les noms de colonnes ou changer un élément tel que DT[i,j]<-v). Ensuite, il effectue une copie de l'objet entier comme base. C'est ce qu'on appelle une copie sur écriture. Serait mieux connu comme copie sur sous-assignation, je pense! IL NE COPIE PAS lors de l'utilisation de l'opérateur spécial :=, ou des fonctions set* fournies par data.table. Si vous avez de grandes données, vous voulez probablement les utiliser à la place. := et set* ne COPIERONT PAS le data.table, MÊME À L'INTÉRIEUR DES FONCTIONS.

Étant donné cet exemple de données :

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

Le suivant "lie" simplement un autre nom DT2 au même objet de données actuellement lié au nom DT :

DT2 <- DT

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

C'est parfait pour data.table aussi. Le := n'est pas fait pour cela. Donc le suivant est une erreur délibérée car := n'est pas juste pour lier des noms d'objets :

DT2 := DT    # ce n'est pas pour cela que := est utilisé, pas défini, donne une belle erreur

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

DT[3,"foo"] := newvalue    # pas comme ça

vous l'utilisez comme ceci :

DT[3,foo:=newvalue]    # comme ça

Cela a modifié DT par référence. Disons que vous ajoutez une nouvelle colonne new par référence à l'objet de données, il n'est pas nécessaire de faire ceci :

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

parce que le RHS a déjà changé DT par référence. Le DT <- supplémentaire est une mauvaise compréhension de ce que fait :=. Vous pouvez l'écrire là, mais c'est superflu.

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

f <- function(X){
    X[,new2:=2L]
    return("quelque chose d'autre")
}
f(DT)   # changera DT

DT2 <- DT
f(DT)   # changera à la fois DT et DT2 (ils sont le même objet de données)

data.table est pour les grands ensembles de données, souvenez-vous. Si vous avez un data.table de 20 Go en mémoire alors vous avez besoin d'une façon de faire cela. C'est une décision de conception très délibérée de data.table.

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

DT3 <- copy(DT)   # plutôt que DT3 <- DT
DT3[,new3:=3L]     # maintenant, cela modifie simplement DT3 car c'est une copie, pas DT aussi.

Pour éviter les copies, n'utilisez pas d'assignation ou de mise à jour de type base :

DT$new4 <- 1L                 # fera une copie alors utilisez :=
attr(DT,"sorted") <- "a"      # fera une copie utilisez setattr() 

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

Écrire := dans j comme cela permet de sous-assigner par référence par groupe. Vous pouvez ajouter une nouvelle colonne par référence par groupe. C'est pourquoi := est fait de cette manière à l'intérieur de [...] :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X