dplyr summarize: equivalente a ".drop = FALSE" per mantenere i gruppi con lunghezza zero in output


97

Quando si utilizza summarisecon plyrla ddplyfunzione di, le categorie vuote vengono eliminate per impostazione predefinita. Puoi modificare questo comportamento aggiungendo .drop = FALSE. Tuttavia, questo non funziona quando si utilizza summarisecon dplyr. C'è un altro modo per mantenere le categorie vuote nel risultato?

Ecco un esempio con dati falsi.

library(dplyr)

df = data.frame(a=rep(1:3,4), b=rep(1:2,6))

# Now add an extra level to df$b that has no corresponding value in df$a
df$b = factor(df$b, levels=1:3)

# Summarise with plyr, keeping categories with a count of zero
plyr::ddply(df, "b", summarise, count_a=length(a), .drop=FALSE)

  b    count_a
1 1    6
2 2    6
3 3    0

# Now try it with dplyr
df %.%
  group_by(b) %.%
  summarise(count_a=length(a), .drop=FALSE)

  b     count_a .drop
1 1     6       FALSE
2 2     6       FALSE

Non esattamente quello che speravo. Esiste un dplyrmetodo per ottenere lo stesso risultato .drop=FALSEdi plyr?


Risposte:


26

Poiché dplyr 0.8 ha group_by ottenuto l' .dropargomento che fa proprio quello che hai chiesto:

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, .drop=FALSE) %>%
  summarise(count_a=length(a))

#> # A tibble: 3 x 2
#>   b     count_a
#>   <fct>   <int>
#> 1 1           6
#> 2 2           6
#> 3 3           0

Una nota aggiuntiva per la risposta di @ Moody_Mudskipper: l'utilizzo .drop=FALSEpuò dare risultati potenzialmente inaspettati quando una o più variabili di raggruppamento non sono codificate come fattori. Vedi esempi di seguito:

library(dplyr)
data(iris)

# Add an additional level to Species
iris$Species = factor(iris$Species, levels=c(levels(iris$Species), "empty_level"))

# Species is a factor and empty groups are included in the output
iris %>% group_by(Species, .drop=FALSE) %>% tally

#>   Species         n
#> 1 setosa         50
#> 2 versicolor     50
#> 3 virginica      50
#> 4 empty_level     0

# Add character column
iris$group2 = c(rep(c("A","B"), 50), rep(c("B","C"), each=25))

# Empty groups involving combinations of Species and group2 are not included in output
iris %>% group_by(Species, group2, .drop=FALSE) %>% tally

#>   Species     group2     n
#> 1 setosa      A         25
#> 2 setosa      B         25
#> 3 versicolor  A         25
#> 4 versicolor  B         25
#> 5 virginica   B         25
#> 6 virginica   C         25
#> 7 empty_level <NA>       0

# Turn group2 into a factor
iris$group2 = factor(iris$group2)

# Now all possible combinations of Species and group2 are included in the output, 
#  whether present in the data or not
iris %>% group_by(Species, group2, .drop=FALSE) %>% tally

#>    Species     group2     n
#>  1 setosa      A         25
#>  2 setosa      B         25
#>  3 setosa      C          0
#>  4 versicolor  A         25
#>  5 versicolor  B         25
#>  6 versicolor  C          0
#>  7 virginica   A          0
#>  8 virginica   B         25
#>  9 virginica   C         25
#> 10 empty_level A          0
#> 11 empty_level B          0
#> 12 empty_level C          0

Created on 2019-03-13 by the reprex package (v0.2.1)

Ho aggiunto una nota aggiuntiva alla tua risposta. Sentiti libero di eliminare se non ti piace la modifica.
eipi10

Ho segnalato un problema su questo su GitHub per scoprire se si tratta di un bug o del comportamento previsto.
eipi10

@ eipi10 leggermente più breve è l'uso di count:iris %>% count(Species, group2, .drop=FALSE)
Tjebo

59

Il problema è ancora aperto, ma nel frattempo, soprattutto perché i tuoi dati sono già presi in considerazione, puoi utilizzare completeda "tidyr" per ottenere ciò che potresti cercare:

library(tidyr)
df %>%
  group_by(b) %>%
  summarise(count_a=length(a)) %>%
  complete(b)
# Source: local data frame [3 x 2]
# 
#        b count_a
#   (fctr)   (int)
# 1      1       6
# 2      2       6
# 3      3      NA

Se vuoi che il valore di sostituzione sia zero, devi specificarlo con fill:

df %>%
  group_by(b) %>%
  summarise(count_a=length(a)) %>%
  complete(b, fill = list(count_a = 0))
# Source: local data frame [3 x 2]
# 
#        b count_a
#   (fctr)   (dbl)
# 1      1       6
# 2      2       6
# 3      3       0

11
Mi ci è voluto un sacco di colpi di testa contro il muro per capirlo, quindi lo menzionerò qui ... Se raggruppate per 2 variabili, e sono caratteri piuttosto che fattori, sarà necessario utilizzare ungroup()prima di completare. Se ti accorgi che completenon lo stai completando, ungroupprobabilmente è necessario.
williamsurles

E se avessi ancora più variabili di raggruppamento? Ottengo un numero enorme di righe (molto più del mio dataframe originale) se utilizzo tutte le variabili di raggruppamento dal mio group_by
TobiO

1
L'ho capito: devi usare l'annidamento :-) Quindi metti tutte le variabili che non dovrebbero essere combinate tra di loro in complete(variablewithdroppedlevels, nesting(var1,var2,var3))(in realtà è nell'aiuto perché completemi ci è voluto un po 'per capirlo
TobiO

20

soluzione dplyr:

Per prima cosa crea df raggruppato

by_b <- tbl_df(df) %>% group_by(b)

quindi riassumiamo quei livelli che si verificano contando con n()

res <- by_b %>% summarise( count_a = n() )

quindi uniamo i nostri risultati in un data frame che contiene tutti i livelli di fattore:

expanded_res <- left_join(expand.grid(b = levels(df$b)),res)

infine, in questo caso, poiché stiamo esaminando i conteggi, i NAvalori vengono modificati in 0.

final_counts <- expanded_res[is.na(expanded_res)] <- 0

Questo può anche essere implementato funzionalmente, vedere le risposte: Aggiungere righe ai dati raggruppati con dplyr?

Un trucco:

Ho pensato di pubblicare un terribile hack che funzioni in questo caso per interesse. Dubito seriamente che dovresti mai farlo, ma mostra come group_by()genera gli attributi come se df$bfosse un vettore di caratteri non un fattore con i livelli. Inoltre, non pretendo di capirlo correttamente, ma spero che questo mi aiuti a imparare, questo è l'unico motivo per cui lo pubblico!

by_b <- tbl_df(df) %>% group_by(b)

definire un valore "fuori limite" che non può esistere nel set di dati.

oob_val <- nrow(by_b)+1

modifica gli attributi in "trucco" summarise():

attr(by_b, "indices")[[3]] <- rep(NA,oob_val)
attr(by_b, "group_sizes")[3] <- 0
attr(by_b, "labels")[3,] <- 3

fare il riassunto:

res <- by_b %>% summarise(count_a = n())

index e sostituisci tutte le occorrenze di oob_val

res[res == oob_val] <- 0

che dà il previsto:

> res
Source: local data frame [3 x 2]

b count_a
1 1       6
2 2       6
3 3       0

11

questo non è esattamente ciò che è stato chiesto nella domanda, ma almeno per questo semplice esempio, potresti ottenere lo stesso risultato usando xtabs, ad esempio:

utilizzando dplyr:

df %>%
  xtabs(formula = ~ b) %>%
  as.data.frame()

o più breve:

as.data.frame(xtabs( ~ b, df))

risultato (uguale in entrambi i casi):

  b Freq
1 1    6
2 2    6
3 3    0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.