dplyr summarize: Odpowiednik „.drop = FALSE”, aby zachować grupy o zerowej długości na wyjściu

97

Podczas korzystania summarisez plyr„s ddplyfunkcji, puste kategorie są odrzucane domyślnie. Możesz zmienić to zachowanie, dodając .drop = FALSE. Jednak to nie działa w przypadku korzystania summarisez dplyr. Czy jest inny sposób na zachowanie pustych kategorii w wyniku?

Oto przykład z fałszywymi danymi.

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

Nie do końca to, na co liczyłem. Czy istnieje dplyrmetoda osiągnięcia takiego samego wyniku jak .drop=FALSEw plyr?

eipi10
źródło

Odpowiedzi:

26

Ponieważ dplyr 0.8 group_by uzyskał .dropargument, który robi dokładnie to, o co prosiłeś:

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

Dodatkowa uwaga związana z odpowiedzią @ Moody_Mudskipper: użycie .drop=FALSEmoże dać potencjalnie nieoczekiwane wyniki, gdy jedna lub więcej zmiennych grupujących nie jest zakodowanych jako czynniki. Zobacz przykłady poniżej:

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)
Moody_Mudskipper
źródło
Dodałem dodatkową uwagę do Twojej odpowiedzi. Jeśli nie podoba Ci się ta zmiana, możesz ją usunąć.
eipi10
Mam złożony problem o tym na github, aby dowiedzieć się, czy jest to błąd czy zamierzone zachowanie.
eipi10
@ eipi10 nieco krótsze jest użycie count:iris %>% count(Species, group2, .drop=FALSE)
Tjebo
59

Problem jest nadal otwarty, ale w międzyczasie, zwłaszcza że Twoje dane są już uwzględnione, możesz użyć completez „tidyr”, aby uzyskać to, czego szukasz:

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

Jeśli chcesz, aby wartość zastąpienia wynosiła zero, musisz to określić za pomocą 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
A5C1D2H2I1M1N2O1R2T1
źródło
11
Zajęło mi dużo głowy w ścianę, aby to rozgryźć, więc wspomnę o tym tutaj ... Jeśli pogrupujesz według 2 zmiennych i są to raczej postacie niż czynniki, będziesz musiał użyć ich ungroup()przed ukończeniem. Jeśli kiedykolwiek zauważysz, że w completerzeczywistości nie ukończono, ungroupprawdopodobnie jest to potrzebne.
williamsurles
Co jeśli masz jeszcze więcej zmiennych grupujących? Mam ogromną liczbę wierszy (znacznie bardziej niż mojego pierwotnego dataframe) jeśli mogę użyć wszystkich zgrupowanie vars z mojego group_by
TobiO
1
Rozgryzłem to: musisz użyć zagnieżdżenia :-) Więc umieść wszystkie zmienne, które nie powinny być również łączone między sobą complete(variablewithdroppedlevels, nesting(var1,var2,var3))(tak naprawdę jest w pomocy, ponieważ completezajęło mi to jeszcze trochę czasu
TobiO)
20

rozwiązanie dplyr:

Najpierw utwórz zgrupowane df

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

następnie podsumowujemy występujące poziomy licząc z n()

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

następnie scalamy nasze wyniki w ramkę danych zawierającą wszystkie poziomy czynników:

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

na koniec w tym przypadku, ponieważ patrzymy na zliczenia, NAwartości są zmieniane na 0.

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

Można to również zaimplementować funkcjonalnie, patrz odpowiedzi: Dodać wiersze do zgrupowanych danych za pomocą dplyr?

Hack:

Pomyślałem, że opublikuję okropny hack, który działa w tym przypadku ze względu na zainteresowanie. Poważnie wątpię, żebyś kiedykolwiek to zrobił, ale pokazuje, jak group_by()generuje atrybuty, jakby df$bwektor znakowy, a nie czynnik z poziomami. Poza tym nie udaję, że dobrze to rozumiem - ale mam nadzieję, że to pomoże mi się nauczyć - to jedyny powód, dla którego to publikuję!

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

zdefiniuj wartość „poza zakresem”, która nie może istnieć w zbiorze danych.

oob_val <- nrow(by_b)+1

zmień atrybuty na „oszukanie” summarise():

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

zrób podsumowanie:

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

zindeksuj i zamień wszystkie wystąpienia oob_val

res[res == oob_val] <- 0

co daje zamierzone:

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

b count_a
1 1       6
2 2       6
3 3       0
npjc
źródło
11

nie jest to dokładnie to, o co pytano w pytaniu, ale przynajmniej w tym prostym przykładzie możesz uzyskać ten sam wynik za pomocą xtabs, na przykład:

używając dplyr:

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

lub krócej:

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

wynik (równy w obu przypadkach):

  b Freq
1 1    6
2 2    6
3 3    0
talat
źródło