dplyr summarize: эквивалент «.drop = FALSE» для сохранения групп с нулевой длиной на выходе

97

При использовании summariseс plyr«S ddplyфункции, пустые категории удаляются по умолчанию. Вы можете изменить это поведение, добавив .drop = FALSE. Однако это не работает при использовании summariseс dplyr. Есть ли другой способ сохранить в результате пустые категории?

Вот пример с поддельными данными.

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

Не совсем то, на что я надеялся. Есть ли dplyrспособ добиться того же результата, что и .drop=FALSEв plyr?

eipi10
источник

Ответы:

26

Поскольку dplyr 0.8 group_by получил .dropаргумент, который делает именно то, о чем вы просили:

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

Еще одно примечание к ответу @Moody_Mudskipper: использование .drop=FALSEможет дать потенциально неожиданные результаты, если одна или несколько группирующих переменных не закодированы как факторы. См. Примеры ниже:

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
источник
Я добавил к вашему ответу дополнительное примечание. Пожалуйста, удалите, если вам не нравится редактирование.
eipi10
Я отправил сообщение об этом на github, чтобы узнать, является ли это ошибкой или предполагаемым поведением.
eipi10
@ eipi10 немного короче - это использование count:iris %>% count(Species, group2, .drop=FALSE)
Tjebo
59

Проблема все еще не решена, но пока что, тем более что ваши данные уже учтены, вы можете использовать completefrom "tidyr", чтобы получить то, что вы, возможно, ищете:

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

Если вы хотите, чтобы значение замены было равно нулю, вам нужно указать это с помощью 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
источник
11
Мне потребовалось много усилий, чтобы понять это, поэтому я упомяну об этом здесь ... Если вы группируете по 2 переменным, и они являются символами, а не факторами, вам нужно будет использовать их ungroup()до завершения. Если вы когда-нибудь заметите, что на completeсамом деле не завершается, ungroupвероятно, это необходимо.
williamsurles
Что делать, если у вас еще больше группирующих переменных? Я получаю огромное количество строк (намного больше, чем мой исходный фрейм данных), если я использую все группирующие вары из моего group_by
TobiO
1
Я понял это: вы должны использовать вложение :-) Так что поместите все переменные, которые также не должны объединяться между собой complete(variablewithdroppedlevels, nesting(var1,var2,var3))(на самом деле это в справке, потому что completeмне все еще потребовалось время, чтобы разобраться
TobiO
20

раствор dplyr:

Сначала сделайте сгруппированный df

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

затем мы суммируем те уровни, которые возникают, подсчитывая с n()

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

затем мы объединяем наши результаты во фрейм данных, который содержит все уровни факторов:

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

наконец, в этом случае, поскольку мы смотрим на счетчики, NAзначения меняются на 0.

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

Это также можно реализовать функционально, см. Ответы: Добавить строки в сгруппированные данные с помощью dplyr?

Взлом:

Думал , ради интереса выложу ужасный хак, который работает в данном случае. Я серьезно сомневаюсь, что вам когда-либо стоит это делать, но он показывает, как group_by()генерируются атрибуты, как если бы это df$bбыл вектор символов, а не фактор с уровнями. Кроме того, я не претендую на то, чтобы понять это должным образом - но я надеюсь, что это поможет мне научиться - это единственная причина, по которой я публикую это!

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

определить значение «вне пределов», которое не может существовать в наборе данных.

oob_val <- nrow(by_b)+1

изменить атрибуты на "уловку" summarise():

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

сделать резюме:

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

проиндексировать и заменить все вхождения oob_val

res[res == oob_val] <- 0

что дает предполагаемое:

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

b count_a
1 1       6
2 2       6
3 3       0
npjc
источник
11

это не совсем то, что было задано в вопросе, но, по крайней мере, для этого простого примера вы можете получить тот же результат, используя xtabs, например:

используя dplyr:

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

или короче:

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

результат (равный в обоих случаях):

  b Freq
1 1    6
2 2    6
3 3    0
талат
источник