---
title: "Programmation avec data.table"
date: "`r Sys.Date()`"
output:
  markdown::html_format
vignette: >
  %\VignetteIndexEntry{Programmation avec data.table}
  %\VignetteEngine{knitr::knitr}
  \usepackage[utf8]{inputenc}
---

```{r init, include = FALSE}
require(data.table)
knitr::opts_chunk$set(
  comment = "#",
    error = FALSE,
     tidy = FALSE,
    cache = FALSE,
 collapse = TRUE
)
```

## Introduction

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

```{r df_print, echo=FALSE}
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
)
```

```{r subset}
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.

## Description du problème

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.

### Exemple

```{r subset_error, error=TRUE, purl=FALSE}
my_subset = function(data, col, val) {
  subset(data, col == val)
}
my_subset(iris, Species, "setosa")
```

### Approches du problème

Il existe plusieurs façons de contourner ce problème.

#### Éviter les *lazy evaluation*

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.

```{r subset_nolazy}
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.

#### Utilisation de `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.

```{r subset_parse}
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 :

- absence de validation syntaxique
- [vulnérabilité à l'injection de code](https://github.com/Rdatatable/data.table/issues/2655#issuecomment-376781159)
- existence de meilleures alternatives

Martin Machler, R Project Core Developer, [a dit](https://stackoverflow.com/a/40164111/2490497) :

> 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ître `substitute()`, `quote()`, et peut-être la puissance de l'utilisation de `do.call(substitute, ......)`.

#### Calculs sur le langage

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

```{r subset_substitute}
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](https://cran.r-project.org/doc/manuals/r-release/R-lang.html). 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.

#### Utiliser des packages tiers

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

## Programmation sur 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`](https://rdatatable.gitlab.io/data.table/library/data.table/html/substitute2.html).

### Substitution de variables et de noms

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}}$

```{r hypotenuse}
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.

```{r hypotenuse_substitute2}
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.

```{r hypotenuse_datable}
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`).

```{r hypotenuse_datable_i_j_by}
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"
  )]
```

### Remplacer des variables et des valeurs de caractères

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.

```{r rank}
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.

```{r substitute2_recursive}
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"))))
)
```

### Substituer des listes de longueur arbitraire

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

```{r splice_sd}
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.

```{r splice_tobe}
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.

```{r splice_datable}
# 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').

```{r splice_enlist}
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`.

```{r splice_not, error=TRUE, purl=FALSE}
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.

```{r splice_substitute2_not}
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`](https://rdatatable.gitlab.io/data.table/library/data.table/html/substitute2.html).

### Substitution d'une requête complexe

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

```{r complexe}
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)]
```

## Interfaces supprimées

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`

```{r old_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`

```{r old_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.

```{r old_eval}
cl = quote(
  .(Petal.Width = mean(Petal.Width), Sepal.Width = mean(Sepal.Width))
)

DT[, eval(cl)]

DT[, cl, env = list(cl = cl)]
```

```{r cleanup, echo=FALSE}
options(.opts)
registerS3method("print", "data.frame", base::print.data.frame)
```
