Dplyr: Conservar grupos de longitud cero

Creado en 20 mar. 2014  ·  44Comentarios  ·  Fuente: tidyverse/dplyr

http://stackoverflow.com/questions/22523131

No estoy seguro de cuál debería ser la interfaz para esto; probablemente debería por defecto drop = FALSE.

feature wip

Comentario más útil

+1: esto es un factor decisivo para muchos análisis

Todos 44 comentarios

Gracias por abrir este número, Hadley.

: +1: me encontré con el mismo problema hoy, drop = FALSE sería de gran ayuda para mí.

¿Alguna idea sobre el marco de tiempo para poner un equivalente .drop = FALSE en dplyr? Necesito esto para que ciertos rCharts se procesen correctamente.

Mientras tanto, obtuve la respuesta en su enlace para que funcione.
http://stackoverflow.com/questions/22523131

Agrupé por dos variables.

+1 para la opción de no eliminar grupos vacíos

Puede haber cierta superposición con # 486 y # 413.

No descartar grupos vacíos sería muy útil. A menudo se necesita al crear tablas de resumen.

+1: esto es un factor decisivo para muchos análisis

Estoy de acuerdo con todo lo anterior - sería muy útil.

@romainfrancois Actualmente build_index_cpp() no respeta el atributo drop:

t1 <- data_frame(
  x = runif(10),
  g1 = rep(1:2, each = 5),
  g2 = factor(g1, 1:3)
)
g1 <- grouped_df(t1, list(quote(g2)), drop = FALSE)
attr(g1, "group_size")
# should be c(5L, 5L, 0L)
attr(g1, "indices")
# shoud be list(0:4, 5:9, integer(0))

El atributo de caída solo se aplica cuando se agrupa por un factor, en cuyo caso necesitamos tener un grupo por nivel de factor, independientemente de si el nivel realmente se aplica o no a los datos.

Esto también afectará a los verbos de una sola tabla de las siguientes maneras:

  • select() : sin efecto
  • arrange() : sin efecto
  • summarise() : las funciones aplicadas a grupos de filas cero deben recibir enteros de nivel 0. n() debería devolver 0, mean(x) debería devolver NaN
  • filter() : el conjunto de grupos debe permanecer constante, incluso si algunos grupos ahora no tienen filas
  • mutate() : no es necesario evaluar expresiones para grupos vacíos

Eventualmente, drop = FALSE será el valor predeterminado, y si es complicado escribir las ramas drop = FALSE y drop = TRUE , felizmente dejaría de ofrecer soporte para drop = FALSE ( ya que siempre puede volver a nivelar el factor usted mismo o utilizar un vector de caracteres en su lugar).

¿Tiene sentido? Si es mucho trabajo, podemos avanzar a 0.4

@statwonk , @wsurles , @jennybc , @slackline , @mcfrank , @ eipi10 Si desea ayudar, lo mejor que puede hacer es trabajar en un conjunto de casos de prueba que ejercite todas las formas en que los diferentes verbos pueden interactuar con grupos de longitud cero.

Ah. Creo que simplemente no sabía qué se suponía que debía hacer drop . Eso lo deja claro. No creo que sea mucho trabajo.

Abrí la solicitud de extracción # 833 que prueba si los verbos de tabla única anteriores manejan grupos de longitud cero correctamente. La mayoría de las pruebas están comentadas, porque dplyr actualmente las falla, por supuesto.

+1, ¿alguna actualización de estado aquí? amor resumir, necesidad de mantener los niveles vacíos!

@ebergelson , aquí está mi truco actual para obtener grupos de longitud cero. A menudo lo necesito para que se apilen mis gráficos de barras.

Aquí df tiene 3 columnas: nombre, grupo y métrica

df2 <- expand.grid(name = unique(df$name), group = unique(df$group)) %>%
    left_join(df, by=c("name","group")) %>%
    mutate(metric = ifelse(is.na(metric),0,metric))

Hago algo similar: compruebo los grupos que faltan, luego, si alguno, genera todas las combinaciones y left_join .

Desafortunadamente, no parece que este problema esté recibiendo mucho amor ... quizás porque existe una solución sencilla.

@wsurles , @bpbond gracias, sí, utilicé una solución similar a lo que sugieres. Me encantaría ver una solución integrada como .drop.

Solo para agregar y estar de acuerdo con todos los anteriores: este es un aspecto súper crítico de muchos análisis. Me encantaría ver una implementación.

Algunos detalles más necesarios aquí:

Si tengo esto:

> df <- data_frame( x = c(1,1,1,2,2), f = factor( c(1,2,3,1,1) ) )
> df
Source: local data frame [5 x 2]

  x f
1 1 1
2 1 2
3 1 3
4 2 1
5 2 1

Y agrupo por x luego f , terminaría con 6 (2x3) grupos donde los grupos (2, 2) y (2,3) están vacíos. Está bien. Puedo lograr implementar eso, creo.

ahora, ¿y si tengo esto?

> df <- data_frame( f = factor( c(1,1,2,2), levels = 1:3), x = c(1,2,1,4) )
> df
Source: local data frame [4 x 2]

  f x
1 1 1
2 1 2
3 2 1
4 2 4

y quiero agrupar por f luego x . ¿Cuáles serían los grupos? @hadley

Tanto stats::aggregate como plyr::ddply devuelven 4 grupos en este caso (1,1; 1,2; 2,1; y 2,4), por lo que sugeriría que ese es el comportamiento a seguir .

¿No debería estar de acuerdo con table() lugar, es decir, devolver 9 grupos?

> table(df$f, df$x)
  1 2 4
1 1 1 0
2 1 0 1
3 0 0 0

Esperaría que df %>% group_by(f, x) %>% tally dé básicamente el mismo resultado que with(df, as.data.frame(table(f, x))) y ddply(df, .(f, x), nrow, .drop=FALSE) .

Pensé que nuestro comportamiento deseado era preservar los grupos de longitud cero si son factores (como .drop in plyr), así que imagino que querríamos la sugerencia de consulte la sugerencia de

Hmmm, es difícil entender exactamente cuál debería ser el comportamiento. ¿Estos experimentos mentales tan simples parecen correctos?

df <- data_frame(x = 1, y = factor(1, levels = 2))
df %>% group_by(x) %>% summarise(n())
#> x n
#> 1 1  

df %>% group_by(y) %>% summarise(n())
#> y n
#> 1 1
#> 2 0

df %>% group_by(x, y) %>% summarise(n()
#> x y n
#> 1 1 1
#> 1 2 0

Pero, ¿qué pasa si x tiene varios valores? ¿Debería funcionar así?

df <- data_frame(x = 1:2, y = factor(1, levels = 2))
df %>% group_by(x, y) %>% summarise(n()
#> x y n
#> 1 1 1
#> 2 1 1
#> 1 1 0
#> 2 2 0

¿Quizás preservar grupos vacíos solo tiene sentido cuando se agrupa por una sola variable? Si lo enmarcamos de manera más realista, por ejemplo, data_frame(age_group = c(40, 60), sex = factor(M, levels = c("F", "M")) , ¿realmente querrías los recuentos de mujeres? Creo que a veces lo harías y otras no. Expandir todas las combinaciones me parece una operación algo diferente (e independiente del uso de factores).

¿Quizás group_by necesita argumentos drop y expand ? drop = FALSE mantendría todos los grupos de tamaño cero generados por niveles de factor que no aparecen en los datos. expand = TRUE mantendría todos los grupos de tamaño cero generados por combinaciones de valores que no aparecen en los datos.

@hadley Tus ejemplos me parecen correctos (asumiendo que te levels = 1:2 , no a levels = 2 ). Y creo que preservar los grupos vacíos tiene sentido incluso cuando se agrupan por varias variables. Por ejemplo, si las variables fueran sex ( male y female ) y answer (en un cuestionario, con niveles disagree , neutral , agree ), y deseaba contar la frecuencia de cada respuesta para cada sexo (por ejemplo, para una tabla o para un trazado posterior), no querría eliminar una categoría de respuesta solo porque ninguna mujer respondió.

También esperaría que las variables de factor sigan siendo variables de factor en los data_frame resultantes (no convertidos en cadenas), y con los _ niveles originales_. (Por lo tanto, al trazar los datos, las categorías de respuesta estarían en el orden correcto, no en orden alfabético agree , disagree , neutral ).

Para su último ejemplo, _en algunos casos_ sería natural eliminar la variable sex (p. Ej., Si _intencionalmente_ no se encuestaron mujeres), y _en otros casos_ no (p. Ej., Al contar el número de defectos de nacimiento estratificados por sexo (y quizás año)). Pero esto puede (y debe) resolverse fácilmente _después_ de agregar los datos. (Una solución diferente sería aceptar un argumento _valuado por vectores_ .drop . Sería bueno, pero supongo que podría complicar las cosas).

(Una solución diferente sería aceptar un argumento .drop con valor vectorial. Sería bueno, pero supongo que podría complicar las cosas).

Sí, probablemente demasiado complicado. De lo contrario, estoy de acuerdo con los comentarios de @huftis .

@hadley
creo
SÍ amplíe todas las combinaciones de valores al group_by si existen en los datos.
NO, no amplíe los niveles de factores que no existen.

Mi caso de uso más frecuente es preparar un conjunto de datos resumidos para un gráfico (durante la exploración). Y los gráficos deben tener todas las combinaciones de valores. Pero no es necesario que tengan niveles de factor que tengan 0 para todos los grupos. Por ejemplo, no se puede apilar un gráfico de barras sin todas las combinaciones. Pero no necesita valores de factor que no existan en los datos, estos solo serían 0 cuando se apilaran y un valor vacío en la leyenda.

Creo que expandir todos los valores al group_by debería ser el predeterminado porque es mucho más fácil (y mucho más intuitivo) filtrar 0 casos después del group by si es necesario. No creo que sea necesario un argumento .drop, porque es bastante fácil filtrar 0 casos después. No usamos argumentos adicionales para ninguna de las otras funciones, por lo que esto rompería el molde. El valor predeterminado debería ser mostrar resultados para todas las combinaciones de valores existentes basados ​​en group_by.

Creo que este sería el comportamiento predeterminado correcto. Aquí, el único solo ampliará los valores existentes en el factor, no todos los niveles de factor. (Esto es lo que ejecuto después de ejecutar un group_by que cae 0 valores)

## Expand data so plot groups works correctly
  df2 <- expand.grid(name = unique(df$name), group = unique(df$group)) %>%
    left_join(df, by=c("name","group")) %>%
    mutate(
      measure = ifelse(is.na(measure),0,measure)
    )

El único caso en el que puedo ver dónde querría un valor a pesar de que todos los grupos tenían cero es con datos de tiempo. Quizás falte un día de datos en algún punto intermedio. Aquí aún sería necesario expandir y unirse en un rango de fechas. El caso del nivel de factor no se aplicaría. Creo que es justo que el analizador de datos maneje las fechas faltantes por su cuenta.

Gracias por todo su gran trabajo en esta biblioteca. El 90% de mi trabajo usa dplyr. :)

Estoy totalmente de acuerdo con @huftis.

No creo que la caída de niveles o combinaciones de niveles deba tener algo que ver con los datos. Es posible que esté creando un prototipo de una función o figura con una pequeña muestra. O haciendo una operación de dividir-aplicar-combinar, en cuyo caso desea una garantía de que la salida de cada grupo será conforme con el resto.

Suavizar mi posición: creo que vale la pena considerar si el comportamiento predeterminado debería diferir cuando la variable de agrupación ya es un factor adecuado frente a cuando se está obligando a factorizar. Puedo ver que la obligación de mantener los niveles no utilizados podría ser menor en el caso de coacción. Pero si me he tomado la molestia de establecer algo como factor y tomar el control de los niveles ... normalmente hay una buena razón y no quiero luchar constantemente para preservar eso.

FYIW, también me gustaría ver esta función. Tengo un escenario similar al descrito por @huftis y tengo que pasar por aros para obtener los resultados que necesito.

Vine aquí desde SO. ¿No es esto en lo que se supone que debe ayudar complete de "tidyr"?

Si lo hace. De hecho, recientemente aprendí acerca de 'completo' y parece lograr esto de una manera reflexiva.

Implementar eso para los backends de SQL parece difícil, porque de forma predeterminada eliminarán todos los grupos. ¿Lo dejamos así y quizás implementemos tidyr :: complete () para SQL?

Creé el número 3033 sin darme cuenta de que este problema ya existía; disculpas por el duplicado. Para agregar mi propia sugerencia humilde, actualmente uso pull() y forcats::fct_count() como solución temporal a este problema.

Sin embargo, no soy fanático de este método porque fct_count() traiciona el principio tidyverse de hacer una salida que siempre es del mismo tipo que la entrada (es decir, esta función crea un tibble a partir de un vector), y tengo para cambiar el nombre de las columnas en la salida. Esto crea 3 pasos ( pull() %>% fct_count() %>% rename() ) cuando dplyr::count() estaba destinado a cubrir uno. Sería fantástico si forcats::fct_count() y dplyr::count() pudieran fusionarse de alguna manera y desaprobar forcats::fct_count() .

¿Funciona tidyr::complete() para los factores?

Todos los niveles de factores y combinaciones de niveles de factores deben conservarse de forma predeterminada. Este comportamiento puede ser controlado por parámetros como drop , expand , etc. Por lo tanto, el comportamiento predeterminado de dplyr::count() debería ser así:

df <- data.frame(x = 1:2, y = factor(c(1, 1), levels = 1:2))
df %>% dplyr::count(x, y)
#>  # A tibble: 4 x 3
#>       x        y       n
#>     <int>   <fct>    <int>
#> 1     1        1       1
#> 2     2        1       1
#> 3     1        2       0
#> 4     2        2       0

Los grupos de longitud cero (combinaciones de grupos) se pueden filtrar más tarde. Pero para el análisis exploratorio debemos ver el cuadro completo.

  1. ¿Hay actualizaciones de estado sobre la solución a este problema?
  2. ¿Hay planes para resolver completamente este problema?

2: si definitivamente
1: Hay algunas dificultades de implementación técnica sobre este problema, pero lo investigaré en las próximas semanas.

Podríamos salirse con la nuestra expandiendo los datos después del hecho, algo como esto:

library(tidyverse)

truly_group_by <- function(data, ...){
  dots <- quos(...)
  data <- group_by( data, !!!dots )

  labels <- attr( data, "labels" )
  labnames <- names(labels)
  labels <- mutate( labels, ..index.. =  attr(data, "indices") )

  expanded <- labels %>%
    tidyr::expand( !!!dots ) %>%
    left_join( labels, by = labnames ) %>%
    mutate( ..index.. = map(..index.., ~if(is.null(.x)) integer() else .x ) )

  indices <- pull( expanded, ..index..)
  group_sizes <- map_int( indices, length)
  labels <- select( expanded, -..index..)

  attr(data, "labels")  <- labels
  attr(data, "indices") <- indices
  attr(data, "group_sizes") <- group_sizes

  data
}

df  <- data_frame(
  x = 1:2,
  y = factor(c(1, 1), levels = 1:2)
)
tally( truly_group_by(df, x, y) )
#> # A tibble: 4 x 3
#> # Groups:   x [?]
#>       x y         n
#>   <int> <fct> <int>
#> 1     1 1         1
#> 2     1 2         0
#> 3     2 1         1
#> 4     2 2         0
tally( truly_group_by(df, y, x) )
#> # A tibble: 4 x 3
#> # Groups:   y [?]
#>   y         x     n
#>   <fct> <int> <int>
#> 1 1         1     1
#> 2 1         2     1
#> 3 2         1     0
#> 4 2         2     0

obviamente en el futuro, esto se manejaría internamente, sin usar tidyr o purrr.

Esto parece resolver la pregunta original así:

> df = data.frame(a=rep(1:3,4), b=rep(1:2,6))
> df$b = factor(df$b, levels=1:3)
> df %>%
+   group_by(b) %>%
+   summarise(count_a=length(a), .drop=FALSE)
# A tibble: 2 x 3
  b     count_a .drop
  <fct>   <int> <lgl>
1 1           6 FALSE
2 2           6 FALSE
> df %>%
+   truly_group_by(b) %>%
+   summarise(count_a=length(a), .drop=FALSE)
# A tibble: 3 x 3
  b     count_a .drop
  <fct>   <int> <lgl>
1 1           6 FALSE
2 2           6 FALSE
3 3           0 FALSE

La clave aquí es esta

 tidyr::expand( !!!dots ) %>%

lo que significa expandir todas las posibilidades independientemente de que las variables sean factores o no.

Yo diría que nosotros tampoco:

  • expandir todo cuando drop=FALSE , potencialmente teniendo muchos grupos de 0 longitudes
  • haz lo que hacemos ahora si drop=TRUE

quizás tenga una función para alternar la caída.

Esta es una operación relativamente barata, diría yo porque solo implica manipular los metadatos, por lo que quizás sea menos riesgoso hacer esto en R primero.

¿Quiso decir crossing() lugar de expand() ?

En cuanto a los aspectos internos, ¿está de acuerdo en que "solo" necesitamos cambiar build_index_cpp() , específicamente la generación del marco de datos labels , para que esto suceda?

¿Podemos quizás comenzar expandiendo solo los factores con drop = FALSE ? Consideré una sintaxis "natural", pero esto puede ser demasiado confuso al final (y quizás incluso no lo suficientemente poderoso):

group_by(data, crossing(col1, col2), col3)

Semántica: usando todas las combinaciones de col1 y col2 , y existen combinaciones con col3 .

Sí, diría que esto solo afecta a build_index_cpp y la generación de los atributos labels , indices y group_sizes que me gustaría aplastar en un estructura ordenada como parte de # 3489

La parte de los "únicos factores de expansión" de esta discusión es lo que tomó tanto tiempo.

Cuáles serían los resultados de estos:

library(dplyr)

d <- data_frame(
  f1 = factor( rep( c("a", "b"), each = 4 ), levels = c("a", "b", "c") ),
  f2 = factor( rep( c("d", "e", "f", "g"), each = 2 ), levels = c("d", "e", "f", "g", "h") ),
  x  = 1:8,
  y  = rep( 1:4, each = 2)
)

f <- function(data, ...){
  group_by(data, !!!quos(...))  %>%
    tally()
}
f(d, f1, f2, x)
f(d, x, f1, f2)

f(d, f1, f2, x, y)
f(d, x, f1, f2, y)

Creo que f(d, f1, f2, x) debería dar los mismos resultados que f(d, x, f1, f2) , si se ignora el orden de las filas. Lo mismo para los otros dos.

También interesante:

f(d, f2, x, f1, y)
d %>% sample_frac(0.3) %>% f(...)

Me gusta la idea de implementar la expansión completa solo por factores. Para datos que no son caracteres (incluidos los lógicos), podríamos definir / usar una clase similar a un factor que hereda el tipo de datos respectivo. ¿Quizás proporcionado por forcats ? Esto hace que sea más difícil dispararse en el pie.

implementación en progreso en # 3492

library(dplyr)
#> 
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#> 
#>     filter, lag
#> The following objects are masked from 'package:base':
#> 
#>     intersect, setdiff, setequal, union
df <- data_frame( f = factor( c(1,1,2,2), levels = 1:3), x = c(1,2,1,4) )

( res1 <- tally(group_by(df,f,x, drop = FALSE)) )
#> # A tibble: 9 x 3
#> # Groups:   f [?]
#>   f         x     n
#>   <fct> <dbl> <int>
#> 1 1        1.     1
#> 2 1        2.     1
#> 3 1        4.     0
#> 4 2        1.     1
#> 5 2        2.     0
#> 6 2        4.     1
#> 7 3        1.     0
#> 8 3        2.     0
#> 9 3        4.     0
( res2 <- tally(group_by(df,x,f, drop = FALSE)) )
#> # A tibble: 9 x 3
#> # Groups:   x [?]
#>       x f         n
#>   <dbl> <fct> <int>
#> 1    1. 1         1
#> 2    1. 2         1
#> 3    1. 3         0
#> 4    2. 1         1
#> 5    2. 2         0
#> 6    2. 3         0
#> 7    4. 1         0
#> 8    4. 2         1
#> 9    4. 3         0

all.equal( res1, arrange(res2, f, x) )
#> [1] TRUE

all.equal( filter(res1, n>0), tally(group_by(df, f, x)) )
#> [1] TRUE
all.equal( filter(res2, n>0), tally(group_by(df, x, f)) )
#> [1] TRUE

Creado el 2018-04-10 por el paquete reprex (v0.2.0).

En cuanto a si complete() resuelve el problema, no, en realidad no. Independientemente de los resúmenes que se estén calculando, sus comportamientos en vectores vacíos deben conservarse, no corregirse después del hecho. Por ejemplo:

data.frame(x=factor(1, levels=1:2), y=4:5) %>%
     group_by(x) %>%
     summarize(min=min(y), sum=sum(y), prod=prod(y))
# Should be:
#> x       min   sum  prod
#> 1         4     9    20
#> 2       Inf     0     1

sum y prod (y en menor medida, min ) (y varias otras funciones) tienen semánticas muy bien definidas en vectores vacíos, y no es bueno tener que hacerlo ven después con complete() y redefine esos comportamientos.

@kenahoo No estoy seguro de entender. Esto es lo que obtienes con la versión de desarrollo actual. Entonces, lo único que no recibe es la advertencia de min()

library(dplyr)

data.frame(x=factor(1, levels=1:2), y=4:5) %>%
  group_by(x) %>%
  summarize(min=min(y), sum=sum(y), prod=prod(y))
#> # A tibble: 2 x 4
#>   x       min   sum  prod
#>   <fct> <dbl> <int> <dbl>
#> 1 1         4     9    20
#> 2 2       Inf     0     1

min(integer())
#> Warning in min(integer()): no non-missing arguments to min; returning Inf
#> [1] Inf
sum(integer())
#> [1] 0
prod(integer())
#> [1] 1

Creado el 2018-05-15 por el paquete reprex (v0.2.0).

@romainfrancois Oh, genial, no me di cuenta de que ya estabas tan

Este antiguo problema se ha bloqueado automáticamente. Si cree que ha encontrado un problema relacionado, presente un nuevo problema (con reprex) y enlace a este problema. https://reprex.tidyverse.org/

¿Fue útil esta página
0 / 5 - 0 calificaciones