Agrupe por varias columnas en dplyr, usando la entrada de vector de cadena

157

Estoy tratando de transferir mi comprensión de plyr a dplyr, pero no puedo entender cómo agrupar por múltiples columnas.

# 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

¿Qué me falta para traducir el ejemplo de plyr en una sintaxis dplyr-esque?

Edición 2017 : Dplyr se ha actualizado, por lo que hay disponible una solución más simple. Ver la respuesta seleccionada actualmente.

sharoz
fuente
3
Acabo de llegar aquí, ya que era el mejor google. Puede usar group_by_ahora explicado envignette("nse")
James Owers
3
@kungfujam: Eso parece agruparse solo por la primera columna, no por el par de columnas
sharoz
1
Necesitas usar .dots. Aquí está la solución adaptada de la respuesta de @hadley a continuación:df %>% group_by_(.dots=list(quote(asihckhdoydk), quote(a30mvxigxkgh))) %>% summarise(n = n())
James Owers
1
Puse el código completo en una respuesta a continuación
James Owers
1
Como alguien señaló en una respuesta al comentario, el objetivo es no requerir nombres de columna codificados.
sharoz

Respuestas:

52

Desde que se publicó esta pregunta, dplyr agregó versiones de alcance de group_by( documentación aquí ). Esto le permite usar las mismas funciones que usaría select, así:

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 

El resultado de su pregunta de ejemplo es el esperado (consulte la comparación con plyr arriba y el resultado abajo):

# 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

Tenga en cuenta que dado que dplyr::summarizesolo elimina una capa de agrupación a la vez, todavía tiene algo de agrupación en el tibble resultante (que en ocasiones puede atrapar a las personas por sorpresa más adelante en la línea). Si desea estar absolutamente a salvo del comportamiento de agrupación inesperado, siempre puede agregar %>% ungroupa su canalización después de resumir.

Empiromancer
fuente
¿Se actualiza para 0.7.0que el sistema de presupuesto entre comillas esté disponible también con varias columnas?
JelenaČuklina
44
También puede utilizar los .dotsargumentos que group_by()como tal: data %>% group_by(.dots = columns) %>% summarize(value = mean(value)).
Paul Rougieux
¿La llamada para one_of()hacer algo aquí? Creo que es redundante en este contexto, ya que la expresión está envuelta en una llamada a vars().
knowah
@Khashir sí, esta respuesta aún funciona @knowah Tienes razón, la llamada a one_of()es redundante en este contexto
Empiromancer
1
@Sos Para aplicar una función en varias columnas usando la selectsintaxis, vea la nueva acrossfunción: dplyr.tidyverse.org/reference/across.html En su caso, se vería algo asísummarize(across(all_of(c(''value_A", "value_B")), mean))
Empiromancer
102

Solo para escribir el código completo, aquí hay una actualización de la respuesta de Hadley con la nueva sintaxis:

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

salida:

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
fuente
1
Esto parece seguir codificando los nombres de las columnas, solo en una fórmula. El punto de la pregunta es cómo usar cadenas para no tener que escribir asihckhdoydk...
Gregor Thomas
1
Tener una solución actualizada dots <- lapply(names(df)[-3], function(x) as.symbol(x))para crear el .dotsargumento
James Owers
44
tratar de clasificar estas respuestas .dots=fue el paso crucial. Si alguien group_bysabe bien por qué es necesario en la llamada, ¿puede editar esta respuesta? En este momento es un poco inescrutable.
Andrew
12
vignette("nse")indica que hay tres formas de citar que son aceptables: fórmula, cita y carácter. A menos que esté preocupado por el entorno del que saldrá, probablemente pueda salirse con la suyagroup_by_(.dots=grp_cols)
Ari B. Friedman
58

El soporte para esto en dplyr es actualmente bastante débil, eventualmente creo que la sintaxis será algo así como:

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

Pero eso probablemente no estará allí por un tiempo (porque necesito pensar en todas las consecuencias).

Mientras tanto, puede usar regroup(), que toma una 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())

Si tiene un vector de caracteres de nombres de columna, puede convertirlos a la estructura correcta con lapply()y as.symbol():

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

df %.% regroup(vars2) %.% summarise(n = n())
Hadley
fuente
66
as.symbollo resuelve ¡Gracias! En caso de que ayude con el desarrollo: este escenario es muy común para mí. Agregue un resultado numérico sobre cada combinación de las otras variables.
sharoz
aparentemente esto solo funciona para este ejemplo en particular y no para otro.
Paulo E. Cardoso
3
Originalmente marqué esto como la respuesta, pero las actualizaciones de dplyr permiten que funcione la respuesta de kungfujam.
sharoz
regrouptambién está en desuso (al menos a partir de la versión 0.4.3).
Berk U.
27

La especificación de cadenas de columnas dplyrahora se admite a través de variantes de las dplyrfunciones con nombres que terminan en un guión bajo. Por ejemplo, correspondiente a la group_byfunción hay una group_by_función que puede tomar argumentos de cadena. Esta viñeta describe la sintaxis de estas funciones en detalle.

El siguiente fragmento resuelve limpiamente el problema que originalmente planteó @sharoz (tenga en cuenta la necesidad de escribir el .dotsargumento):

# Given data and columns from the OP

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

(Tenga en cuenta que dplyr ahora usa el %>%operador y %.%está en desuso).

Eduardo
fuente
17

Hasta que dplyr tenga soporte completo para los argumentos de cadena, quizás esta esencia es útil:

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

Contiene un montón de funciones de contenedor como s_group_by, s_mutate, s_filter, etc. que usan argumentos de cadena. Puede mezclarlos con las funciones normales de dplyr. Por ejemplo

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

Funciona si le pasas los objetos (bueno, no lo eres, pero ...) en lugar de hacerlo como un vector de caracteres:

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

donde dffue tu data.

?group_by dice:

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

lo que interpreto no significa las versiones de los personajes de los nombres, sino cómo se referiría a ellos en foo$bar; barNo se cita aquí. O cómo le gustaría referirse a variables en una fórmula: foo ~ bar.

@Arun también menciona que puedes hacer:

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

Pero no puede pasar algo que no se haya evaluado no es el nombre de una variable en el objeto de datos.

Supongo que esto se debe a los métodos internos que Hadley está utilizando para buscar las cosas que pasa a través del ...argumento.

Gavin Simpson
fuente
1
@Arun Gracias por eso. No me había dado cuenta de eso, pero también tiene sentido. Agregué una nota al respecto, citandote a ti y a tu comentario.
Gavin Simpson
44
Desafortunadamente, no puedo confiar en codificar los nombres de las columnas. Estoy tratando de hacer esto sin tener que especificarlos.
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án
fuente
4

Un caso (pequeño) que falta en las respuestas aquí, que quería hacer explícito, es cuando las variables para agrupar se generan dinámicamente a mitad de camino en una tubería:

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

Básicamente, esto muestra cómo usar grepjunto con group_by_(.dots = ...)para lograr esto.

tchakravarty
fuente
3

Ejemplo general sobre el uso del .dotsargumento como entrada de vector de caracteres para la dplyr::group_byfunción:

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

O sin un nombre codificado para la variable de agrupación (como lo solicitó el OP):

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

Con el ejemplo del OP:

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

Vea también la viñeta dplyr sobre programación que explica pronombres, cuasiquotación, quosures y tidyeval.

Paul Rougieux
fuente