-
Notifications
You must be signed in to change notification settings - Fork 219
/
tidyverse_colwise.Rmd
709 lines (508 loc) · 14.2 KB
/
tidyverse_colwise.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# tidyverse中的列方向和行方向 {#tidyverse-colwise}
```{r, include=FALSE}
knitr::opts_chunk$set(
echo = TRUE,
warning = FALSE,
message = FALSE,
fig.showtext = TRUE
)
```
dplyr宏包是数据科学tidyverse集合的核心部件之一,Hadley Wickham大神说将会在5月15日发布dplyr 1.0版本,欢呼。
为迎接新时代的到来,我在线上同大家一起分享dplyr 1.0版本新的特点和功能,看看都为我们带来哪些惊喜?
## 体验新版本
New dplyr - 8 things to know:
1) Built in `tidyselect`
2) `relocate()`
3) Superpowered `summarise()`
4) colwise using `across()`
5) `cur_data()`, `cur_group()` and `cur_column()`
6) new `rowwise()` grammar
7) easy modeling inside dataframes
8) `nest_by()`
```{r colwise-1, eval=FALSE, echo=FALSE}
devtools::install_github("tidyverse/dplyr")
```
```{r colwise-2, message = FALSE, warning = FALSE}
library(dplyr, warn.conflicts = FALSE)
library(tidyr)
```
## 简单回顾
```{r colwise-3, eval=FALSE}
mutate()
select()
filter()
group_by()
summarise()
arrange()
rename()
left_join()
```
## summarise()更强大了
在dplyr 1.0之前,`summarise()`会把统计结果整理成一行一列的数据框,现在可以根据函数返回的结果,可以有多种形式:
- 长度为 1 的向量,比如,`min(x), n(), or sum(is.na(y))`
- **长度为 n 的向量**,比如,`quantile()`
- **数据框**
```{r colwise-4}
df <- tibble(
grp = rep(c("a", "b"), each = 5),
x = c(rnorm(5, -0.25, 1), rnorm(5, 0, 1.5)),
y = c(rnorm(5, 0.25, 1), rnorm(5, 0, 0.5))
)
df
```
```{r colwise-5}
df %>%
group_by(grp) %>%
summarise(rng = mean(x))
```
当统计函数返回多个值的时候,比如`range()`返回是最小值和最大值,`summarise()`很贴心地将结果整理成多行,这样符合tidy的格式。
```{r colwise-6}
df %>%
group_by(grp) %>%
summarise(rng = range(x))
```
类似的还有`quantile()`函数,也是返回多个值
```{r colwise-7}
df %>%
group_by(grp) %>%
summarise(
rng = quantile(x, probs = c(0.05, 0.5, 0.95))
)
```
```{r colwise-8}
df %>%
group_by(grp) %>%
summarise(
x = quantile(x, c(0.25, 0.5, 0.75)),
q = c(0.25, 0.5, 0.75)
)
```
`summarise()`可以输出数据框,比如
```{r colwise-9}
my_quantile <- function(x, probs) {
tibble(x = quantile(x, probs), probs = probs)
}
mtcars %>%
group_by(cyl) %>%
summarise(my_quantile(disp, c(0.25, 0.75)))
```
再比如:
dplyr 1.0 之前是需要`group_modify()`来实现`数据框进,数据框出`
```{r colwise-10}
mtcars %>%
group_by(cyl) %>%
group_modify(
~ broom::tidy(lm(mpg ~ wt, data = .))
)
```
dplyr 1.0 之后,有了新的方案
```{r colwise-11}
mtcars %>%
group_by(cyl) %>%
summarise(
broom::tidy(lm(mpg ~ wt))
)
```
## summarise()后的分组信息是去是留?
当 `group_by()`与`summarise()`配合使用的时候,`summarise()`默认会**抵消掉**最近一次的分组信息,比如下面按照`cyl`和`vs`分组,但`summarise()`后,就只剩下`cyl`的分组信息了。
```{r colwise-12}
mtcars %>%
group_by(cyl, vs) %>%
summarise(cyl_n = n())
```
```{r colwise-13}
mtcars %>%
group_by(cyl, vs) %>%
summarise(cyl_n = n()) %>%
group_vars()
```
如果想保留vs的分组信息,就需要设置`.groups = keep`参数
```{r colwise-14}
mtcars %>%
group_by(cyl, vs) %>%
summarise(cyl_n = n(), .groups = "keep") %>%
group_vars()
```
当然`summarise()`可以控制输出的更多形式
- 丢弃所有的分组信息
```{r colwise-15}
mtcars %>%
group_by(cyl, vs) %>%
summarise(cyl_n = n(), .groups = "drop") %>%
group_vars()
```
- 变成行方向分组,即,每行是一个分组
```{r colwise-16}
mtcars %>%
group_by(cyl, vs) %>%
summarise(cyl_n = n(), .groups = "rowwise") %>%
group_vars()
```
## 选择某列
- 通过位置索引进行选取
```{r colwise-17}
df %>% select(1, 3)
df %>% select(2:3)
```
- 通过列名
```{r colwise-18}
df %>% select(grp, x, y)
df %>% select(x:y)
```
- 通过函数选取
```{r colwise-19}
df %>% select(starts_with("x"))
df %>% select(ends_with("p"))
df %>% select(contains("x"))
df %>% select(matches("x"))
```
- 通过类型
```{r colwise-20}
df %>% select(where(is.character))
df %>% select(where(is.numeric))
```
- 通过各种组合
```{r colwise-21}
df %>% select(!where(is.character))
df %>% select(where(is.numeric) & starts_with("x"))
df %>% select(starts_with("g") | ends_with("y"))
```
注意any_of和all_of的区别
```{r colwise-22, eval=FALSE}
vars <- c("x", "y", "z")
df %>% select(all_of(vars))
df %>% select(any_of(vars))
```
## 重命名某列
```{r colwise-23}
df %>% rename(group = grp)
```
```{r colwise-24}
df %>% rename_with(toupper)
df %>% rename_with(toupper, is.numeric)
df %>% rename_with(toupper, starts_with("x"))
```
## 调整列的位置
我们前面一章讲过`arrange()`排序,这是行方向的排序, 比如按照x变量绝对值的大小从高到低排序。
```{r colwise-25}
df %>% arrange(desc(abs(x)))
```
我们现在想调整**列的位置**,比如,这里调整数据框三列的位置,让`grp`列放在`x`列的后面
```{r colwise-26}
df %>% select(x, grp, y)
```
如果列变量很多的时候,上面的方法就不太好用,因此推荐大家使用`relocate()`
```{r colwise-27}
df %>% relocate(grp, .after = y)
df %>% relocate(x, .before = grp)
```
还有
```{r colwise-28}
df %>% relocate(grp, .after = last_col())
```
## 强大的across函数
我们必须为这个函数点赞。大爱Hadley Wickham !!!
我们经常需要对数据框的**多列**执行相同的操作。比如
```{r colwise-29}
iris <- iris %>% as_tibble()
iris
```
```{r colwise-30}
iris %>%
group_by(Species) %>%
summarise(
mean_Sepal_Length = mean(Sepal.Length),
mean_Sepal_Width = mean(Sepal.Width),
mean_Petal_Length = mean(Petal.Length),
mean_Petal_Width = mean(Petal.Width)
)
```
dplyr 1.0之后,使用`across()`函数异常简练
```{r colwise-31}
iris %>%
group_by(Species) %>%
summarise(
across(everything(), mean)
)
```
或者更科学的
```{r colwise-32}
iris %>%
group_by(Species) %>%
summarise(
across(is.numeric, mean)
)
```
可以看到,以往是一列一列的处理,现在对**多列同时操作**,这主要得益于`across()`函数,它有两个主要的参数:
```{r colwise-33, eval = FALSE}
across(.cols = , .fns = )
```
- 第一个参数.cols,选取我们要需要的若干列,选取多列的语法与`select()`的语法一致
- 第二个参数.fns,我们要执行的函数(或者多个函数),函数的语法有三种形式可选:
- A function, e.g. mean.
- A purrr-style lambda, e.g. ~ mean(.x, na.rm = TRUE)
- A list of functions/lambdas, e.g. list(mean = mean, n_miss = ~ sum(is.na(.x))
再看看这个案例
```{r colwise-34}
std <- function(x) {
(x - mean(x)) / sd(x)
}
iris %>%
group_by(Species) %>%
summarise(
across(starts_with("Sepal"), std)
)
# purrr style
iris %>%
group_by(Species) %>%
summarise(
across(starts_with("Sepal"), ~ (.x - mean(.x)) / sd(.x))
)
```
```{r colwise-35}
iris %>%
group_by(Species) %>%
summarise(
across(starts_with("Petal"), list(min = min, max = max))
# across(starts_with("Petal"), list(min = min, max = max), .names = "{fn}_{col}")
)
```
```{r colwise-36}
iris %>%
group_by(Species) %>%
summarise(
across(starts_with("Sepal"), mean),
Area = mean(Petal.Length * Petal.Width),
across(c(Petal.Width), min),
n = n()
)
```
除了在`summarise()`里可以使用外,在其它函数也是可以使用的
```{r colwise-37}
iris %>% mutate(across(is.numeric, mean))
iris %>% mutate(across(starts_with("Sepal"), mean))
iris %>% mutate(across(is.numeric, std)) # std function has defined before
iris %>% mutate(
across(is.numeric, ~ .x / 2),
across(is.factor, stringr::str_to_upper)
)
```
## "current" group or "current" variable
- `n()`, 返回当前分组的多少行
- `cur_data()`, 返回当前分组的数据内容(不包含分组变量)
- `cur_group()`, 返回当前分组的分组变量(一行一列的数据框)
- `across(cur_column())`, 返回当前列的列名
这些函数**返回当前分组的信息**,因此只能在特定函数内部使用,比如`summarise()` and `mutate()`
```{r colwise-38}
df <- tibble(
g = sample(rep(letters[1:3], 1:3)),
x = runif(6),
y = runif(6)
)
df
```
```{r colwise-39}
df %>%
group_by(g) %>%
summarise(
n = n()
)
```
```{r colwise-40}
df %>%
group_by(g) %>%
summarise(
data = list(cur_group())
)
```
```{r colwise-41}
df %>%
group_by(g) %>%
summarise(
data = list(cur_data())
)
```
```{r colwise-42}
mtcars %>%
group_by(cyl) %>%
summarise(
broom::tidy(lm(mpg ~ wt, data = cur_data()))
)
```
```{r colwise-43}
df %>%
group_by(g) %>%
mutate(across(everything(), ~ paste(cur_column(), round(.x, 2))))
```
```{r colwise-44}
wt <- c(x = 0.2, y = 0.8)
df %>%
mutate(
across(c(x, y), ~ .x * wt[cur_column()])
)
```
## 行方向操作
数据框中向量de方向,事实上可以看做有两个方向,横着看是row-vector,竖着看是col-vector。
```{r colwise-45, out.width = '100%', fig.align='left', echo = FALSE}
knitr::include_graphics(path = "images/vctr.png")
```
tidyverse遵循的tidy原则,一列表示一个变量,一行表示一次观察。
这种数据的存储格式,对ggplot2很方便,但对**行方向**的操作或者运算不友好。比如
### 行方向上的统计
```{r colwise-46}
df <- tibble(id = letters[1:6], w = 10:15, x = 20:25, y = 30:35, z = 40:45)
df
```
计算每行的均值,
```{r colwise-47}
df %>% mutate(avg = mean(c(w, x, y, z)))
```
好像不对?为什么呢?
- 按照tidy的方法
```{r colwise-48}
df %>%
pivot_longer(
cols = -id,
names_to = "variable",
values_to = "value"
) %>%
group_by(id) %>%
summarize(
r_mean = mean(value)
)
```
如果保留原始数据,就还需要再`left_join()`一次,虽然思路清晰,但还是挺周转的。
- 按照Jenny Bryan的方案,使用`purrr`宏包的`pmap_dbl`函数
```{r colwise-49}
library(purrr)
df %>%
mutate(r_mean = pmap_dbl(select_if(., is.numeric), lift_vd(mean)))
```
但需要学习新的语法,代价也很高。
- `rowwise()`
```{r colwise-50}
df %>%
rowwise() %>%
mutate(avg = mean(c(w, x, y, z)))
```
变量名要是很多的话,又变了体力活了,怎么才能变的轻巧一点呢?
- `rowwise() + c_across()`,现在dplyr 1.0终于给出了一个很好的解决方案
```{r colwise-51}
df %>%
rowwise() %>%
mutate(
avg = mean(c_across(w:z))
)
```
这个很好的解决方案中,`rowwise()`工作原理类似与`group_by()`,是按每一行进行分组,然后按行(行方向)统计
```{r colwise-52}
df %>%
rowwise(id) %>%
mutate(total = mean(c_across(w:z)))
df %>%
rowwise(id) %>%
mutate(mean = mean(c_across(is.numeric)))
df %>%
rowwise(id) %>%
summarise(
m = mean(c_across(is.numeric))
)
```
因此,我们可以总结成下面这张图
```{r colwise-53, out.width = '100%', fig.align='left', echo = FALSE}
knitr::include_graphics(path = "images/colwise_vs_rowwise.jpg")
```
### 行方向处理与列表列是天然一对
`rowwise()`不仅仅用于计算行方向均值这样的简单统计,而是当处理**列表列**时,方才显示出`rowwise()`与`purrr::map`一样的强大。那么,什么是**列表列**?
**列表列**指的是数据框的一列是一个列表, 比如
```{r colwise-54}
tb <- tibble(
x = list(1, 2:3, 4:6)
)
```
如果想显示列表中每个元素的长度,用purrr包,可以这样写
```{r colwise-55}
tb %>% mutate(l = purrr::map_int(x, length))
```
如果从行方向的角度理解,其实很简练
```{r colwise-56}
tb %>%
rowwise() %>%
mutate(l = length(x))
```
### 行方向上的建模
```{r colwise-57}
mtcars <- mtcars %>% as_tibble()
mtcars
```
以cyl分组,计算每组中`mpg ~ wt`的线性模型的系数.
```{r colwise-58}
mtcars %>%
group_by(cyl) %>%
nest()
```
#### 列方向的做法
分组建模后,形成**列表列**,此时列表中的每个元素对应一个模型,我们需要依次提取每次模型的系数,列方向的做法是,借用`purrr::map`完成列表中每个模型的迭代,
```{r colwise-59}
mtcars %>%
group_by(cyl) %>%
nest() %>%
mutate(model = purrr::map(data, ~ lm(mpg ~ wt, data = .))) %>%
mutate(result = purrr::map(model, ~ broom::tidy(.))) %>%
unnest(result)
```
用`purrr::map`实现列表元素一个一个的依次迭代,从数据框的角度来看(数据框是列表的一种特殊形式),因此实质上就是一行一行的处理。所以,尽管purrr很强大,但需要一定学习成本,从解决问题的路径上也比较周折。
#### 行方向的做法
事实上,分组建模后,形成**列表列**,这种存储格式,天然地符合**行处理的范式**,因此一开始就使用行方向分组(这里`nest_by()` 类似于 `group_by()`)
```{r colwise-60}
mtcars %>%
nest_by(cyl) %>%
mutate(model = list(lm(mpg ~ wt, data = data))) %>%
summarise(broom::tidy(model))
```
```{r colwise-61}
# or
mtcars %>%
nest_by(cyl) %>%
summarise(
broom::tidy(lm(mpg ~ wt, data = data))
)
```
至此,tidyverse框架下,实现分组统计中的`数据框进,数据框输出`, 现在有四种方法了
```{r colwise-62,eval = FALSE}
mtcars %>%
group_nest(cyl) %>%
mutate(model = purrr::map(data, ~ lm(mpg ~ wt, data = .))) %>%
mutate(result = purrr::map(model, ~ broom::tidy(.))) %>%
tidyr::unnest(result)
mtcars %>%
group_by(cyl) %>%
group_modify(
~ broom::tidy(lm(mpg ~ wt, data = .))
)
mtcars %>%
nest_by(cyl) %>%
summarise(
broom::tidy(lm(mpg ~ wt, data = data))
)
mtcars %>%
group_by(cyl) %>%
summarise(
broom::tidy(lm(mpg ~ wt, data = cur_data()))
)
# or
mtcars %>%
group_by(cyl) %>%
summarise(broom::tidy(lm(mpg ~ wt)))
```
## 参考资料
- https://dplyr.tidyverse.org/dev/articles/rowwise.html
- https://dplyr.tidyverse.org/dev/articles/colwise.html
```{r colwise-63, echo = F}
# remove the objects
# rm(list=ls())
rm(df, my_quantile, std, tb, wt)
```
```{r colwise-64, echo = F, message = F, warning = F, results = "hide"}
pacman::p_unload(pacman::p_loaded(), character.only = TRUE)
```