ITPub博客

首页 > > > 【R语言】dplyr1.0.0新功能解读

【R语言】dplyr1.0.0新功能解读

作者:浪花小雨滴 时间:2020-06-17 10:45:41 0 删除 编辑

作为 tidyverse 数据操作的核心包 dplyr 在 2020-05-29 迎来了大版本的更新,从此进入 1.0.0 时代。既然是大版本的更新,当然有一些重磅的新功能推出,我也在第一时间做了体验,现在分享给大家。

建议更新方式:

remove.packages("dplyr")
install.packages("dplyr")

顺便说一句,R 4.0 我还没更新,同样是大版本更新,怕有些包用不了,先缓一缓再说。


本篇来介绍如下几个新功能:

  • across() 函数
  • rowwise 行化编程思维
  • slice_*() 系列行切片函数
  • relocate() 调整列序
  • summarise() 支持多返回值的汇总函数

本篇用到的数据:

链接:pan.baidu.com/s/1-EgHgc

提取码:ng8o


示例数据采用虚拟的学生成绩数据,包含随机生成的 20 个 NA:

library(tidyverse)

df = readxl::read_xlsx("ExamDatas_NAs.xlsx")
df

一. across() 函数

  1. across() 简介

across() 人如其名,让零个/一个/多个函数穿过所选择的列,即同时对所选择的多列应用若干函数,基本语法为:

across(.cols = everything(), .fns = NULL, ..., .names = NULL)

其中,

  • .cols: 为根据选择列语法选定的列范围;
  • .fns 为应用到选定列上的函数,它可以是:

- NULL:不对列作变换;

- 一个函数,如 mean;

- 一个 purrr 风格的匿名函数,如 ~ .x * 10

- 多个函数或匿名函数构成的列表

这些函数内部可以使用 cur_column() 和 cur_group() 以访问当前列和分组键值。

  • .names 用来设置输出列的列名样式,默认为"{col}_{fn}"


2. across() 的用途

across() 支持各种选择列语法,与 mutate() 和 summarise() 连用,产生非常强大的同时修改/(多种)汇总多列效果;

也能与 filter() 连用,在限定列范围内筛选"所有值都满足某条件的行",但不擅长筛选"存在值满足某条件的行";

也能与 group_by(), count() 和 distinct() 连用,此时 .fns 为 NULL,只起选择列的作用。


across() 函数的引入,使得可以弃用那些限定列范围的后缀: _all, _if, _at:

  • across(everything(), .fns): 在所有列范围内,代替后缀 _all
  • across(where(), .fns): 在满足条件的列范围内,代替后缀 _if
  • across(.cols, .fns): 在给定的列范围内,代替后缀 _at


3. 选择列语法

(1) 用列名或索引选择列

df %>%
  select(name, sex, math)   # 或者 select(2, 3, 5)

(2) 借助算子符号选择列

  • 用 : 选择连续的若干列
  • 用 ! 选择变量集合的余集(反选)
  • & 和 | 选择变量集合的交或并
  • c( ) 合并多个选择

(3) 借助选择助手函数

■ 选择指定列:

  • everything(): 选择所有列
  • last_col(): 选择最后一列,可以带参数,如 `last_col(5)` 选择倒数第 6 列

■ 选择列名匹配的列:

  • starts_with(): 以某前缀开头的列名
  • ends_with(): 以某后缀结尾的列名
  • contains(): 包含某字符串的列名
  • matches(): 匹配正则表达式的列名
  • num_range(): 匹配数值范围的列名,如 num_range("x", 1:3) 匹配x1, x2, x3

■ 结合函数选择列:

  • where(): 应用函数到所有列,选择返回结果为 TRUE 的列,常用 is.numeric() 等函数连用


4. 若干实例

(1) across() 与 mutate() 连用:对所选择的列应用某个函数

  • 将所有列转化为字符型
df %>% 
  mutate(across(everything(), as.character))  
  • 对数值列做归一化:
rescale <- function(x) {
  rng <- range(x, na.rm = TRUE)
  (x - rng[1]) / (rng[2] - rng[1])
}

df %>% 
  mutate(across(where(is.numeric), rescale))

(2) across() 与 summarise() 连用:以对所选择的列做一个或多个汇总

  • 对包含"h"的列,求平均值
df %>%
  group_by(class, sex) %>%
  summarise(across(contains("h"), mean))
  • 对多列做多个汇总
df_grp <- df %>% drop_na() %>%
  group_by(class) %>%
  summarise(across(where(is.numeric), 
                   list(sum=sum, mean=mean, min=min)))
df_grp

可读性不好,再来个宽变长:

df_grp %>% pivot_longer(-class, 
                        names_to = c("Vars", ".value"), 
                        names_sep = "_")


(3) across() 与 filter() 连用(二者在逻辑上不搭):根据多列值选择行

  • 限定列范围内,筛选"所有值都满足某条件的行"
df[, 4:6] %>%
  filter(across(everything(), ~ .x > 75))  # 选择所有列都>75的行
df %>% 
  filter(across(where(is.numeric), ~ !is.na(.x)))  # 选择数值列都不包含NA的行

注:不能再套 all_val(), 因为 across() 等价于 all_var()。


  • 限定列范围内,筛选"存在值满足某条件的行"

目前的支持还不好,暂时需要借助如下函数实现,期待后续的改进:

rowAny <- function(x) rowSums(x, na.rm = TRUE) > 


df[, 4:6] %>%
  filter(rowAny(across(everything(), ~. > 90)))   # 选择在所有列存在值>90的行

注:从数学逻辑上来说,【存在值>90】等价于【非“所有值都≤90”】,这在 iris 上测试成功,但本例只返回 6 个观测,不知道是不是bug。

二. rowwise行化编程思维

  1. rowwise 按行方式

通常的数据操作逻辑都是按列方式(colwise),这使得按行汇总很困难。dplyr 包提供了 rowwise() 函数为数据框创建按行方式(rowwise),它不是真的改变数据框,只是创建了按行信息。

使用 rowwise() 后,就会改变数据框的内部操作逻辑:

df[,1:6] %>% 
  rowwise() %>% 
  mutate(total = sum(c(chinese, math, english)))

dplyr包还提供了 c_across() 是为 按行方式(rowwise)在选定的列范围汇总数据而设计的,它没有提供 .fns 参数,只能选择列:

df %>% 
  rowwise() %>% 
  mutate(total = sum(c_across(where(is.numeric))))

注:如果只是按行加和或求平均的话,用 R 自带的 rowSums() 和 rowMeans() 更好,这里的rowwise 行化后提供可以做更多的按行汇总的可能。

2. 批量建模技术

如果 rowwise 行化只是上面这点东西的话都不值一写,更让人惊喜的是:它的逐行处理的逻辑 + 嵌套数据框可以更好地实现批量建模。

批量建模可以用嵌套数据框 + purrr map_*() 实现,但这种 rowwise 技术,具有异曲同工之妙。

rowwise 按行方式,可以理解为一种特殊的分组:每一行作为一组。若为 rowwise() 提供行 ID,用 summarise() 做汇总更能体会这一点:

df %>% 
  rowwise(name) %>% 
  summarise(total = sum(c_across(where(is.numeric))))

下面进入正题,用 gapminder 数据集演示,包含 142 个国家的人口、预期寿命、人均 GDP 等:

library(broom)   # tidy方式提取模型信息

load("gapminder.rda")
gapminder

我们想要对每个国家分别建模,像这样:

先对数据框用 nest_by() 做嵌套就得到这样 rowwise 化的嵌套数据框:

by_country <- gapminder %>%
  nest_by(country) 

by_country

一个国家的数据占一行,rowwise 化的逻辑,就是按行操作数据,正好适合逐行地对每个嵌套的数据框建模和提取模型信息:

  • 计算模型列、预测值列
by_country = by_country %>% 
  mutate(model = list(lm(lifeExp ~ year, data = data)),   
         pred = list(predict(model, data)))                     
by_country
  • 计算 rmse 列、R方列、斜率列:
by_country = by_country %>% 
  mutate(rmse = sqrt(mean((pred - data$lifeExp) ^ 2)),
         rsq = summary(model)$r.squared,
         slope = summary(model)$coefficients[2]) 

by_country
  • 提取各个模型的诊断信息
by_country %>%
  summarise(glance(model))
  • 提取每个模型的系数估计及其统计量
by_country %>%
  summarise(tidy(model))


  • 提取每个样本的预测值、残差等模型结果:
by_country %>%
  summarise(augment(model))

嵌套数据框可以同样地筛选行,再用 unnest() 解除嵌套就恢复到原数据级别。

  • 比如提取R方<0.5的原始数据绘图(这些国家是“脆弱”国家):
by_country %>%
  filter(rsq < 0.5) %>% unnest(data) %>%
  ggplot(aes(year, lifeExp)) +
  geom_line(aes(color = country))
  • 比如提取斜率 > 0.6 的国家的原始数据绘图:
by_country %>%
  filter(slope > 0.6) %>% unnest(data) %>%
  ggplot(aes(year, lifeExp)) +
  geom_line(aes(color = country))


可见,在 rowwise 行化模式下,批量建模就像计算新列一样自然,这真是太神奇了!


三. slice_*() 系列行切片函数

以前行切片函数 slice() 存在感不强,现在变成一个系列之后,几乎和 filter() 平起平坐了。

slice(df, 3:7)                        # 选择3-7行
lice_head(df, n, prop)                # 从前面开始选择若干行
slice_tail(df, n, prop)               # 从后面开始选择若干行
slice_min(df, order_by, n, prop)      # 根据order_by选择最小的若干行
slice_max(df, order_by, n, prop)      # 根据order_by选择最大的若干行
slice_sample(df, n, prop)             # 随机选择若干行

它们的共同参数,n 表示选择 n 行,prop 表示选择某比例的行。

例如,选择数学成绩排名前5的行:

df %>% 
  slice_max(math, n = 5)


四. relocate() 更灵活地调整列序

用 relocate() 函数,可将选择的列移到某列之前或之后,基本语法为:

relocate(.data, ..., .before, .after)

例如,将数值列移到 name 列的后面:

df %>% 
  relocate(where(is.numeric), .after = name)

五. summarise() 支持多返回值的汇总函数

以前只支持一个返回值的汇总函数,如 sum, mean 等。现在也支持多返回值的汇总函数,如 range(), quantile() 等。

qs = c(0.25, 0.5, 0.75)

df_q = df %>% drop_na() %>% 
  group_by(sex) %>%
  summarise(math_qs = quantile(math, qs), q = qs) 
df_q

可读性不好,再来个长变宽:

df_q %>% 
  pivot_wider(everything(), names_from = q, 
              values_from = math_qs, names_prefix = "q_")


参考文献:

  1. Hadley Wickham. dplyr 1.0.0: Vignettes. 2020 CRAN - Package dplyr
  2. Amelia McNamara, Hadley Wickham, Introduction to Data Science in the Tidyverse. rstudio::conf 2020, github.com/cwickham/dat


版权声明:原创作品,欢迎转载,转载请注明出处,禁止用于出版。

来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31537575/viewspace-3149729/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-05-16

  • 博文量
    4259
  • 访问量
    39941