Skip to contents

Cette vignette présente la syntaxe de data.table , sa forme générale, comment extraire les lignes, sélectionner et faire des opérations sur les colonnes, et réaliser des agrégations par groupe. Il est avantageux d’être familiarisé avec la structure de données data.frame de base du R, mais cela n’est pas essentiel pour suivre cette vignette.


Analyser des données en utilisant data.table

Les opérations concernant le traitement des données telles que subset, group, update, join, etc. sont toutes intimement liées. En regroupant ces opérations apparentées cela nous permet :

  • syntaxe concise et cohérente quel que soit l’ensemble des opérations que vous souhaitez effectuer pour atteindre votre objectif final.

  • effectuer une analyse fluide sans la charge cognitive de devoir faire correspondre chaque opération à une fonction particulière à partir d’un ensemble potentiellement énorme de fonctions disponibles avant d’effectuer l’analyse.

  • automatiquement optimiser les opérations en interne et de manière très efficace en connaissant précisément les données requises pour chaque opération, ce qui permet d’obtenir un code très rapide et efficace sur le plan de la mémoire.

En résumé, si vous souhaitez réduire drastiquement le temps de programmation et de compilation, alors ce package est fait pour vous. C’est la philosophie suivie par data.table pour rendre cela possible. Notre but est d’illustrer ceci au travers de cette série de vignettes.

Données

Dans cette vignette, nous utiliseront les données NYC-flights14 obtenues du package flights (disponible sur GitHub seulement). Il contient les horaires des vols d’avions du Bureau of Transportation Statistics à propos de tous les vols partant des aéroports de New York City en 2014 (inspiré de nycflights13). Les données ne concernent que les mois de janvier à octobre 2014.

Vous pouvez utiliser le lecteur de fichiers rapide et convivial ‘fread’ de ‘data.table’ pour charger ‘flights’ ditectement ainsi :

input <- if (file.exists("../flights14.csv")) {
   "../flights14.csv"
} else {
  "https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv"
}
flights <- fread(input)
flights
#          year month   day dep_delay arr_delay carrier origin   dest air_time distance  hour
#         <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>    <int> <int>
#      1:  2014     1     1        14        13      AA    JFK    LAX      359     2475     9
#      2:  2014     1     1        -3        13      AA    JFK    LAX      363     2475    11
#      3:  2014     1     1         2         9      AA    JFK    LAX      351     2475    19
#      4:  2014     1     1        -8       -26      AA    LGA    PBI      157     1035     7
#      5:  2014     1     1         2         1      AA    JFK    LAX      350     2475    13
#     ---                                                                                    
# 253312:  2014    10    31         1       -30      UA    LGA    IAH      201     1416    14
# 253313:  2014    10    31        -5       -14      UA    EWR    IAH      189     1400     8
# 253314:  2014    10    31        -8        16      MQ    LGA    RDU       83      431    11
# 253315:  2014    10    31        -4        15      MQ    LGA    DTW       75      502    11
# 253316:  2014    10    31        -5         1      MQ    LGA    SDF      110      659     8
dim(flights)
# [1] 253316     11

A noter : ‘fread’ accepte directement les URLS ‘http’ et ‘https’, ainsi que les commandes système opérationnelles telles que les sorties de ‘sed’ et ‘awk’. Voir ‘?fread’ pour les exemples.

Introduction

Dans cette vignette, nous allons

  1. Commencez par les bases - qu’est-ce qu’un data.table, sa forme générale, comment réaliser un sous-ensemble des lignes, comment sélectionner et effectuer des calculs sur les colonnes;

  2. Nous verrons ensuite comment effectuer des agrégations de données par groupe

1. Les bases

a) ‘data.table’ c’est quoi ?

‘data.table’ est un package R qui fournit une version étendue d’un ‘data.frame’, qui est la structure de données standard pour stocker des données dans la ‘base’ R. Dans la Data section ci-dessus, nous avons vu comment créer une ‘data.table’ avec ‘fread()’, mais on peut aussi en créer une en utilisant la fonction ‘data.table()’ . Voici un exemple :

DT = data.table(
  ID = c("b","b","b","a","a","c"),
  a = 1:6,
  b = 7:12,
  c = 13:18
)
DT
#        ID     a     b     c
#    <char> <int> <int> <int>
# 1:      b     1     7    13
# 2:      b     2     8    14
# 3:      b     3     9    15
# 4:      a     4    10    16
# 5:      a     5    11    17
# 6:      c     6    12    18
class(DT$ID)
# [1] "character"

Vous pouvez aussi convertir des objets existants en une data.table en utilisant setDT() (pour les structures data.frame et list) ou as.data.table() (pour les autres structures). Pour les autres détails concernant les différences (ce qui est hors du champ de cette vignette), voir ?setDT et ?as.data.table.

Notez que :

  • Les numéros de ligne sont imprimés avec un : afin de séparer visuellement le numéro de ligne de la première colonne.

  • Lorsque le nombre de lignes à imprimer dépasse l’option globale datatable.print.nrows (défaut = 100), il n’imprime automatiquement que les 5 premières et les 5 dernières lignes (comme on peut le voir dans la section Data). Pour un grand data.frame, vous avez pu vous retrouver à attendre que des tables plus grandes s’impriment et se mettent en page, parfois sans fin. Cette restriction permet d’y remédier, et vous pouvez demander le nombre par défaut de la façon suivante :

    getOption("datatable.print.nrows")
  • data.table ne définit ni n’utilise jamais de nom de ligne. Nous verrons pourquoi dans la vignette “Sous-ensemble basé sur des clés et recherche binaire rapide”.

b) Forme générale - dans quel sens la ‘data.table’ est-elle étendue ?

Par rapport à un data.frame, vous pouvez faire beaucoup plus de choses qu’extraire des lignes et sélectionner des colonnes dans la structure d’une data.table, par exemple, avec [ ... ] (Notez bien : nous pourrions aussi faire référence à écrire quelque chose dans DT[...] comme “interroger DT”, par analogie ou similairement à SQL). Pour le comprendre il faut d’abord que nous regardions la forme générale de la syntaxe data.table, comme indiqué ci-dessous :

DT[i, j, by]

##   R:                 i                 j        by
## SQL:  where | order by   select | update  group by

Les utilisateurs ayant des connaissances SQL feront peut être directement le lien avec cette syntaxe.

La manière de le lire (à haute voix) est :

Utiliser DT, extraire ou trier les lignes en utilisant i, puis calculer j, grouper avec by.

Commençons par voir ‘i’ et ‘j’ d’abord - en indiçant les lignes et en travaillant sur les colonnes.

c) Regrouper les lignes en ‘i’

– Obtenir tous les vols qui ont “JFK” comme aéroport de départ pendant le mois de juin.

ans <- flights[origin == "JFK" & month == 6L]
head(ans)
#     year month   day dep_delay arr_delay carrier origin   dest air_time distance  hour
#    <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>    <int> <int>
# 1:  2014     6     1        -9        -5      AA    JFK    LAX      324     2475     8
# 2:  2014     6     1       -10       -13      AA    JFK    LAX      329     2475    12
# 3:  2014     6     1        18        -1      AA    JFK    LAX      326     2475     7
# 4:  2014     6     1        -6       -16      AA    JFK    LAX      320     2475    10
# 5:  2014     6     1        -4       -45      AA    JFK    LAX      326     2475    18
# 6:  2014     6     1        -6       -23      AA    JFK    LAX      329     2475    14
  • Dans le cadre d’un data.table, on peut se référer aux colonnes comme s’il s’agissait de variables, un peu comme dans SQL ou Stata. Par conséquent, nous nous référons simplement à origin et month comme s’il s’agissait de variables. Nous n’avons pas besoin d’ajouter le préfixe vol$ à chaque fois. Néanmoins, l’utilisation de flights$origin et flights$month fonctionnerait parfaitement.

  • Les indices de ligne qui satisfont la condition origin == "JFK" & month == 6L sont calculés, et puisqu’il n’y a rien d’autre à faire, toutes les colonnes de flights aux lignes correspondant à ces indices de ligne sont simplement renvoyées sous forme d’un data.table.

  • Une virgule après la condition dans i n’est pas nécessaire. Mais flights[origin == "JFK" & month == 6L, ] fonctionnerait parfaitement. Avec un data.frame, cependant, la virgule est indispensable.

– Récupérer les deux premières lignes de flights.

ans <- flights[1:2]
ans
#     year month   day dep_delay arr_delay carrier origin   dest air_time distance  hour
#    <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>    <int> <int>
# 1:  2014     1     1        14        13      AA    JFK    LAX      359     2475     9
# 2:  2014     1     1        -3        13      AA    JFK    LAX      363     2475    11
  • Dans ce cas, il n’y a pas de condition. Les indices des lignes sont déjà fournis dans i. Nous retournons donc un data.table avec toutes les colonnes de flights aux lignes pour ces index de ligne.

– Trier flights d’abord sur la colonne origin dans l’ordre ascending, puis par dest dans l’ordre descendant :

Nous pouvons utiliser la fonction R ‘order()’ pour faire cela.

ans <- flights[order(origin, -dest)]
head(ans)
#     year month   day dep_delay arr_delay carrier origin   dest air_time distance  hour
#    <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>    <int> <int>
# 1:  2014     1     5         6        49      EV    EWR    XNA      195     1131     8
# 2:  2014     1     6         7        13      EV    EWR    XNA      190     1131     8
# 3:  2014     1     7        -6       -13      EV    EWR    XNA      179     1131     8
# 4:  2014     1     8        -7       -12      EV    EWR    XNA      184     1131     8
# 5:  2014     1     9        16         7      EV    EWR    XNA      181     1131     8
# 6:  2014     1    13        66        66      EV    EWR    XNA      188     1131     9

order() est optimisé en interne

  • Nous pouvons utiliser “-” sur les colonnes character dans le cadre d’un data.table pour trier par ordre décroissant.

  • De plus, order(...) dans le cadre d’un data.table utilise l’ordre radix rapide interne de data.table forder(). Ce tri a apporté une telle amélioration par rapport à base::order de R que le projet R a adopté l’algorithme data.table comme tri par défaut en 2016 pour R 3.3.0 (pour référence, voir ?sort et les R Release NEWS).

Nous discuterons de l’ordonnancement rapide de la data.table plus en détails dans la vignette fonctionnement interne de data.table internals.

d) Sélection de colonne(s) dans j

– Sélectionner la colonne arr_delay, mais la renvoyer en tant que vector.

ans <- flights[, arr_delay]
head(ans)
# [1]  13  13   9 -26   1   0
  • Puisque les colonnes peuvent être appelées comme si elles étaient des variables dans le cadre d’un data.table, nous nous référons directement à la variable dont nous voulons créer un sous-ensemble. Puisque nous voulons toutes les lignes, nous sautons simplement i.

  • Il renvoie toutes les lignes de la colonne arr_delay.

– Sélectionner la colonne arr_delay, mais la renvoyer en tant que data.table.

ans <- flights[, list(arr_delay)]
head(ans)
#    arr_delay
#        <int>
# 1:        13
# 2:        13
# 3:         9
# 4:       -26
# 5:         1
# 6:         0
  • Nous enveloppons les variables (noms de colonnes) dans list(), ce qui assure qu’un data.table est retourné. Dans le cas d’un seul nom de colonne, le fait de ne pas utiliser list() renvoie un vecteur, comme on peut le voir dans l’exemple précédent](#select-j-1d).

  • data.table permet aussi d’envelopper les colonnes avec .() au lieu de list(). C’est un alias de list() ; les deux signifient la même chose. N’hésitez pas à utiliser ce que vous préférez ; nous avons remarqué que la plupart des utilisateurs semblent préférer .() pour la concision, donc nous continuerons à utiliser .() par la suite.

Un data.table (et également un data.frame) est aussi en interne une list , avec la caractéristique que chaque élément a la même longueur et que la list possède un attribut class. En permettant à j de renvoyer une list cela permet de convertir et de renvoyer des data.table très efficacement.

Conseil :

Tant que j-expression renvoie une list, chaque élément de la liste sera converti en colonne dans la data.table résultante. Ce qui fait que j est très puissant, comme nous le verrons bientôt. Il est aussi très important de comprendre cela dans le cas où vous auriez à faire des requêtes plus compliquées !!

– Sélectionner à la fois les colonnes arr_delay et dep_delay.

ans <- flights[, .(arr_delay, dep_delay)]
head(ans)
#    arr_delay dep_delay
#        <int>     <int>
# 1:        13        14
# 2:        13        -3
# 3:         9         2
# 4:       -26        -8
# 5:         1         2
# 6:         0         4

## forme alternative
# ans <- flights[, list(arr_delay, dep_delay)]
  • Enveloppez les deux colonnes dans .(), ou list(). C’est tout.

– Sélectionner à la fois les colonnes arr_delay et dep_delay et les renommer en delay_arr et delay_dep.

Comme .() est juste un alias pour list(), nous pouvons donner un nom quelconque aux colonnes comme si on créait une list.

ans <- flights[, .(delay_arr = arr_delay, delay_dep = dep_delay)]
head(ans)
#    delay_arr delay_dep
#        <int>     <int>
# 1:        13        14
# 2:        13        -3
# 3:         9         2
# 4:       -26        -8
# 5:         1         2
# 6:         0         4

e) Calcul ou do dans ‘j’

– Combien de voyages on eu un retard total < 0 ?

ans <- flights[, sum( (arr_delay + dep_delay) < 0 )]
ans
# [1] 141814

Que se passe-t-il dans ce cas ?

  • Le j de data.table peut gérer plus que la sélection de colonnes - il peut gérer des expressions, c’est-à-dire calculer sur des colonnes. Cela ne devrait pas être surprenant, car on peut se référer aux colonnes comme si elles étaient des variables. Nous devrions donc pouvoir calculer en appelant des fonctions sur ces variables. Et c’est précisément ce qui se passe ici.

f) Sous-ensemble de i et do dans j

– Calculer le nombre moyen de retards des arrivées et des départs pour tous les vols au départ de l’aéroport “JFK” pendant le mois de juin.

ans <- flights[origin == "JFK" & month == 6L,
               .(m_arr = mean(arr_delay), m_dep = mean(dep_delay))]
ans
#       m_arr    m_dep
#       <num>    <num>
# 1: 5.839349 9.807884
  • Nous commençons par effectuer un sous-ensemble dans i pour trouver les indices de ligne correspondants à origin égal à l’aéroport "JFK", et où le mois est égal à 6L. Nous n’effectuons pas encore le sous-ensemble de toutes les data.table correspondant à ces lignes.

  • Maintenant, nous regardons j et nous constatons qu’il n’utilise que deux colonnes. Et ce que nous devons faire, c’est calculer leur moyenne avec mean(). Par conséquent, nous regroupons uniquement les colonnes d’intérêt aux lignes correspondantes, et nous calculons leurs moyennes.

Parce que les trois composants principaux de la requête (i, j et by) figurent ensemble dans [...], data.table peut les voir tous trois et optimiser la requête dans sa totalité avant l’évaluation, plutôt que d’optimiser chacun séparément. Par conséquent nous pouvons éviter le sous-ensemble complet (par exemple trier les colonnes annexes arr_delay et dep_delay), pour la rapidité et l’efficacité de la mémoire.

– Combien de voyages ont été réalisés en 2014 au départ de l’aéroport “JFK” au mois de juin ?

ans <- flights[origin == "JFK" & month == 6L, length(dest)]
ans
# [1] 8422

La fonction length() nécessite un argument d’entrée. Il suffit juste de calculer le nombre de lignes du sous-ensemble. On aurait pu utiliser n’importe quelle colonne comme argument d’entrée de length(). Cette approche est une réminiscence de SELECT COUNT(dest) FROM flights WHERE origin = 'JFK' AND month = 6 en SQL.

Ce type d’opération arrive assez fréquement, particulièrement lors des regroupements (comme nous le verrons dans la section suivante), au point que data.table fournit un symbole spécial .N pour cela.

g) Gérer les éléments absents dans i

– Que se passe-t-il quand on interroge des éléments non-existants ?

Lorsque vous interrogez une data.table pour des éléments qui n’existent pas, le comportement dépend de la méthode utilisée.

setkeyv(flights, "origin")
  • Sous-ensemble basé sur les clés : dt["d"]

Ceci réalise une jointure parfaite sur la colonne clé x, fournissant une rangée avec d et NA pour les colonnes absentes. En utilisant setkeyv, la table est triée en fonction des clés fournies et un index interne est créé, permettant une recherche binaire et des performances optimisées.

flights["XYZ"]
# Retourne:
#    origin year month day dep_time sched_dep_time dep_delay arr_time sched_arr_time arr_delay carrier flight tailnum ...
# 1:    XYZ   NA    NA  NA       NA             NA        NA       NA             NA        NA      NA     NA      NA ...
  • Sous-ensemble logique : dt[x == "d"]

Ceci réalise une opération standard de sous-ensemble qui ne trouve aucune correspondance de lignes et donc renvoie une data.table vide.

  flights[origin == "XYZ"]
# Retourne:
# Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,...
  • Correspondance exacte en utilisant nomatch=NULL

Pour une correspondance stricte sans NA pour les éléments absents, utiliser nomatch=NULL :

flights["XYZ", nomatch=NULL]
# Retourne:
# Empty data.table (0 rows and 19 cols): year,month,day,dep_time,sched_dep_time,dep_delay,arr_time,sched_arr_time,arr_delay,...

En assimilant ces comportements, cela vous ôtera toute confusion lorsque vous trouverez des éléments absents parmi vos données.

Symbol spécial .N:

.N est une variable interne spéciale qui contient le nombre d’observations dans le groupe actuel. Elle est particulièrement utile combinée avec by comme nous le verrons dans la prochaine section. S’il n’y a pas de groupe pour les opérations, le nombre de lignes dans le sous-ensemble sera simplement renvoyé.

Maintenant que nous savons, nous pouvons accomplir la même tâche en utilisant .N ainsi :

ans <- flights[origin == "JFK" & month == 6L, .N]
ans
# [1] 8422
  • Une fois de plus, nous introduisons i pour obtenir les indices de lignes pour lesquels l’aéroport origin est “JFK”, et le mois est 6.

  • Nous voyons que j n’utilise que .N et aucune autre colonne. Par conséquent, le sous-ensemble complet n’est pas matérialisé. Nous renvoyons simplement le nombre de lignes dans le sous-ensemble (qui est juste la longueur des indices de ligne).

  • Notez que nous n’avons pas enveloppé .N avec list() ou .(). Par conséquent, un vecteur est retourné.

On aurait pu faire la même opération en écrivant nrow(flights[origin == "JFK" & month == 6L]). Néanmoins il aurait fallu d’abord dissocier la data.table entière en fonction des indices de lignes dans i puis renvoyer les lignes en utilisant nrow(), ce qui est inutile et pas efficace. Nous aborderons en détails ce sujet et d’autres aspects de l’optimisation dans la vignette architecture de data.table.

h) Super ! Mais comment référencer les colonnes par nom dans j (comme avec un data.frame) ?

Si vous imprimez le nom des colonnes explicitement, il n’y a pas de différence avec un data.frame (depuis v1.9.8).

– Sélectionner simultanément les colonnes arr_delay et dep_delay à la manière d’un data.frame.

ans <- flights[, c("arr_delay", "dep_delay")]
head(ans)
#    arr_delay dep_delay
#        <int>     <int>
# 1:        13        14
# 2:        13        -3
# 3:         9         2
# 4:       -26        -8
# 5:         1         2
# 6:         0         4

Si vous avez stocké les colonnes souhaitées dans un vecteur de caractères, il y a deux options : utiliser le préfixe .. , ou utiliser l’argument with.

– Sélectionnez les colonnes nommées dans une variable en utilisant le préfixe ..

select_cols = c("arr_delay", "dep_delay")
flights[ , ..select_cols]
#         arr_delay dep_delay
#             <int>     <int>
#      1:        13        14
#      2:        13        -3
#      3:         9         2
#      4:       -26        -8
#      5:         1         2
#     ---                    
# 253312:       -30         1
# 253313:       -14        -5
# 253314:        16        -8
# 253315:        15        -4
# 253316:         1        -5

Pour les habitués du terminal Unix, le préfixe .. devrait rappeler la commande de “remontée d’un niveau”, qui est analogue à ce qui se passe ici – le .. demande à data.table de chercher la variable select_cols “un nivau au-dessus”, c’est à dire dans ce cas, dans l’envronnement global.

– Sélectionner les colonnes nommées dans une variable en utilisant with = FALSE

flights[ , select_cols, with = FALSE]
#         arr_delay dep_delay
#             <int>     <int>
#      1:        13        14
#      2:        13        -3
#      3:         9         2
#      4:       -26        -8
#      5:         1         2
#     ---                    
# 253312:       -30         1
# 253313:       -14        -5
# 253314:        16        -8
# 253315:        15        -4
# 253316:         1        -5

L’argument s’appelle with d’après la fonction R with() à cause de la fonctionnalité similaire. Supposez que vous ayiez une data.frame DF et que vous vouliez dissocier toutes les lignes où x > 1. Dans la base R vous pouvez écrire :

DF = data.frame(x = c(1,1,1,2,2,3,3,3), y = 1:8)

## (1) méthode classique
DF[DF$x > 1, ] # data.frame needs that ',' as well
#   x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8

## (2) en utilisant with
DF[with(DF, x > 1), ]
#   x y
# 4 2 4
# 5 2 5
# 6 3 6
# 7 3 7
# 8 3 8
  • L’utilisation de with() dans (2) permet d’utiliser la colonne x de DF comme s’il s’agissait d’une variable.

    D’où le nom de l’argument with dans data.table. Mettre with = FALSE désactive la possibilité de se référer aux colonnes comme si elles étaient des variables, restaurant ainsi le « mode data.frame ».

  • Nous pouvons également désélectionner des colonnes en utilisant - ou !. Par exemple :

    ## pas d'exécution
    
    # renvoie toutes les colonnes sauf arr_delay et dep_delay
    ans <- flights[, !c("arr_delay", "dep_delay")]
    # ou
    ans <- flights[, -c("arr_delay", "dep_delay")]
  • A partir de la v1.9.5+, on peut aussi sélectionner en spécifiant les noms des colonnes de début et de fin, par exemple, year:day pour sélectionner les trois premières colonnes.

    ## pas d'exécution
    
    # renvoie year,month et day
    ans <- flights[, year:day]
    # renvoie day, month et year
    ans <- flights[, day:year]
    # renvoie toutes les colonnes sauf year, month et day
    ans <- flights[, -(year:day)]
    ans <- flights[, !(year:day)]

    Ceci est particulièrement pratique lorsque l’on travaille de manière interactive.

with = TRUE est la valeur par défaut dans data.table car nous pouvons faire plus en permettant à j de gérer des expressions - particulièrement en combinant avec by, comme nous le verrons dans un instant.

2. Aggrégations

Nous avons déjà vu i et j dans la forme générale d’une data.table dans la secton précédente. Dans cette section, nous allons voir comment ils peuvent être combinés ensemble avec by pour réaliser des opérations par groupe. Voyons quelques exemples.

a) Regrouper avec by

– Comment obtenir le nombre de voyages au départ de chaque aéroport ?

ans <- flights[, .(.N), by = .(origin)]
ans
#    origin     N
#    <char> <int>
# 1:    JFK 81483
# 2:    LGA 84433
# 3:    EWR 87400

## ou résultat identique en utilisant un vecteur de chaînes de caractères dans 'by'
# ans <- flights[, .(.N), by = "origin"]
  • Nous savons que .N est une variable spéciale qui contient le nombre de lignes dans le groupe courant. En groupant par origine, on obtient le nombre de lignes, .N, pour chaque groupe.

  • En faisant head(flights) vous pouvez voir que les aéroports d’origine sont dans l’ordre “JFK”, “LGA”, et “EWR”. L’ordre original de regroupement des variables est préservé dans le résultat. Il est important de garder cela à l’esprit!

  • Comme nous n’avons pas fourni de nom pour la colonne retournée dans j, elle a été nommée N automatiquement en reconnaissant le symbole spécial .N.

  • by accepte également un vecteur de caractères de noms de colonnes. Ceci est particulièrement utile pour le codage par programmation, par exemple pour concevoir une fonction avec les colonnes de regroupement (sous la forme d’un vecteur character) comme argument de la fonction.

  • Lorsqu’il n’y a qu’une seule colonne ou expression à laquelle se référer dans j et by, nous pouvons abandonner la notation .(). Ceci est purement pratique. Nous pourrions plutôt faire :

    ans <- flights[, .N, by = origin]
    ans
    #    origin     N
    #    <char> <int>
    # 1:    JFK 81483
    # 2:    LGA 84433
    # 3:    EWR 87400

    Nous utiliserons cette forme pratique chaque fois que cela sera possible.

– Comment calculer le nombre de voyages au départ de chaque aéroport pour le transporteur ayant le code "AA"?

Le code unique de transporteur "AA" correspond à American Airlines Inc.

ans <- flights[carrier == "AA", .N, by = origin]
ans
#    origin     N
#    <char> <int>
# 1:    JFK 11923
# 2:    LGA 11730
# 3:    EWR  2649
  • Nous obtenons d’abord les indices de ligne pour l’expression carrier == "AA" à partir de i.

  • En utilisant ces index de ligne, nous obtenons le nombre de lignes groupées par origine. Une fois de plus, aucune colonne n’est matérialisée ici, car l’expression `j’ ne nécessite aucune colonne pour définir le sous-ensemble et le calcul est donc rapide et peu gourmand en mémoire.

– Comment obtenir le nombre total de voyages pour chaque paire origin, dest du transporteur ayant pour code "AA"?

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]
head(ans)
#    origin   dest     N
#    <char> <char> <int>
# 1:    JFK    LAX  3387
# 2:    LGA    PBI   245
# 3:    EWR    LAX    62
# 4:    JFK    MIA  1876
# 5:    JFK    SEA   298
# 6:    EWR    MIA   848

## ou résultat identique en utilisant une chaîne de caractères dans 'by'
# ans <- flights[carrier == "AA", .N, by = c("origin", "dest")]
  • by accepte plusieurs colonnes. Nous fournissons simplement toutes les colonnes par lesquelles il faut grouper. Notez l’utilisation de .() dans by – encore une fois, c’est juste un raccourci pour list(), et list() peut être utilisé ici aussi. Nous nous en tiendrons à nouveau à .() dans cette vignette.

– Comment obtenir les valeurs moyennes menselles du retard des arrivées et des départs pour chaque paire orig,dest pour le transporteur ayant le code "AA"?

ans <- flights[carrier == "AA",
        .(mean(arr_delay), mean(dep_delay)),
        by = .(origin, dest, month)]
ans
#      origin   dest month         V1         V2
#      <char> <char> <int>      <num>      <num>
#   1:    JFK    LAX     1   6.590361 14.2289157
#   2:    LGA    PBI     1  -7.758621  0.3103448
#   3:    EWR    LAX     1   1.366667  7.5000000
#   4:    JFK    MIA     1  15.720670 18.7430168
#   5:    JFK    SEA     1  14.357143 30.7500000
#  ---                                          
# 196:    LGA    MIA    10  -6.251799 -1.4208633
# 197:    JFK    MIA    10  -1.880184  6.6774194
# 198:    EWR    PHX    10  -3.032258 -4.2903226
# 199:    JFK    MCO    10 -10.048387 -1.6129032
# 200:    JFK    DCA    10  16.483871 15.5161290
  • Comme nous n’avons pas fourni de noms de colonnes pour les expressions dans j, elles ont été automatiquement générées en tant que V1 et V2.

  • Une fois de plus, notez que l’ordre d’entrée des colonnes de regroupement est préservé dans le résultat.

Maintenant qu’adviendrait-il si nous voulions trier les résultats en groupant les colonnes origin, dest et month ?

b) Tri by : keyby

data.table conserve l’ordre original des groupes; c’est intentionnel et défini à la conception. Il existe des cas où conserver l’ordre original est essentiel. Mais à certains moments, nous aimerions trier automatiquement par variables dans notre regroupement.

– Donc comment pourrions-nous trier directement sur toutes les variables de regroupement ?

ans <- flights[carrier == "AA",
        .(mean(arr_delay), mean(dep_delay)),
        keyby = .(origin, dest, month)]
ans
# Key: <origin, dest, month>
#      origin   dest month         V1         V2
#      <char> <char> <int>      <num>      <num>
#   1:    EWR    DFW     1   6.427673 10.0125786
#   2:    EWR    DFW     2  10.536765 11.3455882
#   3:    EWR    DFW     3  12.865031  8.0797546
#   4:    EWR    DFW     4  17.792683 12.9207317
#   5:    EWR    DFW     5  18.487805 18.6829268
#  ---                                          
# 196:    LGA    PBI     1  -7.758621  0.3103448
# 197:    LGA    PBI     2  -7.865385  2.4038462
# 198:    LGA    PBI     3  -5.754098  3.0327869
# 199:    LGA    PBI     4 -13.966667 -4.7333333
# 200:    LGA    PBI     5 -10.357143 -6.8571429
  • Tout ce que nous avons fait, c’est remplacer by par keyby. Cela ordonne automatiquement le résultat par ordre croissant des variables de regroupement. En fait, à cause de l’implémentation interne de by qui nécessite d’abord un tri avant de récupérer l’ordre de la table originale, keyby est typiquement plus rapide que by parce qu’il ne nécessite pas cette seconde étape.

Clés : actuellement keyby en fait un peu plus que simplement trier. Il définit une clé également après le tri en initialisant un attribute appelé sorted.

Nous en apprendrons plus au sujet des clés dans la vignette Clés et sous-ensembles basés sur la recherche binaire rapide; pour l’instant, tout ce que vous devez savoir est que vous pouvez utiliser keyby pour trier automatiquement le résultat selon les colonnes spécifiées dans by.

c) Chaînage

Considérons la tâche consistant à récupérer le nombre total de voyages pour chaque couple origin, dest du transporteur “AA”.

ans <- flights[carrier == "AA", .N, by = .(origin, dest)]

– Comment trier ans en utilisant la colonne origin en mode croissant, et la colonne dest en mode décroissant ?

On peut stocker le résultat intermédiaire dans une variable, puis passer order(origin, -dest) sur cette variable. Cela semble plus direct.

ans <- ans[order(origin, -dest)]
head(ans)
#    origin   dest     N
#    <char> <char> <int>
# 1:    EWR    PHX   121
# 2:    EWR    MIA   848
# 3:    EWR    LAX    62
# 4:    EWR    DFW  1618
# 5:    JFK    STT   229
# 6:    JFK    SJU   690
  • Rappelons que nous pouvons utiliser - sur une colonne character dans order() dans le cadre d’un data.table. Ceci est possible grâce à l’optimisation interne des requêtes de data.table.

  • Rappelez-vous aussi que order(...) dans le contexte d’un data.table est automatiquement optimisé pour utiliser l’algorithme de tri radix rapide interne de data.table forder() pour plus de rapidité.

Mais ceci nécessite d’avoir assigné le résultat intermédiaire et de réécrire ce résultat. On peut faire mieux et éviter cette assignation intermédiaire à une variable temporaire en chaînant les expressions ensemble.

ans <- flights[carrier == "AA", .N, by = .(origin, dest)][order(origin, -dest)]
head(ans, 10)
#     origin   dest     N
#     <char> <char> <int>
#  1:    EWR    PHX   121
#  2:    EWR    MIA   848
#  3:    EWR    LAX    62
#  4:    EWR    DFW  1618
#  5:    JFK    STT   229
#  6:    JFK    SJU   690
#  7:    JFK    SFO  1312
#  8:    JFK    SEA   298
#  9:    JFK    SAN   299
# 10:    JFK    ORD   432
  • Nous pouvons ajouter des expressions l’une après l’autre, formant une chaîne d’opérations, c’est-à-dire DT[ ... ][ ... ][ ... ].

  • Vous pouvez également les enchaîner verticalement :

    DT[ ...
       ][ ...
         ][ ...
           ]

d) Expressions de by

by accepte-t-il également expressions, ou simplement des colonnes ?

Oui, il le fait. Par exemple, si nous avions voulu chercher combien de vols sont partis en retard mais sont arrivés plus tôt (ou à l’heure), ou parts à l’heure mais arrivés en retard, etc…

ans <- flights[, .N, .(dep_delay>0, arr_delay>0)]
ans
#    dep_delay arr_delay      N
#       <lgcl>    <lgcl>  <int>
# 1:      TRUE      TRUE  72836
# 2:     FALSE      TRUE  34583
# 3:     FALSE     FALSE 119304
# 4:      TRUE     FALSE  26593
  • La dernière ligne correspond à dep_delay > 0 = TRUE et arr_delay > 0 = FALSE. Nous pouvons voir que les vols 26593 ont commencé en retard mais sont arrivés en avance (ou à l’heure).

  • Notez que nous n’avons pas fourni de noms à by-expression. Par conséquent, les noms ont été automatiquement assignés dans le résultat. Comme pour j, vous pouvez nommer ces expressions comme vous le feriez pour des éléments de n’importe quelle liste, comme par exemple DT[, .N, .(dep_delayed = dep_delay>0, arr_delayed = arr_delay>0)].

  • Vous pouvez fournir d’autres colonnes avec des expressions, par exemple : DT[, .N, by = .(a, b>0)].

e) Colonnes multiples dans j - .SD

– Faut-il calculer mean() pour chaque colonne individuellement ?

Bien sûr il n’est pas pratique de devoir entrer mean(myCol) pour chaque colonne, une par une. Et s’il fallait faire la moyenne mean() sur 100 colonnes ?

Comment faire cela de manière efficace et concise ? Pour y arriver, relisons ce conseil - “Tant que la j-expression renvoie une list, chaque élément de cette list sera converti en une colonne de la data.table résultat”. Si nous pouvons adresser le sous-ensemble de données de chaque groupe comme une variable de regroupement, nous pourrons ensuite boucler sur toutes les colonnes de cette variables en utilisant la fonction de base familière (ou en passe de le devenir) lapply(). Il n’y a pas de nouveaux noms à apprendre particuliers pour data.table.

Symbole spécial .SD:

data.table fournit le symbole spécial .SD. Il tire son nom de Sous-ensemble de Données. C’est une data.table qui contient les données du groupe actuel tel qu’il a été défini avec by.

Souvenez-vous qu’une data.table est représentée en interne comme une list dont toutes les colonnes ont la même longueur.

Utilisons la data.table DT précédente pour avoir un aperçu de ce à quoi ressemble .SD .

DT
#        ID     a     b     c
#    <char> <int> <int> <int>
# 1:      b     1     7    13
# 2:      b     2     8    14
# 3:      b     3     9    15
# 4:      a     4    10    16
# 5:      a     5    11    17
# 6:      c     6    12    18

DT[, print(.SD), by = ID]
#        a     b     c
#    <int> <int> <int>
# 1:     1     7    13
# 2:     2     8    14
# 3:     3     9    15
#        a     b     c
#    <int> <int> <int>
# 1:     4    10    16
# 2:     5    11    17
#        a     b     c
#    <int> <int> <int>
# 1:     6    12    18
# Empty data.table (0 rows and 1 cols): ID
  • .SD contient toutes les colonnes à l’exception des colonnes de regroupement par défaut.

  • Il est également généré en conservant l’ordre original - les données correspondant à ID = "b", puis ID = "a", et enfin ID = "c".

Pour calculer sur uneou plusieurs colonnes vous pouvez utiliser simplement la fonction de base R lapply().

DT[, lapply(.SD, mean), by = ID]
#        ID     a     b     c
#    <char> <num> <num> <num>
# 1:      b   2.0   8.0  14.0
# 2:      a   4.5  10.5  16.5
# 3:      c   6.0  12.0  18.0
  • .SD contient les lignes correspondant aux colonnes a, b et c pour ce groupe. Nous calculons la moyenne avec mean() sur chacune de ces colonnes en utilisant la fonction de base déjà familière lapply().

  • Chaque groupe renvoie une liste de trois éléments contenant la valeur moyenne qui deviendra les colonnes du data.table résultant.

  • Puisque lapply() renvoie une liste, il n’est pas nécessaire de l’entourer d’un .() supplémentaire (si nécessaire, référez-vous à cette astuce).

Nous y sommes presque. Il reste encore une petite chose à régler. Dans notre data.table flights , nous avons voulu calculer seulement la mean() des deux colonnes arr_delay et dep_delay. Mais .SD contiendrait par défaut toutes les colonnes autres que les variables de groupement.

– Comment spécifier uniquement les colonnes sur lesquelles nous voulons appliquer mean() ?

.SDcols

En utilisant l’argument .SDcols. Il accepte soit des noms soit des indices de colonnes. Par exemple, .SDcols = c("arr_delay", "dep_delay") permet que .SD ne comporte que ces deux colonnes pour chaque groupe.

De la même manière que part g), vous pouvez également spécifier les colonnes à supprimer au lieu des colonnes à garder en utilisant le - ou !. De plus, vous pouvez sélectionner des colonnes consécutives avec colA:colB et les désélectionner avec !(colA:colB) ou -(colA:colB).

Maintenant essayons d’utiliser .SD avec .SDcols pour obtenir la moyenne mean() des colonnes arr_delay et dep_delay groupées par origin, dest et month.

flights[carrier == "AA",                       ## Seulement les vols sur porteurs "AA"
        lapply(.SD, mean),                     ## calcule la moyenne
        by = .(origin, dest, month),           ## pour chaque 'origin,dest,month'
        .SDcols = c("arr_delay", "dep_delay")] ## pour seulement ceux spécifiés dans .SDcols
#      origin   dest month  arr_delay  dep_delay
#      <char> <char> <int>      <num>      <num>
#   1:    JFK    LAX     1   6.590361 14.2289157
#   2:    LGA    PBI     1  -7.758621  0.3103448
#   3:    EWR    LAX     1   1.366667  7.5000000
#   4:    JFK    MIA     1  15.720670 18.7430168
#   5:    JFK    SEA     1  14.357143 30.7500000
#  ---                                          
# 196:    LGA    MIA    10  -6.251799 -1.4208633
# 197:    JFK    MIA    10  -1.880184  6.6774194
# 198:    EWR    PHX    10  -3.032258 -4.2903226
# 199:    JFK    MCO    10 -10.048387 -1.6129032
# 200:    JFK    DCA    10  16.483871 15.5161290

f) Extraire .SD pour chaque groupe :

– Comment renvoyer les deux premières lignes de chque ’month`?

ans <- flights[, head(.SD, 2), by = month]
head(ans)
#    month  year   day dep_delay arr_delay carrier origin   dest air_time distance  hour
#    <int> <int> <int>     <int>     <int>  <char> <char> <char>    <int>    <int> <int>
# 1:     1  2014     1        14        13      AA    JFK    LAX      359     2475     9
# 2:     1  2014     1        -3        13      AA    JFK    LAX      363     2475    11
# 3:     2  2014     1        -1         1      AA    JFK    LAX      358     2475     8
# 4:     2  2014     1        -5         3      AA    JFK    LAX      358     2475    11
# 5:     3  2014     1       -11        36      AA    JFK    LAX      375     2475     8
# 6:     3  2014     1        -3        14      AA    JFK    LAX      368     2475    11
  • .SD est un data.table qui contient toutes les lignes de ce groupe. Nous allons simplement subdiviser les deux premières lignes comme nous l’avons déjà vu ici.

  • Pour chaque groupe, head(.SD, 2) renvoie les deux premières lignes sous forme de data.table, qui est également une liste, ce qui nous évite de l’entourer de .().

g) Pourquoi garder j si flexible ?

Ainsi nous avons une syntaxe cohérente et continuons l’utilisation de fonctions de base déja existantes (et familières) au lieu d’apprendre de nouvelles fonctions. Pour illustrer cela utilisons la data.table DT que nous avons créée tout au début dans la section Qu’est-ce qu’une data.table ?.

– Comment concaténer les colonnes a et b pour chaque groupe de ID ?

DT[, .(val = c(a,b)), by = ID]
#         ID   val
#     <char> <int>
#  1:      b     1
#  2:      b     2
#  3:      b     3
#  4:      b     7
#  5:      b     8
#  6:      b     9
#  7:      a     4
#  8:      a     5
#  9:      a    10
# 10:      a    11
# 11:      c     6
# 12:      c    12
  • C’est tout. Aucune syntaxe particulière n’est requise. Tout ce que nous avons besoin de connaître est la fonction de base c() qui concatène des vecteurs, ainsi que l’astuce de tout à l’heure.

– Que se passerait-il si nous voulions avoir toutes les valeurs des colonnes a et b concaténées, mais renvoyées en tant que colonne de liste ?

DT[, .(val = list(c(a,b))), by = ID]
#        ID         val
#    <char>      <list>
# 1:      b 1,2,3,7,8,9
# 2:      a  4, 5,10,11
# 3:      c        6,12
  • Ici, nous concaténons d’abord les valeurs avec c(a,b) pour chaque groupe, et nous les enveloppons avec list(). Ainsi, pour chaque groupe, nous renvoyons une liste de toutes les valeurs concaténées.

  • Notez que ces virgules ne servent qu’à l’affichage. Une colonne de liste peut contenir n’importe quel objet dans chaque cellule et, dans cet exemple, chaque cellule est elle-même un vecteur et certaines cellules contiennent des vecteurs plus longs que d’autres.

Une fois que vous commencerez à utiliser j, vous découvrirez la puissance de sa syntaxe. Une manière pratique de l’aborder est de la tester en utilisant print().

Par exemple :

## inspectez la différence entre
DT[, print(c(a,b)), by = ID] # (1)
# [1] 1 2 3 7 8 9
# [1]  4  5 10 11
# [1]  6 12
# Empty data.table (0 rows and 1 cols): ID

## et
DT[, print(list(c(a,b))), by = ID] # (2)
# [[1]]
# [1] 1 2 3 7 8 9
# 
# [[1]]
# [1]  4  5 10 11
# 
# [[1]]
# [1]  6 12
# Empty data.table (0 rows and 1 cols): ID

Dans (1), pour chaque groupe, un vecteur est renvoyé, de longueur = 6,4,2 ici. Néanmoins, (2) renvoie une liste de longueur 1 pour chaque groupe, dont chaque premier élément contient des vecteurs de longueur 6,4,2. C’est pourquoi, (1) a pour longueur totale 6+4+2 =12, alors que (2) renvoie 1+1+1=3.

Résumé

La forme générale de la syntaxe de data.table est :

DT[i, j, by]

Jusqu’ici nous avons vu que,

En utilisant i :

  • Nous pouvons subdiviser les lignes comme dans un data.frame - sauf que vous n’avez pas besoin d’utiliser DT$ de façon répétitive puisque les colonnes dans le contexte d’un data.table sont vues comme si elles étaient des variables.

  • Nous pouvons également trier un data.table en utilisant order(), qui utilise en interne l’algorithme de tri rapide de data.table pour de meilleures performances.

Nous pouvons faire beaucoup plus dans i en créant une data.table avec clés, ce qui permet de réaliser rapidement les sous-ensembles et les jointures. Nous verrons cela dans les vignettes “Clés et sous-ensembles basés sur la recherche binaire rapide” et “Jointures et jointures liées au temps”.

En utilisant j :

  1. Sélectionner les colonnes à la manière de data.table : DT[, .(colA, colB)].

  2. Sélectionner les colonnes à la manière de data.frame : DT[, c("colA", "colB")].

  3. Effectuer des calculs sur les colonnes : DT[, .(sum(colA), mean(colB))].

  4. Indiquer les noms si nécessaire : DT[, .(sA =sum(colA), mB = mean(colB))].

  5. Combiner avec i : DT[colA > valeur, sum(colB)].

En utilisant by :

  • En utilisant by, nous pouvons grouper par colonnes en spécifiant une liste de colonnes ou un vecteur de caractères de noms de colonnes ou même des expressions. La flexibilité de j, combinée à by et i, en fait une syntaxe très puissante.

  • by peut gérer plusieurs colonnes ainsi que des expressions.

  • Nous pouvons regrouper les colonnes par ‘keyby’ pour trier automatiquement les résultats groupés.

  • Nous pouvons utiliser .SD et .SDcols dans j pour opérer sur plusieurs colonnes en utilisant des fonctions de base déjà connues. Voici quelques exemples:

    1. DT[, lapply(.SD, fun), by = ..., .SDcols = ...] - applique fun à toutes les colonnes spécifiées dans .SDcols tout en groupant par les colonnes spécifiées dans by.

    2. DT[, head(.SD, 2), by = ...] - renvoie les deux premières lignes pour chaque groupe.

    3. DT[col > val, head(.SD, 1), by = ...] - combine i avec j et by.

Et souvenez-vous du conseil :

Tant que j renvoie un objet list, chaque élément de la liste va devenir une colonne du data.table résultant.

Nous verrons dans la vignette suivante comment ajouter / mettre à jour / supprimer des colonnes par référence et comment les combiner avec i et by .