Grupuj według wielu kolumn w programie dplyr, używając danych wejściowych typu string

157

Próbuję przenieść moje rozumienie plyr do dplyr, ale nie mogę dowiedzieć się, jak grupować według wielu kolumn.

# make data with weird column names that can't be hard coded
data = data.frame(
  asihckhdoydkhxiydfgfTgdsx = sample(LETTERS[1:3], 100, replace=TRUE),
  a30mvxigxkghc5cdsvxvyv0ja = sample(LETTERS[1:3], 100, replace=TRUE),
  value = rnorm(100)
)

# get the columns we want to average within
columns = names(data)[-3]

# plyr - works
ddply(data, columns, summarize, value=mean(value))

# dplyr - raises error
data %.%
  group_by(columns) %.%
  summarise(Value = mean(value))
#> Error in eval(expr, envir, enclos) : index out of bounds

Czego mi brakuje, aby przetłumaczyć przykład plyr na składnię dplyr?

Edycja 2017 : Dplyr został zaktualizowany, więc dostępne jest prostsze rozwiązanie. Zobacz aktualnie wybraną odpowiedź.

sharoz
źródło
3
Właśnie tu dotarłem, ponieważ był to top google. Możesz użyć group_by_teraz wyjaśnionego wvignette("nse")
James Owers
3
@kungfujam: Wygląda na to, że grupuje się tylko według pierwszej kolumny, a nie pary kolumn
sharoz,
1
Musisz użyć .dots. Oto rozwiązanie dostosowane na podstawie odpowiedzi @hadleya poniżej:df %>% group_by_(.dots=list(quote(asihckhdoydk), quote(a30mvxigxkgh))) %>% summarise(n = n())
James Owers
1
Umieść
1
Jak ktoś wskazał w odpowiedzi na komentarz, celem jest, aby nie wymagać zakodowanych na stałe nazw kolumn.
sharoz

Odpowiedzi:

52

Odkąd to pytanie zostało opublikowane, dplyr dodał wersje z określonym zakresem group_by( dokumentacja tutaj ). Dzięki temu możesz używać tych samych funkcji, których używałbyś z select, na przykład:

data = data.frame(
    asihckhdoydkhxiydfgfTgdsx = sample(LETTERS[1:3], 100, replace=TRUE),
    a30mvxigxkghc5cdsvxvyv0ja = sample(LETTERS[1:3], 100, replace=TRUE),
    value = rnorm(100)
)

# get the columns we want to average within
columns = names(data)[-3]

library(dplyr)
df1 <- data %>%
  group_by_at(vars(one_of(columns))) %>%
  summarize(Value = mean(value))

#compare plyr for reference
df2 <- plyr::ddply(data, columns, plyr::summarize, value=mean(value))
table(df1 == df2, useNA = 'ifany')
## TRUE 
##  27 

Wynik twojego przykładowego pytania jest zgodny z oczekiwaniami (zobacz porównanie z plyr powyżej i wyjście poniżej):

# A tibble: 9 x 3
# Groups:   asihckhdoydkhxiydfgfTgdsx [?]
  asihckhdoydkhxiydfgfTgdsx a30mvxigxkghc5cdsvxvyv0ja       Value
                     <fctr>                    <fctr>       <dbl>
1                         A                         A  0.04095002
2                         A                         B  0.24943935
3                         A                         C -0.25783892
4                         B                         A  0.15161805
5                         B                         B  0.27189974
6                         B                         C  0.20858897
7                         C                         A  0.19502221
8                         C                         B  0.56837548
9                         C                         C -0.22682998

Zwróć uwagę, że ponieważ dplyr::summarizew danym momencie usuwa się tylko jedną warstwę grupowania, w wynikowym tibble nadal zachodzi pewne grupowanie (które może czasem zaskoczyć ludzi później). Jeśli chcesz się całkowicie zabezpieczyć przed nieoczekiwanym zachowaniem związanym z grupowaniem, zawsze możesz dodać %>% ungroupdo potoku po podsumowaniu.

Empiromancer
źródło
czy aktualizuje się, 0.7.0aby system quote-unquote był dostępny z kilkoma kolumnami?
JelenaČuklina
4
Można również użyć .dotsargumentów group_by()jako takie: data %>% group_by(.dots = columns) %>% summarize(value = mean(value)).
Paul Rougieux,
Czy wezwanie do one_of()zrobienia czegoś tutaj? Myślę, że jest to zbędne w tym kontekście, ponieważ wyrażenie jest opakowane w wywołanie vars().
knowah
@Khashir tak, to odpowiedź nadal działa @knowah masz rację, wywołanie one_of()jest zbędne w tym kontekście
Empiromancer
1
@Sos Aby zastosować funkcję w wielu kolumnach przy użyciu selectskładni, zobacz nową acrossfunkcję: dplyr.tidyverse.org/reference/across.html W twoim przypadku wyglądałoby to summarize(across(all_of(c(''value_A", "value_B")), mean))
mniej więcej
102

Aby napisać cały kod, oto aktualizacja odpowiedzi Hadley z nową składnią:

library(dplyr)

df <-  data.frame(
    asihckhdoydk = sample(LETTERS[1:3], 100, replace=TRUE),
    a30mvxigxkgh = sample(LETTERS[1:3], 100, replace=TRUE),
    value = rnorm(100)
)

# Columns you want to group by
grp_cols <- names(df)[-3]

# Convert character vector to list of symbols
dots <- lapply(grp_cols, as.symbol)

# Perform frequency counts
df %>%
    group_by_(.dots=dots) %>%
    summarise(n = n())

wynik:

Source: local data frame [9 x 3]
Groups: asihckhdoydk

  asihckhdoydk a30mvxigxkgh  n
1            A            A 10
2            A            B 10
3            A            C 13
4            B            A 14
5            B            B 10
6            B            C 12
7            C            A  9
8            C            B 12
9            C            C 10
James Owers
źródło
1
Wydaje się, że nadal jest to zakodowane na stałe nazwy kolumn, tylko w formule. Chodzi o to, jak używać ciągów znaków, aby nie musieć pisać asihckhdoydk...
Gregor Thomas
1
Zaktualizowałem rozwiązanie za pomocą dots <- lapply(names(df)[-3], function(x) as.symbol(x))do tworzenia .dotsargumentu
James Owers
4
próba uporządkowania tych odpowiedzi .dots=była kluczowym krokiem. jeśli ktoś dobrze orientuje się, dlaczego jest to wymagane w group_byrozmowie, czy możesz edytować tę odpowiedź? teraz jest to trochę nieodgadnione.
Andrew,
12
vignette("nse")wskazuje, że istnieją trzy sposoby cytowania, które są dopuszczalne: wzór, cytat i znak. O ile nie martwisz się o to, z jakiego środowiska będzie pochodzić, prawdopodobnie możesz uciecgroup_by_(.dots=grp_cols)
Ari B. Friedman
58

Obsługa tego w dplyr jest obecnie dość słaba, ostatecznie myślę, że składnia będzie wyglądać mniej więcej tak:

df %.% group_by(.groups = c("asdfgfTgdsx", "asdfk30v0ja"))

Ale to prawdopodobnie nie będzie przez jakiś czas (ponieważ muszę przemyśleć wszystkie konsekwencje).

W międzyczasie możesz użyć regroup(), który pobiera listę symboli:

library(dplyr)

df <-  data.frame(
  asihckhdoydk = sample(LETTERS[1:3], 100, replace=TRUE),
  a30mvxigxkgh = sample(LETTERS[1:3], 100, replace=TRUE),
  value = rnorm(100)
)

df %.%
  regroup(list(quote(asihckhdoydk), quote(a30mvxigxkgh))) %.%
  summarise(n = n())

Jeśli masz wektor znaków nazw kolumn, możesz przekonwertować je na właściwą strukturę za pomocą lapply()i as.symbol():

vars <- setdiff(names(df), "value")
vars2 <- lapply(vars, as.symbol)

df %.% regroup(vars2) %.% summarise(n = n())
hadley
źródło
6
as.symbolrozwiązuje to. Dzięki! W przypadku, gdy pomaga to w rozwoju: ten scenariusz jest dla mnie bardzo powszechny. Zagreguj wynik liczbowy dla każdej kombinacji innych zmiennych.
sharoz
najwyraźniej działa to tylko dla tego konkretnego przykładu i żadnego innego.
Paulo E. Cardoso
3
Początkowo oznaczyłem to jako odpowiedź, ale aktualizacje dplyr pozwalają odpowiedzi kungfujam działać.
sharoz
regroupjest również przestarzała (przynajmniej od wersji 0.4.3).
Berk U.
27

Specyfikacja łańcuchów kolumn w programie dplyrjest teraz obsługiwana przez warianty dplyrfunkcji z nazwami zakończonymi podkreśleniem. Na przykład, odpowiadająca group_byfunkcji istnieje group_by_funkcja, która może przyjmować argumenty w postaci łańcuchów. Ta winieta szczegółowo opisuje składnię tych funkcji.

Poniższy fragment jednoznacznie rozwiązuje problem, który pierwotnie postawił @sharoz (zwróć uwagę na potrzebę zapisania .dotsargumentu):

# Given data and columns from the OP

data %>%
    group_by_(.dots = columns) %>%
    summarise(Value = mean(value))

(Zauważ, że dplyr używa teraz %>%operatora i %.%jest przestarzały).

Edward
źródło
17

Dopóki dplyr nie będzie w pełni obsługiwał argumentów łańcuchowych, być może ta treść jest przydatna:

https://gist.github.com/skranz/9681509

Zawiera kilka funkcji opakowujących, takich jak s_group_by, s_mutate, s_filter itp., Które używają argumentów łańcuchowych. Można je mieszać z normalnymi funkcjami programu dplyr. Na przykład

cols = c("cyl","gear")
mtcars %.%
  s_group_by(cols) %.%  
  s_summarise("avdisp=mean(disp), max(disp)") %.%
  arrange(avdisp)
Sebastian Kranz
źródło
11

Działa, jeśli przekażesz mu obiekty (cóż, nie jesteś, ale ...), a nie jako wektor znakowy:

df %.%
    group_by(asdfgfTgdsx, asdfk30v0ja) %.%
    summarise(Value = mean(value))

> df %.%
+   group_by(asdfgfTgdsx, asdfk30v0ja) %.%
+   summarise(Value = mean(value))
Source: local data frame [9 x 3]
Groups: asdfgfTgdsx

  asdfgfTgdsx asdfk30v0ja        Value
1           A           C  0.046538002
2           C           B -0.286359899
3           B           A -0.305159419
4           C           A -0.004741504
5           B           B  0.520126476
6           C           C  0.086805492
7           B           C -0.052613078
8           A           A  0.368410146
9           A           B  0.088462212

gdzie dfbył twój data.

?group_by mówi:

 ...: variables to group by. All tbls accept variable names, some
      will also accept functons of variables. Duplicated groups
      will be silently dropped.

które interpretuję jako oznaczające nie wersje znakowe imion, ale sposób, w jaki można się do nich odnieść foo$bar; barnie jest tutaj cytowany. Albo jak chcesz odwołać się do zmiennych w formule: foo ~ bar.

@Arun wspomina również, że możesz:

df %.%
    group_by("asdfgfTgdsx", "asdfk30v0ja") %.%
    summarise(Value = mean(value))

Ale nie możesz przekazać czegoś, co nie zostało ocenione jako nazwa zmiennej w obiekcie danych.

Przypuszczam, że jest to spowodowane wewnętrznymi metodami, których Hadley używa do wyszukiwania rzeczy, które przekazujesz za pomocą ...argumentu.

Gavin Simpson
źródło
1
@Arun Dzięki za to. Nie zauważyłem tego, ale to też ma sens. Dodałem uwagę w tym względzie, cytując Ciebie i Twój komentarz.
Gavin Simpson
4
Niestety nie mogę polegać na twardym zakodowaniu nazw kolumn. Próbuję to zrobić bez konieczności ich określania.
sharoz
4
data = data.frame(
  my.a = sample(LETTERS[1:3], 100, replace=TRUE),
  my.b = sample(LETTERS[1:3], 100, replace=TRUE),
  value = rnorm(100)
)

group_by(data,newcol=paste(my.a,my.b,sep="_")) %>% summarise(Value=mean(value))
Jordania
źródło
4

Jeden (mały) przypadek, którego brakuje w odpowiedziach tutaj, który chciałem wyjaśnić, to sytuacja, gdy zmienne do grupowania są generowane dynamicznie w potoku:

library(wakefield)
df_foo = r_series(rnorm, 10, 1000)
df_foo %>% 
  # 1. create quantized versions of base variables
  mutate_each(
    funs(Quantized = . > 0)
  ) %>% 
  # 2. group_by the indicator variables
  group_by_(
    .dots = grep("Quantized", names(.), value = TRUE)
    ) %>% 
  # 3. summarize the base variables
  summarize_each(
    funs(sum(., na.rm = TRUE)), contains("X_")
  )

To w zasadzie pokazuje, jak używać grepw połączeniu z, group_by_(.dots = ...)aby to osiągnąć.

tchakravarty
źródło
3

Ogólny przykład użycia .dotsargumentu jako wejścia wektora znakowego do dplyr::group_byfunkcji:

iris %>% 
    group_by(.dots ="Species") %>% 
    summarise(meanpetallength = mean(Petal.Length))

Lub bez zakodowanej na stałe nazwy zmiennej grupującej (zgodnie z zapytaniem OP):

iris %>% 
    group_by(.dots = names(iris)[5]) %>% 
    summarise_at("Petal.Length", mean)

Na przykładzie PO:

data %>% 
    group_by(.dots =names(data)[-3]) %>% 
    summarise_at("value", mean)

Zobacz także winietę dplyr dotyczącą programowania, która wyjaśnia zaimki, quasi-cudzysłowy, kłamstwa i tidyeval.

Paul Rougieux
źródło