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

```{r, echo = FALSE, message = FALSE}
require(data.table)
knitr::opts_chunk$set(
  comment = "#",
    error = FALSE,
     tidy = FALSE,
    cache = FALSE,
 collapse = TRUE)
.old.th = setDTthreads(1)
```

Cette vignette traite de l'utilisation par défaut des fonctions de transformation `melt` (du format large au long) et `dcast` (du format long à large) pour les *data.tables* ainsi que des **nouvelles fonctionnalités étendues** de transformation `melt` et `cast` sur *plusieurs colonnes* disponibles depuis la version `v1.9.6`.

***

```{r echo = FALSE}
options(with = 100L)
```

## Données

Nous chargerons les ensembles de données directement dans chaque section.

## Introduction

Les fonctions `melt` et `dcast` pour `data.table` sont respectivement utilisées pour la restructuration de large en long et de long en large des données ; les implémentations sont spécifiquement conçues pour gérer de grandes quantités de données en mémoire (par exemple 10Go).

Dans cette vignette, nous allons

1. Examiner brievement l'utilisation par défaut des fonctions de transformation `melt` et `dcast` sur les data.tables pour les convertir du format *large* au format *long* et *vice versa*

2. Examiner des scénarios où les fonctionnalités actuelles deviennent fastidieuses et inefficaces.

3. Enfin, explorer les nouvelles améliorations apportées aux méthodes `melt` et `dcast` pour les objets de type `data.table` afin de gérer plusieurs colonnes simultanément.

Les fonctionnalités étendues sont conformes à la philosophie de `data.table` qui consiste à effectuer des opérations de manière efficace et simple.

## 1. Fonctionnalité par défaut

### a) Transformation (`melt`) des colonnes dans une `data.table` (format large vers long)

Supposons que nous ayons un `data.table` (données artificielles) comme indiqué ci-dessous :

```{r}
s1 <- "family_id age_mother dob_child1 dob_child2 dob_child3
1         30 1998-11-26 2000-01-29         NA
2         27 1996-06-22         NA         NA
3         26 2002-07-11 2004-04-05 2007-09-02
4         32 2004-10-10 2009-08-27 2012-07-21
5         29 2000-12-05 2005-02-28         NA"
DT <- fread(s1)
DT

## dob signifie date de naissance.

str(DT)
```

#### - Convertir `DT` en format *long* où chaque `dob` est une observation séparée.

Nous pouvons réaliser ceci en utilisant `melt()` en spécifiant les arguments `id.vars` et `measure.vars` comme suit :

```{r}
DT.m1 = melt(DT, id.vars = c("family_id", "age_mother"),
                measure.vars = c("dob_child1", "dob_child2", "dob_child3"))
DT.m1
str(DT.m1)
```

* `measure.vars` spécifie l'ensemble des colonnes que nous souhaitons fusionner (ou combiner).

* Nous pouvons également spécifier les *indices* des colonnes au lieu de leurs *noms*.

* Par défaut, la colonne `variable` est de type `facteur`. Mettez l'argument `variable.factor` à `FALSE` si vous souhaitez retourner un vecteur de type *`caractère`* à la place.

* Par défaut, les colonnes fusionnées sont automatiquement nommées `variable` et `value`.

* `melt` préserve les attributs des colonnes.

#### - Nommez les colonnes `variable` et `value` respectivement `child` et `dob`

```{r}
DT.m1 = melt(DT, measure.vars = c("dob_child1", "dob_child2", "dob_child3"),
               variable.name = "child", value.name = "dob")
DT.m1
```

* Par défaut, lorsque l'une des variables `id.vars` ou `measure.vars` est manquante, les autres colonnes sont *automatiquement affectées* à l'argument manquant.

* Lorsque ni `id.vars` ni `measure.vars` ne sont spécifiés, comme mentionné sous `?melt`, toutes les colonnes *non*-`numériques`, `intégrales`, `logiques` seront assignées à `id.vars`.

    De plus, un message d'avertissement est émis pour mettre en évidence les colonnes qui sont automatiquement considérées comme des `id.vars`.

### b) Transformation (`dcast`) des lignes (format long au large)

Dans la section précédente, nous avons vu comment passer de la forme large à la forme longue. Dans cette section, nous verrons l'opération inverse.

#### - Comment revenir à la table de données originale `DT` à partir de `DT.m1` ?

En d'autres termes, nous aimerions collecter toutes les observations *enfants* correspondant à chaque `family_id, age_mother` dans la même ligne. Nous pouvons le faire en utilisant la fonction `dcast` comme suit :

```{r}
dcast(DT.m1, family_id + age_mother ~ child, value.var = "dob")
```

* `dcast` utilise la notation *formule* ( *formula* ). Les variables du côté gauche (*LHS*) de la formule correspondent aux variables *id* et celles sur le côté droit (*RHS*) aux variables *measure*.

* `value.var` indique la colonne à remplir lors du passage au format large.

* `dcast` essaie également de préserver les attributs du résultat dans la mesure du possible.

#### - En partant de `DT.m1`, comment obtenir le nombre d'enfants dans chaque famille ?

Vous pouvez également passer une fonction d'agrégation dans `dcast` avec l'argument `fun.aggregate`. Ceci est particulièrement essentiel lorsque la formule fournie ne permet pas d'identifier une seule observation pour chaque cellule.

```{r}
dcast(DT.m1, family_id ~ ., fun.agg = function(x) sum(!is.na(x)), value.var = "dob")
```

Voir `?dcast` pour d'autres arguments utiles et des exemples supplémentaires.

## 2. Limitations des approches actuelles `melt/dcast`

Jusqu'à présent, nous avons vu des fonctionnalités de `melt` et `dcast` qui sont implémentées efficacement pour les objets `data.table`, en utilisant la machinerie interne de `data.table` (*tri par base rapide*, *recherche binaire* etc...).

Cependant, il existe des situations où l'opération souhaitée ne s'exprime pas de manière simple. Par exemple, considérons l'objet `data.table` présenté ci-dessous :

```{r}
s2 <- "family_id age_mother dob_child1 dob_child2 dob_child3 gender_child1 gender_child2 gender_child3
1         30 1998-11-26 2000-01-29         NA             1             2            NA
2         27 1996-06-22         NA         NA             2            NA            NA
3         26 2002-07-11 2004-04-05 2007-09-02             2             2             1
4         32 2004-10-10 2009-08-27 2012-07-21             1             1             1
5         29 2000-12-05 2005-02-28         NA             2             1            NA"
DT <- fread(s2)
DT

## 1 = femme, 2 = homme
```

Et vous aimeriez combiner (avec `melt`) toutes les colonnes `dob` ensemble, ainsi que toutes les colonnes `gender` ensemble. Avec la fonctionnalité actuelle, nous pouvons faire quelque chose comme ceci :

```{r}
DT.m1 = melt(DT, id = c("family_id", "age_mother"))
DT.m1[, c("variable", "child") := tstrsplit(variable, "_", fixed = TRUE)]
DT.c1 = dcast(DT.m1, family_id + age_mother + child ~ variable, value.var = "value")
DT.c1

str(DT.c1) ## la colonne 'gender' est un type de caractère maintenant !
```

#### Problèmes

1. Ce que nous voulions faire était de combiner toutes les colonnes de type `dob` ensemble, et toutes les colonnes de type `gender` ensemble. Au lieu de cela, nous combinons tout, puis nous les scindons à nouveau. On voit aisément que c'est une approche détournée (et inefficace).

    Comme analogie, imaginez un placard avec quatre étagères de vêtements, et vous souhaitez rassembler les vêtements des étagères 1 et 2 (dans l'étagère 1), et ceux des étagères 3 et 4 (dans l'étagère 3). Ce que nous faisons, en quelque sorte, c'est de mélanger tous les vêtements ensemble, puis de les séparer à nouveau sur les étagères 1 et 3 !

2. Les colonnes à transformer (`melt`) peuvent être de types différents, comme c'est le cas ici (types `character` et `integer`). En les transformant toutes ensemble avec `melt`, les colonnes seront forcées d'être du même type, comme l'explique le message d'avertissement ci-dessus, et on le voit dans la sortie de str(DT.c1), où la colonne `gender` a été convertie en type `character`.

3. Nous générons une colonne supplémentaire en scindant la colonne variable en deux colonnes, dont l'utilité est plutôt obscure. Nous faisons cela parce que nous en avons besoin pour la transformation (`cast`) dans l'étape suivante.

4. Enfin, nous transformons le jeu de données. Mais le problème est qu'il s'agit d'une opération beaucoup plus coûteuse en calcul que *melt*. En particulier, il faut calculer l'ordre des variables dans la formule, ce qui est coûteux.

En fait, `stats::reshape` est capable d'effectuer cette opération de manière très simple. C'est une fonction extrêmement utile et souvent sous-estimée. Vous devriez vraiment l'essayer !

## 3. (nouvelle) Fonctionnalité améliorée

### a) `melt` améliorée

Puisque nous aimerions que `data.table` effectue cette opération de façon simple et efficace en utilisant la même interface, nous avons donc implémenté une *fonctionnalité additionnelle*, où nous pouvons appliquer la fonction `melt` sur plusieurs colonnes *simultanément*.

#### - Appliquer `melt` sur plusieurs colonnes simultanément

L'idée est assez simple. Nous passons une liste de colonnes à `measure.vars`, où chaque élément de la liste contient les colonnes qui doivent être combinées ensemble.

```{r}
colA = paste0("dob_child", 1:3)
colB = paste0("gender_child", 1:3)
DT.m2 = melt(DT, measure = list(colA, colB), value.name = c("dob", "gender"))
DT.m2

str(DT.m2) ## le type de col est préservé
```

* Nous pouvons supprimer la colonne `variable` si nécessaire.

* Cette fonctionnalité est entièrement implémentée en C, ce qui la rend à la fois *rapide* et *économe en mémoire* en plus d'être *simple à utiliser*.

#### - Utilisation de `patterns()`

En général, dans ce type de problème, les colonnes que l'on souhaite transformer avec `melt` peuvent être distinguées par un motif commun. Nous pouvons utiliser la fonction `patterns()`, implémentée pour faciliter cette tâche, pour fournir des expressions régulières correspondant aux colonnes à combiner ensemble. L'opération ci-dessus peut alors être réécrite comme suit :

```{r}
DT.m2 = melt(DT, measure = patterns("^dob", "^gender"), value.name = c("dob", "gender"))
DT.m2
```

#### - Utilisation de `measure()` pour spécifier `measure.vars` via un séparateur ou un motif

Si, comme dans les données ci-dessus, les colonnes d'entrée à transformer (`melt `) ont des noms réguliers, alors nous pouvons utiliser `measure`, qui permet de spécifier les colonnes à transformer via un séparateur ou une expression régulière. Par exemple, considérons les données `iris`,

```{r}
(two.iris = data.table(datasets::iris)[c(1,150)])
```

Les données iris possèdent quatre colonnes numériques avec une structure régulière : d'abord la partie de la fleur, suivie d'un point, puis le type de mesure. Pour spécifier que nous voulons transformer (`melt`) ces quatre colonnes, nous pouvons utiliser `measure` avec `sep="."` ce qui signifie utiliser `strsplit` sur tous les noms de colonnes ; les colonnes qui résultent en un nombre maximum de groupes après division seront utilisées comme `measure.vars` :

```{r}
melt(two.iris, measure.vars = measure(part, dim, sep="."))
```

Les deux premiers arguments de `measure` dans le code ci-dessus (`part` et `dim`) sont utilisés pour nommer les colonnes de sortie ; le nombre d'arguments doit être égal au nombre maximum de groupes après division avec `sep`.

Si nous voulons deux colonnes de valeurs, une pour chaque partie, nous pouvons utiliser le mot-clé spécial `value.name`, qui signifie produire une colonne de valeurs pour chaque nom unique trouvé dans ce groupe :

```{r}
melt(two.iris, measure.vars = measure(value.name, dim, sep="."))
```

En utilisant le code ci-dessus, nous obtenons une colonne de valeurs par partie de fleur. Si nous voulons une colonne de valeurs pour chaque type de mesure, nous pouvons faire

```{r}
melt(two.iris, measure.vars = measure(part, value.name, sep="."))
```

En revenant à l'exemple des données sur les familles et les enfants, nous pouvons voir une utilisation plus complexe de `measure`, impliquant une fonction utilisée pour convertir les valeurs de la chaîne `child` en entiers :

```{r}
DT.m3 = melt(DT, measure = measure(value.name, child=as.integer, sep="_child"))
DT.m3
```

Dans le code ci-dessus, nous avons utilisé `sep="_child"`, ce qui entraîne la transformation des colonnes uniquement si elle contiennent cette chaîne (six noms de colonnes séparés en deux groupes chacun). L'argument `child=as.integer` signifie que le second groupe donnera lieu à une colonne de sortie nommée `child` avec des valeurs définies en appliquant la fonction `as.integer` aux chaînes de caractères de ce groupe.

Enfin, nous considérons un exemple (emprunté au package tidyr) où nous devons définir les groupes à l'aide d'une expression régulière plutôt qu'un séparateur.

```{r}
(who <- data.table(id=1, new_sp_m5564=2, newrel_f65=3))
melt(who, measure.vars = measure(
  diagnosis, gender, ages, pattern="new_?(.*)_(.)(.*)"))
```

Lorsque vous utilisez l'argument `pattern`, il doit s'agir d'une expression régulière compatible avec Perl contenant le même nombre de groupes de capture (sous-expressions entre parenthèses) que le nombre d'autres arguments (noms de groupes). Le code ci-dessous montre comment utiliser une expression régulière plus complexe avec cinq groupes, deux colonnes de sortie numériques et une fonction de conversion de type anonyme,

```{r}
melt(who, measure.vars = measure(
  diagnosis, gender, age,
  ymin=as.numeric,
  ymax=function(y) ifelse(nzchar(y), as.numeric(y), Inf),
  pattern="new_?(.*)_(.)(([0-9]{2})([0-9]{0,2}))"
))
```

### b) `dcast` améliorée

Parfait ! Nous pouvons maintenant transformer (`melt`) plusieurs colonnes simultanément. Maintenant, étant donné le jeu de données `DT.m2`, comment pouvons-nous revenir au même format que le jeu de données avec lequel nous avons commencé ?

Si nous utilisons la fonctionnalité actuelle de `dcast`, nous devrions effectuer la transformation via `cast` deux fois et combiner les résultats. Mais c'est une fois de plus verbeux, compliqué et inefficace.

#### - Transformation (`cast`) de plusieurs `value.var`s simultanément

Nous pouvons désormais fournir **plusieurs colonnes `value.var`** à `dcast` pour les objets `data.table` directement, de sorte que les opérations soient gérées en interne de manière efficace.

```{r}
## nouvelle fonctionnalité 'cast' - plusieurs value.vars
DT.c2 = dcast(DT.m2, family_id + age_mother ~ variable, value.var = c("dob", "gender"))
DT.c2
```

* Les attributs sont préservés dans le résultat dans la mesure du possible.

* Tout est pris en charge de manière interne et efficace. En plus d'être rapide, il est également très économe en mémoire.

# 

#### Plusieurs fonctions pour `fun.aggregate` :

Vous pouvez également *plusieurs fonctions* à `fun.aggregate` dans `dcast` pour les *data.tables*. Consultez les exemples dans `?dcast` qui illustrent cette fonctionnalité.

```{r, echo=FALSE}
setDTthreads(.old.th)
```

# 

***
