Agrupe por várias colunas no dplyr, usando a entrada de vetor de sequência

157

Estou tentando transferir meu entendimento do plyr para o dplyr, mas não consigo descobrir como agrupar por várias colunas.

# 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

O que estou perdendo para traduzir o exemplo plyr em uma sintaxe do tipo dplyr?

Edit 2017 : Dplyr foi atualizado, então uma solução mais simples está disponível. Veja a resposta atualmente selecionada.

sharoz
fonte
3
Acabei de chegar aqui, pois foi top google. group_by_Agora você pode usar o explicado emvignette("nse")
James Owers
3
@kungfujam: Isso parece único grupo pela primeira coluna, e não o par de colunas
sharoz
1
Você precisa usar .dots. Aqui está a solução adaptada da resposta da @hadley abaixo: #df %>% group_by_(.dots=list(quote(asihckhdoydk), quote(a30mvxigxkgh))) %>% summarise(n = n())
James Owers
1
Ter colocado código completo em uma resposta abaixo
James Owers
1
Como alguém apontou em uma resposta ao comentário, o objetivo é não exigir nomes de colunas codificados.
sharoz

Respostas:

52

Desde que esta pergunta foi publicada, o dplyr adicionou versões com escopo de group_by( documentação aqui ). Isso permite que você use as mesmas funções que usaria select, da seguinte forma:

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 

O resultado da sua pergunta de exemplo é o esperado (veja a comparação do plyr acima e o resultado abaixo):

# 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

Observe que, como dplyr::summarizeapenas retira uma camada de agrupamento de cada vez, você ainda tem alguns agrupamentos na mistura resultante (que às vezes pode pegar as pessoas de surpresa posteriormente). Se você deseja estar absolutamente seguro de comportamentos inesperados de agrupamento, sempre poderá adicionar %>% ungroupao seu pipeline depois de resumir.

Empiromancer
fonte
a atualização para 0.7.0disponibilizar o sistema de aspas entre aspas também com várias colunas?
JelenaČuklina
4
Você também pode usar os .dotsargumentos para group_by()tais como: data %>% group_by(.dots = columns) %>% summarize(value = mean(value)).
Paul Rougieux 19/10/19
A ligação one_of()faz alguma coisa aqui? Eu acho que é redundante nesse contexto, pois a expressão está envolta em uma chamada para vars().
knowah
@Khashir sim, essa resposta ainda funciona @knowah você está certo, a chamada para one_of()é redundante neste contexto
Empiromancer
1
@Sos Para aplicar uma função em várias colunas usando a selectsintaxe, consulte a nova acrossfunção: dplyr.tidyverse.org/reference/across.html No seu caso, seria algo parecido com istosummarize(across(all_of(c(''value_A", "value_B")), mean))
Empiromancer
102

Para escrever o código na íntegra, aqui está uma atualização da resposta de Hadley com a nova sintaxe:

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

resultado:

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
fonte
1
Ainda parece estar codificando os nomes das colunas, apenas em uma fórmula. O objetivo da pergunta é como usar seqüências de caracteres para não precisar digitar asihckhdoydk...
Gregor Thomas
1
Tem solução atualizada usando dots <- lapply(names(df)[-3], function(x) as.symbol(x))para criar o .dotsargumento
James Owers
4
tentar resolver essas respostas .dots=foi o passo crucial. se alguém souber por que isso é necessário na group_byligação, você pode editar esta resposta? agora é um pouco inescrutável.
Andrew
12
vignette("nse")indica que há três maneiras de citar aceitáveis: fórmula, citação e caractere. A menos que você esteja preocupado com o ambiente em que ele se retirará, provavelmente você pode se safargroup_by_(.dots=grp_cols)
Ari B. Friedman
58

O suporte para isso no dplyr é atualmente bastante fraco, eventualmente acho que a sintaxe será algo como:

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

Mas isso provavelmente não estará lá por um tempo (porque eu preciso pensar em todas as consequências).

Enquanto isso, você pode usar regroup(), o que leva uma lista de símbolos:

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

Se você tiver um vetor de caracteres de nomes de colunas, poderá convertê-los na estrutura correta com lapply()eas.symbol() :

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

df %.% regroup(vars2) %.% summarise(n = n())
Hadley
fonte
6
as.symbolresolve isso. Obrigado! Caso isso ajude no desenvolvimento: esse cenário é realmente comum para mim. Agregue um resultado numérico em todas as combinações das outras variáveis.
sharoz
aparentemente isso só funciona para este exemplo em particular e para nenhum outro.
Paulo E. Cardoso
3
Eu originalmente marquei isso como a resposta, mas as atualizações no dplyr permitem que a resposta do kungfujam funcione.
sharoz
regrouptambém está obsoleto (pelo menos a partir da versão 0.4.3).
Berk U.
27

A especificação de sequência de colunas em dplyragora é suportada por variantes das dplyrfunções com nomes terminando em um sublinhado. Por exemplo, correspondente à group_byfunção, existe uma group_by_função que pode receber argumentos de string. Esta vinheta descreve a sintaxe dessas funções em detalhes.

O fragmento a seguir resolve de maneira clara o problema que o @sharoz originalmente colocou (observe a necessidade de escrever o .dotsargumento):

# Given data and columns from the OP

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

(Observe que o dplyr agora usa o %>%operador e %.%está obsoleto).

Edward
fonte
17

Até o dplyr ter suporte total para argumentos de string, talvez essa essência seja útil:

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

Ele contém várias funções de wrapper, como s_group_by, s_mutate, s_filter, etc. que usam argumentos de string. Você pode misturá-los com as funções normais do dplyr. Por exemplo

cols = c("cyl","gear")
mtcars %.%
  s_group_by(cols) %.%  
  s_summarise("avdisp=mean(disp), max(disp)") %.%
  arrange(avdisp)
Sebastian Kranz
fonte
11

Funciona se você passar os objetos (bem, você não é, mas ...), e não como um vetor de caractere:

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

onde dfestava seudata .

?group_by diz:

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

que eu interpreto para significar não as versões dos nomes dos personagens, mas como você se referiria a eles foo$bar; barnão é citado aqui. Ou como você se referiria às variáveis ​​em uma fórmula:foo ~ bar .

O @Arun também menciona que você pode fazer:

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

Mas você não pode passar algo que não avaliado é o nome de uma variável no objeto de dados.

Suponho que isso se deva aos métodos internos que Hadley está usando para procurar as coisas que você passa através do ...argumento.

Gavin Simpson
fonte
1
@ Arun Obrigado por isso. Eu não tinha notado isso, mas também faz sentido. Eu adicionei uma nota a esse respeito, citando você e seu comentário.
Gavin Simpson
4
Infelizmente, não posso confiar em codificar os nomes das colunas. Estou tentando fazer isso sem precisar especificá-los.
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))
Jordânia
fonte
4

Um caso (minúsculo) que está faltando nas respostas aqui, que eu queria tornar explícito, é quando as variáveis ​​a serem agrupadas são geradas dinamicamente no meio do caminho em um pipeline:

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_")
  )

Isso basicamente mostra como usar grepem conjunto com group_by_(.dots = ...)para conseguir isso.

tchakravarty
fonte
3

Exemplo geral de uso do .dotsargumento como entrada de vetor de caracteres para a dplyr::group_byfunção:

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

Ou sem um nome codificado para a variável de agrupamento (conforme solicitado pelo OP):

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

Com o exemplo do OP:

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

Veja também a vinheta dplyr sobre programação, que explica pronomes, quase-cotação, quosures e tidyeval.

Paul Rougieux
fonte