02 mai 2018
Fonctions primitives et structures de données de base.
Exemples : fonction sum et données de type matrix:
pryr::otype(sum)
## [1] "base"
pryr::otype(matrix(1))
## [1] "base"
Langage orienté objet.
Classes déclaratives.
MonPrenom <- "Eric" class(MonPrenom) <- "Prenom"
Les méthodes S3 sont liées aux fonctions, pas aux objets.
# Affichage par défaut MonPrenom
## [1] "Eric" ## attr(,"class") ## [1] "Prenom"
print.Prenom <- function(x) cat("Le prénom est", x)
# Affichage modifié
MonPrenom
## Le prénom est Eric
print est une méthode générique ("un générique") déclaré dans base.
help(print) pryr::otype(print)
Son code se résume à une déclaration UseMethod("print"):
## function (x, ...)
## UseMethod("print")
## <bytecode: 0x000000001b5ae880>
## <environment: namespace:base>
Il existe beaucoup de méthodes S3 pour print:
head(methods("print"))
## [1] "print.acf" "print.AES" ## [3] "print.anova" "print.aov" ## [5] "print.aovlist" "print.ar"
Chacune s'applique à une classe. print.default est utilisée en dernier ressort et s'appuie sur le type (R de base), pas la classe (S3).
typeof(MonPrenom)
## [1] "character"
pryr::otype(MonPrenom)
## [1] "S3"
Un objet peut appartenir à plusieurs classes.
class(MonPrenom) <- c("PrenomFrancais", "Prenom")
inherits(MonPrenom, what = "PrenomFrancais")
## [1] TRUE
inherits(MonPrenom, what = "Prenom")
## [1] TRUE
Le générique cherche une méthode pour chaque classe, dans l'ordre.
print.PrenomFrancais <- function(x) cat("Prénom français:",
x)
MonPrenom
## Prénom français: Eric
S3 est le langage courant de R.
Presque tous les packages sont écrits en S3.
Les génériques sont partout mais passent inaperçu:
library("entropart")
## Loading required package: ggplot2
.S3methods(class = "SpeciesDistribution")
## [1] autoplot plot ## see '?methods' for accessing help and source code
# help(InternalMethods)
S4 structure les classes :
slots pour les données ;
constructeur explicite.
setClass("Personne", slots = list(Nom = "character",
Prenom = "character"))
Moi <- new("Personne", Nom = "Marcon", Prenom = "Eric")
pryr::otype(Moi)
## [1] "S4"
Les méthodes appartiennent toujours aux fonctions:
setMethod("print", signature = "Personne", function(x,
...) {
cat("La personne est:", x@Prenom, x@Nom)
})
print(Moi)
## La personne est: Eric Marcon
S4 est plus rigoureux que S3.
Quelques packages sur CRAN : Matrix, sp, odbc,… et beaucoup sur Bioconductor.
RC a été introduit dans R 2.12 (2010) avec le package methods.
Les méthodes appartiennent aux classes, comme en C++.
library("methods")
PersonneRC <- setRefClass("PersonneRC",
fields = list(Nom = "character", Prenom = "character"),
methods = list(print = function() cat(Prenom, Nom)))
MoiRC <- new("PersonneRC", Nom = "Marcon", Prenom ="Eric")
pryr::otype(MoiRC)
## [1] "RC"
MoiRC$print()
## Eric Marcon
S6 perfectionne RC mais n'est pas inclus dans R.
Les attributs et les méthodes peuvent être publics ou privés.
Une méthode initialize() est utilisée comme constructeur.
library(R6)
PersonneR6 <- R6Class("PersonneR6", public = list(Nom = "character",
Prenom = "character", initialize = function(Nom = NA,
Prenom = NA) {
self$Nom <- Nom
self$Prenom <- Prenom
}, print = function() cat(self$Prenom, self$Nom)))
MoiR6 <- PersonneR6$new(Nom = "Marcon", Prenom = "Eric")
MoiR6$print()
## Eric Marcon
Très peu utilisés, plutôt considérés comme des exercices de style.
S6 permet de programmer rigoureusement en objet.
Les performances sont inférieures à celles de S3.
Pas d'obligation mais cohérence à assurer.
Nommage des objets :
CamelCase : les mots sont compactés, les majuscules assurent la lisibilité ;
tiret_bas : les espaces sont remplacés par des _, pas de majuscule.
Les points sont interdits : séparateurs des génériques. Hélas : data.frame, t.test().
Utiliser des noms clairs, pas de valeur=12 ou TarbrInv.
Utiliser impérativement <- et réserver = aux arguments de fonctions.
a <- b <- 12 a
## [1] 12
b
## [1] 12
(a <- runif(1)) * (rnorm(1) -> b)
## [1] -0.1892117
Entourer <- par des espaces pour éviter la confusion avec -.
Respecter l'espacement standard du code autant que possible. knitr avec l'option tidy=TRUE met en forme automatiquement le code des documents RMarkdown.
Aller à la ligne entre les commandes, éviter ;.
Les accolades commencent sur la ligne de la commande, se terminent sur une ligne seule.
Indenter avec deux espaces. Tabulations interdites.
if (a > b) {
print("Plus grand")
} else {
print("Plus petit")
}
## [1] "Plus grand"
Dans le code, commenter toute ligne non évidente (de l'ordre de 2 sur 3). Commenter le pourquoi, pas le comment sauf extraordinaire.
Le commentaire précède la ligne de code ou va en fin de ligne.
# Calcul de la surface
if (is.null(Largeur)) {
# Longueur seulement: carré
Longueur^2
} else {
# Vrai rectangle. Formule de xxx(1920).
Longueur * Largeur
}
Bien choisir la langue des commentaires. Accents interdits dans les packages.
Commentaires de blocs par :
# Première partie #### x <- 1 # Deuxième partie #### y <- 2
Ces blocs sont repliables dans RStudio (menu Edit / Folding).
Les fonctions du package base sont toujours disponibles. Les autres non.
Les packages chargés par défaut peuvent être déchargés: utils, graphics, stats
Bonne pratique :
usage interactif : taper le nom de la fonction seulement : fonction()
code à conserver : préciser le package (une fonction peut être masquée par un autre package) : package::fonction().
Principes :
library("package") charge loadNamespace() et attache attachNamespace() un package
le package exporte des fonctions : package::fonction()
les fonctions sont accessibles par leur nom fonction()
si nouveaupackage exporte nouveaupackage::fonction(), la nouvelle fonction masque l'ancienne.
R démarre dans l'environnement vide.
Chaque package chargé crée un environnement fils.
La console se trouve dans l'environnement global, fils du dernier package chargé.
search()
## [1] ".GlobalEnv" "package:R6" ## [3] "package:entropart" "package:ggplot2" ## [5] "package:stats" "package:graphics" ## [7] "package:grDevices" "package:utils" ## [9] "package:datasets" "package:methods" ## [11] "Autoloads" "package:base"
Le code d'une fonction appelée de la console s'excute dans un environnement fils de .GlobalEnv
environment()
## <environment: R_GlobalEnv>
f <- function() environment() f()
## <environment: 0x0000000020d4be88>
parent.env(f())
## <environment: R_GlobalEnv>
La recherche part de l'environnement global (ou de celui d'une fonction appelée) et descend la colonne de droite.
Les packages doivent être attachés pour y être.
Un package chargé est dans la colonne centrale: son espace de noms est accessible mais ses objets ne sont pas inclus dans la recherche.
devtools peut être chargé sans être attaché :
unloadNamespace("devtools")
isNamespaceLoaded("devtools")
## [1] FALSE
loadNamespace("devtools")
## <environment: namespace:devtools>
search()
## [1] ".GlobalEnv" "package:R6" ## [3] "package:entropart" "package:ggplot2" ## [5] "package:stats" "package:graphics" ## [7] "package:grDevices" "package:utils" ## [9] "package:datasets" "package:methods" ## [11] "Autoloads" "package:base"
isNamespaceLoaded("devtools")
## [1] TRUE
La fonction dr_devtools() ne peut pas être trouvée mais elle peut être utilisée :
tryCatch(dr_devtools, error = function(e) print(e))
## <simpleError in doTryCatch(return(expr), name, parentenv, handler): objet 'dr_devtools' introuvable>
devtools::dr_devtools()
## DR_DEVTOOLS SAYS YOU LOOK HEALTHY
loadNamespace() n'est jamais utilisé :
Appeler package::fonction() charge le package,
Attacher un package le charge.
Les objets non exportés par un package sont accessible dans son espace de nom avec trois :
package:::fonction()
Les autres aussi, mais c'est inutile :
stats:::sd(rnorm(100))
## [1] 1.031927
Les méthodes S3 ne sont normalement pas exportées, seul le générique l'est.
names(formals(plot))
## [1] "x" "y" "..."
tryCatch(names(formals(entropart::plot.SpeciesDistribution)),
error = function(e) print(e))
## <simpleError: 'plot.SpeciesDistribution' is not an exported object from 'namespace:entropart'>
names(formals(entropart:::plot.SpeciesDistribution))
## [1] "x" "..." "Distribution" ## [4] "type" "log" "main" ## [7] "xlab" "ylab"
Les packages s'appuient sur d'autres packages.
Ils peuvent les importer : ggplot2 importe plyr.
Ou en dépendre : ggplot2 dépend de reshape.
Un package qui exporte une méthode S3 dépend forcément du package contenant le générique.
Réel
typeof(1.1)
## [1] "double"
Entier : forcer le type en précisant L
typeof(1L)
## [1] "integer"
Logique
typeof(TRUE)
## [1] "logical"
Complexe
# help(complex) typeof(sqrt(-1 + (0+0i)))
## [1] "complex"
Caractère
typeof("Bonjour")
## [1] "character"
Brut
# help(raw) typeof(raw(1))
## [1] "raw"
is.character("Bonjour")
## [1] TRUE
is.double(1.2)
## [1] TRUE
is.logical(1 > 0)
## [1] TRUE
Attention à is.integer() :
is.integer(1)
## [1] FALSE
typeof(1)
## [1] "double"
is.numeric() est vrai pour les réels et les entiers.
5 structures de données :
| Atomique | Récursif | |
|---|---|---|
| Unidimensionnel | vector | list |
| Bidimensionnel | matrix | data.frame |
| n-dimensionnel | array |
is.atomic() teste la structure d'une variable.
(MonVecteur <- 1:3)
## [1] 1 2 3
Tous les éléments sont du même type :
c(1, TRUE, "a")
## [1] "1" "TRUE" "a"
(MaMatrice <- matrix(1:6, nrow = 2))
## [,1] [,2] [,3] ## [1,] 1 3 5 ## [2,] 2 4 6
La multiplication matricielle est très performante
MaMatrice %*% matrix(1:3, ncol = 1)
## [,1] ## [1,] 22 ## [2,] 28
(MonTableau <- array(1:12, dim = c(2, 3, 2)))
## , , 1 ## ## [,1] [,2] [,3] ## [1,] 1 3 5 ## [2,] 2 4 6 ## ## , , 2 ## ## [,1] [,2] [,3] ## [1,] 7 9 11 ## [2,] 8 10 12
(MaListe <- list(Premier = 1:2, Deuxieme = "a"))
## $Premier ## [1] 1 2 ## ## $Deuxieme ## [1] "a"
identical(MaListe[[2]], MaListe$Deuxieme)
## [1] TRUE
Les types de données sont uniques par colonne.
(Mondf <- data.frame(Article = c("Pommes", "Poires"),
Prix = c(2, 3)))
## Article Prix ## 1 Pommes 2 ## 2 Poires 3
identical(Mondf[, 1], Mondf$Article)
## [1] TRUE
Elements fondamentaux du langage.
Toute opération repose sur des fonctions y compris + :
(`+`)
## function (e1, e2) .Primitive("+")
Les fonctions primitives sont écrites en C : ce sont les plus rapides.
Les fonctions internes aussi, mais doivent être appelées par un mécanisme spécial, moins efficace :
cbind
## function (..., deparse.level = 1) ## .Internal(cbind(deparse.level, ...)) ## <bytecode: 0x000000001b7a76c0> ## <environment: namespace:base>
La majorité des fonctions est écrite en R.
Leur type est closure par opposition à primitive.
apply
## function (X, MARGIN, FUN, ...)
## {
## FUN <- match.fun(FUN)
## dl <- length(dim(X))
## if (!dl)
## stop("dim(X) must have a positive length")
## if (is.object(X))
## X <- if (dl == 2L)
## as.matrix(X)
## else as.array(X)
## d <- dim(X)
## dn <- dimnames(X)
## ds <- seq_len(dl)
## if (is.character(MARGIN)) {
## if (is.null(dnn <- names(dn)))
## stop("'X' must have named dimnames")
## MARGIN <- match(MARGIN, dnn)
## if (anyNA(MARGIN))
## stop("not all elements of 'MARGIN' are names of dimensions")
## }
## s.call <- ds[-MARGIN]
## s.ans <- ds[MARGIN]
## d.call <- d[-MARGIN]
## d.ans <- d[MARGIN]
## dn.call <- dn[-MARGIN]
## dn.ans <- dn[MARGIN]
## d2 <- prod(d.ans)
## if (d2 == 0L) {
## newX <- array(vector(typeof(X), 1L), dim = c(prod(d.call),
## 1L))
## ans <- forceAndCall(1, FUN, if (length(d.call) < 2L) newX[,
## 1] else array(newX[, 1L], d.call, dn.call), ...)
## return(if (is.null(ans)) ans else if (length(d.ans) <
## 2L) ans[1L][-1L] else array(ans, d.ans, dn.ans))
## }
## newX <- aperm(X, c(s.call, s.ans))
## dim(newX) <- c(prod(d.call), d2)
## ans <- vector("list", d2)
## if (length(d.call) < 2L) {
## if (length(dn.call))
## dimnames(newX) <- c(dn.call, list(NULL))
## for (i in 1L:d2) {
## tmp <- forceAndCall(1, FUN, newX[, i], ...)
## if (!is.null(tmp))
## ans[[i]] <- tmp
## }
## }
## else for (i in 1L:d2) {
## tmp <- forceAndCall(1, FUN, array(newX[, i], d.call,
## dn.call), ...)
## if (!is.null(tmp))
## ans[[i]] <- tmp
## }
## ans.list <- is.recursive(ans[[1L]])
## l.ans <- length(ans[[1L]])
## ans.names <- names(ans[[1L]])
## if (!ans.list)
## ans.list <- any(lengths(ans) != l.ans)
## if (!ans.list && length(ans.names)) {
## all.same <- vapply(ans, function(x) identical(names(x),
## ans.names), NA)
## if (!all(all.same))
## ans.names <- NULL
## }
## len.a <- if (ans.list)
## d2
## else length(ans <- unlist(ans, recursive = FALSE))
## if (length(MARGIN) == 1L && len.a == d2) {
## names(ans) <- if (length(dn.ans[[1L]]))
## dn.ans[[1L]]
## ans
## }
## else if (len.a == d2)
## array(ans, d.ans, dn.ans)
## else if (len.a && len.a%%d2 == 0L) {
## if (is.null(dn.ans))
## dn.ans <- vector(mode = "list", length(d.ans))
## dn1 <- list(ans.names)
## if (length(dn.call) && !is.null(n1 <- names(dn <- dn.call[1])) &&
## nzchar(n1) && length(ans.names) == length(dn[[1]]))
## names(dn1) <- n1
## dn.ans <- c(dn1, dn.ans)
## array(ans, c(len.a%/%d2, d.ans), if (!is.null(names(dn.ans)) ||
## !all(vapply(dn.ans, is.null, NA)))
## dn.ans)
## }
## else ans
## }
## <bytecode: 0x000000001ab55e20>
## <environment: namespace:base>
Arguments : passés à la fonction.
args(apply)
## function (X, MARGIN, FUN, ...) ## NULL
Corps : le code de la fonction
deparse(body(apply))[1:3]
## [1] "{"
## [2] " FUN <- match.fun(FUN)"
## [3] " dl <- length(dim(X))"
Environnement : l'ensemble des objets déclarés et un pointeur vers l'environnement parent.
environment(apply)
## <environment: namespace:base>
ls(environment(apply))[1:2]
## [1] "-" "-.Date"
parent.env(environment(apply))
## <environment: R_GlobalEnv>
Une fonction est exécutée dans son propre environnement :
environment()
## <environment: R_GlobalEnv>
f <- function() environment() f()
## <environment: 0x00000000201e2a20>
Son environnement parent est celui du code qui l'a appelé :
parent.env(f())
## <environment: R_GlobalEnv>
Code R standard.
Surface <- function(Longueur, Largeur) {
return(Longueur * Largeur)
}
Surface(Longueur = 2, Largeur = 3)
## [1] 6
Retourne un résultat avec return() ou la dernière valeur calculée.
Volume <- function(Longueur, Largeur) {
return(Longueur * Largeur * Hauteur)
}
Longueur <- 5
Hauteur <- 10
Volume(Longueur = 2, Largeur = 3)
## [1] 60
Variables locales (définies dans l'environnement de la fonction) : Longueur et Largeur.
Variables manquantes recherchées dans les environnnements parents : Hauteur.
Evaluation tardive (lazy) de Hauteur.
Nommés. Appel par leur nom ou dans leur ordre :
c(Surface(Largeur = 3, Longueur = 2), Surface(Longueur = 2,
Largeur = 3))
## [1] 6 6
c(Surface(Longueur = 2, 3), Surface(2, 3))
## [1] 6 6
Et même (mais illisible) :
Surface(3, Longueur = 2)
## [1] 6
Donner des noms explicites aux arguments. Le premier s'appelle souvent x dans les génériques.
Donner autant que possible des valeurs par défaut aux arguments.
Surface <- function(Longueur, Largeur = Longueur) {
return(Longueur * Largeur)
}
Surface(Longueur = 2)
## [1] 4
Nommer tous les arguments à partir du deuxième lors de l'appel :
x <- runif(10, min = 5, max = 10) mean(x, na.rm = FALSE)
## [1] 7.001771
Ne jamais abréger les noms des arguments ou T pour TRUE.
Les génériques prévoient tous des arguments libres avec ... :
names(formals(plot))
## [1] "x" "y" "..."
Les méthodes ont la même signature que les génériques :
names(formals(entropart:::plot.SpeciesDistribution))
## [1] "x" "..." "Distribution" ## [4] "type" "log" "main" ## [7] "xlab" "ylab"
La méthode plot pour la classe SpeciesDistribution accepte tous arguments à la place de ... et les utilise dans une de ses lignes de code :
deparse(entropart:::plot.SpeciesDistribution)[15:16]
## [1] " graphics::plot(Ns, type = type, log = log, main = main, xlab = xlab, " ## [2] " ylab = ylab, axes = FALSE, ...)"
Tous les arguments non reconnus par plot.SpeciesDistribution sont passés à plot().
Les ... ne sont pas réservés aux génériques :
f <- function(x) x
g <- function(y, ...) f(...)
g("Rien", x = "Argument x passé à f par g")
## [1] "Argument x passé à f par g"
Mais il faut que tout argument soit reconnu par une fonction :
tryCatch(g("Rien", z = 2), error = function(e) print(e))
## <simpleError in f(...): argument inutilisé (z = 2)>
Les opérateurs de R sont en fait des fonctions:
identical(2 + 2, `+`(2, 2))
## [1] TRUE
Les opérateurs définis par l'utilisateur sont obligatoirement entre % :
`%+%` <- function(a, b) paste(a, b) "Nouvelle" %+% "chaîne"
## [1] "Nouvelle chaîne"
La plupart des fonctions de R sont vectorielles :
x1 <- runif(3) x2 <- runif(3) sqrt(x1)
## [1] 0.5359577 0.8990074 0.6667354
x1 + x2
## [1] 1.068098 0.983405 1.268514
Raisonner en termes de vecteurs plutôt que de scalaires.
Ecrire des fonctions vectorielles sur leur premier argument :
entropart::lnq
## function (x, q)
## {
## if (q == 1) {
## return(log(x))
## }
## else {
## Log <- (x^(1 - q) - 1)/(1 - q)
## Log[x < 0] <- NA
## return(Log)
## }
## }
## <bytecode: 0x000000001ff78f30>
## <environment: namespace:entropart>
Exceptions à la règle : fonctions d'un vecteur, résultat scalaire.
sum(x1)
## [1] 1.540001
sapply() applique une fonction à chaque élément d'un vecteur ou d'une liste.
x1 <- runif(1000) identical(sqrt(x1), sapply(x1, FUN = sqrt))
## [1] TRUE
library("microbenchmark")
mb <- microbenchmark(sqrt(x1), sapply(x1, FUN = sqrt),
lapply(x1, sqrt), vapply(x1, sqrt, FUN.VALUE = 0))
summary(mb)[, c("expr", "median")]
## expr median ## 1 sqrt(x1) 9.4365 ## 2 sapply(x1, FUN = sqrt) 727.9995 ## 3 lapply(x1, sqrt) 530.2565 ## 4 vapply(x1, sqrt, FUN.VALUE = 0) 572.5125
Infiniment plus lent qu'une fonction vectorielle.
lapply() renvoie une liste (économise le temps de simplify2array())
vapply() économise le temps de détermination du type du vecteur
Les boucles sont plus rapides !
Boucle <- function(x) {
Racine <- vector("numeric", length = length(x))
for (i in 1:length(x)) Racine[i] <- sqrt(x[i])
return(Racine)
}
Vapply <- function(x) vapply(x, FUN = sqrt, 0)
mb <- microbenchmark(Vapply(x1), Boucle(x1))
summary(mb)[, c("expr", "median")]
## expr median ## 1 Vapply(x1) 396.7180 ## 2 Boucle(x1) 110.7695
Les boucles longues permettent un suivi
Boucle <- function(x) {
pgb <- txtProgressBar(min = 0, max = length(x))
Racine <- vector("numeric", length = length(x))
for (i in 1:length(x)) {
Racine[i] <- sqrt(x[i])
setTxtProgressBar(pgb, i)
}
return(Racine)
}
RacineX <- Boucle(x1)
## ==================================================
replicate() répète une instruction.
replicate(3, runif(1))
## [1] 0.2517168 0.4917887 0.6179944
est équivalent à runif(3).
vectorize() rend vectorielle une fonction qui ne l'est pas par des boucles. Ecrire plutôt les boucles.
Données : inventaire d'une parcelle de Paracou, 4 carrés distincts.
Objectif : calculer le nombre d'arbres par espèce, le nombre d'arbres par carré, la biodiversité par carré.
Technique : utiliser les fonctions vectorielles, les fonctions de type apply, éventuellement des boucles.
Installer le package EcoFoG à partir de GitHub
devtools::install_github("EcoFoG/EcoFoG")
Extraire les données
library("EcoFoG")
## Loading required package: magrittr
Paracou15 <- Paracou2df(WHERE = "Plot='15' AND CensusYear=2016")
Création d'un tableau croisé :
Paracou15X <- as.data.frame.matrix(xtabs(~paste(Family,
Genus, Species) + SubPlot, data = Paracou15))
Paracou15X[1:2, ]
## 1 2 3 4 ## Anacardiaceae Anacardium spruceanum 1 2 4 2 ## Anacardiaceae Tapirira guianensis 2 1 0 0
as.data.frame.matrix est la méthode de conversion des matrices en dataframes…
apply() applique une fonction aux lignes ou colonnes d'un objet 2D.
colSums() et semblables (colMeans(), rowMeans()) sont optimisées.
mb <- microbenchmark(apply(Paracou15X, 2, sum), colSums(Paracou15X))
summary(mb)[, c("expr", "median")]
## expr median ## 1 apply(Paracou15X, 2, sum) 121.231 ## 2 colSums(Paracou15X) 85.539
colSums(Paracou15X)
## 1 2 3 4 ## 835 1142 1087 1087
apply() ou préparation des données
mb <- microbenchmark(apply(Paracou15X, 2, function(x) x >
0), colSums(Paracou15X > 0))
summary(mb)[, c("expr", "median")]
## expr ## 1 apply(Paracou15X, 2, function(x) x > 0) ## 2 colSums(Paracou15X > 0) ## median ## 1 363.4875 ## 2 149.9485
colSums(Paracou15X > 0)
## 1 2 3 4 ## 183 198 186 191
Estimation de la richesse spécifique avec entropart
library("entropart")
apply(Paracou15X, 2, Richness)
## 1 2 3 4 ## 314.9133 277.9428 244.3140 312.3882
apply()Comparaison avec une boucle
Boucle <- function(Cmnt) {
Richesse <- vector("numeric", length = ncol(Cmnt))
for (i in 1:ncol(Cmnt)) Richesse[i] <- Richness(Cmnt[,
i])
return(Richesse)
}
Apply <- function(Cmnt) apply(Cmnt, 2, Richness)
mb <- microbenchmark(Boucle(Paracou15X), Apply(Paracou15X))
summary(mb)[, c("expr", "median")]
## expr median ## 1 Boucle(Paracou15X) 2.442665 ## 2 Apply(Paracou15X) 3.202869
apply() clarifie (vectorise) le traitement mais ne l'accélère pas.