28 votes

Écriture de fonctions (procédures) pour les objets data.table

Dans le livre Logiciel pour l'analyse des données : Programmation avec R , John Chambers insiste sur le fait que les fonctions ne doivent généralement pas être écrites pour leur effet secondaire ; plutôt, qu'une fonction doit retourner une valeur sans modifier aucune variable dans son environnement d'appel. Inversement, l'écriture d'un bon script utilisant des objets data.table devrait spécifiquement éviter l'utilisation de l'assignation d'objet avec <- La fonction de gestion des données, généralement utilisée pour stocker le résultat d'une fonction.

Tout d'abord, une question technique. Imaginez une fonction R appelée proc1 qui accepte un data.table objet x comme argument (en plus, peut-être, d'autres paramètres). proc1 retourne NULL mais modifie x en utilisant := . D'après ce que j'ai compris, proc1 en appelant proc1(x=x1) fait une copie de x1 juste à cause de la façon dont les promesses fonctionnent. Cependant, comme démontré ci-dessous, l'objet original x1 est toujours modifié par proc1 . Pourquoi/comment est-ce le cas ?

> require(data.table)
> x1 <- CJ(1:2, 2:3)
> x1
   V1 V2
1:  1  2
2:  1  3
3:  2  2
4:  2  3
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> proc1(x1)
NULL
> x1
   V1 V2 y
1:  1  2 2
2:  1  3 3
3:  2  2 4
4:  2  3 6
> 

En outre, il semble que l'utilisation proc1(x=x1) n'est pas plus lent que d'exécuter la procédure directement sur x, ce qui indique que ma vague compréhension des promesses est erronée et qu'elles fonctionnent d'une manière pass-by-reference :

> x1 <- CJ(1:2000, 1:500)
> x1[, paste0("V",3:300) := rnorm(1:nrow(x1))]
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> system.time(proc1(x1))
   user  system elapsed 
   0.00    0.02    0.02 
> x1 <- CJ(1:2000, 1:500)
> system.time(x1[,y:= V1*V2])
   user  system elapsed 
   0.03    0.00    0.03 

Ainsi, étant donné que le fait de passer un argument data.table à une fonction n'ajoute pas de temps, il est possible d'écrire des procédures pour les objets data.table, en incorporant à la fois la vitesse de data.table et la généralisation d'une fonction. Cependant, étant donné ce que John Chambers a dit, à savoir que les fonctions ne devraient pas avoir d'effets secondaires, est-il vraiment "correct" d'écrire ce type de programmation procédurale dans R ? Pourquoi soutient-il que les effets secondaires sont "mauvais" ? Si je ne tiens pas compte de ses conseils, à quels pièges dois-je être attentif ? Que puis-je faire pour écrire de "bonnes" procédures data.table ?

19voto

flodel Points 41487

Oui, l'ajout, la modification, la suppression de colonnes dans data.table se fait par reference . En un sens, c'est un bon chose parce qu'un data.table contient généralement beaucoup de données, et il serait très coûteux en mémoire et en temps de les réaffecter toutes à chaque fois qu'une modification y est apportée. D'un autre côté, c'est un mauvais chose parce que ça va à l'encontre de la no-side-effect l'approche de programmation fonctionnelle que R tente de promouvoir en utilisant pass-by-value par défaut. Avec la programmation sans effet secondaire, il n'y a pas de quoi s'inquiéter lorsque vous appelez une fonction : vous pouvez être sûr que vos entrées ou votre environnement ne seront pas affectés, et vous pouvez vous concentrer sur la sortie de la fonction. C'est simple, donc confortable.

Bien sûr, vous pouvez ignorer les conseils de John Chambers si vous savez ce que vous faites. En ce qui concerne l'écriture de "bonnes" procédures data.tables, voici quelques règles que je prendrais en considération si j'étais vous, comme moyen de limiter la complexité et le nombre d'effets secondaires :

  • une fonction ne doit pas modifier plus d'une table, c'est-à-dire que la modification de cette table doit être le seul effet secondaire,
  • si une fonction modifie un tableau, alors ce tableau devient la sortie de la fonction. Bien sûr, vous ne voudrez pas la réaffecter : il suffit de lancer la fonction do.something.to(table) et non table <- do.something.to(table) . Si, au contraire, la fonction avait une autre sortie ("réelle"), alors, lors de l'appel de la fonction result <- do.something.to(table) il est facile d'imaginer comment vous pouvez concentrer votre attention sur la sortie et oublier que l'appel de la fonction a eu un effet secondaire sur votre tableau.

Alors que les fonctions "une sortie / aucun effet secondaire" sont la norme en R, les règles ci-dessus permettent "une sortie ou un effet secondaire". Si vous êtes d'accord sur le fait qu'un effet secondaire est d'une certaine manière une forme de sortie, vous conviendrez alors que je ne contourne pas trop les règles en m'en tenant vaguement au style de programmation fonctionnelle à une sortie de R. Autoriser les fonctions à avoir de multiples effets secondaires serait un peu plus exagéré ; non pas que vous ne puissiez pas le faire, mais j'essaierais de l'éviter si possible.

10voto

Matt Dowle Points 20936

La documentation pourrait être améliorée (les suggestions sont les bienvenues), mais voici ce qu'elle contient pour le moment. Peut-être devrait-on dire "même à l'intérieur des fonctions" ?

Sur ?":=" :

Les tableaux data.tables ne sont pas copiés lors des modifications par :=, setkey ou toute autre fonction set*. Voir copie.

DT est modifié par référence et la nouvelle valeur est renvoyée. Si vous avez besoin d'une copie, prenez d'abord une copie (en utilisant DT2=copy(DT)). Rappelez-vous que ce paquetage est destiné aux données volumineuses (de types de colonnes mixtes, avec des clés multi-colonnes) où les mises à jour par référence peuvent être beaucoup plus rapides que la copie de la table entière.

et en ?copy (mais je me rends compte que cela est confondu avec setkey) :

L'entrée est modifiée par référence, et retournée (de manière invisible) afin qu'elle puisse être utilisée dans des instructions composées ; par exemple, setkey(DT,a)[J("foo")]. Si vous Si vous avez besoin d'une copie, faites-en une d'abord (en utilisant DT2=copy(DT)). copy() peut parfois être utile avant que := ne soit utilisé pour sous-assigner une colonne par référence. référence. Voir ?copy. Notez que setattr est également dans le package bit. Les deux paquets Les deux paquets exposent simplement la fonction interne setAttrib de R au niveau C, mais diffèrent par la valeur de retour. bit::setattr renvoie NULL (de manière invisible) pour vous rappeler que la fonction est utilisée pour son effet secondaire. data.table::setattr retourne l'objet modifié (de manière invisible), pour une utilisation dans les déclarations composées.

où les deux dernières phrases sur bit::setattr se rapportent au point 2 de Flodel, de façon intéressante.

Voir aussi ces questions connexes :

Comprendre exactement quand une table de données est une référence à (vs une copie de) une autre table de données.
Passer par référence : L'opérateur := dans le paquetage data.table
table.de.données 1.8.1. : "DT1 = DT2" n'est pas la même chose que DT1 = copy(DT2) ?

J'aime beaucoup cette partie de votre question :

qui permet d'écrire des procédures pour les objets data.table, incorporant à la fois la rapidité de data.table et la généralité de d'une fonction.

Oui, c'est certainement l'une des intentions. Considérez le fonctionnement d'une base de données : de nombreux utilisateurs/programmes différents modifient par référence (insertion/mise à jour/suppression) une ou plusieurs (grandes) tables dans la base de données. Cela fonctionne bien dans le monde des bases de données, et ressemble plus à la façon de penser de data.table. D'où la vidéo de svSocket sur la page d'accueil, et le désir de insert y delete (par référence, verbe seulement, fonctions à effet de bord).

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