Fonctions de regroupement (tapply, by ,gregate) et la famille * apply

1041

Chaque fois que je veux faire quelque chose de "map" py dans R, j'essaie généralement d'utiliser une fonction dans la applyfamille.

Cependant, je ne l' ai jamais très bien compris les différences entre eux - comment { sapply, lapplyetc.} appliquer la fonction à l'entrée / entrée groupée, ce que la sortie ressemblera, ou même ce que l'entrée peut être - donc je souvent il suffit de les parcourir jusqu'à ce que j'obtienne ce que je veux

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

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

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

  2. lapply(vec, f): identique à 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 iest f (ligne / col i de la matrice)
  4. tapply(vector, grouping, f): la sortie est une matrice / tableau, où un élément de la matrice / tableau est la valeur de flors d'un regroupement gdu vecteur, et gest poussé vers les noms de ligne / col
  5. by(dataframe, grouping, f): gsoit un groupement. s'appliquent fà chaque colonne du groupe / trame de données. joli imprimer le regroupement et la valeur de fà chaque colonne.
  6. aggregate(matrix, grouping, f): similaire à by, mais au lieu d'imprimer joliment la sortie, l'agrégat colle tout dans une trame de données.

Question Côté: Je n'ai pas encore appris plyr ou Reshape - serait plyrou reshaperemplacer tous ces entièrement?

grautur
la source
33
à votre question secondaire: pour beaucoup de choses plyr est un remplacement direct pour *apply()et 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 beaucoup de tracas.
JD Long
12
Aussi, je recommanderais d'ajouter: doByet les capacités de sélection et d'application de data.table.
Iterator
7
sapplyest juste lapplyavec l'ajout de simplify2arraysur la sortie. applycontraint au vecteur atomique, mais la sortie peut être un vecteur ou une liste. bydivise les cadres de données en sous-cadres de données, mais il n'utilise pas fles colonnes séparément. Ce n'est que s'il existe une méthode pour la classe 'data.frame' fque la colonne peut être appliquée by. aggregateest générique, donc différentes méthodes existent pour différentes classes du premier argument.
IRTFM
8
Mnémonique: l est pour 'liste', s est pour 'simplifier', t est pour 'par type' (chaque niveau du groupement est un type)
Lutz Prechelt
Il existe également des fonctions dans le package Rfast, comme: eachcol.apply, apply.condition, et plus encore, qui sont plus rapides que les équivalents de R
Stefanos

Réponses:

1330

R a de nombreuses fonctions * apply qui sont décrites de manière efficace dans les fichiers d'aide (par exemple ?apply). Cependant, il y en a suffisamment pour que les utilisateurs débutants aient du mal à décider lequel convient à leur situation ou même à s'en souvenir. Ils peuvent avoir le sentiment général que «je devrais utiliser une fonction * appliquer ici», mais il peut être difficile de les garder tous droits au début.

Malgré le fait (noté dans d'autres réponses) que la plupart des fonctionnalités de la famille * apply sont couvertes par le plyrpackage extrêmement populaire , les fonctions de base restent utiles et méritent d'être connues.

Cette réponse est destinée à agir comme une sorte de panneau pour les nouveaux utilisateurs afin de les diriger vers la fonction * appliquer correcte pour leur problème particulier. Remarque, ceci n'est pas destiné à simplement régurgiter ou remplacer la documentation R! L'espoir est que cette réponse vous aide à décider quelle fonction * appliquer convient à votre situation, puis c'est à vous de la rechercher davantage. À une exception près, les différences de performances ne seront pas traitées.

  • appliquer - Lorsque vous souhaitez appliquer une fonction aux lignes ou colonnes d'une matrice (et aux analogues de dimension supérieure); généralement déconseillé pour les trames de données car il contraindra d'abord une matrice.

    # Two dimensional matrix
    M <- matrix(seq(1,16), 4, 4)
    
    # apply min to rows
    apply(M, 1, min)
    [1] 1 2 3 4
    
    # apply max to columns
    apply(M, 2, max)
    [1]  4  8 12 16
    
    # 3 dimensional array
    M <- array( seq(32), dim = c(4,4,2))
    
    # Apply sum across each M[*, , ] - i.e Sum across 2nd and 3rd dimension
    apply(M, 1, sum)
    # Result is one-dimensional
    [1] 120 128 136 144
    
    # Apply sum across each M[*, *, ] - i.e Sum across 3rd dimension
    apply(M, c(1,2), sum)
    # Result is two-dimensional
         [,1] [,2] [,3] [,4]
    [1,]   18   26   34   42
    [2,]   20   28   36   44
    [3,]   22   30   38   46
    [4,]   24   32   40   48

    Si vous voulez ligne / colonne ou des moyens de sommes pour une matrice 2D, assurez - vous d'enquêter sur le hautement optimisé, rapide comme l' éclair colMeans, rowMeans, colSums, rowSums.

  • lapply - Lorsque vous souhaitez appliquer une fonction à chaque élément d'une liste tour à tour et récupérer une liste.

    C'est le cheval de bataille de la plupart des autres fonctions * apply. Décollez leur code et vous le trouverez souvent en lapplydessous.

    x <- list(a = 1, b = 1:3, c = 10:100) 
    lapply(x, FUN = length) 
    $a 
    [1] 1
    $b 
    [1] 3
    $c 
    [1] 91
    lapply(x, FUN = sum) 
    $a 
    [1] 1
    $b 
    [1] 6
    $c 
    [1] 5005
  • sapply - Lorsque vous voulez appliquer une fonction à tour de rôle à chaque élément d'une liste, mais que vous voulez un vecteur de retour, plutôt qu'une liste.

    Si vous vous retrouvez à taper unlist(lapply(...)), arrêtez-vous et réfléchissez sapply.

    x <- list(a = 1, b = 1:3, c = 10:100)
    # Compare with above; a named vector, not a list 
    sapply(x, FUN = length)  
    a  b  c   
    1  3 91
    
    sapply(x, FUN = sum)   
    a    b    c    
    1    6 5005 

    Dans des utilisations plus avancées, sapplyil tentera de contraindre le résultat à un tableau multidimensionnel, le cas échéant. Par exemple, si notre fonction renvoie des vecteurs de même longueur, sapplyles utilisera comme colonnes d'une matrice:

    sapply(1:5,function(x) rnorm(3,x))

    Si notre fonction renvoie une matrice bidimensionnelle, sapplyfera essentiellement la même chose, en traitant chaque matrice retournée comme un seul vecteur long:

    sapply(1:5,function(x) matrix(x,2,2))

    Sauf indication contraire simplify = "array", auquel cas il utilisera les matrices individuelles pour construire un tableau multidimensionnel:

    sapply(1:5,function(x) matrix(x,2,2), simplify = "array")

    Chacun de ces comportements est bien sûr tributaire de notre fonction renvoyant des vecteurs ou des matrices de même longueur ou dimension.

  • vapply - Lorsque vous souhaitez utiliser sapplymais que vous avez peut-être besoin de réduire la vitesse de votre code.

    Pour vapply, vous donnez essentiellement à R un exemple de ce que votre fonction renverra, ce qui peut gagner du temps en contraignant les valeurs renvoyées à tenir dans un seul vecteur atomique.

    x <- list(a = 1, b = 1:3, c = 10:100)
    #Note that since the advantage here is mainly speed, this
    # example is only for illustration. We're telling R that
    # everything returned by length() should be an integer of 
    # length 1. 
    vapply(x, FUN = length, FUN.VALUE = 0L) 
    a  b  c  
    1  3 91
  • mapply - Lorsque vous avez plusieurs structures de données (par exemple des vecteurs, des listes) et que vous souhaitez appliquer une fonction aux premiers éléments de chacun, puis aux seconds éléments de chacun, etc., en contraignant le résultat à un vecteur / tableau comme dans sapply.

    Ceci est multivarié dans le sens où votre fonction doit accepter plusieurs arguments.

    #Sums the 1st elements, the 2nd elements, etc. 
    mapply(sum, 1:5, 1:5, 1:5) 
    [1]  3  6  9 12 15
    #To do rep(1,4), rep(2,3), etc.
    mapply(rep, 1:4, 4:1)   
    [[1]]
    [1] 1 1 1 1
    
    [[2]]
    [1] 2 2 2
    
    [[3]]
    [1] 3 3
    
    [[4]]
    [1] 4
  • Map - Un wrapper mapplyavec SIMPLIFY = FALSE, il est donc garanti de renvoyer une liste.

    Map(sum, 1:5, 1:5, 1:5)
    [[1]]
    [1] 3
    
    [[2]]
    [1] 6
    
    [[3]]
    [1] 9
    
    [[4]]
    [1] 12
    
    [[5]]
    [1] 15
  • rapply - Pour quand vous voulez appliquer une fonction à chaque élément d'une structure de liste imbriquée , récursivement.

    Pour vous donner une idée de la rareté rapply, je l'ai oublié lors de la première publication de cette réponse! Évidemment, je suis sûr que beaucoup de gens l'utilisent, mais YMMV. rapplyest mieux illustré avec une fonction définie par l'utilisateur à appliquer:

    # Append ! to string, otherwise increment
    myFun <- function(x){
        if(is.character(x)){
          return(paste(x,"!",sep=""))
        }
        else{
          return(x + 1)
        }
    }
    
    #A nested list structure
    l <- list(a = list(a1 = "Boo", b1 = 2, c1 = "Eeek"), 
              b = 3, c = "Yikes", 
              d = list(a2 = 1, b2 = list(a3 = "Hey", b3 = 5)))
    
    
    # Result is named vector, coerced to character          
    rapply(l, myFun)
    
    # Result is a nested list like l, with values altered
    rapply(l, myFun, how="replace")
  • tapply - Lorsque vous souhaitez appliquer une fonction à des sous - ensembles d'un vecteur et que les sous-ensembles sont définis par un autre vecteur, généralement un facteur.

    Les moutons noirs de la famille * s'appliquent, en quelque sorte. L'utilisation par le fichier d'aide de l'expression «tableau irrégulier» peut être un peu déroutante , mais elle est en fait assez simple.

    Un vecteur:

    x <- 1:20

    Un facteur (de même longueur!) Définissant les groupes:

    y <- factor(rep(letters[1:5], each = 4))

    Additionnez les valeurs xdans chaque sous-groupe défini par y:

    tapply(x, y, sum)  
     a  b  c  d  e  
    10 26 42 58 74 

    Des exemples plus complexes peuvent être traités où les sous-groupes sont définis par les combinaisons uniques d'une liste de plusieurs facteurs. tapplyest similaire dans l' esprit à la scission-apply-combiner des fonctions qui sont communes dans R ( aggregate, by, ave, ddply, etc.) D' où son statut de mouton noir.

joran
la source
32
Croyez que vous trouverez que byc'est purement fendu et que aggregatec'est tapplyà leur cœur . Je pense que le mouton noir fait un excellent tissu.
IRTFM
21
Réponse fantastique! Cela devrait faire partie de la documentation officielle de R :). Une petite suggestion: peut-être ajouter des puces sur l'utilisation de aggregateet byaussi? (Je les comprends enfin après votre description!, Mais ils sont assez courants, il pourrait donc être utile de les séparer et d'avoir des exemples spécifiques pour ces deux fonctions.)
grautur
3
@grautur J'élaguais activement les choses de cette réponse pour éviter qu'elle soit (a) trop longue et (b) une réécriture de la documentation. J'ai décidé que tout aggregate, by, etc. , sont basés sur * appliquer des fonctions, la façon dont vous abordez les utiliser est assez différent du point de vue des utilisateurs qu'ils doivent être résumés dans une réponse distincte. Je peux essayer cela si j'ai le temps, ou peut-être que quelqu'un d'autre me battra et gagnera mon vote positif.
joran
4
aussi, en ?Maptant que parent demapply
baptiste
3
@jsanders - Je ne serais pas du tout d'accord avec ça. data.frames sont une partie absolument centrale de R et en tant listqu'objet sont fréquemment manipulés en utilisant en lapplyparticulier. Ils agissent également comme des conteneurs pour regrouper des vecteurs / facteurs de nombreux types dans un ensemble de données rectangulaire traditionnel. Bien que data.tableet plyrpuissent ajouter un certain type de syntaxe que certains pourraient trouver plus confortables, ils étendent et agissent data.framerespectivement sur s.
lemailail
191

En marge , voici comment les différentes plyrfonctions correspondent aux *applyfonctions de base (de l'intro au document plyr de la page web plyr http://had.co.nz/plyr/ )

Base function   Input   Output   plyr function 
---------------------------------------
aggregate        d       d       ddply + colwise 
apply            a       a/l     aaply / alply 
by               d       l       dlply 
lapply           l       l       llply  
mapply           a       a/l     maply / mlply 
replicate        r       a/l     raply / rlply 
sapply           l       a       laply 

L'un des objectifs de plyrest de fournir des conventions de dénomination cohérentes pour chacune des fonctions, en codant les types de données d'entrée et de sortie dans le nom de la fonction. Il fournit également une cohérence dans la sortie, en ce sens que la sortie de dlply()est facilement passable ldply()pour produire une sortie utile, etc.

Conceptuellement, l'apprentissage plyrn'est pas plus difficile que la compréhension des *applyfonctions de base .

plyret les reshapefonctions ont remplacé presque toutes ces fonctions dans mon utilisation quotidienne. Mais, également du document Intro to Plyr:

Fonctions connexes tapplyet sweepne remplissent aucune fonction correspondante plyr, et restent utiles. mergeest utile pour combiner des résumés avec les données d'origine.

JoFrhwld
la source
13
Quand j'ai commencé à apprendre le R à partir de zéro, j'ai trouvé plyr BEAUCOUP plus facile à apprendre que la *apply()famille de fonctions. Pour moi, ddply()c'était très intuitif car je connaissais les fonctions d'agrégation SQL. ddply()est devenu mon marteau pour résoudre de nombreux problèmes, dont certains auraient pu être mieux résolus avec d'autres commandes.
JD Long
1
Je suppose que j'ai pensé que le concept derrière les plyrfonctions est similaire aux *applyfonctions, donc si vous pouvez en faire une, vous pouvez faire l'autre, mais les plyrfonctions sont plus faciles à retenir. Mais je suis totalement d'accord sur le ddply()marteau!
JoFrhwld
1
Le package plyr a la join()fonction qui effectue des tâches similaires à la fusion. C'est peut-être plus au point de le mentionner dans le contexte de la plyr.
marbel
N'oublions pas les pauvres, oubliéseapply
JDL
Excellente réponse en général, mais je pense que cela minimise l'utilité vapplyet les inconvénients de sapply. Un avantage majeur vapplyest qu'il applique le type et la longueur de sortie, vous vous retrouverez donc avec la sortie attendue exacte ou une erreur informative. D'un autre côté, sapplyessaiera de simplifier la sortie en suivant des règles qui ne sont pas toujours évidentes, et retombera dans une liste sinon. Par exemple, essayez de prédire le type de sortie cela produira: sapply(list(1:5, 6:10, matrix(1:4, 2)), function(x) head(x, 1)). Et alors sapply(list(matrix(1:4, 2), matrix(1:4, 2)), ...)?
Alexey Shiklomanov
133

De la diapositive 21 de http://www.slideshare.net/hadley/plyr-one-data-analytic-strategy :

appliquer, sévèrement, paresseusement, par, agréger

(J'espère qu'il est clair que cela applycorrespond à @ Hadley aaplyet aggregatecorrespond à @ Hadley, ddplyetc. La diapositive 20 du même diaporama clarifiera si vous ne l'obtenez pas de cette image.)

(à gauche est entrée, en haut est sortie)

isomorphismes
la source
4
y a-t-il une faute de frappe dans la diapositive? La cellule en haut à gauche devrait être assez
JHowIX
100

Commençons d'abord par l'excellente réponse de Joran - tout ce qui est douteux peut améliorer cela.

Ensuite, les mnémoniques suivantes peuvent aider à se souvenir des distinctions entre chacune. Alors que certains sont évidents, d'autres peuvent l'être moins --- pour ceux-ci, vous trouverez une justification dans les discussions de Joran.

Mnémotechnique

  • lapplyest une application de liste qui agit sur une liste ou un vecteur et renvoie une liste.
  • sapplyest simple lapply (la fonction renvoie par défaut un vecteur ou une matrice lorsque cela est possible)
  • vapplyest une application vérifiée (permet de spécifier le type d'objet de retour)
  • rapplyest une application récursive pour les listes imbriquées, c'est-à-dire les listes dans les listes
  • tapplyest une application balisée où les balises identifient les sous-ensembles
  • apply est générique : applique une fonction aux lignes ou colonnes d'une matrice (ou, plus généralement, aux dimensions d'un tableau)

Construire le bon arrière-plan

Si l'utilisation de la applyfamille vous semble encore un peu étrangère, il se peut que vous manquiez un point de vue clé.

Ces deux articles peuvent vous aider. Ils fournissent le contexte nécessaire pour motiver les techniques de programmation fonctionnelle fournies par la applyfamille de fonctions.

Les utilisateurs de Lisp reconnaîtront immédiatement le paradigme. Si vous n'êtes pas familier avec Lisp, une fois que vous vous familiariserez avec FP, vous aurez acquis un point de vue puissant pour une utilisation dans R - et cela applyaura beaucoup plus de sens.

Assad Ebrahim
la source
51

Depuis que je me suis rendu compte que (les très excellentes) réponses de ce post manquent byet d' aggregateexplications. Voici ma contribution.

PAR

La byfonction, comme indiqué dans la documentation, peut cependant être un "wrapper" pour tapply. La puissance de bysurgit lorsque nous voulons calculer une tâche qui tapplyne peut pas être gérée. Un exemple est ce code:

ct <- tapply(iris$Sepal.Width , iris$Species , summary )
cb <- by(iris$Sepal.Width , iris$Species , summary )

 cb
iris$Species: setosa
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.300   3.200   3.400   3.428   3.675   4.400 
-------------------------------------------------------------- 
iris$Species: versicolor
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.000   2.525   2.800   2.770   3.000   3.400 
-------------------------------------------------------------- 
iris$Species: virginica
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.200   2.800   3.000   2.974   3.175   3.800 


ct
$setosa
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.300   3.200   3.400   3.428   3.675   4.400 

$versicolor
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.000   2.525   2.800   2.770   3.000   3.400 

$virginica
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
  2.200   2.800   3.000   2.974   3.175   3.800 

Si nous imprimons ces deux objets, ctet cb, nous avons "essentiellement" les mêmes résultats et les seules différences sont dans la façon dont ils sont affichés et les différents classattributs, respectivement bypour cbet arraypour ct.

Comme je l'ai dit, le pouvoir de bysurgit lorsque nous ne pouvons pas utiliser tapply; le code suivant en est un exemple:

 tapply(iris, iris$Species, summary )
Error in tapply(iris, iris$Species, summary) : 
  arguments must have same length

R dit que les arguments doivent avoir les mêmes longueurs, disons "nous voulons calculer la summaryvariable de iristout le long du facteur Species": mais R ne peut tout simplement pas faire cela car il ne sait pas comment gérer.

Avec la byfonction R, distribuez une méthode spécifique pour la data frameclasse, puis laissez la summaryfonction fonctionner même si la longueur du premier argument (et le type aussi) est différente.

bywork <- by(iris, iris$Species, summary )

bywork
iris$Species: setosa
  Sepal.Length    Sepal.Width     Petal.Length    Petal.Width          Species  
 Min.   :4.300   Min.   :2.300   Min.   :1.000   Min.   :0.100   setosa    :50  
 1st Qu.:4.800   1st Qu.:3.200   1st Qu.:1.400   1st Qu.:0.200   versicolor: 0  
 Median :5.000   Median :3.400   Median :1.500   Median :0.200   virginica : 0  
 Mean   :5.006   Mean   :3.428   Mean   :1.462   Mean   :0.246                  
 3rd Qu.:5.200   3rd Qu.:3.675   3rd Qu.:1.575   3rd Qu.:0.300                  
 Max.   :5.800   Max.   :4.400   Max.   :1.900   Max.   :0.600                  
-------------------------------------------------------------- 
iris$Species: versicolor
  Sepal.Length    Sepal.Width     Petal.Length   Petal.Width          Species  
 Min.   :4.900   Min.   :2.000   Min.   :3.00   Min.   :1.000   setosa    : 0  
 1st Qu.:5.600   1st Qu.:2.525   1st Qu.:4.00   1st Qu.:1.200   versicolor:50  
 Median :5.900   Median :2.800   Median :4.35   Median :1.300   virginica : 0  
 Mean   :5.936   Mean   :2.770   Mean   :4.26   Mean   :1.326                  
 3rd Qu.:6.300   3rd Qu.:3.000   3rd Qu.:4.60   3rd Qu.:1.500                  
 Max.   :7.000   Max.   :3.400   Max.   :5.10   Max.   :1.800                  
-------------------------------------------------------------- 
iris$Species: virginica
  Sepal.Length    Sepal.Width     Petal.Length    Petal.Width          Species  
 Min.   :4.900   Min.   :2.200   Min.   :4.500   Min.   :1.400   setosa    : 0  
 1st Qu.:6.225   1st Qu.:2.800   1st Qu.:5.100   1st Qu.:1.800   versicolor: 0  
 Median :6.500   Median :3.000   Median :5.550   Median :2.000   virginica :50  
 Mean   :6.588   Mean   :2.974   Mean   :5.552   Mean   :2.026                  
 3rd Qu.:6.900   3rd Qu.:3.175   3rd Qu.:5.875   3rd Qu.:2.300                  
 Max.   :7.900   Max.   :3.800   Max.   :6.900   Max.   :2.500     

cela fonctionne en effet et le résultat est très surprenant. C'est un objet de classe byqui le long Species(disons, pour chacun d'eux) calcule le summaryde chaque variable.

Notez que si le premier argument est un data frame, la fonction distribuée doit avoir une méthode pour cette classe d'objets. Par exemple, si nous utilisons ce code avec la meanfonction, nous aurons ce code qui n'a aucun sens:

 by(iris, iris$Species, mean)
iris$Species: setosa
[1] NA
------------------------------------------- 
iris$Species: versicolor
[1] NA
------------------------------------------- 
iris$Species: virginica
[1] NA
Warning messages:
1: In mean.default(data[x, , drop = FALSE], ...) :
  argument is not numeric or logical: returning NA
2: In mean.default(data[x, , drop = FALSE], ...) :
  argument is not numeric or logical: returning NA
3: In mean.default(data[x, , drop = FALSE], ...) :
  argument is not numeric or logical: returning NA

AGRÉGAT

aggregatepeut être considéré comme un autre mode d'utilisation différent tapplysi nous l'utilisons de cette manière.

at <- tapply(iris$Sepal.Length , iris$Species , mean)
ag <- aggregate(iris$Sepal.Length , list(iris$Species), mean)

 at
    setosa versicolor  virginica 
     5.006      5.936      6.588 
 ag
     Group.1     x
1     setosa 5.006
2 versicolor 5.936
3  virginica 6.588

Les deux différences immédiates sont que le deuxième argument de aggregate doit être une liste tandis que tapply peut (non obligatoire) être une liste et que la sortie de aggregateest une trame de données tandis que celle de tapplyest un array.

La puissance de aggregateest qu'il peut gérer facilement des sous-ensembles de données avec subsetargument et qu'il a des méthodes pour les tsobjets et formulaaussi.

Ces éléments aggregatefacilitent le travail avec cela tapplydans certaines situations. Voici quelques exemples (disponibles dans la documentation):

ag <- aggregate(len ~ ., data = ToothGrowth, mean)

 ag
  supp dose   len
1   OJ  0.5 13.23
2   VC  0.5  7.98
3   OJ  1.0 22.70
4   VC  1.0 16.77
5   OJ  2.0 26.06
6   VC  2.0 26.14

Nous pouvons obtenir la même chose avec tapplymais la syntaxe est légèrement plus difficile et la sortie (dans certaines circonstances) moins lisible:

att <- tapply(ToothGrowth$len, list(ToothGrowth$dose, ToothGrowth$supp), mean)

 att
       OJ    VC
0.5 13.23  7.98
1   22.70 16.77
2   26.06 26.14

Il y a d'autres moments où nous ne pouvons pas utiliser byou tapplyet nous devons utiliser aggregate.

 ag1 <- aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, mean)

 ag1
  Month    Ozone     Temp
1     5 23.61538 66.73077
2     6 29.44444 78.22222
3     7 59.11538 83.88462
4     8 59.96154 83.96154
5     9 31.44828 76.89655

Nous ne pouvons pas obtenir le résultat précédent avec tapplyen un seul appel mais nous devons calculer la moyenne Monthpour chaque élément et ensuite les combiner (notez également que nous devons appeler le na.rm = TRUE, car les formulaméthodes de la aggregatefonction ont par défaut le na.action = na.omit):

ta1 <- tapply(airquality$Ozone, airquality$Month, mean, na.rm = TRUE)
ta2 <- tapply(airquality$Temp, airquality$Month, mean, na.rm = TRUE)

 cbind(ta1, ta2)
       ta1      ta2
5 23.61538 65.54839
6 29.44444 79.10000
7 59.11538 83.90323
8 59.96154 83.96774
9 31.44828 76.90000

alors qu'avec bynous, nous ne pouvons tout simplement pas y parvenir en fait, l'appel de fonction suivant renvoie une erreur (mais il est très probablement lié à la fonction fournie mean):

by(airquality[c("Ozone", "Temp")], airquality$Month, mean, na.rm = TRUE)

D'autres fois, les résultats sont les mêmes et les différences ne concernent que la classe (et ensuite comment elle est affichée / imprimée et pas seulement - par exemple, comment la sous-définir).

byagg <- by(airquality[c("Ozone", "Temp")], airquality$Month, summary)
aggagg <- aggregate(cbind(Ozone, Temp) ~ Month, data = airquality, summary)

Le code précédent atteint le même objectif et les mêmes résultats, à certains moments, quel outil utiliser n'est qu'une question de goûts et de besoins personnels; les deux objets précédents ont des besoins très différents en termes de sous-ensemble.

SabDeM
la source
Comme je l'ai dit, la puissance de by apparaît lorsque nous ne pouvons pas utiliser tapply; le code suivant en est un exemple: CE SONT LES MOTS QUE VOUS AVEZ UTILISÉS CI-DESSUS. Et vous avez donné un exemple de calcul du résumé. Disons bien que les statistiques récapitulatives ne peuvent être calculées que pour qu'elles nécessitent un nettoyage: par exemple, data.frame(tapply(unlist(iris[,-5]),list(rep(iris[,5],ncol(iris[-5])),col(iris[-5])),summary))c'est une utilisation de tapply . With the right splitting there is nothing you cant do with tapply . The only thing is it returns a matrix. Please be careful by saying we cant use tapply`
Onyambu
35

Il y a beaucoup de bonnes réponses qui discutent des différences dans les cas d'utilisation pour chaque fonction. Aucune des réponses ne traite des différences de performances. C'est raisonnable car diverses fonctions attendent diverses entrées et produisent différentes sorties, mais la plupart d'entre elles ont un objectif général commun à évaluer par séries / groupes. Ma réponse va se concentrer sur la performance. En raison de ce qui précède, la création d'entrée à partir des vecteurs est incluse dans la synchronisation, la applyfonction n'est pas non plus mesurée.

J'ai testé deux fonctions différentes sumet lengthà la fois. Le volume testé est de 50M en entrée et de 50K en sortie. J'ai également inclus deux packages actuellement populaires qui n'étaient pas largement utilisés au moment où la question a été posée, data.tableet dplyr. Les deux valent vraiment la peine d'être recherchés si vous visez 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
jangorecki
la source
Est-il normal que dplyr soit inférieur aux fonctions applt?
Mostafa
1
@DimitriPetrenko Je ne pense pas, je ne sais pas pourquoi c'est ici. Il est préférable de tester vos propres données, car de nombreux facteurs entrent en jeu.
jangorecki
28

Malgré toutes les bonnes réponses ici, il y a 2 autres fonctions de base qui méritent d'être mentionnées, la outerfonction utile et la eapplyfonction obscure

extérieur

outerest une fonction très utile cachée comme plus banale. Si vous lisez l'aide pour outersa 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 que cela n'est utile que pour les choses de type algèbre linéaire. Cependant, il peut être utilisé comme mapplypour appliquer une fonction à deux vecteurs d'entrées. La différence est que mapplyla fonction sera appliquée aux deux premiers éléments, puis les deux autres, etc., tandis que outerla fonction sera appliquée à chaque combinaison d'un élément du premier vecteur et d'un 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.

empressé

eapplyest similaire, lapplysauf qu'au lieu d'appliquer une fonction à chaque élément d'une liste, il applique une fonction à chaque élément d'un environnement. Par exemple, si vous souhaitez rechercher 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 créez de nombreux packages ou créez de nombreux environnements, cela peut être utile.

John Paul
la source
25

Cela vaut peut-être la peine d'être mentionné ave. aveest tapplyle cousin amical. Il renvoie les résultats sous une forme que vous pouvez directement reconnecter à votre bloc 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 package de base qui fonctionne comme avepour les trames de données entières ( bycomme tapplypour les trames de données). Mais vous pouvez le truquer:

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
## ...

la source
12

J'ai récemment découvert la sweepfonction plutôt utile et l'ajoute ici par souci d'exhaustivité:

balayage

L'idée de base est de balayer par un row- de tableau ou en colonne et retourner un tableau modifié. Un exemple rendra cela clair (source: datacamp ):

Supposons que vous ayez une matrice et que vous souhaitiez la normaliser en colonnes:

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,"-")
print(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
# 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)

vonjd
la source
1
Est-ce lié au regroupement?
Frank
2
@Frank: Eh bien, pour être honnête avec vous, le titre de cet article est assez trompeur: lorsque vous lisez la question elle-même, il s'agit de "la famille des candidats". sweepest une fonction d'ordre supérieur comme tous les autres mentionnés ici, par exemple apply, sapply, lapplydonc pourrait se poser la même question au sujet de la réponse acceptée avec plus de 1000 upvotes et les exemples qui y sont données. Jetez un œil à l'exemple donné applyici.
vonjd
2
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 1ère entrée et cette nouvelle matrice. Peut - être mieux illustrée par: sweep(matrix(1:6,nrow=2),2,7:9,list). C'est généralement plus efficace que applyparce que là où les applyboucles sweepsont capables d'utiliser des fonctions vectorisées.
Moody_Mudskipper
2

Dans le package d' effondrement récemment publié sur CRAN, j'ai tenté de compresser la plupart des fonctionnalités d'application courantes en seulement 2 fonctions:

  1. dapply(Data-Apply) applique des fonctions aux lignes ou aux colonnes (par défaut) des matrices et 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 drop = TRUE). Les performances sont comparables à lapplycelles des colonnes data.frame et environ 2 fois plus rapides que celles applydes lignes ou colonnes de 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. BYest un générique S3 pour le calcul d'application de combinaison et de fractionnement avec méthode vectorielle, matrice et data.frame. Il est nettement plus rapide que tapply, byet aggregate( et aussi plus rapide que plyr, sur de grandes données dplyrest plus rapide cependant).

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 g.

Parler de performance: Un objectif principal de l' effondrement est de favoriser la programmation haute performance en R et d'aller au-delà de la combinaison d'application-séparation-combinaison. A cet effet , le paquet a un ensemble complet de fonctions C ++ à base rapide génériques: fmean, fmedian, fmode, fsum, fprod, fsd, fvar, fmin, fmax, ffirst, flast, fNobs, fNdistinct, fscale, fbetween, fwithin, fHDbetween, fHDwithin, flag, fdiffet fgrowth. Ils effectuent des calculs groupés en un seul passage à travers 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 de package, je fournis des repères. La programmation avec les fonctions rapides est beaucoup plus rapide que la programmation avec dplyr ou data.table , en particulier sur des données plus petites, mais aussi sur des données volumineuses.

Sébastien
la source