Appliquer plusieurs fonctions de synthèse sur plusieurs variables par groupe en un seul appel

91

J'ai la trame de données suivante

x <- read.table(text = "  id1 id2 val1 val2
1   a   x    1    9
2   a   x    2    4
3   a   y    3    5
4   a   y    4    9
5   b   x    1    7
6   b   y    4    4
7   b   x    3    9
8   b   y    2    8", header = TRUE)

Je veux calculer la moyenne de val1 et val2 regroupées par id1 et id2, et compter simultanément le nombre de lignes pour chaque combinaison id1-id2. Je peux effectuer chaque calcul séparément:

# calculate mean
aggregate(. ~ id1 + id2, data = x, FUN = mean)

# count rows
aggregate(. ~ id1 + id2, data = x, FUN = length)

Afin de faire les deux calculs en un seul appel, j'ai essayé

do.call("rbind", aggregate(. ~ id1 + id2, data = x, FUN = function(x) data.frame(m = mean(x), n = length(x))))

Cependant, j'obtiens une sortie déformée avec un avertissement:

#     m   n
# id1 1   2
# id2 1   1
#     1.5 2
#     2   2
#     3.5 2
#     3   2
#     6.5 2
#     8   2
#     7   2
#     6   2
# Warning message:
#   In rbind(id1 = c(1L, 2L, 1L, 2L), id2 = c(1L, 1L, 2L, 2L), val1 = list( :
#   number of columns of result is not a multiple of vector length (arg 1)

Je pourrais utiliser le package plyr, mais mon jeu de données est assez volumineux et plyr est très lent (presque inutilisable) lorsque la taille du jeu de données augmente.

Comment puis-je utiliser aggregateou d'autres fonctions pour effectuer plusieurs calculs en un seul appel?

brocoli
la source
À côté de aggregatementionné dans les réponses, il y a aussi byet tapply.
Roman Luštrik

Réponses:

152

Vous pouvez tout faire en une seule étape et obtenir un étiquetage approprié:

> aggregate(. ~ id1+id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) )
#   id1 id2 val1.mn val1.n val2.mn val2.n
# 1   a   x     1.5    2.0     6.5    2.0
# 2   b   x     2.0    2.0     8.0    2.0
# 3   a   y     3.5    2.0     7.0    2.0
# 4   b   y     3.0    2.0     6.0    2.0

Cela crée un dataframe avec deux colonnes id et deux colonnes matricielles:

str( aggregate(. ~ id1+id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) ) )
'data.frame':   4 obs. of  4 variables:
 $ id1 : Factor w/ 2 levels "a","b": 1 2 1 2
 $ id2 : Factor w/ 2 levels "x","y": 1 1 2 2
 $ val1: num [1:4, 1:2] 1.5 2 3.5 3 2 2 2 2
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr  "mn" "n"
 $ val2: num [1:4, 1:2] 6.5 8 7 6 2 2 2 2
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr  "mn" "n"

Comme indiqué par @ lord.garbage ci-dessous, cela peut être converti en un dataframe avec des colonnes "simples" en utilisant do.call(data.frame, ...)

str( do.call(data.frame, aggregate(. ~ id1+id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) ) ) 
    )
'data.frame':   4 obs. of  6 variables:
 $ id1    : Factor w/ 2 levels "a","b": 1 2 1 2
 $ id2    : Factor w/ 2 levels "x","y": 1 1 2 2
 $ val1.mn: num  1.5 2 3.5 3
 $ val1.n : num  2 2 2 2
 $ val2.mn: num  6.5 8 7 6
 $ val2.n : num  2 2 2 2

Voici la syntaxe de plusieurs variables sur le LHS:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) )
IRTFM
la source
1
Merci beaucoup. En remarque, comment obtenir l'agrégat pour résumer une seule colonne. Si j'ai plusieurs colonnes numériques, je ne veux pas qu'il additionne les colonnes que je ne veux pas. Je pourrais bien sûr jeter les colonnes une fois l'agrégation terminée, mais les cycles du processeur seraient déjà dépensés à ce moment-là.
brocoli
Vous ne lui donnez que les facteurs à regrouper et les colonnes à agréger. Utilisez éventuellement une indexation de colonne négative dans les données ou placez les colonnes de votre choix sur la LHS de la formule. (Voir edit.)
IRTFM
2
J'ai rencontré le bogue mentionné par user2659402 dans sa mise à jour lors de l'utilisation de RStudio 0.98.1014 sur une machine Windows 7. Si vous sortez le bloc de données sur la console comme indiqué, cela semble normal, mais si vous l'enregistrez dans d, puis essayez d'accéder à d $ val1.mn, il renvoie NULL. d apparaît également mal formé si vous exécutez view (d). L'utilisation du code dans la mise à jour l'a corrigé.
JHowIX
4
La raison pour laquelle vous rencontrez des difficultés est que les "vals" sont renvoyés sous forme de matrices avec deux colonnes chacune, plutôt que sous forme de colonnes ordinaires. Essayez de d$val1[ , ""mn"]regarder la structure avec str.
IRTFM
5
Vous pouvez lier les colonnes qui contiennent des matrices dans le bloc de données: agg <- aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x)))en utilisant agg_df <- do.call(data.frame, agg). Voir aussi ici .
lord.garbage
30

Compte tenu de cela dans la question:

Je pourrais utiliser le package plyr, mais mon jeu de données est assez volumineux et plyr est très lent (presque inutilisable) lorsque la taille du jeu de données augmente.

Ensuite, dans data.table( 1.9.4+), vous pouvez essayer:

> DT
   id1 id2 val1 val2
1:   a   x    1    9
2:   a   x    2    4
3:   a   y    3    5
4:   a   y    4    9
5:   b   x    1    7
6:   b   y    4    4
7:   b   x    3    9
8:   b   y    2    8

> DT[ , .(mean(val1), mean(val2), .N), by = .(id1, id2)]   # simplest
   id1 id2  V1  V2 N
1:   a   x 1.5 6.5 2
2:   a   y 3.5 7.0 2
3:   b   x 2.0 8.0 2
4:   b   y 3.0 6.0 2

> DT[ , .(val1.m = mean(val1), val2.m = mean(val2), count = .N), by = .(id1, id2)]  # named
   id1 id2 val1.m val2.m count
1:   a   x    1.5    6.5     2
2:   a   y    3.5    7.0     2
3:   b   x    2.0    8.0     2
4:   b   y    3.0    6.0     2

> DT[ , c(lapply(.SD, mean), count = .N), by = .(id1, id2)]   # mean over all columns
   id1 id2 val1 val2 count
1:   a   x  1.5  6.5     2
2:   a   y  3.5  7.0     2
3:   b   x  2.0  8.0     2
4:   b   y  3.0  6.0     2

Pour comparer les temps aggregate(utilisés dans la question et les 3 autres réponses) pour data.tablevoir ce repère (les cas agget agg.x).

Matt Dowle
la source
12

Vous pouvez ajouter une countcolonne, agréger avec sum, puis réduire pour obtenir le mean:

x$count <- 1
agg <- aggregate(. ~ id1 + id2, data = x,FUN = sum)
agg
#   id1 id2 val1 val2 count
# 1   a   x    3   13     2
# 2   b   x    4   16     2
# 3   a   y    7   14     2
# 4   b   y    6   12     2

agg[c("val1", "val2")] <- agg[c("val1", "val2")] / agg$count
agg
#   id1 id2 val1 val2 count
# 1   a   x  1.5  6.5     2
# 2   b   x  2.0  8.0     2
# 3   a   y  3.5  7.0     2
# 4   b   y  3.0  6.0     2

Il a l'avantage de préserver vos noms de colonnes et de créer une seule countcolonne.

flodel
la source
12

En utilisant le dplyrpackage, vous pouvez y parvenir en utilisant summarise_all. Avec cette fonction de synthèse, vous pouvez appliquer d'autres fonctions (dans ce cas meanet n()) à chacune des colonnes non groupées:

x %>%
  group_by(id1, id2) %>%
  summarise_all(funs(mean, n()))

qui donne:

     id1    id2 val1_mean val2_mean val1_n val2_n
1      a      x       1.5       6.5      2      2
2      a      y       3.5       7.0      2      2
3      b      x       2.0       8.0      2      2
4      b      y       3.0       6.0      2      2

Si vous ne voulez pas appliquer la (les) fonction (s) à toutes les colonnes non groupées, vous spécifiez les colonnes auxquelles elles doivent être appliquées ou en excluant les non-voulues avec un moins en utilisant la summarise_at()fonction:

# inclusion
x %>%
  group_by(id1, id2) %>%
  summarise_at(vars(val1, val2), funs(mean, n()))

# exclusion
x %>%
  group_by(id1, id2) %>%
  summarise_at(vars(-val2), funs(mean, n()))
Jaap
la source
10

Vous souhaitez peut-être fusionner ?

x.mean <- aggregate(. ~ id1+id2, p, mean)
x.len  <- aggregate(. ~ id1+id2, p, length)

merge(x.mean, x.len, by = c("id1", "id2"))

  id1 id2 val1.x val2.x val1.y val2.y
1   a   x    1.5    6.5      2      2
2   a   y    3.5    7.0      2      2
3   b   x    2.0    8.0      2      2
4   b   y    3.0    6.0      2      2
neilfws
la source
4

Vous pouvez également utiliser plyr::each()pour introduire plusieurs fonctions:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = plyr::each(avg = mean, n = length))
heschmat
la source
1

Une autre dplyroption est celle acrossqui fait partie de la version de développement actuelle

#devtools::install_github("tidyverse/dplyr")
library(dplyr)

x %>% 
  group_by(id1, id2) %>% 
  summarise(across(starts_with("val"), list(mean = mean, n = length)))

Résultat

# A tibble: 4 x 4
# Groups:   id1 [2]
  id1   id2   mean$val1 $val2 n$val1 $val2
  <fct> <fct>     <dbl> <dbl>  <int> <int>
1 a     x           1.5   6.5      2     2
2 a     y           3.5   7        2     2
3 b     x           2     8        2     2
4 b     y           3     6        2     2

packageVersion("dplyr")
[1]0.8.99.9000
Markus
la source