2.5 分组操作

group_by()是最重要的分组动词,需要一个数据框和一个或多个变量进行分组:

详情: https://cloud.r-project.org/web/packages/dplyr/vignettes/grouping.html

2.5.1 添加分组

by_species <- starwars %>% group_by(species)
by_sex_gender <- starwars %>% group_by(sex, gender)

在添加分组后,print()打印时可以看到分组:

by_species
#> # A tibble: 87 x 14
#> # Groups:   species [38]
#>   name     height  mass hair_color  skin_color eye_color birth_year sex   gender
#>   <chr>     <int> <dbl> <chr>       <chr>      <chr>          <dbl> <chr> <chr> 
#> 1 Luke Sk~    172    77 blond       fair       blue            19   male  mascu~
#> 2 C-3PO       167    75 <NA>        gold       yellow         112   none  mascu~
#> 3 R2-D2        96    32 <NA>        white, bl~ red             33   none  mascu~
#> 4 Darth V~    202   136 none        white      yellow          41.9 male  mascu~
#> 5 Leia Or~    150    49 brown       light      brown           19   fema~ femin~
#> 6 Owen La~    178   120 brown, grey light      blue            52   male  mascu~
#> # ... with 81 more rows, and 5 more variables: homeworld <chr>, species <chr>,
#> #   films <list>, vehicles <list>, starships <list>

计算每个分组的行数,可以通过sort参数控制排序方式。

by_species %>% tally()
#> # A tibble: 38 x 2
#>   species      n
#>   <chr>    <int>
#> 1 Aleena       1
#> 2 Besalisk     1
#> 3 Cerean       1
#> 4 Chagrian     1
#> 5 Clawdite     1
#> 6 Droid        6
#> # ... with 32 more rows
by_sex_gender %>% tally(sort = TRUE)
#> # A tibble: 6 x 3
#> # Groups:   sex [5]
#>   sex            gender        n
#>   <chr>          <chr>     <int>
#> 1 male           masculine    60
#> 2 female         feminine     16
#> 3 none           masculine     5
#> 4 <NA>           <NA>          4
#> 5 hermaphroditic masculine     1
#> 6 none           feminine      1

在数据探索时比较有用。

除了按照现有变量分组外,还可以按照函数处理后的变量分组,等效在mutate()之后执行group_by:

bmi_breaks <- c(0, 18.5, 25, 30, Inf)
starwars %>%
  group_by(bmi_cat = cut(mass/(height/100)^2, breaks=bmi_breaks)) %>%
  tally()
#> # A tibble: 5 x 2
#>   bmi_cat       n
#>   <fct>     <int>
#> 1 (0,18.5]     10
#> 2 (18.5,25]    24
#> 3 (25,30]      13
#> 4 (30,Inf]     12
#> 5 <NA>         28

group_by()可通过在group_by()计算产生的新字段分组

2.5.2 查看分组

使用group_keys()查看数据的分组,每个组一行,每个分组变量占一列:

by_species %>% group_keys()
#> # A tibble: 38 x 1
#>   species 
#>   <chr>   
#> 1 Aleena  
#> 2 Besalisk
#> 3 Cerean  
#> 4 Chagrian
#> 5 Clawdite
#> 6 Droid   
#> # ... with 32 more rows

by_sex_gender %>% group_keys()
#> # A tibble: 6 x 2
#>   sex            gender   
#>   <chr>          <chr>    
#> 1 female         feminine 
#> 2 hermaphroditic masculine
#> 3 male           masculine
#> 4 none           feminine 
#> 5 none           masculine
#> 6 <NA>           <NA>

也可以使用命令group_indices()查看每行属于哪个组:

by_species %>% group_indices()
#>  [1] 11  6  6 11 11 11 11  6 11 11 11 11 34 11 24 12 11 11 36 11 11  6 31 11 11
#> [26] 18 11 11  8 26 11 21 11 10 10 10 38 30  7 38 11 37 32 32 33 35 29 11  3 20
#> [51] 37 27 13 23 16  4 11 11 11  9 17 17 11 11 11 11  5  2 15 15 11  1  6 25 19
#> [76] 28 14 34 11 38 22 11 11 11  6 38 11

该特性方便增加组别列。

df <- tibble(a = c('a','a','a','b','b','b','d','e','f'))
df %>% 
  group_by(a) %>%
  mutate(组别列 = group_indices() )
#> Warning: `group_indices()` was deprecated in dplyr 1.0.0.
#> Please use `cur_group_id()` instead.
#> # A tibble: 9 x 2
#> # Groups:   a [5]
#>   a     组别列
#>   <chr>  <int>
#> 1 a          1
#> 2 a          1
#> 3 a          1
#> 4 b          2
#> 5 b          2
#> 6 b          2
#> # ... with 3 more rows

上述用法在dplyr 1.0.0 中弃用,用cur_group_id()代替,如下所示:

df %>% 
  group_by(a) %>%
  mutate(组别列 = cur_group_id())
#> # A tibble: 9 x 2
#> # Groups:   a [5]
#>   a     组别列
#>   <chr>  <int>
#> 1 a          1
#> 2 a          1
#> 3 a          1
#> 4 b          2
#> 5 b          2
#> 6 b          2
#> # ... with 3 more rows

dplyr 在我看来 API 变化较快,所以我个人习惯使用 data.table 包处理数据。所以在学习的时候用最新的版本学习。

group_rows()每个组包含哪些行:

by_species %>% group_rows() %>% head()
#> <list_of<integer>[6]>
#> [[1]]
#> [1] 72
#> 
#> [[2]]
#> [1] 68
#> 
#> [[3]]
#> [1] 49
#> 
#> [[4]]
#> [1] 56
#> 
#> [[5]]
#> [1] 67
#> 
#> [[6]]
#> [1]  2  3  8 22 73 85

group_vars()返回分组变量的名称,请使用:

by_species %>% group_vars()
#> [1] "species"

by_sex_gender %>% group_vars()
#> [1] "sex"    "gender"

2.5.3 更改和添加分组变量

如果将group_by()应用已经分组的数据集,将覆盖现有的分组变量。例如,下面的分组变量是homeworld而不是之前的species

 by_species %>% 
  group_by(homeworld) %>% 
  tally()
#> # A tibble: 49 x 2
#>   homeworld          n
#>   <chr>          <int>
#> 1 Alderaan           3
#> 2 Aleen Minor        1
#> 3 Bespin             1
#> 4 Bestine IV         1
#> 5 Cato Neimoidia     1
#> 6 Cerea              1
#> # ... with 43 more rows

要增加分组变量,使用.add=TRUE参数即可。例如:

by_species %>% 
  group_by(homeworld,.add = TRUE) %>% 
  tally()
#> # A tibble: 58 x 3
#> # Groups:   species [38]
#>   species  homeworld       n
#>   <chr>    <chr>       <int>
#> 1 Aleena   Aleen Minor     1
#> 2 Besalisk Ojom            1
#> 3 Cerean   Cerea           1
#> 4 Chagrian Champala        1
#> 5 Clawdite Zolan           1
#> 6 Droid    Naboo           1
#> # ... with 52 more rows

2.5.4 删除分组变量

要删除所有分组变量,使用ungroup():

by_species %>%
  ungroup() %>%
  tally()
#> # A tibble: 1 x 1
#>       n
#>   <int>
#> 1    87

还可以通过列出要删除的变量来有选择的删除分组变量:

by_sex_gender %>% 
  ungroup(sex) %>% 
  tally()
#> # A tibble: 3 x 2
#>   gender        n
#>   <chr>     <int>
#> 1 feminine     17
#> 2 masculine    66
#> 3 <NA>          4

2.5.5 动词影响

group_by()是如何影响 dplyr 的主要动词。

  • summarise

summarise() 计算每个组的汇总,表示从group_keys()开始,在右侧添加summarise()的汇总变量。

by_species %>%
  summarise(
    n = n(),
    height = mean(height, na.rm = TRUE)
  )
#> # A tibble: 38 x 3
#>   species      n height
#>   <chr>    <int>  <dbl>
#> 1 Aleena       1    79 
#> 2 Besalisk     1   198 
#> 3 Cerean       1   198 
#> 4 Chagrian     1   196 
#> 5 Clawdite     1   168 
#> 6 Droid        6   131.
#> # ... with 32 more rows

.groups=参数控制输出的分组结构。删除右侧分组变量的历史行为对应于.groups = “drop_last”没有提示消息,或.groups = NULL有消息(默认值)。

by_sex_gender %>% 
  summarise(n = n()) %>% 
  group_vars()
#> `summarise()` has grouped output by 'sex'. You can override using the `.groups` argument.
#> [1] "sex"

by_sex_gender %>% 
  summarise(n = n(),.groups = 'drop_last') %>% 
  group_vars()
#> [1] "sex"

从1.0.0版开始,分组信息可以保留(.groups = "keep")或删除 (.groups = 'drop)

a <- by_species %>%
  summarise(
    n = n(),
    height = mean(height, na.rm = TRUE),.groups='drop') 

b <- by_species %>%
  summarise(
    n = n(),
    height = mean(height, na.rm = TRUE),.groups='keep') 
object.size(a)
#> 4088 bytes
object.size(b)
#> 10896 bytes

以上,可以看到保留分组信息的比没保留的对象大了两倍多,在实际使用中,当数据较大时会占据更多内存,所以我们需要根据实际情况决定是否保留分组信息,我在大部分时候都会删除分组信息。

  • arrange

默认情况下,分组和不分组的数据集应用在在arrange()效果相同。除非设置.by_group = TRUE,这时首先按照分组变量排序。

df <- tibble(a = c('a','b','a','a','d','b','b','b','d','e','f'),b=1:11) %>% 
  group_by(a)

df %>% 
  arrange(desc(b)) 
#> # A tibble: 11 x 2
#> # Groups:   a [5]
#>   a         b
#>   <chr> <int>
#> 1 f        11
#> 2 e        10
#> 3 d         9
#> 4 b         8
#> 5 b         7
#> 6 b         6
#> # ... with 5 more rows
df %>% 
  arrange(desc(b),.by_group = TRUE)
#> # A tibble: 11 x 2
#> # Groups:   a [5]
#>   a         b
#>   <chr> <int>
#> 1 a         4
#> 2 a         3
#> 3 a         1
#> 4 b         8
#> 5 b         7
#> 6 b         6
#> # ... with 5 more rows

实际上就是,默认情况下arrange()会忽略分组变量,除非通过设置参数.by_group参数。

  • mutate and transmute

根据 mutate 中函数不同,返回值视情况而定。

df <- tibble(a = c('a','b','a','a','d','b','b','b','d','e','f'),b=1:11)

res1 <- df %>% 
  mutate( d = mean(b))

res2 <- df %>% 
  group_by(a) %>% 
  mutate(d = mean(b))

# 分组后返回不同值,此时的 mutate 相当于汇总函数
identical(res1,res2)
#> [1] FALSE
  • filter

filter 受 group_by 影响。

df %>% 
  group_by(a) %>% 
  filter(b == max(b))
#> # A tibble: 5 x 2
#> # Groups:   a [5]
#>   a         b
#>   <chr> <int>
#> 1 a         4
#> 2 b         8
#> 3 d         9
#> 4 e        10
#> 5 f        11

以上代码本质是先执行 mutate(max(b)),再filter,最后只保留 TRUE 所在行。如下所示:

df %>% 
  group_by(a) %>% 
  mutate(max_num = max(b)) %>% 
  filter(b==max_num) %>% 
  select(-max_num)
#> # A tibble: 5 x 2
#> # Groups:   a [5]
#>   a         b
#>   <chr> <int>
#> 1 a         4
#> 2 b         8
#> 3 d         9
#> 4 e        10
#> 5 f        11

想想以下代码的含义?

df %>% 
  group_by(a) %>% 
  filter(n()!=1)
#> # A tibble: 9 x 2
#> # Groups:   a [3]
#>   a         b
#>   <chr> <int>
#> 1 a         1
#> 2 b         2
#> 3 a         3
#> 4 a         4
#> 5 d         5
#> 6 b         6
#> # ... with 3 more rows

删除只有一行记录的数据行

  • slice 系列

选择每个分组变量的第一个观测值。

by_species %>% 
  relocate(species) %>% 
  slice(1)
#> # A tibble: 38 x 14
#> # Groups:   species [38]
#>   species  name   height  mass hair_color skin_color  eye_color birth_year sex  
#>   <chr>    <chr>   <int> <dbl> <chr>      <chr>       <chr>          <dbl> <chr>
#> 1 Aleena   Ratts~     79    15 none       grey, blue  unknown           NA male 
#> 2 Besalisk Dexte~    198   102 none       brown       yellow            NA male 
#> 3 Cerean   Ki-Ad~    198    82 white      pale        yellow            92 male 
#> 4 Chagrian Mas A~    196    NA none       blue        blue              NA male 
#> 5 Clawdite Zam W~    168    55 blonde     fair, gree~ yellow            NA fema~
#> 6 Droid    C-3PO     167    75 <NA>       gold        yellow           112 none 
#> # ... with 32 more rows, and 5 more variables: gender <chr>, homeworld <chr>,
#> #   films <list>, vehicles <list>, starships <list>

同样,我同样可以使用slice_min()来选择 变量的最大值。

by_species %>% 
  filter(!is.na(height)) %>% 
  slice_max(height,n=3)
#> # A tibble: 51 x 14
#> # Groups:   species [38]
#>   name    height  mass hair_color skin_color   eye_color birth_year sex   gender
#>   <chr>    <int> <dbl> <chr>      <chr>        <chr>          <dbl> <chr> <chr> 
#> 1 Ratts ~     79    15 none       grey, blue   unknown           NA male  mascu~
#> 2 Dexter~    198   102 none       brown        yellow            NA male  mascu~
#> 3 Ki-Adi~    198    82 white      pale         yellow            92 male  mascu~
#> 4 Mas Am~    196    NA none       blue         blue              NA male  mascu~
#> 5 Zam We~    168    55 blonde     fair, green~ yellow            NA fema~ femin~
#> 6 IG-88      200   140 none       metal        red               15 none  mascu~
#> # ... with 45 more rows, and 5 more variables: homeworld <chr>, species <chr>,
#> #   films <list>, vehicles <list>, starships <list>