require(data.table) knitr::opts_chunk$set( comment = "#", error = FALSE, tidy = FALSE, cache = FALSE, collapse = TRUE )
data.table
, dès ses premières versions, a permis l'utilisation des fonctions subset
et with
(ou within
) en définissant la méthode [.data.table
. subset
et with
sont des fonctions de base de R qui sont utiles pour réduire les répétitions dans le code, améliorer la lisibilité, et réduire le nombre total de caractères que l'utilisateur doit taper. Cette fonctionnalité est possible dans R grâce à une fonction unique appelée évaluation paresseuse ('lazy evaluation'). Cette fonctionnalité permet à une fonction de récupérer ses arguments, avant qu'ils ne soient évalués, et de les évaluer dans un cadre différente de celle dans laquelle ils ont été appelés. Récapitulons l'utilisation de la fonction subset
.
registerS3method("print", "data.frame", function(x, ...) { base::print.data.frame(head(x, 2L), ...) cat("...\n") invisible(x) }) .opts = options( datatable.print.topn=2L, datatable.print.nrows=20L )
subset(iris, Species == "setosa")
Ici, subset
prend le second argument et l'évalue dans le cadre du data.frame
donné comme premier argument. Cela supprime le besoin de répéter les variables, ce qui réduit le risque d'erreurs et rend le code plus lisible.
Le problème de ce type d'interface est qu'il n'est pas facile de paramétrer le code qui l'utilise. En effet, les expressions passées à ces fonctions sont substituées avant d'être évaluées.
my_subset = function(data, col, val) { subset(data, col == val) } my_subset(iris, Species, "setosa")
Il existe plusieurs façons de contourner ce problème.
La solution la plus simple est d'éviter les évaluations paresseuses ('lazy evaluation'), et de se rabattre sur des approches moins intuitives et plus sujettes aux erreurs comme df[["variable"]]
, etc.
my_subset = function(data, col, val) { data[data[[col]] == val & !is.na(data[[col]]), ] } my_subset(iris, col = "Species", val = "setosa")
Ici, nous calculons un vecteur logique de longueur nrow(iris)
, puis ce vecteur est fourni à l'argument i
de [.data.frame
pour effectuer un sous-ensemble ordinaire basé sur un "vecteur logique". Pour s'aligner avec subset()
, qui supprime aussi les NA, nous devons inclure une utilisation supplémentaire de data[[col]]
. Cela fonctionne assez bien pour cet exemple simple, mais cela manque de flexibilité, introduit des répétitions de variables, et demande à l'utilisateur de changer l'interface de la fonction pour passer le nom de la colonne comme un caractère plutôt qu'un symbole sans guillemet. Plus l'expression à paramétrer est complexe, moins cette approche est pratique.
parse
/ eval
Cette méthode est généralement préférée par les nouveaux venus dans R, car elle est peut-être la plus simple sur le plan conceptuel. Cette méthode consiste à produire l'expression requise à l'aide de la concaténation de chaînes, à l'analyser, puis à l'évaluer.
my_subset = function(data, col, val) { data = deparse(substitute(data)) col = deparse(substitute(col)) val = paste0("'", val, "'") text = paste0("subset(", data, ", ", col, " == ", val, ")") eval(parse(text = text)[[1L]]) } my_subset(iris, Species, "setosa")
Nous devons utiliser deparse(substitute(...))
pour récupérer les noms réels des objets passés à la fonction, afin de pouvoir construire l'appel à la fonction subset
en utilisant ces noms originaux. Bien que cela offre une flexibilité illimitée avec une complexité relativement faible, l'utilisation de eval(parse(...))
devrait être évitée. Les raisons principales sont les suivantes :
Martin Machler, R Project Core Developer, a dit :
Désolé, mais je ne comprends pas pourquoi tant de gens pensent qu'une chaîne de caractères est quelque chose qui peut être évalué. Il faut vraiment changer d'état d'esprit. Oubliez toutes les connexions entre les chaînes d'un côté et les expressions, les appels, l'évaluation de l'autre côté. La (possible) seule connexion est via
parse(text = ....)
et tous les bons programmeurs R devraient savoir que c'est rarement un moyen efficace ou sûr de construire des expressions (ou des appels). Apprenez plutôt à connaîtresubstitute()
,quote()
, et peut-être la puissance de l'utilisation dedo.call(substitute, ......)
.
Les fonctions mentionnées ci-dessus, ainsi que quelques autres (y compris as.call
, as.name
/as.symbol
, bquote
, et eval
), peuvent être catégorisées comme des fonctions pour calculer sur le langage, puisqu'elles opèrent sur des objets du langage (par exemple call
, name
/symbol
).
my_subset = function(data, col, val) { eval(substitute(subset(data, col == val))) } my_subset(iris, Species, "setosa")
Ici, nous avons utilisé la fonction de base R substitute
pour transformer l'appel subset(data, col = val)
en subset(iris, Species == "setosa")
en remplaçant data
, col
, et val
par leurs noms (ou valeurs) d'origine dans leur environnement parent. Les avantages de cette approche par rapport aux précédentes devraient être clairs. Notez que parce que nous opérons au niveau des objets du langage, et que nous n'avons pas à recourir à la manipulation de chaînes de caractères, nous nous référons à cela comme calcul sur le langage ('computing on the language'). Il existe un chapitre dédié au calcul sur le langage dans le Manuel du langage R. Bien qu'il ne soit pas nécessaire pour programmer sur data.table, nous encourageons les lecteurs à lire ce chapitre afin de mieux comprendre cette fonctionnalité puissante et unique du langage R.
Il existe des packages tiers qui peuvent réaliser ce que les routines de calcul du R de base sur le langage font (pryr
, lazyeval
et rlang
, pour n'en citer que quelques-uns).
Bien qu'ils puissent être utiles, nous discuterons ici d'une approche propre à data.table
.
Maintenant que nous avons établi la bonne façon de paramétrer le code qui utilise l'évaluation paresseuse ('lazy evaluation'), nous pouvons passer au sujet principal de cette vignette, la programmation sur data.table.
A partir de la version 1.15.0, data.table fournit un mécanisme robuste pour paramétrer les expressions passées aux arguments i
, j
, et by
(ou keyby
) de [.data.table
. Il est construit sur la fonction de base R substitute
, et imite son interface. Nous présentons ici substitute2
comme une version plus robuste et plus conviviale de la fonction substitute
de R de base. Pour une liste complète des différences entre base::substitute
et data.table::substitute2
, veuillez lire le manuel substitute2
.
Disons que nous voulons une fonction générale qui applique une fonction à la somme de deux arguments auxquels une autre fonction a été appliquée. Comme exemple concret, nous avons ci-dessous une fonction qui calcule la longueur de l'hypoténuse dans un triangle droit, connaissant la longueur de ses côtés.
${\displaystyle c = \sqrt{a^2 + b^2}}$
square = function(x) x^2 quote( sqrt(square(a) + square(b)) )
L'objectif est de faire en sorte que chaque nom dans l'appel ci-dessus puisse être passé en tant que paramètre.
substitute2( outer(inner(var1) + inner(var2)), env = list( outer = "sqrt", inner = "square", var1 = "a", var2 = "b" ) )
Nous pouvons voir dans la sortie que les noms des fonctions, ainsi que les noms des variables passées à ces fonctions, ont été remplacés. Nous avons utilisé substitute2
par commodité. Dans ce cas simple, le substitute
de base R aurait pu être utilisé aussi, bien qu'il aurait fallu utiliser lapply(env, as.name)
.
Maintenant, pour utiliser la substitution à l'intérieur de [.data.table
, nous n'avons pas besoin d'appeler la fonction substitute2
. Comme elle est maintenant utilisée en interne, tout ce que nous avons à faire est de fournir l'argument env
, de la même manière que nous l'avons fourni à la fonction substitute2
dans l'exemple ci-dessus. La substitution peut être appliquée aux arguments i
, j
et by
(ou keyby
) de la méthode [.data.table
. Notez que le fait de mettre l'argument verbose
à TRUE
peut être utilisé pour afficher les expressions après que la substitution ait été appliquée. Ceci est très utile pour le débogage.
Utilisons le jeu de données iris
comme démonstration. A titre d'exemple, imaginons que nous voulions calculer la Sepal.Hypotenuse
, en traitant la largeur et la longueur du sépale comme s'il s'agissait des côtés d'un triangle rectangle.
DT = as.data.table(iris) str( DT[, outer(inner(var1) + inner(var2)), env = list( outer = "sqrt", inner = "square", var1 = "Sepal.Length", var2 = "Sepal.Width" )] ) # retourner le résultat sous forme de data.table DT[, .(Species, var1, var2, out = outer(inner(var1) + inner(var2))), env = list( outer = "sqrt", inner = "square", var1 = "Sepal.Length", var2 = "Sepal.Width", out = "Sepal.Hypotenuse" )]
Dans le dernier appel, nous avons ajouté un autre paramètre, out = "Sepal.Hypotenuse"
, qui transmet le nom prévu de la colonne de sortie. Contrairement à substitute
de base R, substitute2
gérera également la substitution des noms des arguments d'appel.
La substitution fonctionne également pour i
et by
(ou keyby
).
DT[filter_col %in% filter_val, .(var1, var2, out = outer(inner(var1) + inner(var2))), by = by_col, env = list( outer = "sqrt", inner = "square", var1 = "Sepal.Length", var2 = "Sepal.Width", out = "Sepal.Hypotenuse", filter_col = "Species", filter_val = I(c("versicolor", "virginica")), by_col = "Species" )]
Dans l'exemple ci-dessus, nous avons vu une fonctionnalité pratique de substitute2
: la conversion automatique de chaînes de caractères en noms/symboles. Une question évidente se pose : que se passe-t-il si nous voulons substituer un paramètre par une valeur caractère, afin d'avoir le comportement substitute
de R de base. Nous fournissons un mécanisme pour échapper à la conversion automatique en enveloppant les éléments dans l'appel de base R I()
. La fonction I
marque un objet comme AsIs, empêchant ses arguments d'être convertis automatiquement de caractère à symbole. (Lisez la documentation ?AsIs
pour plus de détails.) Si le comportement de R de base est souhaité pour l'ensemble de l'argument env
, alors il est préférable d'envelopper l'ensemble de l'argument dans I()
. Alternativement, chaque élément de la liste peut être enveloppé dans I()
individuellement. Explorons les deux cas ci-dessous.
substitute( # comportement de base de R rank(input, ties.method = ties), env = list(input = as.name("Sepal.Width"), ties = "first") ) substitute2( # imite le comportement "substitute" de base R en utilisant "I" rank(input, ties.method = ties), env = I(list(input = as.name("Sepal.Width"), ties = "first")) ) substitute2( # seuls certains éléments de env sont utilisés "AsIs" rank(input, ties.method = ties), env = list(input = "Sepal.Width", ties = I("first")) )
Notez que la conversion s'effectue de manière récursive sur chaque élément de la liste, y compris le mécanisme d'échappement bien sûr.
substitute2( # tous sont des symboles f(v1, v2), list(v1 = "a", v2 = list("b", list("c", "d"))) ) substitute2( # 'a' et 'd' doivent rester des chaines de caractères f(v1, v2), list(v1 = I("a"), v2 = list("b", list("c", I("d")))) )
L'exemple présenté ci-dessus illustre un moyen propre et puissant de rendre votre code plus dynamique. Cependant, il existe de nombreux autres cas beaucoup plus complexes auxquels un développeur peut être confronté. Un problème courant consiste à gérer une liste d'arguments de longueur arbitraire.
Un cas d'utilisation évident pourrait être d'imiter la fonctionnalité .SD
en injectant un appel list
dans l'argument j
.
cols = c("Sepal.Length", "Sepal.Width") DT[, .SD, .SDcols = cols]
Avec le paramètre cols
, nous voudrions l'intégrer dans un appel list
, en faisant ressembler l'argument j
au code ci-dessous.
DT[, list(Sepal.Length, Sepal.Width)]
Le 'splicing' est une opération où une liste d'objets doit être intégrée dans une expression comme une séquence d'arguments à appeler. Dans R de base, le 'splicing' de cols
dans une liste
peut être réalisé en utilisant as.call(c(quote(list), lapply(cols, as.name)))
. De plus, à partir de R 4.0.0, il y a une nouvelle interface pour une telle opération dans la fonction bquote
.
Dans data.table, nous facilitons les choses en transformant automatiquement en liste une liste d'objets en un appel de liste avec ces objets. Cela signifie que tout objet list
à l'intérieur de l'argument env
list sera transformé en call
list, rendant l'API pour ce cas d'utilisation aussi simple que présenté ci-dessous.
# cela fonctionne DT[, j, env = list(j = as.list(cols)), verbose = TRUE] # cela ne fonctionnera pas #DT[, list(cols), # env = list(cols = cols)]
Il est important de fournir un appel à as.list
, plutôt qu'une simple liste, à l'intérieur de l'argument list de env
, comme le montre l'exemple ci-dessus.
Examinons plus en détail la question de l'ajout à la liste ('enlist-ing').
DT[, j, # data.table met automatiquement en liste les listes imbriquées dans des appels de liste env = list(j = as.list(cols)), verbose = TRUE] DT[, j, # transformer la liste 'j' ci-dessus en un appel de liste env = list(j = quote(list(Sepal.Length, Sepal.Width))), verbose = TRUE] DT[, j, # la même chose que ci-dessus mais accepte un vecteur de caractères env = list(j = as.call(c(quote(list), lapply(cols, as.name)))), verbose = TRUE]
Essayons maintenant de passer une liste de symboles, plutôt qu'un appel de liste à ces symboles. Nous utiliserons I()
pour échapper à la mise en liste (enlist-ing) automatique, mais comme cela désactivera aussi la conversion des caractères en symboles, nous devrons aussi utiliser as.name
.
DT[, j, # liste de symboles env = I(list(j = lapply(cols, as.name))), verbose = VRAI] DT[, j, # encore une fois de la meilleure façon, ajout automatique de la liste à l'appel de liste env = list(j = as.list(cols)), verbose = TRUE]
Notez que les deux expressions, bien qu'elles semblent visuellement identiques, ne le sont pas.
str(substitute2(j, env = I(list(j = lapply(cols, as.name))))) str(substitute2(j, env = list(j = as.list(cols))))
Pour une explication plus détaillée à ce sujet, veuillez consulter les exemples dans la documentation substitute2
.
Prenons l'exemple d'une fonction plus complexe, le calcul de la moyenne quadratique.
${\displaystyle x_{\text{RMS}}={\sqrt{{\frac{1}{n}}\left(x_{1}^{2}+x_{2}^{2}+\cdots +x_{n}^{2}\right)}}}$
Il prend un nombre arbitraire de variables en entrée, mais maintenant nous ne pouvons pas simplement ajouter (splice) une liste d'arguments dans un appel de liste parce que chacun de ces arguments doit être enveloppé dans un appel square
. Dans ce cas, nous devons faire l'opération à la main plutôt que de compter sur la transformation automatique en liste ('enlist') de data.table.
Tout d'abord, nous devons construire des appels à la fonction square
pour chacune des variables (voir inner_calls
). Ensuite, nous devons réduire la liste des appels en un seul appel, avec une séquence imbriquée d'appels +
(voir add_calls
). Enfin, nous devons substituer l'appel construit dans l'expression environnante (voir rms
).
outer = "sqrt" inner = "square" vars = c("Sepal.Length", "Sepal.Width", "Petal.Length", "Petal.Width") syms = lapply(vars, as.name) to_inner_call = function(var, fun) call(fun, var) inner_calls = lapply(syms, to_inner_call, inner) print(inner_calls) to_add_call = function(x, y) call("+", x, y) add_calls = Reduce(to_add_call, inner_calls) print(add_calls) rms = substitute2( expr = outer((add_calls) / len), env = list( outer = outer, add_calls = add_calls, len = length(vars) ) ) print(rms) str( DT[, j, env = list(j = rms)] ) # idem, mais en sautant le dernier appel à substitute2 et en utilisant directement add_calls str( DT[, outer((add_calls) / len), env = list( outer = outer, add_calls = add_calls, len = length(vars) )] ) # retourner le résultat en tant que data.table j = substitute2(j, list(j = as.list(setNames(nm = c(vars, "Species", "rms"))))) j[["rms"]] = rms print(j) DT[, j, env = list(j = j)] # ou alors : j = as.call(c( quote(list), lapply(setNames(nm = vars), as.name), list(Species = as.name("Species")), list(rms = rms) )) print(j) DT[, j, env = list(j = j)]
Dans [.data.table
, il est aussi possible d'utiliser d'autres mécanismes pour la substitution de variables ou pour passer des expressions entre guillemets. Ceux-ci incluent get
et mget
pour l'injection en ligne de variables en fournissant leurs noms sous forme de chaînes, et eval
qui indique à [.data.table
que l'expression passée en argument est une expression entre guillemets et qu'elle doit être traitée différemment. Ces interfaces doivent maintenant être considérées comme retirées et nous recommandons d'utiliser le nouvel argument env
à la place.
get
v1 = "Petal.Width" v2 = "Sepal.Width" DT[, .(total = sum(get(v1), get(v2)))] DT[, .(total = sum(v1, v2)), env = list(v1 = v1, v2 = v2)]
mget
v = c("Petal.Width", "Sepal.Width") DT[, lapply(mget(v), mean)] DT[, lapply(v, mean), env = list(v = as.list(v))] DT[, lapply(v, mean), env = list(v = as.list(setNames(nm = v)))]
eval
Au lieu d'utiliser la fonction eval
, nous pouvons fournir une expression citée dans l'élément de l'argument env
, aucun appel supplémentaire à eval
n'est alors nécessaire.
cl = quote( .(Petal.Width = mean(Petal.Width), Sepal.Width = mean(Sepal.Width)) ) DT[, eval(cl)] DT[, cl, env = list(cl = cl)]
options(.opts) registerS3method("print", "data.frame", base::print.data.frame)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.