相对频率/比例与dplyr

242

假设我想计算每个组中不同值的比例。例如,使用mtcars数据,如何使用dplyr一次性计算按am(自动/手动)分组的gears数量的相对频率?

library(dplyr)
data(mtcars)
mtcars <- tbl_df(mtcars)

# count frequency
mtcars %>%
  group_by(am, gear) %>%
  summarise(n = n())

# am gear  n
#  0    3 15 
#  0    4  4 
#  1    4  8  
#  1    5  5 

我的目标是什么:

am gear  n rel.freq
 0    3 15      0.7894737
 0    4  4      0.2105263
 1    4  8      0.6153846
 1    5  5      0.3846154

1
这些百分比是你想要的实际数字吗?它们从代数上来说是从哪里来的?啊,79%是15/(15+4),21%是4/(15+4),然后对于am==1,62%是8/(8+5)等等。明白了。 - Spacedman
1
@Spacedman 是的,那就是我想要的数字,Frank 是正确的,它们通过 am 变量相加为 100% (79+21) 和 (62+38)。 - jenswirf
5
似乎需要一个本地的dplyr实现prop.table()/sweep()。另外,在其他问题中,一些人要求包括变量或变量交互的零计数选项。 - smci
10个回答

433

试一下这个:

mtcars %>%
  group_by(am, gear) %>%
  summarise(n = n()) %>%
  mutate(freq = n / sum(n))

#   am gear  n      freq
# 1  0    3 15 0.7894737
# 2  0    4  4 0.2105263
# 3  1    4  8 0.6153846
# 4  1    5  5 0.3846154

dplyr文档中得知:

当你按多个变量进行分组时,每个摘要将剥离一个级别的分组。这使得逐步汇总数据集变得容易。

因此,在summarise之后,指定在group_by中的最后一组变量'gear'被剥离。在mutate步骤中,数据被剩余的分组变量(这里是'am')分组。您可以使用groups检查每个步骤的分组情况。

剥离结果当然取决于在group_by调用中分组变量的顺序。您可能希望进行后续的group_by(am),以使您的代码更明确。

对于四舍五入和美化,请参考@Tyler Rinker给出的很好的答案。


8
我刚刚也发现了这个解决方案,但我不知道为什么sum(n)am组上有效,在gear组上则无效... - Spacedman
11
请参阅vignette:“当您按多个变量分组时,每个汇总操作将剥离一个分组级别。” - Henrik
你也可以用tally()替换summarise(n = n())这一行,因为它就是该语句的包装器。 - daneshjai

53

您可以使用count()函数,不过这个函数在dplyr的版本不同时有不同的行为:

  • dplyr 0.7.1:返回一个未分组的表格:您需要再次按am分组。

  • dplyr < 0.7.1:返回一个已分组的表格,因此不需要重新分组,但您可能希望在后续操作中使用ungroup()解除分组。

dplyr 0.7.1

mtcars %>%
  count(am, gear) %>%
  group_by(am) %>%
  mutate(freq = n / sum(n))

dplyr < 0.7.1

mtcars %>%
  count(am, gear) %>%
  mutate(freq = n / sum(n))

这将导致生成一个分组表,如果您想将其用于进一步的分析,使用ungroup()可以去除分组属性可能会很有用。


2
这似乎是在dplyr 0.7.1上的无效答案。它对“gear”进行整体频率计算,而不是在“am”的每个级别内进行计算。 - Edwin

35

@Henrik的更适合可用性,因为这将使列成为字符而不再是数值,但与您要求的相匹配...

mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n()) %>%
  mutate(rel.freq = paste0(round(100 * n/sum(n), 0), "%"))

##   am gear  n rel.freq
## 1  0    3 15      79%
## 2  0    4  4      21%
## 3  1    4  8      62%
## 4  1    5  5      38%

编辑 因为 Spacedman 请求 :-)

as.rel_freq <- function(x, rel_freq_col = "rel.freq", ...) {
    class(x) <- c("rel_freq", class(x))
    attributes(x)[["rel_freq_col"]] <- rel_freq_col
    x
}

print.rel_freq <- function(x, ...) {
    freq_col <- attributes(x)[["rel_freq_col"]]
    x[[freq_col]] <- paste0(round(100 * x[[freq_col]], 0), "%")   
    class(x) <- class(x)[!class(x)%in% "rel_freq"]
    print(x)
}

mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n()) %>%
  mutate(rel.freq = n/sum(n)) %>%
  as.rel_freq()

## Source: local data frame [4 x 4]
## Groups: am
## 
##   am gear  n rel.freq
## 1  0    3 15      79%
## 2  0    4  4      21%
## 3  1    4  8      62%
## 4  1    5  5      38%

7
你可以创建一个 S3 “百分比”类,并使用 format 方法添加百分号来实现这个功能... #有点过度设计 - Spacedman
实现这个也许会很有趣:https://dev59.com/wmYr5IYBdhLWcg3wq8CI - Spacedman

19
尽管已经有很多答案,但还有一种方法可以使用prop.table与'dplyr'或'data.table'相结合。
自'dplyr' v. >= 1.1.0以来,我们可以在mutate中使用.by参数。
library(dplyr)

mtcars %>%
  count(am, gear) %>% 
  mutate(freq = prop.table(n), .by = am)

#>   am gear  n      freq
#> 1  0    3 15 0.7894737
#> 2  0    4  4 0.2105263
#> 3  1    4  8 0.6153846
#> 4  1    5  5 0.3846154

在'dplyr' v. < 1.1.0之前,一种方法是:

mtcars %>%
  group_by(am, gear) %>% 
  tally() %>% 
  mutate(freq = prop.table(n))

#> # A tibble: 4 × 4
#> # Groups:   am [2]
#>      am  gear     n  freq
#>   <dbl> <dbl> <int> <dbl>
#> 1     0     3    15 0.789
#> 2     0     4     4 0.211
#> 3     1     4     8 0.615
#> 4     1     5     5 0.385

使用 'data.table' 我们可以做到:

library(data.table)
cars_dt <- as.data.table(mtcars)
cars_dt[, .(n = .N), keyby = .(am, gear)][, freq := prop.table(n), by = "am"][]

#>    am gear  n      freq
#> 1:  0    3 15 0.7894737
#> 2:  0    4  4 0.2105263
#> 3:  1    4  8 0.6153846
#> 4:  1    5  5 0.3846154

使用reprex v2.0.2在2022年10月22日创建


11
为了完整回答这个常见问题,自 dplyr 1.0.0 版本以来,参数.groups 控制 group_bysummarise 函数的分组结构。使用 .groups = "drop_last"summarise 将删除最后一级分组。在 1.0.0 版本之前,这是唯一的结果。summarise 帮助
library(dplyr)
library(scales)

original <- mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n()) %>%
  mutate(rel.freq =  scales::percent(n/sum(n), accuracy = 0.1))
#> `summarise()` regrouping output by 'am' (override with `.groups` argument)

original
#> # A tibble: 4 x 4
#> # Groups:   am [2]
#>      am  gear     n rel.freq
#>   <dbl> <dbl> <int> <chr>   
#> 1     0     3    15 78.9%   
#> 2     0     4     4 21.1%   
#> 3     1     4     8 61.5%   
#> 4     1     5     5 38.5%

new_drop_last <- mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n(), .groups = "drop_last") %>%
  mutate(rel.freq =  scales::percent(n/sum(n), accuracy = 0.1))

dplyr::all_equal(original, new_drop_last)
#> [1] TRUE

使用.groups = "drop",所有分组级别都将被删除。结果将转换为独立的tibble,并且不会保留以前的group_by的任何痕迹。
# .groups = "drop"
new_drop <- mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n(), .groups = "drop") %>%
  mutate(rel.freq =  scales::percent(n/sum(n), accuracy = 0.1))

new_drop
#> # A tibble: 4 x 4
#>      am  gear     n rel.freq
#>   <dbl> <dbl> <int> <chr>   
#> 1     0     3    15 46.9%   
#> 2     0     4     4 12.5%   
#> 3     1     4     8 25.0%   
#> 4     1     5     5 15.6%

如果.groups = "keep",则与.data(在此情况下为mtcars)具有相同的分组结构。 summarise 不会剥离任何用于group_by的变量。
最后,如果.groups = "rowwise",每行都是自己的一组。在这种情况下,它等同于"keep"。
# .groups = "keep"
new_keep <- mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n(), .groups = "keep") %>%
  mutate(rel.freq =  scales::percent(n/sum(n), accuracy = 0.1))

new_keep
#> # A tibble: 4 x 4
#> # Groups:   am, gear [4]
#>      am  gear     n rel.freq
#>   <dbl> <dbl> <int> <chr>   
#> 1     0     3    15 100.0%  
#> 2     0     4     4 100.0%  
#> 3     1     4     8 100.0%  
#> 4     1     5     5 100.0%

# .groups = "rowwise"
new_rowwise <- mtcars %>%
  group_by (am, gear) %>%
  summarise (n=n(), .groups = "rowwise") %>%
  mutate(rel.freq =  scales::percent(n/sum(n), accuracy = 0.1))

dplyr::all_equal(new_keep, new_rowwise)
#> [1] TRUE

另一个可能感兴趣的点是,在应用group_bysummarise之后,有时需要添加摘要行。

# create a subtotal line to help readability
subtotal_am <- mtcars %>%
  group_by (am) %>% 
  summarise (n=n()) %>%
  mutate(gear = NA, rel.freq = 1)
#> `summarise()` ungrouping output (override with `.groups` argument)

mtcars %>% group_by (am, gear) %>%
  summarise (n=n()) %>% 
  mutate(rel.freq = n/sum(n)) %>%
  bind_rows(subtotal_am) %>%
  arrange(am, gear) %>%
  mutate(rel.freq =  scales::percent(rel.freq, accuracy = 0.1))
#> `summarise()` regrouping output by 'am' (override with `.groups` argument)
#> # A tibble: 6 x 4
#> # Groups:   am [2]
#>      am  gear     n rel.freq
#>   <dbl> <dbl> <int> <chr>   
#> 1     0     3    15 78.9%   
#> 2     0     4     4 21.1%   
#> 3     0    NA    19 100.0%  
#> 4     1     4     8 61.5%   
#> 5     1     5     5 38.5%   
#> 6     1    NA    13 100.0%

本文创建于2020年11月9日,使用reprex包 (v0.3.0)

希望您会发现这个答案有用。


10

我为这个重复的任务编写了一个小函数:

count_pct <- function(df) {
  return(
    df %>%
      tally %>% 
      mutate(n_pct = 100*n/sum(n))
  )
}

我可以像这样使用它:

mtcars %>% 
  group_by(cyl) %>% 
  count_pct

它返回:

# A tibble: 3 x 3
    cyl     n n_pct
  <dbl> <int> <dbl>
1     4    11  34.4
2     6     7  21.9
3     8    14  43.8

6
这里是一个通用函数,实现了Henrik在dplyr 0.7.1上的解决方案。
freq_table <- function(x, 
                       group_var, 
                       prop_var) {
  group_var <- enquo(group_var)
  prop_var  <- enquo(prop_var)
  x %>% 
    group_by(!!group_var, !!prop_var) %>% 
    summarise(n = n()) %>% 
    mutate(freq = n /sum(n)) %>% 
    ungroup
}

5
此外,尝试使用 add_count() (用于绕过繁琐的 group_by .groups)。
mtcars %>% 
  count(am, gear) %>% 
  add_count(am, wt = n, name = "nn") %>% 
  mutate(proportion = n / nn)

2

这里是使用基础R语言的答案,使用aggregateave函数:

df1 <- with(mtcars, aggregate(list(n = mpg), list(am = am, gear = gear), length))
df1$prop <- with(df1, n/ave(n, am, FUN = sum))
#Also with prop.table
#df1$prop <- with(df1, ave(n, am, FUN = prop.table))
df1

#  am gear  n      prop
#1  0    3 15 0.7894737
#2  0    4  4 0.2105263
#3  1    4  8 0.6153846
#4  1    5  5 0.3846154 

我们也可以使用 prop.table,但输出结果会有所不同。
prop.table(table(mtcars$am, mtcars$gear), 1)
   
#            3         4         5
#  0 0.7894737 0.2105263 0.0000000
#  1 0.0000000 0.6153846 0.3846154

1
这个答案基于Matifou的回答。
首先,我修改了它,以确保使用scipen选项时不返回freq列作为科学计数法列。
然后我将答案乘以100,以获得百分比而不是小数,使freq列更容易被读作百分比。
getOption("scipen") 
options("scipen"=10) 
mtcars %>%
count(am, gear) %>% 
mutate(freq = (n / sum(n)) * 100)

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