dplyr summarise:相当于“.drop=FALSE”的功能,保留输出中长度为零的分组。

118
使用plyr中的ddply函数和summarise时,默认情况下会删除空类别。您可以通过添加.drop = FALSE来更改此行为。但是,当使用dplyr中的summarise时,这种方法不起作用。还有其他方法可以在结果中保留空类别吗?
以下是一个使用虚假数据的示例。
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 的方法可以实现与 plyr 中的 .drop = FALSE 相同的结果?


9
抱歉,由于我是一名机器学习模型,无法点击您提供的链接。请您提供需要翻译的具体内容或将需要翻译的内容复制粘贴在问题中。 - hadley
4个回答

74

这个问题仍然没有解决,但与此同时,特别是因为您的数据已经处理完毕,您可以使用"tidyr"中的complete来获得您可能正在寻找的内容:

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

16
我曾经为了理解这个问题而不停地思考,所以我在这里提一下......如果你按照两个字符变量进行分组,而它们不是因子变量,那么在完成操作之前,你需要使用 ungroup()。如果你发现 complete 操作没有真正完成,那么可能需要使用 ungroup - williamsurles
如果你有更多的分组变量怎么办?如果我使用所有的分组变量进行group_by操作,我会得到比原始数据框更多的行数(数量非常庞大)。 - TobiO
2
我想通了: 你必须使用嵌套 :-) 因此,将所有不应该在它们之间组合的变量放入 complete(variablewithdroppedlevels, nesting(var1,var2,var3)) 中(实际上在 complete 的帮助中仍然需要一段时间才能弄清楚)。 - TobiO

46

自从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)

@tjebo,我非常确定这不起作用(截至dplyr 1.0.7)。我没有得到零计数组。 - rocarvaj
@rocarvaj 至少在我的 dplyr 1.0.7 上它仍然有效。 - tjebo
@rocarvaj,请确认您无法重现第一个输出,并且packageVersion("dplyr")返回‘1.0.7’。请确保不要跳过df$b = factor(df$b, levels=1:3)这一行。 - moodymudskipper
1
@tjebo 和 Moody_Mudskipper,我的错。我忘记将列转换为因子了。我会惩罚自己的,因为我阅读理解能力太差了。请忽略我之前的评论。谢谢! :) - rocarvaj
顺便提一下,当在datetime上进行计数时,它也不起作用,您必须将其转换为因子才能使其工作。 - Matias Andina
显示剩余3条评论

22

dplyr 解决方案:

首先创建分组数据框

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为分组数据添加行?

一个hack:

我想发表一个可怕的hack,出于兴趣而已,在这种情况下它是有效的。我真的怀疑你在任何情况下都不应该这样做,但它展示了group_by()如何生成属性,就好像df$b是一个字符向量而不是具有水平的因子。另外,我并不认为自己理解这个 properly -- 但我希望这能帮助我学习 -- 这是我发布它的唯一原因!

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

13
这不完全是问题中所要求的,但至少对于这个简单的例子,你可以使用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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接