作者:黄天元,复旦大学博士在读,热爱数据科学与开源工具(R),致力于利用数据科学迅速积累行业经验优势和科学知识发现,涉猎内容包括但不限于信息计量、机器学习、数据可视化、应用统计建模、知识图谱等,著有《R语言数据高效处理指南》(《R语言数据高效处理指南》(黄天元)【摘要 书评 试读】- 京东图书,《R语言数据高效处理指南》(黄天元)【简介_书评_在线阅读】 - 当当图书)。知乎专栏:R语言数据挖掘。邮箱:huang.tian-yuan@qq.com.欢迎合作交流。
dplyr的1.0.0版本中推出了across函数,以对多列进行同时的汇总或更新。tidyfst包在这之前已经构造了vars族的两个函数对其进行实现,本来应该去学习dplyr去构造一组across的函数,但是发现构造出来在语法结构上反而更复杂,不利于用户使用,因此就不再把问题复杂化。下面,我们给出例子进行对比,并用代码进行实现。这里的例子主要参考了:
Across (dplyr 1.0.0): applying dplyr functions simultaneously across multiple columns首先先加载数据和环境:
remotes::install_github("allisonhorst/palmerpenguins")
library(palmerpenguins)
library(dplyr)
library(tidyfst)
我们要分析的是一份企鹅的数据:
penguins
#> # A tibble: 344 x 7
#> species island bill_length_mm bill_depth_mm flipper_length_~ body_mass_g
#> <fct> <fct> <dbl> <dbl> <int> <int>
#> 1 Adelie Torge~ 39.1 18.7 181 3750
#> 2 Adelie Torge~ 39.5 17.4 186 3800
#> 3 Adelie Torge~ 40.3 18 195 3250
#> 4 Adelie Torge~ NA NA NA NA
#> 5 Adelie Torge~ 36.7 19.3 193 3450
#> 6 Adelie Torge~ 39.3 20.6 190 3650
#> 7 Adelie Torge~ 38.9 17.8 181 3625
#> 8 Adelie Torge~ 39.2 19.6 195 4675
#> 9 Adelie Torge~ 34.1 18.1 193 3475
#> 10 Adelie Torge~ 42 20.2 190 4250
#> # ... with 334 more rows, and 1 more variable: sex <fct>
我们要完成一个很简单的任务,看每一列中有多少个独立的值(数值、因子都一样)。比如我们对species列来做这个操作:
# dplyr
penguins %>%
summarise(distinct_species = n_distinct(species))
#> # A tibble: 1 x 1
#> distinct_species
#> <int>
#> 1 3
# tidyfst
penguins %>%
summarise_dt(distinct_species = uniqueN(species))
#> distinct_species
#> <int>
#> 1: 3
只做一个的时候,两者差异不大,只是dplyr用n_distinct函数,而tidyfst用了data.table内置的uniqueN函数。下面,我们对species, island, sex三列同时来做这个操作,进行一下比较。
# dplyr
penguins %>%
summarise(across(c(species, island, sex),
n_distinct))
#> # A tibble: 1 x 3
#> species island sex
#> <int> <int> <int>
#> 1 3 3 3
# tidyfst
penguins %>%
summarise_vars("species|island|sex",uniqueN)
#> species island sex
#> <int> <int> <int>
#> 1: 3 3 3
我们可以看到,dplyr又要嵌套across又要用c来做向量,其实是非常不便于操作的。而tidyfst利用正则表达式选择列名称,不仅便于减少代码量,在实际的使用中也方便代码自动生成(构造字符)。这只是其一,那么我们试试,求除了这三列以外别的列有多少个特殊值。
# dplyr
penguins %>%
summarise(across(!c(species, island, sex),
n_distinct))
#> # A tibble: 1 x 4
#> bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <int> <int> <int> <int>
#> 1 165 81 56 95
# tidyfst
penguins %>%
summarise_vars(!"species|island|sex",uniqueN)
#> bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <int> <int> <int> <int>
#> 1: 165 81 56 95
友情提示,tidyfst中惊叹号(!)和减号(-)都可以用来表达除了这些以外的列,表排斥。
下面,我们来对以bill开头的列进行这个操作:
# dplyr
penguins %>%
summarise(across(starts_with("bill"), n_distinct))
#> # A tibble: 1 x 2
#> bill_length_mm bill_depth_mm
#> <int> <int>
#> 1 165 81
# tidyfst
penguins %>%
summarise_vars("^bill",uniqueN)
#> bill_length_mm bill_depth_mm
#> <int> <int>
#> 1: 165 81
dplyr又是across又是starts_with,这个代码量就太大了。而tidyfst利用正则表达式,只需要用“^”表明是以什么开头即可(熟悉正则的小伙伴应该还知道,“$”就表示以此为结尾)。如果是包含某个字符串就匹配,dplyr需要用contains,而tidyfst直接什么都不需要,利用字符串就能匹配。例子如下:
# dplyr
penguins %>%
summarise(across(contains("length"), n_distinct))
#> # A tibble: 1 x 2
#> bill_length_mm flipper_length_mm
#> <int> <int>
#> 1 165 56
# tidyfst
penguins %>%
summarise_vars("length",uniqueN)
#> bill_length_mm flipper_length_mm
#> <int> <int>
#> 1: 165 56
如果要对所有列进行计算,dplyr还要everything一下,而tidyfst直接缺省列信息,但是显式传递参数.func即可,例子如下:
# dplyr
penguins %>%
summarise(across(everything(), n_distinct))
#> # A tibble: 1 x 7
#> species island bill_length_mm bill_depth_mm flipper_length_~ body_mass_g sex
#> <int> <int> <int> <int> <int> <int> <int>
#> 1 3 3 165 81 56 95 3
# tidyfst
penguins %>%
summarise_vars(.func = uniqueN)
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <int> <int> <int> <int> <int> <int>
#> 1: 3 3 165 81 56 95
#> sex
#> <int>
#> 1: 3
后,实名反对引入的所谓简洁的变成规范,即这种代码:
# dplyr
penguins %>%
group_by(species, island) %>%
mutate(across(where(is.numeric),
~if_else(condition = is.na(.),
true = mean(., na.rm = T),
false = as.numeric(.)))) %>%
ungroup()
上面的代码是要先按照物种和岛屿分组,然后对数值型的变量,进行一个函数操作,这个函数本质是:
replace0 <- function(x) {
if_else(condition = is.na(x),
true = mean(x,na.rm = T),
false = as.numeric(x))
}
上面的函数,是编程的规范所在。当然为了临时的自由而选择之前包含“~”和“.”的写法,只能作为平时尝试的“便捷”,但是不建议作为编程规范作为推广。里面一定有雷,不便于维护。此外,关于在select函数中使用across的情况,之前已经在帖子(HopeR:为什么你要使用tidyfst的select_dt)中进行过说明,这里不再赘述。感兴趣的小伙伴,可以去给dplyr包pull一个request,让dplyr的设计更加人性化和规范化一些。tidyfst是dplyr与data.table两个老师教导下的学生,一定再接再厉去学习的特性,为用户提供更便捷的服务。