26 mai 2017

Qui suis-je?

  • Natif du Saguenay-Lac-St-Jean (1991)
  • B.Sc. en actuariat - Université Laval (2013)
  • Chargé de cours à l'École d'actuariat de l'Université Laval (2013)
  • Analyste principal en actuariat chez Promutuel Assurance (2015)
  • M.Sc. en statistique - Université Laval (en cours)



  • Passionné de R (Ad vitam æternam)

Objectifs de la présentation

  • Initier un utilisateur intermédiaire de R à la librairie data.table
  • Être une bougie d'allumage

La gestion des données en R

À la base, les données sont entreposées sous la forme d'un vecteur en R.

Quelques classes de base importantes :

  • character
  • expression
  • integer
  • logical
  • numeric

La classe array

  • Un objet de classe array est tout simplement un vecteur avec des dimensions
  • Chacun de ses éléments doit donc appartenir à la même classe
array(seq(1, 8), rep(2, 3))
## , , 1
## 
##      [,1] [,2]
## [1,]    1    3
## [2,]    2    4
## 
## , , 2
## 
##      [,1] [,2]
## [1,]    5    7
## [2,]    6    8

La classe list

  • Un objet de classe list contient d'autres objets
  • Ces objets peuvent être de classe différente
list(c(1, 2, 3), "bonjour", list(c(1, 2, 3), "bonjour"))
## [[1]]
## [1] 1 2 3
## 
## [[2]]
## [1] "bonjour"
## 
## [[3]]
## [[3]][[1]]
## [1] 1 2 3
## 
## [[3]][[2]]
## [1] "bonjour"

La classe matrix

  • Un objet de classe matrix est un objet de classe array comportant 2 dimensions
  • De ce fait, chacun de ses éléments doit appartenir à la même classe
matrix(seq(1,9), 3)
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9

La classe data.frame

  • Un objet de classe data.frame est une liste de vecteurs de même longueur
  • On le présente sous la forme d'une table
  • Il contient généralement des données structurées
data.frame(v1=seq(1, 10), v2=letters[seq(1, 10)])
##    v1 v2
## 1   1  a
## 2   2  b
## 3   3  c
## 4   4  d
## 5   5  e
## 6   6  f
## 7   7  g
## 8   8  h
## 9   9  i
## 10 10  j

La classe data.table

  • La classe data.table est une extension de la classe data.frame
  • On l'utilise aux mêmes fins que celle-ci
  • Certaines fonctionnalités ont été ajoutées/modifiées pour améliorer l'efficacité
data.table(v1=seq(1, 10), v2=letters[seq(1, 10)])
##     v1 v2
##  1:  1  a
##  2:  2  b
##  3:  3  c
##  4:  4  d
##  5:  5  e
##  6:  6  f
##  7:  7  g
##  8:  8  h
##  9:  9  i
## 10: 10  j

La librairie data.table

  • La principale composante de la librairie est l'implantation de la classe data.table
  • Composantes secondaires :
    • Gestion rapide des dates
    • Fonction fread() et fwrite() pour lecture rapide de *.csv

Pourquoi utiliser data.table

  • Réduire le temps de programmation
    • Syntaxe compacte et intuitive
  • Réduire le temps de calcul et la mémoire utilisée
    • Travail en référence plutôt qu'en copie
    • Clés générées automatiquement pour optimiser les filtres et les jointures

À qui s'adresse data.table

  • Jeux de données en mémoire
  • "Petites données massives"



"Big RAM is eating big data" - Szilard Pafka

Exemple appliqué

On travaillera un exemple appliqué pour la suite de la présentation. Celui-ci nous apprendra à manipuler un data.table efficacement.

  • Ville de Montréal
  • Comptage des véhicules et piétons aux feux de circulation

Le jeu de données peut être téléchargé sur le site de Données Québec.

On charge le jeu de données en mémoire à l'aide de la fonction fread() présentée précédemment.

data <- fread("data.csv")

Héritage

  • Un objet de classe data.table est implicitement un objet de classe data.frame
class(data)
## [1] "data.table" "data.frame"
  • Permet d'utiliser les méthodes développées pour un data.frame si aucune méthode n'a été implantée pour un data.table
nrow(data)
## [1] 823278

Exploration des données

Afin de comprendre les prochains exemples, on explore le jeu de données.

##  [1] "Id_Reference"            "Id_Intersection"        
##  [3] "Nom_Intersection"        "Date"                   
##  [5] "Periode"                 "Heure"                  
##  [7] "Minute"                  "Seconde"                
##  [9] "Code_Banque"             "Description_Code_Banque"
## [11] "NBLT"                    "NBT"                    
## [13] "NBRT"                    "SBLT"                   
## [15] "SBT"                     "SBRT"                   
## [17] "EBLT"                    "EBT"                    
## [19] "EBRT"                    "WBLT"                   
## [21] "WBT"                     "WBRT"                   
## [23] "Approche_Nord"           "Approche_Sud"           
## [25] "Approche_Est"            "Approche_Ouest"         
## [27] "Localisation_X"          "Localisation_Y"         
## [29] "Longitude"               "Latitude"

Nettoyage des données

On nettoie le jeu de données afin de le rendre plus facilement utilisable.

source("clean_data.R")
colnames(data)
## [1] "Id_Intersection"  "Nom_Intersection" "Latitude"        
## [4] "Longitude"        "Date"             "Periode"         
## [7] "Type"             "Saison"           "Nb_Passage"

La syntaxe générale

  • Intuitif pour les programmeurs SQL
DT[i, j, by]
  • i : WHERE
  • j : SELECT | UPDATE
  • by : GROUP BY

La syntaxe du i (1/4)

On affiche les 4 premières observations du jeu de données.

data[1:4]
##    Id_Intersection        Nom_Intersection Latitude Longitude       Date
## 1:               2 Docteur-Penfield / Peel  45.5034  -73.5795 2016-02-18
## 2:               2 Docteur-Penfield / Peel  45.5034  -73.5795 2016-02-18
## 3:               2 Docteur-Penfield / Peel  45.5034  -73.5795 2016-02-18
## 4:               2 Docteur-Penfield / Peel  45.5034  -73.5795 2016-02-18
##     Periode          Type Saison Nb_Passage
## 1: 06:00:00     Véhicules  Hiver         67
## 2: 06:00:00 Piétons/vélos  Hiver          4
## 3: 06:00:00     Véhicules  Hiver          9
## 4: 06:15:00     Véhicules  Hiver        108

La syntaxe du i (2/4)

On affiche toutes les observations pour lesquelles on a observé plus de 2900 passages.

data[Nb_Passage>2900L]
##    Id_Intersection                  Nom_Intersection Latitude Longitude
## 1:             344 McGill College / Sainte-Catherine  45.5019  -73.5711
## 2:             344 McGill College / Sainte-Catherine  45.5019  -73.5711
## 3:             344 McGill College / Sainte-Catherine  45.5019  -73.5711
##          Date  Periode          Type  Saison Nb_Passage
## 1: 2016-10-06 12:30:00 Piétons/vélos Automne       2933
## 2: 2016-10-06 12:45:00 Piétons/vélos Automne       3494
## 3: 2016-10-06 17:00:00 Piétons/vélos Automne       2911
  • On remarque l'utilisation directe de l'objet Nb_Passage.

La syntaxe du i (3/4)

On affiche toutes les observations pour lesquelles on a observé plus de 2000 passages de véhicules.

data[Nb_Passage>2000L & Type=="Véhicules"]
##    Id_Intersection    Nom_Intersection Latitude Longitude       Date
## 1:            3308 Industriel / Pie-IX  45.5839  -73.6299 2016-11-13
## 2:            3308 Industriel / Pie-IX  45.5839  -73.6299 2016-11-13
## 3:            3308 Industriel / Pie-IX  45.5839  -73.6299 2016-11-13
##     Periode      Type  Saison Nb_Passage
## 1: 17:00:00 Véhicules Automne       2053
## 2: 17:15:00 Véhicules Automne       2134
## 3: 17:30:00 Véhicules Automne       2154

La syntaxe du i (4/4)

On affiche toutes les observations pour lesquelles on a observé entre 1900 et 1950 passages, à l'automne ou à l'hiver.

data[Nb_Passage>1900L & Nb_Passage<1950L & Saison %in% c("Automne", "Hiver")]
##    Id_Intersection                  Nom_Intersection Latitude Longitude
## 1:             344 McGill College / Sainte-Catherine  45.5019  -73.5711
## 2:             344 McGill College / Sainte-Catherine  45.5019  -73.5711
## 3:             356           Peel / Sainte-Catherine  45.5000  -73.5729
## 4:             356           Peel / Sainte-Catherine  45.5000  -73.5729
##          Date  Periode          Type  Saison Nb_Passage
## 1: 2016-10-06 12:15:00 Piétons/vélos Automne       1910
## 2: 2016-10-06 16:45:00 Piétons/vélos Automne       1927
## 3: 2016-03-13 16:30:00 Piétons/vélos   Hiver       1938
## 4: 2016-03-13 17:15:00 Piétons/vélos   Hiver       1949

La syntaxe du j (1/4)

On reprend le dernier exemple mais on n'affique que le nombre de passages.

data[Nb_Passage>1900L & Nb_Passage<1950L & Saison %in% c("Automne", "Hiver"),
     Nb_Passage]
## [1] 1910 1927 1938 1949
  • Le résultat est retourné sous la forme d'un vecteur.

La syntaxe du j (2/4)

Pour le retourner sous la forme d'un objet de classe data.table, on passe un objet de classe list à l'argument j.

data[Nb_Passage>1900L & Nb_Passage<1950L & Saison %in% c("Automne", "Hiver"),
     list(Nb_Passage)]
##    Nb_Passage
## 1:       1910
## 2:       1927
## 3:       1938
## 4:       1949

La syntaxe du j (3/4)

En utilisant la logique du dernier exemple, on en déduit qu'on peut retourner un objet de classe data.table comportant plus d'une colonne.

data[Nb_Passage>1900L & Nb_Passage<1950L & Saison %in% c("Automne", "Hiver"),
     .(Nb_Passage, Saison)]
##    Nb_Passage  Saison
## 1:       1910 Automne
## 2:       1927 Automne
## 3:       1938   Hiver
## 4:       1949   Hiver
  • On remarque l'utilisation du .() qui est une abbréviation de list() lorsqu'utilisé dans un objet de classe data.table.

La syntaxe du j (4/4)

On veut calculer la moyenne et l'écart-type de tous les passages de piétons et de vélos qui ont été observés l'hiver.

data[Saison=="Hiver" & Type=="Piétons/vélos",
     .(moyenne=mean(Nb_Passage), `écart-type`=sd(Nb_Passage))]
##     moyenne écart-type
## 1: 42.97122    109.375

La syntaxe du by

On reprend le dernier exemple, mais on veut le nombre d'observations de plus de 100 passages, la moyenne et l'écart-type de ces passages par type (véhicules ou piétons/vélos) et saison.

data[Nb_Passage>100,
     .(nombre=.N, moyenne=mean(Nb_Passage), `écart-type`=sd(Nb_Passage)),
     .(Type, Saison)]
##             Type    Saison nombre  moyenne écart-type
## 1:     Véhicules     Hiver  11839 370.5202   200.3873
## 2:     Véhicules Printemps  29143 426.3281   228.4219
## 3: Piétons/vélos     Hiver   1683 250.0992   247.3075
## 4: Piétons/vélos Printemps   4149 227.8867   174.6006
## 5:     Véhicules       Été  15130 397.0447   201.2567
## 6: Piétons/vélos       Été   2261 216.9239   165.0621
## 7:     Véhicules   Automne  20979 421.3046   227.3149
## 8: Piétons/vélos   Automne   3688 251.9412   217.2101
  • La commande .N retourne le nombre de lignes pour chaque groupement.

L'enchaînement des commandes

On reprend le dernier exemple, mais on veut trier le résultat, d'abord en ordre alphabétique croissant de type et, ensuite, en ordre décroissant alphabétique de saison.

data[Nb_Passage>100,
     .(nombre=.N, moyenne=mean(Nb_Passage), `écart-type`=sd(Nb_Passage)),
     .(Type, Saison) ][
       order(Type, -Saison) ]
##             Type    Saison nombre  moyenne écart-type
## 1: Piétons/vélos       Été   2261 216.9239   165.0621
## 2: Piétons/vélos Printemps   4149 227.8867   174.6006
## 3: Piétons/vélos     Hiver   1683 250.0992   247.3075
## 4: Piétons/vélos   Automne   3688 251.9412   217.2101
## 5:     Véhicules       Été  15130 397.0447   201.2567
## 6:     Véhicules Printemps  29143 426.3281   228.4219
## 7:     Véhicules     Hiver  11839 370.5202   200.3873
## 8:     Véhicules   Automne  20979 421.3046   227.3149

Référence vs copie

Le fonctionnement naturel de R est de toujours créer une copie d'un objet, travailler sur celle-ci et d'ensuite remplacer l'objet original par la copie de l'objet modifiée.

Un peu comme travaille le langage Python, les objets de classe data.table se manipulent directement en référence, sans créer de copie.

Ce comportement a deux avantages significatifs :

  • Gain de temps de calcul
  • Utilisation restreinte de la mémoire

Travail en référence (1/2)

On commence par se créer une copie de notre objet data original afin de pouvoir le travailler sans l'altérer. Pour ce faire, on ne peut utiliser simplement la flèche d'assignation, on doit utiliser la commande copy().

data2 <- copy(data)

Regardons l'effet d'utiliser la flèche d'assignation.

data3 <- data2

Utilisons la commande :=() sur data3 et regardons l'effet sur data2.

data3[, Nb_Passage:=NULL]
rm(data3)
colnames(data2)
## [1] "Id_Intersection"  "Nom_Intersection" "Latitude"        
## [4] "Longitude"        "Date"             "Periode"         
## [7] "Type"             "Saison"

Travail en référence (2/2)

Pour sa part, data n'a pas été modifié puisqu'on a forcé une copie réelle de l'objet au lieu d'une copie du pointeur.

colnames(data)
## [1] "Id_Intersection"  "Nom_Intersection" "Latitude"        
## [4] "Longitude"        "Date"             "Periode"         
## [7] "Type"             "Saison"           "Nb_Passage"

Assignations multiples

Supposons qu'on veule calculer le logarithme et l'exponentielle du nombre de passages.

data2 <- copy(data)
data2[, `:=`(log_passage=log(Nb_Passage), exp_passage=exp(Nb_Passage)) ][
  1:3, .(Nb_Passage, log_passage, exp_passage) ]
##    Nb_Passage log_passage  exp_passage
## 1:         67    4.204693 1.252363e+29
## 2:          4    1.386294 5.459815e+01
## 3:          9    2.197225 8.103084e+03

Utilisation des clés (1/2)

Les objets de classe data.table sont optimisés pour filtrer les données et se joindre entre eux efficacement grâce à un système de clés.

Les filtres et les jointures fonctionnent aussi sans clé, mais sont moins efficaces.

data2 <- copy(data)
key(data2)
## NULL
data2[Id_Intersection==1730][1:5, .(Id_Intersection, Saison, Nb_Passage)]
##    Id_Intersection    Saison Nb_Passage
## 1:            1730 Printemps         89
## 2:            1730   Automne         56
## 3:            1730 Printemps          7
## 4:            1730 Printemps         10
## 5:            1730   Automne         15

Utilisation des clés (2/2)

La manière efficace d'effectuer le filtre du dernier exemple serait plutôt

data2 <- copy(data)
setkey(data2, Id_Intersection)
data2[.(1730)][1:5, .(Id_Intersection, Saison, Nb_Passage)]
##    Id_Intersection    Saison Nb_Passage
## 1:            1730 Printemps         89
## 2:            1730   Automne         56
## 3:            1730 Printemps          7
## 4:            1730 Printemps         10
## 5:            1730   Automne         15

En réalité, si on ne fait qu'une opération de filtre sur un objet de classe data.table, les deux techniques proposées sont presque équivalentes en temps de calcul. Cependant, aussitôt qu'on fait plus d'un filtres différents, la deuxième technique devient significativement plus efficace.

Clés multiples

Une clé peut être formée de plus d'une colonnes.

data2 <- copy(data)
setkey(data2, Id_Intersection, Saison)
data2[.(1730, "Automne")][1:5, .(Id_Intersection, Saison, Nb_Passage)]
##    Id_Intersection  Saison Nb_Passage
## 1:            1730 Automne         56
## 2:            1730 Automne         15
## 3:            1730 Automne          1
## 4:            1730 Automne          6
## 5:            1730 Automne          2

Jointures (1/2)

On peut aussi utiliser les clés pour faire des jointures de tables rapide.

On crée premièrement une table data3 qu'on va joindre à data2.

set.seed(20170526)
data3 <- data.table(Id=seq(1, data2[, max(Id_Intersection)]),
                    Value=sample(100, data2[, max(Id_Intersection)], replace=TRUE))
setkey(data3, Id)
data3[.(c(49, 114, 1730))]
##      Id Value
## 1:   49    76
## 2:  114    94
## 3: 1730    51

Jointures (2/2)

On joint ensuite les deux tables. Celles-ci seront automatiquement jointes en utilisant leur clé respective.

data2 <- copy(data)
setkey(data2, Id_Intersection)
data2[data3, Value_d3:=Value][
  , .(Nb_Passage=sum(Nb_Passage),
      Value_d3=first(Value_d3)),
  by=.(Id_Intersection)][.(c(49, 114, 1730))]
##    Id_Intersection Nb_Passage Value_d3
## 1:              49       6663       76
## 2:             114      16588       94
## 3:            1730      44284       51

Exemple d'utilisation

Les exemples appliqués tout au long de la présentation sont simples et on ne peut voir un bénéfice d'échelle grâce à ceux-ci.

L'utilisation d'objets de classe data.table prend tout son sens lorsqu'on doit faire plusieurs filtres ou jointures successivement. Une application naturelle est donc d'utiliser cette classe dans une application Shiny permettant à l'utilisateur de jouer avec un filtre sur les données.

Analyse du gain d'efficacité

Un extrait de code de la fonction server de l'application Shiny

observe({
    leafletProxy("map", data = filteredData()) %>%
      clearMarkers() %>%
      addCircleMarkers(~Longitude, ~Latitude, color=~col, radius=~rad, popup=~Text)
  })

On voit donc que les données qui alimentent la carte sont filtrées et sommarisées à chaque fois que l'utilisateur change un paramètre d'entrée de l'interface. Le fait d'utiliser un objet de classe data.table pour charger les données complètes en mémoire et de lui assigner une clé réduit donc le temps de calcul à chaque fois où l'utilisateur rafraîchit la carte.

Résumé

  • Un data.table est aussi un data.frame
  • Réduction du temps de programmation et du temps de calcul
  • Données en mémoire
  • Gain d'échelle important lorsqu'utilisé avec une application Shiny


  • Passionné de R (Ad vitam æternam)

Remerciements

  • Comité organisateur
  • Tous les commanditaires de l'événement
  • Promutuel Assurance
  • Stéphane Caron : un ancien étudiant, un collègue, un ami




Merci pour votre écoute!