这个笔记的起因是在学习DataExplorer 包的时候,发现:
这我乍一看,牛批啊。这语法还挺长见识的。
转念思考了一下🤔,其实目的也就是将数据框中的指定列转换为因子。换句话说,就是如何可以批量的对数据框的指定行或者列进行某种操作。(这里更多强调的是对原始数据框的直接操作,如果是统计计算直接找summarise 和它的小伙伴们,其他的玩意儿也各有不同,掉头左转:
34. R 数据整理(六:根据分类新增列的种种方法 1.0)
其实按照我的思路,还是惯用的循环了,对数据框的列名判断一下,如果所取的列在数据框中,就修改一下其格式,重新赋值:
data(cancer, package = "survival")
colon3 <- colon
tmp <- sapply(vars, function(x) {
if (x %in% colnames(colon3)) colon3[, x] <<- as.factor(colon3[,x])
})
str(colon3)
现在来总结一下,具体参见:vignette("colwise"):
这里示例数据如下:
test2 <- data.frame(id = sample(LETTERS[1:10], 30, replace = T),
a = sample(1:10, 30, replace = T),
b = sample(1:100, 30, replace = T),
c = sample(1:1000, 30, replace = T))
str(test2)
'data.frame': 30 obs. of 4 variables:
$ id: chr "I" "C" "B" "H" ...
$ a : int 4 4 10 4 7 1 9 5 3 6 ...
$ b : int 56 100 94 11 45 30 66 1 36 81 ...
$ c : int 610 158 715 181 862 91 945 204 63 218 ...
如果需要批量计算统计数据,需要借助summarise 函数。
比较粗暴的就是,一行一行的手动写。
批量有两种操作:
(也可以先select 再summarise_all) :
> test2 %>% summarise_at(
+ c("a", "b", "c"),
+ list(avg = ~mean(.), std = ~sd(.)), na.rm=TRUE)
a_avg b_avg c_avg a_std b_std c_std
1 5.066667 51.2 425.8 2.8519 30.93865 315.7111
这时候有不聪明的小朋友要问了,如果取反呢?比如我的数据里,只有一个分类数据,对其取反取数更加容易。
我这里举两个例子:
> test2 %>% select(-any_of("id")) %>% summarise_all(
+ list(avg = ~mean(.), std = ~sd(.)), na.rm=TRUE)
a_avg b_avg c_avg a_std b_std c_std
1 5.066667 51.2 425.8 2.8519 30.93865 315.7111
test2 %>% summarise_at(
setdiff(colnames(test2), "id"),
list(avg = ~mean(.), std = ~sd(.)), na.rm=TRUE)
本质都是取反。
test2 %>% summarise(across(-any_of("id"), mean))
across 必须要在mutate 或summarise 这类函数内部,对数据框的列进行类似apply 的批量操作。其第一个参数需要是列名。也可以:
test2 %>% summarise(across(-where(is.character), mean))
其中where 类似base 中的which,相当于接受逻辑值,以返回对应位置。
和summarise_all 一样,其本质也可以接受list 传递函数:
test2 %>% summarise(
across(-where(is.character), list(avg = ~ mean(.), std = ~ sd(.))))
非常的简单,加个group_by 即可:
> test2 %>% group_by(id) %>% summarise_at(
+ setdiff(colnames(test2), "id"),
+ list(avg = ~ mean(.), std = ~ sd(.)), na.rm=TRUE)
# A tibble: 10 × 7
id a_avg b_avg c_avg a_std b_std c_std
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 A 3 28 655 NA NA NA
2 B 8.5 69.5 788. 2.12 34.6 104.
3 C 4 86.5 406. 0.816 15.3 372.
4 D 5 54.5 497. 3.16 21.0 388.
5 E 2.67 35.3 402. 1.53 32.6 380.
6 F 7.33 48.5 434. 1.86 36.8 289.
7 G 4 4 69 NA NA NA
8 H 7.5 36.8 440. 2.38 33.9 359.
9 I 2.5 72 560 2.12 22.6 70.7
10 J 1.67 37.7 56.7 1.15 8.62 37.9
如果你问NA 值怎么办~ 我会说,加个什么,加个什么,加个filter(n() > 1) 试试吧~
across 还提供了参数,可以自定义返回的名称:
> test2 %>% summarise(
+ across(-where(is.character), list(avg = ~ mean(.), std = ~ sd(.)),
+ .names = "test2_{.col}_{.fn}"))
test2_a_avg test2_a_std test2_b_avg test2_b_std test2_c_avg test2_c_std
1 5.066667 2.8519 51.2 30.93865 425.8 315.7111
默认的是:
> test2 %>% summarise(
+ across(-where(is.character), list(avg = ~ mean(.), std = ~ sd(.))))
a_avg a_std b_avg b_std c_avg c_std
1 5.066667 2.8519 51.2 30.93865 425.8 315.7111
如果不用list 传递的话,因为没有名字,所以需要自定义名称了:
> test3 %>% summarise(
+ across(c("a","b"), ~ mean(.), .names = "{.col}_{.fn}"),
+ across(c("a", "b"), ~ sd(.), .names = "{.col}_std"),
+ )
# A tibble: 1 × 4
a_1 b_1 a_std b_std
<dbl> <dbl> <dbl> <dbl>
1 5.07 51.2 2.85 30.9
比如:
> test2 %>% summarise(
+ across(-where(is.character),
+ list(avg = ~ mean(.),
+ std = ~ sd(.)),
+ .names = "test2_{.col}_{.fn}",
+ na.rm = T
+ )
+ ) %>% relocate(ends_with("avg"))
test2_a_avg test2_b_avg test2_c_avg test2_a_std test2_b_std test2_c_std
1 5.066667 51.2 425.8 2.8519 30.93865 315.7111
这样就可以让结果以avg 结尾的先输出。
和select 这些一样,他们也有一些挑列的专属函数:
select(test, starts_with("Petal")) #选中..开头的列
select(test, ends_with("Width")) #选中..结尾的列
select(test, contains("etal")) #选中包含..的列
select(test, matches(".t.")) #选中符合某正则表达的列
select(test, everything()) #选中所有列,可以使指定的列先提前
select(test, last_col()) #选中最后一列
select(test, last_col(offset = 1)) #选中倒数第二列。offset 表示忽略n个。忽略最后一个即表示选择倒数第二个。
如果我们先进行了某步运算,比如统计数目,再across 循环某种运算,则有NA 风险:
df <- data.frame(x = c(1, 2, 3), y = c(1, 4, 9))
df %>%
summarise(n = n(), across(where(is.numeric), sd))
#> n x y
#> 1 NA 1 4.041452
因为across 也对 n = 3 这一数据计算了sd。
有两种策略:
df %>%
summarise(across(where(is.numeric), sd), n = n())
#> x y n
#> 1 1 4.041452 3
df %>%
summarise(n = n(), across(where(is.numeric) & !n, sd))
#> n x y
#> 1 3 1 4.041452
ps:发现这个 ! 取反的方法只对across 的.cols 参数生效。
> test2 %>% group_by(id) %>% summarise_at(
+ colnames(test2,) & !id,
+ list(avg = ~ mean(.), std = ~ sd(.)), na.rm=TRUE)
Error in !id : 参数种类不对
> rescale01 <- function(x) {
+ rng <- range(x, na.rm = TRUE)
+ (x - rng[1]) / (rng[2] - rng[1])
+ }
> df <- tibble(x = 1:4, y = rnorm(4))
> df %>% summarise(across(where(is.numeric), rescale01))
# A tibble: 4 × 2
x y
<dbl> <dbl>
1 0 0.127
2 0.333 1
3 0.667 0.756
4 1 0
这里就回到开始的问题了,如果是希望对数据框本身进行处理,而非统计学运算呢?
也很简单,无非是summarise 换成了mutate。
比如:
df <- tibble(x = 1:3, y = 3:5, z = 5:7)
mult <- list(x = 1, y = 10, z = 100)
df %>% mutate(across(all_of(names(mult)), ~ .x * mult[[cur_column()]]))
#> # A tibble: 3 x 3
#> x y z
#> <dbl> <dbl> <dbl>
#> 1 1 30 500
#> 2 2 40 600
#> 3 3 50 700
因为它好用啊。
image.png