On regroupe ici quelques astuces pour optimiser le temps d’exécution d’un code R. On en propose pour l’instant quatre, mais le post pourra être actualisé par la suite. L’idée est de regrouper des situations auxquelles chacun pourrait être confronté. Les points explorés dans cette note sont les suivants :
- Pour base R : la question de l’application d’une fonction apply aux colonnes d’un data.frame.
- Pour dplyr : la création d’une variable directement à l’intérieur de summarise().
- Pour dplyr : le temps d’exécution d’un group_by par une variable caractère.
- Pour dplyr : les summarise() sur des booléens
Définition d’une fonction apply sur les colonnes d’un dataframe
Imaginons que vous souhaitiez appliquer une fonction à un ensemble de variables d’un data.frame, définies dans une liste (par exemple pour faire une fonction qui appliquerait des statistiques sur un ensemble de variables choisies par l’utilisateur). On définit donc une telle liste de variables, dans la table flights
du package nycflights13
:
var <- c("dep_delay", "arr_delay", "air_time")
On sort ensuite les moyennes de ces trois variables avec sapply
. En base R, cela peut s’écrire ainsi :
Option 1
sapply(var, function(x) sum(flights[[x]], na.rm = TRUE))
## dep_delay arr_delay air_time
## 4152200 2257174 49326610
Mais aussi ainsi :
Option 2
sapply(flights[var], function(x) sum(x, na.rm = TRUE))
## dep_delay arr_delay air_time
## 4152200 2257174 49326610
Cette dernière option pouvant se simplifier, puisqu’on n’a pas vraiment besoin de définir notre fonction à la volée dans ce cas :
Option 2 bis
sapply(flights[var], sum, na.rm = TRUE)
## dep_delay arr_delay air_time
## 4152200 2257174 49326610
Ainsi, l’option 2 peut sembler à juste titre plus intuitive (ne serait-ce que parce qu’elle se simplifie avec l’option 2bis), pourtant l’option 1 est significativement plus rapide, comme le montre la fonction microbenchmark
:
## Unit: milliseconds
## expr min lq mean median uq max neval
## Option 1 1.568429 1.578101 1.664026 1.580945 1.595168 2.863791 50
## Option 2 1.917159 1.926831 1.991547 1.935932 1.980875 2.935472 50
## Option 2 bis 1.910902 1.920004 1.956117 1.934794 1.962102 2.326759 50
C’est bon à savoir, mais pour ce genre de traitement ça vaut le coup de s’intéresser aux méthodes dplyr
et data.table
qui offrent des solutions faciles et efficaces.
group_by par une variable caractère
Quelque chose de très simple à faire pour optimiser ses codes en dplyr : ne pas faire de group_by sur des variables caractères mais sur des factors. On montre ici un exemple très simple sur la même base flights. Tout d’abord, faisons une moyenne des retards à l’arrivée groupée par le lieu d’origine :
flightstib %>% group_by(origin) %>%
summarize(mean_delay = mean(arr_delay, na.rm = TRUE))
On compare la rapidité de cette instruction, à celle-ci, qui fait la même chose sur une variable factor :
flightstib$originfac <- as.factor(flightstib$origin)
flightstib %>% group_by(originfac) %>%
summarize(mean_delay = mean(arr_delay, na.rm = TRUE))
Le résultat de la fonction microbenchmark
appliquée à ces deux instructions donne :
## Unit: milliseconds
## expr min lq mean median uq max
## group by character 14.53741 14.74278 15.99699 14.82441 15.64134 25.96469
## group by factor 11.86932 12.05932 13.04685 12.13243 12.29911 18.96564
## neval
## 100
## 100
La différence est de l’ordre de 20% et peut peser encore plus sur des tables plus volumineuses. Elle est attendue car le type factor est plus léger et convient parfaitement pour des statistiques groupées. Mais cela semble jouer particulièrement pour les instructions sur dplyr, comme le suggère cette discussion sur le github de dplyr. À noter qu’on ne compte pas dans la comparaison le temps de transposition d’une variable caractère en factor, puisque celui-ci peut être appliqué une seule fois pour de nombreuses instructions ou être appliqué au moment de l’import des bases.
Ralentissement de la vitesse d’exécution pour une création de variable directement à l’intérieur de summarise()
Si on reprend l’exemple donné dans le précédent post, vous avez pu remarquer que :
# Rappel : df <- data.frame(id1 = c(1:100), idgpe = sample(50), replace = TRUE)
df %>% as_tibble() %>% mutate(var = rnorm(100)) %>%
group_by(idgpe) %>% summarise(var_mean = mean(var)) -> output_tibble
pouvait se réécrire de manière plus directe (comme le fait d’ailleurs la partie sur data.table) ainsi :
df %>% as_tibble() %>% group_by(idgpe) %>%
summarise(var_mean = mean(rnorm(100))) -> output_tibble
C’est-à-dire en se passant du mutate
pour remplacer var
par sa valeur dans summarise
.
Hé bien, cette instruction n’est pas seulement présentée ainsi pour le plaisir de vous montrer la fonction mutate
, mais aussi parce que la première option est bien plus rapide que la seconde, comme le montre la fonction microbenchmark
:
microbenchmark::microbenchmark(times=100L, dplyr1 = {
df %>% as_tibble() %>% mutate(var = rnorm(100)) %>%
group_by(idgpe) %>% summarise(var_mean = mean(var))
}, dplyr2 = {
df %>% as_tibble() %>% group_by(idgpe) %>%
summarise(var_mean = mean(rnorm(100)))
})
## Unit: milliseconds
## expr min lq mean median uq max neval
## dplyr1 1.510403 1.553923 1.598069 1.574118 1.609106 1.888715 100
## dplyr2 2.557160 2.628840 2.894916 2.655293 2.713036 12.905264 100
Ca peut sembler secondaire pour cet exemple, mais sur des grosses tables la différence va vraiment peser. Regardons par exemple les différences de performance de deux instructions dplyr
agrégeant par heure une variable égale au pourcentage de retard à l’arrivée par rapport à la durée du vol en utilisant les données de nycflights13
:
microbenchmark::microbenchmark(times=10L, dplyr_mutate = {
flightstib %>% mutate(propor_delay = arr_delay / air_time) %>%
group_by(time_hour) %>%
summarise(propor_delay = mean(propor_delay)) -> output_dplyr
}, dplyr_sans_mutate = {
flightstib %>% group_by(time_hour) %>%
summarise(propor_delay = mean(arr_delay / air_time)) -> output_dplyr
})
## Unit: milliseconds
## expr min lq mean median uq
## dplyr_mutate 24.1488 24.64203 25.23982 24.67844 25.79005
## dplyr_sans_mutate 203.7348 205.64452 211.34958 208.19116 213.98445
## max neval
## 28.70845 10
## 238.19298 10
Les performances changent du tout au tout. Il semblerait donc que cela soit une très mauvais pratique d’essayer de “sauter” l’étape du mutate()
, sans doute parce que le group_by
peine à traiter le regroupement d’une opération de variables pas encore regroupées. C’est une propriété de summarise
qu’il est important d’avoir à l’esprit.
Ralentissement de la vitesse d’exécution pour une statistique sur une variable booléenne
Imaginons maintenant que l’on crée une variable booléenne indiquant si le pourcentage de retard à l’arrivée est supérieur à 20%.
flightstib %>% mutate(bool_delay = (arr_delay / air_time > 0.20))
On peut vouloir savoir combien de vols connaisent un retard supérieur à 20% à chaque heure, en agrégeant cette première variable et en appliquant un sum() sur celle-ci.
flightstib %>% mutate(bool_delay = (arr_delay / air_time > 0.20)) %>%
group_by(time_hour) %>% summarise(delay_over_20p = sum(bool_delay)) -> stats
Cette instruction tourne sans problèmes, mais lentement du fait de la difficulté de dplyr
à traiter une somme sur un booléen. Il vaut mieux alors définir dans mutate
la variable bool_delay
comme une variable numérique avec as.numeric(arr_delay / air_time > 0.20)
pour optimiser la rapidité du code. Le tableau suivant donne le résultat du microbenchmark de ces deux options :
## Unit: milliseconds
## expr min lq mean median uq
## bool 150.76373 154.0661 171.61047 157.65526 160.14871
## as.numeric(bool) 25.38386 25.6666 27.45456 26.93977 28.94454
## max neval
## 301.75275 10
## 31.40669 10
Les gains en temps d’exécution sont particulièrement importants. On ne constate pas une telle différence avec data.table
par exemple.