Zastosuj kilka funkcji podsumowujących do kilku zmiennych według grupy w jednym wywołaniu

92

Mam następującą ramkę danych

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)

Chcę obliczyć średnią z wartości val1 i val2 pogrupowanych według id1 i id2 i jednocześnie policzyć liczbę wierszy dla każdej kombinacji id1-id2. Mogę wykonać każdą kalkulację osobno:

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

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

Próbowałem wykonać oba obliczenia podczas jednej rozmowy

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

Jednak otrzymuję zniekształcony wynik wraz z ostrzeżeniem:

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

Mógłbym skorzystać z pakietu plyr, ale mój zestaw danych jest dość duży, a plyr jest bardzo powolny (prawie bezużyteczny), gdy rośnie rozmiar zestawu danych.

Jak mogę użyć aggregatelub innych funkcji, aby wykonać kilka obliczeń w jednym połączeniu?

brokuły
źródło
Oprócz aggregatewymienionych w odpowiedziach są również byi tapply.
Roman Luštrik

Odpowiedzi:

154

Możesz to wszystko zrobić w jednym kroku i uzyskać odpowiednie oznakowanie:

> 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

Spowoduje to utworzenie ramki danych z dwiema kolumnami id i dwiema kolumnami macierzowymi:

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"

Jak wskazano w @ lord.garbage poniżej, można to przekonwertować na ramkę danych z „prostymi” kolumnami, używając 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

Oto składnia wielu zmiennych w LHS:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x) ) )
IRTFM
źródło
1
Dzięki wielkie. Na marginesie, jak uzyskać agregację, aby podsumować tylko jedną kolumnę. Jeśli mam kilka kolumn liczbowych, nie chcę, aby sumowały się kolumny, których nie chcę. Mógłbym oczywiście wyrzucić kolumny po zakończeniu agregacji, ale cykle procesora byłyby już zużyte.
brokuły
Podajesz tylko czynniki, według których mają być grupowane i kolumny, które mają być agregowane. Ewentualnie użyj ujemnego indeksowania kolumn w danych lub umieść żądane kolumny na LHS formuły. (Zobacz
red
2
Napotkałem błąd, o którym wspomniał użytkownik 2659402 w swojej aktualizacji podczas korzystania z RStudio 0.98.1014 na komputerze z systemem Windows 7. Jeśli wyślesz ramkę danych do konsoli, jak pokazano, wygląda to normalnie, jednak jeśli zapiszesz ją do d, a następnie spróbujesz uzyskać dostęp do d $ val1.mn, zwraca NULL. d pojawia się również zniekształcony, jeśli uruchomisz widok (d). Użycie kodu w aktualizacji naprawiło to.
JHowIX
4
Powodem tych trudności jest to, że „wartości” są zwracane jako macierze z dwiema kolumnami, a nie jako zwykłe kolumny. Spróbuj d$val1[ , ""mn"]spojrzeć na strukturę za pomocą str.
IRTFM
5
Możesz powiązać kolumny zawierające macierze z powrotem w ramce danych: agg <- aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = function(x) c(mn = mean(x), n = length(x)))używając agg_df <- do.call(data.frame, agg). Zobacz także tutaj .
lord.garbage
30

Biorąc to pod uwagę w pytaniu:

Mógłbym skorzystać z pakietu plyr, ale mój zestaw danych jest dość duży, a plyr jest bardzo powolny (prawie bezużyteczny), gdy rośnie rozmiar zestawu danych.

Następnie w data.table( 1.9.4+) możesz spróbować:

> 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

Do porównania czasów aggregate(zastosowanych w pytaniu i wszystkich 3 innych odpowiedzi), aby data.tablezobaczyć ten punkt odniesienia ( przypadki aggi agg.x).

Matt Dowle
źródło
12

Możesz dodać countkolumnę, zagregować ją sum, a następnie skalować z powrotem, aby uzyskać 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

Ma tę zaletę, że zachowuje nazwy kolumn i tworzy jedną countkolumnę.

flodel
źródło
12

Korzystając z dplyrpakietu, możesz to osiągnąć za pomocą summarise_all. Dzięki tej funkcji podsumowania możesz zastosować inne funkcje (w tym przypadku meani n()) do każdej z kolumn niegrupujących:

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

co daje:

     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

Jeśli nie chcesz zastosować funkcji do wszystkich kolumn niegrupujących, określ kolumny, do których mają zostać zastosowane lub wykluczając niepotrzebne z minusem za pomocą summarise_at()funkcji:

# 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
źródło
10

Może chcesz się połączyć ?

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
źródło
4

Możesz także użyć plyr::each()do wprowadzenia wielu funkcji:

aggregate(cbind(val1, val2) ~ id1 + id2, data = x, FUN = plyr::each(avg = mean, n = length))
heschmat
źródło
1

Inną dplyropcją jest to, acrossco jest częścią aktualnej wersji deweloperskiej

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

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

Wynik

# 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
źródło