Ching-Chuan Chen's Blogger

Statistics, Machine Learning and Programming

0%

introduction to dplyr

這是我自己在PTT PO的文,詳細介紹dplyr,以下是正文~~

這篇重點放在dplyr

plyr與dplyr有不少函數是重疊的

不過都會以dplyr為主

plyr跟dplyr有一些名字不同,但功能相似的函數

我會一併介紹

先列一下這章要介紹的函數 (沒標註的就是來自dplyr)

A. 基本整理的函數:arragnge, filter, mutate, select, group_by, summarise, n

B. 增併rownames為變數:add_rownames, plyr:::name_rows

C. list to data.frame:as_data_frame

D. by var 合併函數:join, plyr:::join, data.table:::merge, base:::merge

E. col/row 合併函數:bind_rows, data.table:::rbindlist, bind_cols

F. 取唯一列:distinct, data.table:::unique

G. 列行運算:rowwise, plyr:::colwise

H. 值映射(對應修改):plyr:::mapvalues, plyr:::revalue

I. 其他函數:summarise_each, mutate_each

J. 特殊函數:plyr:::here

  1. 基本整理函數

arrange: 根據你選定的變數做排列 (可以是多個變數)

filter: 根據你設定的條件做row 篩選(or selection)

mutate: 根據你給定的值賦予新變數,或是變更舊變數

select: 根據給定的變數名稱做選擇,也可以做刪除變數

group_by: 根據給定變數做group,以銜接summarise

summarise: 資料整併

n: 計算資料個數

用一個簡單例子來展示用法:

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
  set.seed(100)
(dt = data.table(V1 = rpois(5, 3),
V2 = sample(c("g1", "g2"), 5, 1), V3 = rnorm(5)))
# V1 V2 V3
# 1: 2 g1 0.3186301
# 2: 2 g2 -0.5817907
# 3: 3 g1 0.7145327
# 4: 1 g2 -0.8252594
# 5: 3 g1 -0.3598621

dt %>% arrange(V1, V2, V3)
# V1 V2 V3
# 1: 1 g2 -0.8252594
# 2: 2 g1 0.3186301
# 3: 2 g2 -0.5817907
# 4: 3 g1 -0.3598621
# 5: 3 g1 0.7145327

dt %>% filter(V1 <= 2, V3 < 0)
# V1 V2 V3
# 1: 2 g2 -0.5817907
# 2: 1 g2 -0.8252594

dt %>% mutate(V5 = V1 * V3, V6 = substr(V2, 2, 2),
V7 = round(V3), V8 = 1L, V3 = V3 **2)
# V1 V2 V3 V5 V6 V7 V8
# 1: 2 g1 0.1015251 0.6372602 1 0 1
# 2: 2 g2 0.3384804 -1.1635814 2 -1 1
# 3: 3 g1 0.5105570 2.1435981 1 1 1
# 4: 1 g2 0.6810531 -0.8252594 2 -1 1
# 5: 3 g1 0.1295008 -1.0795864 1 0 1

dt %>% select(V1, V2)
# V1 V2
# 1: 2 g1
# 2: 2 g2
# 3: 3 g1
# 4: 1 g2
# 5: 3 g1

dt %>% group_by(V2) %>% summarise(size_g = n(), m_V3 = mean(V3),
s_V1 = sum(V1))
# V2 size_g m_V3 s_V1
# 1 g1 3 0.2244336 8
# 2 g2 2 -0.7035251 3

上面的例子是一些簡單運用的範例

先介紹一下tbl_df, tbl_dt的class

tbl_df跟tbl_dt只會列出一部分的資料

做操作時比較不會因為太多資料的輸出造成當機

要更改列出的資料量,可以這樣做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  set.seed(100)
(dt = data.table(V1 = rpois(50, 3), V2 = sample(c("g1", "g2"), 50, 1),
V3 = rnorm(50))) %>% tbl_dt(FALSE)
# V1 V2 V3
# 1 2 g1 -0.4470622
# 2 2 g1 -1.7385979
# 3 3 g1 0.1788648
# 4 1 g1 1.8974657
# 5 3 g2 -2.2719255
# 6 3 g1 0.9804641
# 7 4 g1 -1.3988256
# 8 2 g1 1.8248724
# 9 3 g2 1.3812987
# 10 1 g1 -0.8388519
# .. .. .. ...

print(dt, n = 5)
# V1 V2 V3
# 1 2 g1 -0.4470622
# 2 2 g1 -1.7385979
# 3 3 g1 0.1788648
# 4 1 g1 1.8974657
# 5 3 g2 -2.2719255
# .. .. .. ...

再介紹一些這些函數的其他用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  dt = data.table(V1 = rpois(20, 3), V2 = sample(c("g1", "g2"), 20, 1),
V3 = rnorm(20), V4 = rgamma(20, 5, 3))
# 你可以直接用一個你想要使用的變數放入,不須先立變數
dt %>% arrange(V1*V3, V3)
dt %>% filter(abs(V1*V3) > 1)
# desc是dplyr的函數提供反向排列
dt %>% arrange(V1)
dt %>% arrange(desc(V1))
# 兩種做變數刪除的方式 (我偏好第二種)
dt %>% mutate(V4 = NULL)
dt %>% select(-V4)
# select 還有提供各種特別函數於select中使用
dt %>% select(starts_with("V"))
dt %>% select(ends_with("1"))
dt %>% select(contains("2"))
dt %>% select(matches("\\w\\d"))
dt %>% select(num_range("V", 1:2))
  1. 增併rownames為變數

如標題所示,直接看範例

1
2
3
4
dat = data.frame(A = 1:5, row.names = paste0("City_", LETTERS[1:5]))
dat %>% name_rows
dat %>% add_rownames
dat %>% add_rownames("city") # add_rownames可以改成你要的名稱
  1. list to data.frame

as_data_frame提供比as.data.frame有效率的轉換方法

我之前也沒用過,不過看到manual寫到這個函數,就忍不住想分享一下

不過這個函數強迫list的element要有name,使用上要注意一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  library(microbenchmark)
dat_list = lapply(rep(1e6, 200), rnorm)
names(dat_list) <- paste0("A", 1:200)
microbenchmark(
as_data_frame(dat_list),
as.data.frame(dat_list)
)
#Unit: milliseconds
# expr min lq mean median uq
# as_data_frame(dat_list) 1.22642 1.281156 1.418296 1.311944 1.339027
# as.data.frame(dat_list) 19.83196 20.199147 21.397833 20.350524 21.143335
# expr max neval
# as_data_frame(dat_list) 6.957693 100
# as.data.frame(dat_list) 33.307182 100

看起來是沒差很多啦(汗顏,可能資料不夠大

  1. by var 合併函數

先介紹base的merge,這個函數是用來合併兩個data.frame

除了input的兩個data.frame,還有其他五個input (其他input之後再提)

a. by - 合併根據的變數
b. by.x - 合併根據的變數 於第一個data.frame的名稱
c. by.y - 合併根據的變數 於第二個data.frame的名稱
d. all.x - 是否保留來自第一個data.frame的values
e. all.y - 是否保留來自第一個data.frame的values

註:還有一個input是 all 可以一次控制all.x跟all.y

我用簡單的範例去介紹這幾個選項

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
## 產生資料
set.seed(75)
(x = data.frame(cat1 = sample(c("A", "B", NA), 5, 1),
cat2 = sample(c(1, 2, NA), 5, 1), v = rpois(5, 3),
stringsAsFactors = FALSE))
# cat1 cat2 v
# 1 A 1 4
# 2 A 2 3
# 3 <NA> NA 4
# 4 B NA 4
# 5 A 1 4
(y = data.frame(cat1 = sample(c("A", "B", NA), 5, 1),
cat2 = sample(c(1, 2, NA), 5, 1), v = rpois(5, 3),
stringsAsFactors = FALSE))
# cat1 cat2 v
# 1 A 2 1
# 2 A 1 8
# 3 <NA> NA 2
# 4 B 2 5
# 5 <NA> 1 3

## 兩個data.frame的資料都不保留 (預設值)
merge(x, y, by = c("cat1","cat2"), all.x = FALSE, all.y = FALSE)
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 1 4 8
# 3 A 2 3 1
# 4 <NA> NA 4 2

## 保留第一個data.frame的全部資料
merge(x, y, by = c("cat1","cat2"), all.x = TRUE, all.y = FALSE)
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 1 4 8
# 3 A 2 3 1
# 4 B NA 4 NA
# 5 <NA> NA 4 2

## 保留第二個data.frame的全部資料
merge(x, y, by = c("cat1","cat2"), all.x = FALSE, all.y = TRUE)
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 1 4 8
# 3 A 2 3 1
# 4 B 2 NA 5
# 5 <NA> 1 NA 3
# 6 <NA> NA 4 2

## 保留兩個data.frame全部的資料
merge(x, y, by = c("cat1","cat2"), all.x = TRUE, all.y = TRUE)
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 1 4 8
# 3 A 2 3 1
# 4 B 2 NA 5
# 5 B NA 4 NA
# 6 <NA> 1 NA 3
# 7 <NA> NA 4 2

all.x跟all.y這四種組合分別對應到dplyr的四種join

a. inner_join - merge(…, all.x = FALSE, all.y = FALSE)
b. left_join - merge(…, all.x = TRUE , all.y = FALSE)
c. right_join - merge(…, all.x = FASLE, all.y = TRUE)
d. full_join - merge(…, all.x = TRUE , all.y = TRUE)

但是merge跟dplyr的join還是有些微不同

dplyr的join不會去比對by variable都是NA的情況

給一個例子就好

1
2
3
4
5
6
  inner_join(x, y, by = c("cat1","cat2"))
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 2 3 1
# 3 <NA> NA 4 2
# 4 A 1 4 8

PS: If you use dplyr 0.4.1, there is something wrong. You’re gonna find the

output do not contain the line: 3 NA 4 2. Please update your dplyr

to 0.4.2 or higher version.

至於plyr:::join就沒有這個問題

1
2
3
4
5
6
  join(x, y, by = c("cat1","cat2"), 'inner')
# cat1 cat2 v v
# 1 A 1 4 8
# 2 A 2 3 1
# 3 <NA> NA 4 2
# 4 A 1 4 8

plyr:::join用法其實大同小異,它是用type去控制join方式

最後是data.table:::merge

1
2
3
4
5
6
7
8
  setDT(x)
setDT(y)
merge(x, y, by = c("cat1","cat2"))
# cat1 cat2 v.x v.y
# 1: NA NA 4 2
# 2: A 1 4 8
# 3: A 1 4 8
# 4: A 2 3 1

其實用法跟merge一模一樣,不贅述

介紹完by, all.x, 跟all.y之後,我們來介紹by.x跟by.y

用一個簡單例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
  set.seed(75)
x = data.frame(cat1 = sample(c("A", "B", NA), 5, 1),
cat2 = sample(c(1, 2, NA), 5, 1), v = rpois(5, 3),
stringsAsFactors = FALSE)
y = data.frame(cat3 = sample(c("A", "B", NA), 5, 1),
cat4 = sample(c(1, 2, NA), 5, 1), v = rpois(5, 3),
stringsAsFactors = FALSE)
merge(x, y, by.x = c("cat1","cat2"), by.y = c("cat3","cat4"))
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 1 4 8
# 3 A 2 3 1
# 4 <NA> NA 4 2

我想這個例子已經很好說明了by.x跟by.y了

接著是再dplyr怎麼做?

1
2
3
4
5
  inner_join(x, y, by = c("cat1" = "cat3", "cat2" = "cat4"))
# cat1 cat2 v.x v.y
# 1 A 1 4 8
# 2 A 2 3 1
# 3 A 1 4 8

至於plyr:::join跟data.table:::merge就沒有支援這種功能了

dplyr還提供兩種join: semi_join跟anti_join

簡單說明一下,semi_join就是只保留第一個data.frame變數的inner_join

anti_join則semi_join沒有配對的組合

這兩個有興趣再去玩玩看,這裡就不提供例子了

最後是一個實際問題

我如果要merge超過三個的df怎麼辦?

可以參考一下 #1LaHm_aH (R_Language)

這裡完整介紹一下使用這幾個套件要怎麼解決

1
2
3
4
5
6
7
8
9
  DF_list = replicate(5, data.frame(cat1 = sample(c("A", "B"), 5, 1),
cat2 = sample(c(1, 2), 5, 1), v = rnorm(5)), simplify = FALSE)
# 下列兩種會變成橫表,每一個data.frame的v都會保留
Reduce(function(x, y) merge(x, y, by = c("cat1","cat2"), all=TRUE), DF_list)
Reduce(function(x, y) full_join(x, y, by = c("cat1","cat2")), DF_list)
# 只保留第一個data.frame的值
join_all(DF_list, by = c("cat1","cat2"), type = "full")
# 直表,保留全部的v,等同於全部做rbind
join_all(DF_list, by = c("cat1","cat2", "v"), type = "full")

其實這樣每一個方法的結果都會很混亂,非常不建議,除非你知道你目標是什麼

  1. col/row 合併函數

bind_rows跟rbindlist其實就是在做 do.call(rbind, .)或是 Reduce(rbind, .)

只是這兩個function更加有效率

如果還不懂do.call(rbind, .)跟Reduce(rbind, .)再做什麼

剛好可以利用這個機會去弄懂他們在幹嘛

1
2
3
4
DF_list = replicate(5, data.frame(cat1 = sample(c("A", "B"), 5, 1),
cat2 = sample(c(1, 2), 5, 1), v = rnorm(5)), simplify = FALSE)
bind_rows(DF_list)
rbindlist(DF_list)

bind_cols等同於 do.call(cbind, .)

1
2
3
DT_list = lapply(1:5, function(x) data.table(rnorm(5)) %>%
setnames(paste0("V", x)))
bind_cols(DT_list)

此為下半部部分,下部分要介紹的function如下:

A. 取唯一列:distinct, data.table:::unique

B. 列行運算:rowwise, plyr:::colwise

C. 值映射(對應修改):plyr:::mapvalues, plyr:::revalue

D. 其他函數:summarise_each, mutate_each

E. 特殊函數:plyr:::here

F. function with underscore suffix

  1. 取唯一列

我在第一章 data.table提過 unique

distinct是一樣的方法

就從data.table:::unique的例子開始

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
  DT = data.table(A = rbinom(5, 1, 0.5), B = rbinom(5, 1, 0.5))
# A B
# 1: 0 0
# 2: 0 1
# 3: 1 0
# 4: 0 1
# 5: 0 0

unique(DT)
# A B
# 1: 0 0
# 2: 0 1
# 3: 1 0

distinct(DT)
# A B
# 1: 0 0
# 2: 0 1
# 3: 1 0

unique(DT, by = "A")
# A B
# 1: 0 0
# 2: 1 0

distinct(DT, A)
# A B
# 1: 0 0
# 2: 1 0

但是如mutate, filter等函數

distinct也可以給由變數組成的其他條件來做取唯一列的動作

給個簡單例子:

1
2
3
  distinct(DT, A*B)
# A B A * B
# 1: 0 0 0
  1. 列行運算

mutate提供了列運算的函數 rowwise

雖然沒有apply好寫,不過效率確實好上不少

至少在chain上可以比較輕易地套用了

要介紹rowwise,還需要介紹do這個函數

do提供一些廣泛的運算方式

像是你可以根據不同變數設立模型如範例所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  set.seed(100)
DF = data.frame(A = sample(1:5, 50, TRUE),
y = rnorm(50), x = rnorm(50))
models = DF %>% group_by(A) %>% do(model = lm(y ~ x, data = .))
models %>% summarise(mse = mean(model$residuals**2))
# mse
# 1 0.07560431
# 2 0.03617592
# 3 0.71000090
# 4 0.35385491
# 5 1.39799410

DT = data.table(A = sample(1:5, 50, TRUE),
y = rnorm(50), x = rnorm(50))
models = group_by(DT, A) %>% do(model = lm(y ~ x, data = .))
models %>% rowwise %>% summarise(mse = mean(model$residuals**2))
# mse
# 1 0.8953635
# 2 0.3954457
# 3 0.1469698
# 4 0.6303710
# 5 0.2966472

data.table出來的結果不會自動group_by row

要自己手動加入rowwise,這點需要注意

do應該有更多應用,需要各位好好開發。

再來,給一個rowwise的應用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  set.seed(100)
(DT = data.table(A = rnorm(5), B = rnorm(5), C = rnorm(5)))
# A B C
# 1: -0.50219235 0.3186301 0.08988614
# 2: 0.13153117 -0.5817907 0.09627446
# 3: -0.07891709 0.7145327 -0.20163395
# 4: 0.88678481 -0.8252594 0.73984050
# 5: 0.11697127 -0.3598621 0.12337950

DT %>% rowwise %>%
do(k = c(.$A, .$B, .$C)[between(c(.$A, .$B, .$C), 0, 1)]) %>%
summarise(size = length(k), sums = sum(k))
# size sums
# 1 2 0.4085162
# 2 2 0.2278056
# 3 1 0.7145327
# 4 2 1.6266253
# 5 2 0.2403508

colwise提供每一行的做一個動作的方法

其實等同於你用sapply做

只是它還可以讓你選擇你要做的column去做

簡單例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  set.seed(100)
(DT = data.table(A = rnorm(5), B = rnorm(5), C = rnorm(5)))
# A B C
# 1: -0.50219235 0.3186301 0.08988614
# 2: 0.13153117 -0.5817907 0.09627446
# 3: -0.07891709 0.7145327 -0.20163395
# 4: 0.88678481 -0.8252594 0.73984050
# 5: 0.11697127 -0.3598621 0.12337950

colwise(function(x) c(mean(x), sd(x)))(DT)
# A B C
# 1 0.1600685 0.1890685 -0.6537084
# 2 0.8140039 0.7824803 0.5610936

colwise(function(x) c(mean(x), sd(x)), .cols = .(A, B))(DT)
# A B
# 1 0.1600685 0.1890685
# 2 0.8140039 0.7824803

f = function(x) quantile(x, c(0.05, 0.95))
colwise(f)(DT)
# A B C
# 1 -0.4175373 -0.7765657 -0.1433299
# 2 0.7357341 0.6353522 0.6165483
  1. 值映射(對應修改)

這一節要介紹兩個很好用的函數 plyr:::mapvalues, plyr:::revalue

這兩個函數再處理資料時一定很常用到

我們再整理資料時,有時候會遇到奇異值

我們要把它改成我們想要的值時

大部分人應該都會遇到這個問題

或是有時候我們需要組別整併時也會遇到

給一個簡單的example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DT = data.table(col1 = sample(c("A", "B", "N"), 30, TRUE),
col2 = sample(c("A1", "B3", "C4", "U9", "Z5"), 30, TRUE),
col3 = sample(-2:2, 30, TRUE))
DT2 = copy(DT)


## example 1 - 當初keyin資料出問題,col1出現了"N"這個值,我們要通通改成NA
## 此處提供數種方法做
set(DT, which(DT$col1 == "N"), "col1", NA)
DT2 %>% mutate(col1 = ifelse(col1 == "N", NA, col1)) # transform同理
DT2 %>% mutate(col1 = revalue(col1, c(N = NA)))
DT2 %>% mutate(col1 = mapvalues(col1, "N", NA))

## example 2 - 我們想要把col2做調整 A1 改成 B3, U9改成Z5
## 這裡就直接提供用revalue以及mapvalues的方法
DT2 %>% mutate(col2 = revalue(col2, c(A1 = "B3", U9 = "Z5")))
DT2 %>% mutate(col2 = mapvalues(col2, c("A1", "U9"), c("B3", "Z5")))

## example 3 - 當初keyin時不小心搞錯col3的-2跟2的正負號,我們要把-2跟2做互換
DT2 %>% mutate(col3 = mapvalues(col3, c(-2, 2), c(2, -2)))

最後提示幾個重點,

revalue可以用在factor跟character上面,不能用在數值上

mapvalues可以用在integer, double跟character上面,factor要先轉chr

  1. 其他函數

這兩個函數 summarise_each, mutate_each,我自己也很少用

我提供一些例子做操作,先用簡單的例子

1
2
3
4
5
6
7
8
9
10
  DT = data.table(X = rnorm(100), Y = rnorm(100))
DT %>% summarise_each(funs(c(median(.), mean(.))))
# X Y
# 1: -0.02923809 0.08238550
# 2: -0.09154615 0.05153845
DT = data.table(X = c(3,2), Y = c(1, 4))
DT %>% mutate_each(funs(half = . / 2))
# X Y
# 1: 1.5 0.5
# 2: 1.0 2.0

這例子只是很簡單的介紹他們的功能

可能需要時間來累積看看是否有什麼好方法去用這兩個函數

  1. 特殊函數

介紹一個我覺得很有趣的函數 here

here可以幫助你再使用sapply系列時

可以使用mutate等

提一個簡單的例子

1
2
DTs = resplicate(5, data.table(A = rnorm(5)), simplify = FALSE)
DTs = lapply(DTs, here(mutate), Asq = A**2)
  1. function with underscore suffix

本章最後一個重點

怎麼使用像是filter_, select_, mutate_, …這些以_結尾的function

這些以_結尾的function提供使用者使用string作為input

中間執行的過程透過 lazyeval 來執行

(其實dplyr大部分函數都透過這個套件)

來看幾個簡單的例子

1
2
3
4
5
DT = data.table(A = rnorm(5), B = rnorm(5))
DT %>% select_("A")
DT %>% select(A)
DT %>% mutate(C = A*B)
(DT2 = DT %>% mutate_("A*B") %>% setnames("A*B", "C"))

這幾個例子應該可以很簡單的解釋有_的函數要怎麼使用

最後給個.dots的用法,這個代表說你可以製作適當的string放入

1
2
3
4
DT = data.table(x = rnorm(100), y = rnorm(100))
(DT %<>% mutate_(.dots = c("x^2", "y^2")) %>%
setnames(c("x^2", "y^2"), c("xsq", "ysq")))
DT %>% select_(.dots = c("y", "xsq"))

再給一個簡單的例子

1
2
3
4
DT = data.table(x1 = rnorm(100), x2 = rnorm(100))
cmds = paste0("x", 1:2, "^2")
newnames = c("xsq", "ysq")
DT %<>% mutate_(.dots = cmds) %>% setnames(cmds, newnames))