1091 votes

Les fonctions de regroupement (tapply, by, aggregate) et la famille *apply

Lorsque je veux faire quelque chose de "map "py dans R, j'essaie généralement d'utiliser une fonction dans le répertoire apply famille.

Cependant, je n'ai jamais vraiment compris les différences entre les deux comment ? sapply , lapply appliquent la fonction à l'entrée ou à l'entrée groupée, à ce à quoi ressemblera la sortie, ou même à ce que peut être l'entrée - je les passe souvent en revue jusqu'à ce que j'obtienne ce que je veux.

Quelqu'un peut-il expliquer comment utiliser lequel et quand ?

Ma compréhension actuelle (probablement incorrecte/incomplète) est la suivante...

  1. sapply(vec, f) : l'entrée est un vecteur. la sortie est un vecteur/matrice, où l'élément i es f(vec[i]) en vous donnant une matrice si f a une sortie multi-éléments

  2. lapply(vec, f) : même chose que sapply mais la sortie est une liste ?

  3. apply(matrix, 1/2, f) : l'entrée est une matrice. la sortie est un vecteur, où l'élément i est f(ligne/col i de la matrice)

  4. tapply(vector, grouping, f) La sortie est une matrice/réseau, où un élément de la matrice/réseau est la valeur de f à un regroupement g du vecteur, et g est poussé vers les noms de ligne/col

  5. by(dataframe, grouping, f) : laisser g être un groupement. appliquer f à chaque colonne du groupe ou du cadre de données. Le regroupement et la valeur de la variable f à chaque colonne.

  6. aggregate(matrix, grouping, f) : similaire à by mais au lieu d'imprimer la sortie, l'agrégat place tout dans un cadre de données.

Question secondaire : Je n'ai toujours pas appris le plyr ou le reshape -- est-ce que plyr o reshape remplacer entièrement tous ces éléments ?

35 votes

À votre question secondaire : pour beaucoup de choses, plyr est un remplacement direct de *apply() y by . plyr (du moins pour moi) semble beaucoup plus cohérent dans la mesure où je sais toujours exactement quel format de données il attend et exactement ce qu'il va cracher. Cela m'évite bien des tracas.

14 votes

Aussi, je recommanderais d'ajouter : doBy et les capacités de sélection et d'application de data.table .

8 votes

sapply est juste lapply avec l'ajout de simplify2array sur la sortie. apply se transforme en vecteur atomique, mais la sortie peut être un vecteur ou une liste. by divise les cadres de données en sous-cadres de données, mais n'utilise pas la fonction f sur les colonnes séparément. Seulement s'il y a une méthode pour la classe 'data.frame'. f sont appliqués en colonne par by . aggregate est générique, de sorte que différentes méthodes existent pour différentes classes du premier argument.

38voto

Jan Gorecki Points 316

Il y a beaucoup d'excellentes réponses qui traitent des différences dans les cas d'utilisation de chaque fonction. Aucune de ces réponses ne traite des différences de performance. C'est raisonnable, car diverses fonctions attendent des entrées différentes et produisent des sorties différentes, mais la plupart d'entre elles ont un objectif commun général d'évaluation par séries/groupes. Ma réponse va se concentrer sur les performances. En raison de ce qui précède, la création de l'entrée à partir des vecteurs est incluse dans le timing, de même que la création de la sortie. apply n'est pas mesurée.

J'ai testé deux fonctions différentes sum y length en même temps. Le volume testé est de 50M en entrée et 50K en sortie. J'ai également inclus deux paquets actuellement populaires qui n'étaient pas largement utilisés à l'époque où la question a été posée, data.table y dplyr . Tous deux valent vraiment la peine d'être examinés si vous recherchez de bonnes performances.

library(dplyr)
library(data.table)
set.seed(123)
n = 5e7
k = 5e5
x = runif(n)
grp = sample(k, n, TRUE)

timing = list()

# sapply
timing[["sapply"]] = system.time({
    lt = split(x, grp)
    r.sapply = sapply(lt, function(x) list(sum(x), length(x)), simplify = FALSE)
})

# lapply
timing[["lapply"]] = system.time({
    lt = split(x, grp)
    r.lapply = lapply(lt, function(x) list(sum(x), length(x)))
})

# tapply
timing[["tapply"]] = system.time(
    r.tapply <- tapply(x, list(grp), function(x) list(sum(x), length(x)))
)

# by
timing[["by"]] = system.time(
    r.by <- by(x, list(grp), function(x) list(sum(x), length(x)), simplify = FALSE)
)

# aggregate
timing[["aggregate"]] = system.time(
    r.aggregate <- aggregate(x, list(grp), function(x) list(sum(x), length(x)), simplify = FALSE)
)

# dplyr
timing[["dplyr"]] = system.time({
    df = data_frame(x, grp)
    r.dplyr = summarise(group_by(df, grp), sum(x), n())
})

# data.table
timing[["data.table"]] = system.time({
    dt = setnames(setDT(list(x, grp)), c("x","grp"))
    r.data.table = dt[, .(sum(x), .N), grp]
})

# all output size match to group count
sapply(list(sapply=r.sapply, lapply=r.lapply, tapply=r.tapply, by=r.by, aggregate=r.aggregate, dplyr=r.dplyr, data.table=r.data.table), 
       function(x) (if(is.data.frame(x)) nrow else length)(x)==k)
#    sapply     lapply     tapply         by  aggregate      dplyr data.table 
#      TRUE       TRUE       TRUE       TRUE       TRUE       TRUE       TRUE 

# print timings
as.data.table(sapply(timing, `[[`, "elapsed"), keep.rownames = TRUE
              )[,.(fun = V1, elapsed = V2)
                ][order(-elapsed)]
#          fun elapsed
#1:  aggregate 109.139
#2:         by  25.738
#3:      dplyr  18.978
#4:     tapply  17.006
#5:     lapply  11.524
#6:     sapply  11.326
#7: data.table   2.686

1 votes

Est-il normal que dplyr soit plus bas que les fonctions applt ?

1 votes

@DimitriPetrenko Je ne pense pas, je ne sais pas pourquoi il est ici. Il est préférable de tester contre vos propres données, car il y a beaucoup de facteurs qui entrent en jeu.

30voto

John Paul Points 7239

Malgré toutes les excellentes réponses données ici, il existe 2 autres fonctions de base qui méritent d'être mentionnées, les utiles outer et l'obscure fonction eapply fonction

extérieur

outer est une fonction très utile cachée sous une fonction plus banale. Si vous lisez l'aide de outer sa description dit :

The outer product of the arrays X and Y is the array A with dimension  
c(dim(X), dim(Y)) where element A[c(arrayindex.x, arrayindex.y)] =   
FUN(X[arrayindex.x], Y[arrayindex.y], ...).

ce qui donne l'impression qu'il n'est utile que pour des choses de type algèbre linéaire. Cependant, il peut être utilisé comme mapply pour appliquer une fonction à deux vecteurs d'entrées. La différence est la suivante mapply appliquera la fonction aux deux premiers éléments, puis aux deux autres, etc. outer appliquera la fonction à chaque combinaison d'un élément du premier vecteur et d'un élément du second. Par exemple :

 A<-c(1,3,5,7,9)
 B<-c(0,3,6,9,12)

mapply(FUN=pmax, A, B)

> mapply(FUN=pmax, A, B)
[1]  1  3  6  9 12

outer(A,B, pmax)

 > outer(A,B, pmax)
      [,1] [,2] [,3] [,4] [,5]
 [1,]    1    3    6    9   12
 [2,]    3    3    6    9   12
 [3,]    5    5    6    9   12
 [4,]    7    7    7    9   12
 [5,]    9    9    9    9   12

Je l'ai personnellement utilisé lorsque j'ai un vecteur de valeurs et un vecteur de conditions et que je souhaite voir quelles valeurs répondent à quelles conditions.

eapply

eapply c'est comme lapply sauf qu'au lieu d'appliquer une fonction à chaque élément d'une liste, elle applique une fonction à chaque élément d'un environnement. Par exemple, si vous voulez trouver une liste de fonctions définies par l'utilisateur dans l'environnement global :

A<-c(1,3,5,7,9)
B<-c(0,3,6,9,12)
C<-list(x=1, y=2)
D<-function(x){x+1}

> eapply(.GlobalEnv, is.function)
$A
[1] FALSE

$B
[1] FALSE

$C
[1] FALSE

$D
[1] TRUE 

Franchement, je ne l'utilise pas beaucoup, mais si vous construisez beaucoup de paquets ou créez beaucoup d'environnements, cela peut s'avérer utile.

26voto

Il est peut-être utile de mentionner ave . ave es tapply Il renvoie les résultats sous une forme que vous pouvez insérer directement dans votre cadre de données. Il renvoie les résultats sous une forme que vous pouvez insérer directement dans votre cadre de données.

dfr <- data.frame(a=1:20, f=rep(LETTERS[1:5], each=4))
means <- tapply(dfr$a, dfr$f, mean)
##  A    B    C    D    E 
## 2.5  6.5 10.5 14.5 18.5 

## great, but putting it back in the data frame is another line:

dfr$m <- means[dfr$f]

dfr$m2 <- ave(dfr$a, dfr$f, FUN=mean) # NB argument name FUN is needed!
dfr
##   a f    m   m2
##   1 A  2.5  2.5
##   2 A  2.5  2.5
##   3 A  2.5  2.5
##   4 A  2.5  2.5
##   5 B  6.5  6.5
##   6 B  6.5  6.5
##   7 B  6.5  6.5
##   ...

Il n'y a rien dans le paquet de base qui fonctionne comme ave pour des trames de données entières (comme by c'est comme tapply pour les cadres de données). Mais il est possible d'y remédier :

dfr$foo <- ave(1:nrow(dfr), dfr$f, FUN=function(x) {
    x <- dfr[x,]
    sum(x$m*x$m2)
})
dfr
##     a f    m   m2    foo
## 1   1 A  2.5  2.5    25
## 2   2 A  2.5  2.5    25
## 3   3 A  2.5  2.5    25
## ...

14voto

vonjd Points 1882

J'ai récemment découvert le très utile sweep et l'ajouter ici par souci d'exhaustivité :

balayage

L'idée de base est de balayage à travers un tableau par ligne ou par colonne et retourne un tableau modifié. Un exemple permettra d'y voir plus clair (source : datacamp ) :

Disons que vous avez une matrice et que vous voulez standardiser en colonne :

dataPoints <- matrix(4:15, nrow = 4)

# Find means per column with `apply()`
dataPoints_means <- apply(dataPoints, 2, mean)

# Find standard deviation with `apply()`
dataPoints_sdev <- apply(dataPoints, 2, sd)

# Center the points 
dataPoints_Trans1 <- sweep(dataPoints, 2, dataPoints_means,"-")

# Return the result
dataPoints_Trans1
##      [,1] [,2] [,3]
## [1,] -1.5 -1.5 -1.5
## [2,] -0.5 -0.5 -0.5
## [3,]  0.5  0.5  0.5
## [4,]  1.5  1.5  1.5

# Normalize
dataPoints_Trans2 <- sweep(dataPoints_Trans1, 2, dataPoints_sdev, "/")

# Return the result
dataPoints_Trans2
##            [,1]       [,2]       [,3]
## [1,] -1.1618950 -1.1618950 -1.1618950
## [2,] -0.3872983 -0.3872983 -0.3872983
## [3,]  0.3872983  0.3872983  0.3872983
## [4,]  1.1618950  1.1618950  1.1618950

NB : pour cet exemple simple, le même résultat peut bien sûr être obtenu plus facilement en
apply(dataPoints, 2, scale)

1 votes

Est-ce lié au regroupement ?

3 votes

@Frank : Eh bien, pour être honnête avec vous, le titre de ce billet est plutôt trompeur : lorsque vous lisez la question elle-même, il s'agit de "la famille candidate". sweep est une fonction d'ordre supérieur comme toutes les autres mentionnées ici, par exemple apply , sapply , lapply On pourrait donc poser la même question à propos de la réponse acceptée avec plus de 1 000 votes positifs et des exemples qui y sont donnés. Jetez un coup d'œil à l'exemple donné pour apply là.

3 votes

Sweep a un nom trompeur, des valeurs par défaut trompeuses, et un nom de paramètre trompeur :). Il est plus facile pour moi de le comprendre de cette façon : 1) STATS est un vecteur ou une valeur unique qui sera répétée pour former une matrice de la même taille que la première entrée, 2) FUN sera appliqué sur la première entrée et cette nouvelle matrice. Peut-être mieux illustré par : sweep(matrix(1:6,nrow=2),2,7:9,list) . C'est généralement plus efficace que apply parce que où apply boucles, sweep est capable d'utiliser des fonctions vectorisées.

6voto

Sebastian Points 459

En el effondrement récemment publié sur CRAN, j'ai essayé de comprimer la plupart des fonctionnalités courantes de l'application en seulement 2 fonctions :

  1. dapply (Data-Apply) applique des fonctions aux lignes ou (par défaut) aux colonnes de matrices et de data.frames et (par défaut) renvoie un objet du même type et avec les mêmes attributs (sauf si le résultat de chaque calcul est atomique et que drop = TRUE ). Les performances sont comparables à lapply pour les colonnes data.frame, et environ 2x plus rapide que apply pour les lignes ou les colonnes de la matrice. Le parallélisme est disponible via mclapply (uniquement pour MAC).

Syntaxe :

dapply(X, FUN, ..., MARGIN = 2, parallel = FALSE, mc.cores = 1L, 
       return = c("same", "matrix", "data.frame"), drop = TRUE)

Exemples :

# Apply to columns:
dapply(mtcars, log)
dapply(mtcars, sum)
dapply(mtcars, quantile)
# Apply to rows:
dapply(mtcars, sum, MARGIN = 1)
dapply(mtcars, quantile, MARGIN = 1)
# Return as matrix:
dapply(mtcars, quantile, return = "matrix")
dapply(mtcars, quantile, MARGIN = 1, return = "matrix")
# Same for matrices ...
  1. BY est un générique S3 pour le calcul split-apply-combine avec la méthode vectorielle, matricielle et data.frame. Il est significativement plus rapide que tapply , by y aggregate (et aussi plus rapide que plyr sur de grandes données dplyr est cependant plus rapide).

Syntaxe :

BY(X, g, FUN, ..., use.g.names = TRUE, sort = TRUE,
   expand.wide = FALSE, parallel = FALSE, mc.cores = 1L,
   return = c("same", "matrix", "data.frame", "list"))

Exemples :

# Vectors:
BY(iris$Sepal.Length, iris$Species, sum)
BY(iris$Sepal.Length, iris$Species, quantile)
BY(iris$Sepal.Length, iris$Species, quantile, expand.wide = TRUE) # This returns a matrix 
# Data.frames
BY(iris[-5], iris$Species, sum)
BY(iris[-5], iris$Species, quantile)
BY(iris[-5], iris$Species, quantile, expand.wide = TRUE) # This returns a wider data.frame
BY(iris[-5], iris$Species, quantile, return = "matrix") # This returns a matrix
# Same for matrices ...

Des listes de variables de regroupement peuvent également être fournies à la commande g .

Parler de performance : Un des principaux objectifs de effondrement est d'encourager la programmation haute performance en R et de dépasser le principe du "split-apply-combine". À cette fin, le paquet dispose d'un ensemble complet de fonctions génériques rapides basées sur C++ : fmean , fmedian , fmode , fsum , fprod , fsd , fvar , fmin , fmax , ffirst , flast , fNobs , fNdistinct , fscale , fbetween , fwithin , fHDbetween , fHDwithin , flag , fdiff y fgrowth . Ils effectuent des calculs groupés en un seul passage dans les données (c'est-à-dire sans fractionnement ni recombinaison).

Syntaxe :

fFUN(x, g = NULL, [w = NULL,] TRA = NULL, [na.rm = TRUE,] use.g.names = TRUE, drop = TRUE)

Exemples :

v <- iris$Sepal.Length
f <- iris$Species

# Vectors
fmean(v)             # mean
fmean(v, f)          # grouped mean
fsd(v, f)            # grouped standard deviation
fsd(v, f, TRA = "/") # grouped scaling
fscale(v, f)         # grouped standardizing (scaling and centering)
fwithin(v, f)        # grouped demeaning

w <- abs(rnorm(nrow(iris)))
fmean(v, w = w)      # Weighted mean
fmean(v, f, w)       # Weighted grouped mean
fsd(v, f, w)         # Weighted grouped standard-deviation
fsd(v, f, w, "/")    # Weighted grouped scaling
fscale(v, f, w)      # Weighted grouped standardizing
fwithin(v, f, w)     # Weighted grouped demeaning

# Same using data.frames...
fmean(iris[-5], f)                # grouped mean
fscale(iris[-5], f)               # grouped standardizing
fwithin(iris[-5], f)              # grouped demeaning

# Same with matrices ...

Dans les vignettes du paquet, je fournis des points de repère. La programmation à l'aide des fonctions rapides est nettement plus rapide que la programmation à l'aide de dplyr o table.de.données surtout sur les petites données, mais aussi sur les grandes.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X