Ching-Chuan Jamal Chen

introduction to dplyr

03 Jul 2015

這是我自己在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: 計算資料個數

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

  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只會列出一部分的資料

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

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

  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
# .. .. ..        ...

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

  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為變數

如標題所示,直接看範例

  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,使用上要注意一下

  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

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

## 產生資料
  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的情況

給一個例子就好

  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就沒有這個問題

  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

  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

用一個簡單例子:

  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怎麼做?

  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)

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

  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, .)再做什麼

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

  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, .)

  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的例子開始

  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也可以給由變數組成的其他條件來做取唯一列的動作

給個簡單例子:

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

mutate提供了列運算的函數 rowwise

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

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

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

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

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

  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的應用:

  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去做

簡單例子如下:

  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

  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,我自己也很少用

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

  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等

提一個簡單的例子

  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大部分函數都透過這個套件)

來看幾個簡單的例子

  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放入

  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"))

再給一個簡單的例子

  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))

comments powered by Disqus