Dplyr: Preservar grupos de comprimento zero

Criado em 20 mar. 2014  ·  44Comentários  ·  Fonte: tidyverse/dplyr

http://stackoverflow.com/questions/22523131

Não tenho certeza de qual deve ser a interface para isso - provavelmente o padrão deve ser drop = FALSE.

feature wip

Comentários muito úteis

+1 - este é um fator decisivo para muitas análises

Todos 44 comentários

Obrigado por abrir este problema, Hadley.

: +1: encontrei o mesmo problema hoje, drop = FALSE seria uma grande ajuda para mim!

Alguma ideia sobre o prazo para colocar um equivalente .drop = FALSE em dplyr? Eu preciso disso para que certos rCharts sejam renderizados corretamente.

Nesse ínterim, recebi a resposta em seu link para o trabalho.
http://stackoverflow.com/questions/22523131

Eu agrupei por duas variáveis.

+1 para a opção de não descartar grupos vazios

Pode haver alguma sobreposição com # 486 e # 413.

Não abandonar grupos vazios seria muito útil. Freqüentemente necessário ao criar tabelas de resumo.

+1 - este é um fator decisivo para muitas análises

Concordo com tudo o que precede - seria muito útil.

@romainfrancois Atualmente build_index_cpp() não respeita o 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))

O atributo drop somente se aplica ao agrupar por um fator, caso em que precisamos ter um grupo por nível de fator, independentemente se o nível realmente se aplica aos dados ou não.

Isso também afetará os verbos da tabela única das seguintes maneiras:

  • select() : nenhum efeito
  • arrange() : nenhum efeito
  • summarise() : funções aplicadas a grupos de linha zero devem receber inteiros de nível 0. n() deve retornar 0, mean(x) deve retornar NaN
  • filter() : o conjunto de grupos deve permanecer constante, mesmo que alguns grupos agora não tenham linhas
  • mutate() : não precisa avaliar expressões para grupos vazios

Eventualmente, drop = FALSE será o padrão, e se for um incômodo escrever os ramos drop = FALSE e drop = TRUE , eu ficaria feliz em cancelar o suporte para drop = FALSE ( já que você pode sempre renivelar o fator sozinho ou usar um vetor de caractere).

Isso faz sentido? Se for muito trabalho, podemos avançar para 0,4

@statwonk , @wsurles , @jennybc , @slackline , @mcfrank , @ eipi10 Se você quiser ajudar, a melhor coisa a fazer seria trabalhar em um conjunto de casos de teste que exercita todas as maneiras como os diferentes verbos podem interagir com grupos de comprimento zero.

Ah. Acho que simplesmente não sabia o que drop deveria fazer. Isso deixa tudo claro. Não acho que seja muito trabalhoso.

Abri a solicitação pull # 833 que testa se os verbos de tabela única acima lidam com grupos de comprimento zero corretamente. A maioria dos testes são comentados, porque dplyr atualmente os reprova, é claro.

+1, alguma atualização de status aqui? amor resumir, precisa manter os níveis vazios!

@ebergelson , aqui está meu hack atual para obter grupos de comprimento zero. Freqüentemente preciso disso para que meus gráficos de barras sejam empilhados.

Aqui, df tem 3 colunas: nome, grupo e 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))

Eu faço algo semelhante - verifico se há grupos ausentes e, se houver, todas as combinações são geradas e left_join .

Infelizmente, não parece que esse problema esteja recebendo muito amor ... talvez porque haja uma solução alternativa simples.

@wsurles , @bpbond obrigado, sim, usei uma solução alternativa semelhante ao que você sugere! adoraria ver uma correção integrada como .drop.

Apenas para adicionar e concordar com todos acima - este é um aspecto supercrítico de muitas análises. Adoraria ver uma implementação.

Mais alguns detalhes necessários aqui:

Se eu tiver isso:

> 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

E eu agrupar por x então f , eu terminaria com 6 (2x3) grupos onde os grupos (2, 2) e (2,3) estão vazios. Isso está ok. Eu posso conseguir implementar isso, eu acho.

agora, e se eu tiver isso:

> 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

e eu quero agrupar por f então x . Quais seriam os grupos? @hadley

Ambos stats::aggregate e plyr::ddply retornam 4 grupos neste caso (1,1; 1,2; 2,1; e 2,4), então eu sugiro que esse seja o comportamento a seguir .

Não deveria concordar com table() vez disso, ou seja, retornar 9 grupos?

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

Eu esperaria que df %>% group_by(f, x) %>% tally fornecesse basicamente o mesmo resultado que with(df, as.data.frame(table(f, x))) e ddply(df, .(f, x), nrow, .drop=FALSE) .

Achei que nosso comportamento desejado era preservar grupos de comprimento zero se eles fossem fatores (como .drop em plyr), então eu imagino que gostaríamos da sugestão de @huftis . Eu sugeriria que o padrão seja drop = TRUE, para que o comportamento padrão não mude, re-sugestão de @bpbond .

Hmmm, é difícil entender exatamente qual deveria ser o comportamento. Esses experimentos mentais muito simples parecem corretos?

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

Mas e se x tiver vários valores? Deve funcionar assim?

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

Talvez preservar grupos vazios só faça sentido ao agrupar por uma única variável? Se enquadrarmos de forma mais realista, por exemplo, data_frame(age_group = c(40, 60), sex = factor(M, levels = c("F", "M")) , você realmente gostaria de contar com as mulheres? Acho que às vezes você faria e às vezes não. Expandir todas as combinações parece uma operação um tanto diferente para mim (e independente do uso de fatores).

Talvez group_by precise dos argumentos drop e expand ? drop = FALSE manteria todos os grupos de tamanho zero gerados por níveis de fator que não aparecem nos dados. expand = TRUE manteria todos os grupos de tamanho zero gerados por combinações de valores que não aparecem nos dados.

@hadley Seus exemplos parecem certos para mim (assumindo que você quis dizer levels = 1:2 , não levels = 2 ). E acho que preservar grupos vazios faz sentido mesmo ao agrupar por várias variáveis. Por exemplo, se as variáveis ​​eram sex ( male e female ) e answer (em um questionário, com níveis disagree , neutral , agree ), e você quisesse contar a frequência de cada resposta para cada sexo (por exemplo, para uma mesa ou para plotagem posterior), você não gostaria de abandonar uma categoria de resposta apenas porque nenhuma mulher respondeu.

Eu também esperaria que as variáveis ​​de fator permanecessem variáveis ​​de fator nos data_frame resultantes (não convertidos em strings) e com os _níveis originais_. (Assim, ao plotar os dados, as categorias de resposta estariam na ordem correta, não em ordem alfabética agree , disagree , neutral ).

Para o seu último exemplo, seria _em alguns casos_ natural descartar a variável sex (por exemplo, se _intencionalmente_ nenhuma mulher fosse pesquisada), e _em outros casos_ não (por exemplo, ao contar o número de defeitos de nascença estratificados por sexo (e talvez ano)). Mas isso pode (e deve) ser facilmente tratado _após_ a agregação dos dados. (Uma solução diferente seria aceitar um argumento com _vetorial_ .drop . Isso seria bom, mas acho que pode complicar as coisas?)

(Uma solução diferente seria aceitar um argumento .drop com valor vetorial. Isso seria bom, mas acho que pode complicar as coisas?)

Sim, provavelmente muito complicado. Caso contrário, concordo com os comentários de @huftis .

@hadley
eu penso
SIM expande todas as combinações de valores para group_by se eles existirem nos dados.
NÃO, não expanda os níveis de fator que não existem.

Meu caso de uso mais frequente é preparar um conjunto de dados resumidos para um gráfico (durante a exploração). E os gráficos precisam ter todas as combinações de valores. Mas eles não precisam ter níveis de fator com 0 para todos os grupos. Por exemplo, você não pode empilhar um gráfico de barras sem todas as combinações. Mas você não precisa de valores de fator que não existam nos dados, eles seriam apenas 0 quando empilhados e um valor vazio na legenda.

Acredito que expandir todos os valores para group_by deve ser o padrão porque é muito mais fácil (e muito mais intuitivo) filtrar 0 casos após o grupo, se necessário. Não acho que um argumento .drop seja necessário, porque é fácil filtrar 0 casos depois. Não usamos argumentos adicionais para qualquer uma das outras funções, então isso quebraria o molde. O padrão deve ser apenas mostrar os resultados de todos os combos de valores existentes com base em group_by.

Acho que esse seria o comportamento padrão correto. Aqui, o único se expandirá apenas nos valores existentes no fator, não em todos os níveis do fator. (Isso é o que eu executo depois de executar um group_by que descarta 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)
    )

O único caso que posso ver onde você deseja um valor, embora todos os grupos tenham zero, é com os dados de tempo. Talvez um dia de dados esteja faltando em algum lugar no meio. Aqui, uma expansão e uma junção em um intervalo de datas ainda seriam necessárias. O caso de nível de fator não se aplicaria. Eu acho que é justo para o processador de dados lidar com as datas ausentes por conta própria.

Obrigado por todo o seu excelente trabalho nesta biblioteca. 90% do meu trabalho está usando dplyr. :)

Eu concordo totalmente com @huftis.

Não acho que a queda de níveis ou combinações de níveis deva ter algo a ver com os dados. Você pode fazer o protótipo de uma função ou figura usando uma pequena amostra. Ou fazendo uma operação dividir-aplicar-combinar, caso em que você deseja uma garantia de que a saída de cada grupo será compatível com todo o resto.

Suavizando minha posição: acho que vale a pena considerar se o comportamento padrão deve ser diferente quando a variável de agrupamento já é um fator adequado vs. quando está sendo coagida a fatorar. Posso ver que a obrigação de manter os níveis não utilizados pode ser menor no caso de coerção. Mas se eu me dei ao trabalho de definir algo como um fator e assumir o controle dos níveis ... geralmente há uma boa razão e não quero lutar constantemente para preservá-la.

FYIW, eu gostaria de ver esse recurso também. Eu tenho um cenário semelhante ao descrito por @huftis e tenho que muitos obstáculos para obter os resultados de que preciso.

Vim aqui do SO. Não é isso que complete de "tidyr" deveria ajudar?

Sim. Na verdade, acabei de aprender sobre 'completar' recentemente e parece fazer isso de uma forma cuidadosa.

Implementar isso para back-ends SQL parece difícil, porque eles irão, por padrão, remover todos os grupos. Devemos deixar por isso mesmo e talvez implementar tidyr :: complete () para SQL?

Eu criei o problema nº 3033 sem perceber que ele já existia - desculpas pela duplicata. Para adicionar minha própria sugestão humilde, atualmente uso pull() e forcats::fct_count() como uma solução alternativa para esse problema.

Não sou fã desse método porque fct_count() trai o princípio do inverso de fazer uma saída que é sempre do mesmo tipo que a entrada (ou seja, esta função cria um tibble de um vetor), e eu tenho para renomear as colunas na saída. Isso cria 3 etapas ( pull() %>% fct_count() %>% rename() ) quando dplyr::count() deveria cobrir uma. Seria fantástico se forcats::fct_count() e dplyr::count() pudessem ser amalgamados de alguma forma e descontinuar forcats::fct_count() .

tidyr::complete() funciona para fatores?

Todos os níveis de fatores e combinações de níveis de fatores devem ser preservados por padrão. Este comportamento pode ser controlado por parâmetros como drop , expand , etc. Portanto, o comportamento padrão de dplyr::count() deve ser assim:

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

Grupos de comprimento zero (combinações de grupos) podem ser filtrados posteriormente. Mas, para a análise exploratória, devemos ver o quadro completo.

  1. Há alguma atualização de status sobre a solução para esse problema?
  2. Existem planos para resolver completamente este problema?

2: sim definitivamente
1: Existem algumas dificuldades de implementação técnica sobre esse problema, mas vou analisá-las nas próximas semanas.

Podemos conseguir isso expandindo os dados após o fato, algo assim:

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, no futuro, isso seria tratado internamente, sem usar tidyr ou purrr.

Isso parece resolver a questão original assim:

> 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

A chave aqui é esta

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

o que significa expandir todas as possibilidades, independentemente das variáveis ​​serem fatores ou não.

Eu diria que nós também:

  • expandir tudo quando drop=FALSE , potencialmente tendo muitos grupos de 0 comprimento
  • faça o que fazemos agora se drop=TRUE

talvez tenha uma função para alternar a queda.

Esta é uma operação relativamente barata, eu diria porque envolve apenas a manipulação de metadados, então talvez seja menos arriscado fazer isso em R primeiro?

Você quis dizer crossing() vez de expand() ?

Olhando internamente, você concorda que "apenas" precisamos mudar build_index_cpp() , especificamente a geração do quadro de dados labels , para que isso aconteça?

Podemos começar expandindo apenas os fatores com drop = FALSE ? Eu considerei uma sintaxe "natural", mas isso pode ser muito confuso no final (e talvez até não poderoso o suficiente):

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

Semântica: Usando todas as combinações de col1 e col2 , e as combinações existentes com col3 .

Sim, eu diria que isso afeta apenas build_index_cpp e a geração dos atributos labels , indices e group_sizes que gostaria de esmagar em um estrutura arrumada como parte de # 3489

A parte dos "únicos fatores de expansão" desta discussão é o que demorou tanto.

Quais seriam os resultados destes:

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)

Acho que f(d, f1, f2, x) deve dar os mesmos resultados que f(d, x, f1, f2) , se a ordem das linhas for ignorada. O mesmo vale para os outros dois.

Também interessante:

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

Gosto da ideia de implementar a expansão total apenas por fatores. Para dados sem caracteres (incluindo lógicos), podemos definir / usar uma classe semelhante a um fator que herda o respectivo tipo de dados. Talvez fornecido por forcats ? Isso torna mais difícil atirar no próprio pé.

implementação em andamento em # 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

Criado em 10/04/2018 pelo pacote reprex (v0.2.0).

Quanto a complete() resolver o problema - não, não realmente. Quaisquer que sejam os resumos sendo computados, seus comportamentos em vetores vazios precisam ser preservados, não corrigidos após o fato. Por exemplo:

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 e prod (e em menor grau, min ) (e várias outras funções) têm uma semântica muito bem definida em vetores vazios, e não é bom ter que venha depois com complete() e redefina esses comportamentos.

@kenahoo Não tenho certeza se entendi. Isso é o que você obtém com a versão dev atual. Portanto, a única coisa que você não recebe é o aviso 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

Criado em 15/05/2018 pelo pacote reprex (v0.2.0).

@romainfrancois Que legal, não sabia que você já estava tão

Este problema antigo foi bloqueado automaticamente. Se você acredita que encontrou um problema relacionado, registre um novo problema (com reprex) e crie um link para esse problema. https://reprex.tidyverse.org/

Esta página foi útil?
0 / 5 - 0 avaliações