R Learning Notes: Manipulation(Dplyr)

Kevin Hsu
17 min readMay 29, 2017

--

此篇僅做記錄用,本文所有文字內容,皆來自: R語言翻轉教室

— — — — — — — — — — — — — — — — — — — — — — —

前半段的課程,大部分著重於從資料中萃取出資訊,並且整理成一個data.frame(結構化)。

這門課程則是要開始討論,當資訊已經被結構化之後,我們要如何整理結構化的資料。

R 有許多解決這類問題的函數,例如:`melt`、`subset`等等,族繁不及備載。這些函數的名稱不容易記憶,而且效能並不是很好。

dplyr套件是Hadley Wickham和Romain Francois在2014上架的一個套件,是目前我認為在R中做資料整理上最完善的套件之一。

dplyr提供了直觀的函數,並且能夠和SQL expression做對應,效能也被Romain透過C++進行優化過,上述的優勢讓我決定跳過傳統R整理資料的工具,直接教大家dplyr。

Hadley等人都是開發R套件界的大大,他們對vignette是非常重視的,所以我們會看到許多vignette。

Introduction to dplyr

When working with data you must:

  • Figure out what you want to do.
  • Describe those tasks in the form of a computer program.
  • Execute the program.

The dplyr package makes these steps fast and easy:

  • By constraining your options, it simplifies how you can think about common data manipulation tasks.
  • It provides simple “verbs”, functions that correspond to the most common data manipulation tasks, to help you translate those thoughts into code.
  • It uses efficient data storage backends, so you spend less time waiting for the computer.

This document introduces you to dplyr’s basic set of tools, and shows you how to apply them to data frames. Other vignettes provide more details on specific topics:

  • databases: Besides in-memory data frames, dplyr also connects to out-of-memory, remote databases. And by translating your R code into the appropriate SQL, it allows you to work with both types of data using the same set of tools.
  • benchmark-baseball: see how dplyr compares to other tools for data manipulation on a realistic use case.
  • window-functions: a window function is a variation on an aggregation function. Where an aggregate function uses n inputs to produce 1 output, a window function uses n inputs to produce n outputs.

dplyr can work with data frames as is, but if you’re dealing with large data, it’s worthwhile to convert them to a tbl_df: this is a wrapper around a data frame that won’t accidentally print a lot of data to the screen.

Dplyr aims to provide a function for each basic verb of data manipulation:

  • filter() (and slice())
  • arrange()
  • select() (and rename())
  • distinct()
  • mutate() (and transmute())
  • summarise()
  • sample_n() (and sample_frac())

library(nycflights13)

這個套件提供的flights資料集合,內容為所有於2013年在紐約起降的飛機資料。

Vignette中宣稱flights共有336776筆資料。我們進行驗證一下,也順便讓同學複習data.frame的操作。 請同學用指令查詢flights共有多少資料:

> nrow(flights)
[1] 336776

在dplyr中,處理data.frame的函數共有:`filter`、`slice`、`arrange`、`select`、`distinct`、`mutate`、`summarise`和`sample_n`。接下來的課程中,我們一邊操作,一邊講解。

Filter ( 數值&文字)

`filter`的目的是用來做列方向的過濾,所以經過`filter`處理後,資料的個數(`nrow`)會下降。

`filter`函數的用法為,第一個參數放我們要處理data.frame,後面接著不同的過濾條件。

filter(flights, month == 1, day == 1)

# A tibble: 842 × 16
year month day dep_time dep_delay arr_time arr_delay carrier tailnum flight
1 2013 1 1 517 2 830 11 UA N14228 1545
2 2013 1 1 533 4 850 20 UA N24211 1714

輸出結果中`month`和`day`都是1 了。

因為`filter`會將資料一筆一筆的對後面的語句(`month == 1`和`day == 1`)做檢查。 R在解析這些語句時,會自動將這些變數對應到`flights`的欄位。

所以在`filter`中的`month`就等同於`flights$month`,`day`就等同於`flights$day`。

熟悉SQL expression的同學請注意,`filter`就是SQL中的`WHERE`。

練習將flight裡面 month ==1 & day==1的資料取出來:

answer01 <- local({
month_is_1 <- flights$month == 1
day_is_1 <- flights$day == 1
is_target <- month_is_1 & day_is_1
flights[is_target,] })

實務上,熟悉R的使用者會直接寫一行:

flights[flights$month == 1 & flights$day == 1,]

這是一種程式碼的壓縮。

程式碼的壓縮,讓使用者可以減少打字及減少設定暫存變數,如剛剛寫的`month_is_1`、`day_is_1`和`is_target`等等。

但是即使如此,dplyr提供的`filter`可以用更簡潔程式碼達到一樣的效果。

背後的原因就在於`flights$month`和`flights$day`的`flights`被省略了。

一般來說,`filter`的第一個參數,代表著要做處理的data.frame,而後面的參數皆都為條件。

每個條件就是一個expression,並且輸出布林向量。`filter`則只會回傳那些滿足所有條件的資料。

filter(flights, month == 1, day == 2)

filter也會自動過濾掉結果為 NA 的條件。

answer02 <- local({
target <- filter(flights,dep_delay > 0)
nrow(target) })

尋找班機中delay的次數:

nrow(filter(flights, dep_delay > 0))

有時候我們希望找出符合特定條件的文字

舉例來說,如果我們要找出tailnum中包含有"AA"的文字,要怎麼做呢? R 內建有一個grepl的函數可以解決這個問題。 請同學先輸入?grepl打開它的說明文件。

`grepl`的參數很多,但是這裡我們只要學三個參數:`pattern`、`x`和、`fixed`。

第一個參數`pattern`,代表的是我們要找的模式。

舉例來說,如果我們要找`”AA”`,pattern就會是`”AA”`。 但是如果是在`fixed =FALSE`狀態下,R 會使用「正則表示式」(Regular Expression)來處理`pattern`,有時候會有預期外的結果。

這部份在同學學會正則表示式之前,都請先設定`fixed = TRUE`比較單純。

`x`就是我們要搜尋文字。

我們想輸出的程式碼是:

filter(flights, grepl(pattern ="AA", x = flights$tailnum , fixed =TRUE))`

這裡透過適當的設定`grepl`的參數,就可以獲得我們想要的比對結果,也就是「tailnum中是不是有包含`”AA”`這樣的文字」的結果。

而`filter`再利用這樣的結果對資料做篩選。

slice

`slice`很單純,`slice(flights, 1:6)`就等價於`flights[1:6,]`。

arrange

`arrange`會把data.frame的資料,根據後面的expression做排序。

arrange(flights, month, day, dep_time)

# A tibble: 336,776 × 16
year month day dep_time dep_delay arr_time arr_delay carrier tailnum flight
1 2013 1 1 517 2 830 11 UA N14228 1545
2 2013 1 1 533 4 850 20 UA N24211 1714
3 2013 1 1 542 2 923 33 AA N619AA 1141
4 2013 1 1 544 -1 1004 -18 B6 N804JB 725
5 2013 1 1 554 -6 812 -25 DL N668DN 461

`arrange`會先比較`month`的值,當`month`平手時就比較`day`的值,以此類推。 所以我們看到的輸出中,最前面的結果就是`month`、`day`皆為最小的資料中,`dep_time`值最小的資料。

我們是不能確定517是dep_time中最小的,因為有些資料可能有更小的`dep_time`,但是他們的`day`和`month`太大,所以被排到後面去了。

min(flights$dep_time)
[1] NA

我們注意到dep_time的資料有missing data。 導致上一題的輸出為`NA`。我們要怎麼忽略`NA`呢?

`min(flights$dep_time, na.rm = TRUE)`。

[1] 1

我們也可以把比較的順序從由小到大改成由大到小,只要加上`desc`就行了。
arrange(flights, desc(month), desc(day), desc(dep_time))

給熟悉SQL的同學,`arrange`的用法很類似SQL的`ORDER BY`。

select

通常一個很大的data.frame中,我們一次只會對少數幾欄資料有興趣,`select`可以讓我們挑出我們有興趣的欄位

select(flights, year, month, day)

# A tibble: 336,776 × 3
year month day
<int> <int> <int>
1 2013 1 1
2 2013 1 1
3 2013 1 1
4 2013 1 1
5 2013 1 1

> colnames(flights)
[1] “year” “month” “day” “dep_time” “dep_delay” “arr_time” “arr_delay”
[8] “carrier” “tailnum” “flight” “origin” “dest” “air_time” “distance”
[15] “hour” “minute”

我們也可以用`select(flights, year:day)`來選取`year`和`day`之間的欄位。

select(flights, year:day)

# A tibble: 336,776 × 3
year month day
<int> <int> <int>
1 2013 1 1
2 2013 1 1
3 2013 1 1

如果是要反面選取,剃除掉`year`到`day`之間的欄位,只要使用:

select(flights, -(year:day))

請同學選出`flights`中`dep_time`為NA的資料,並只挑出`year`、`month`和`day`這三欄。

answer03 <- local({
result1 <- filter(flights,is.na(dep_time) )
select(result1,year,month,day)})

上一題的目的是想看看dep_time為”NA”是不是有異常的模式。 例如,都聚集在某一天。

實務上在做資料分析時,常常需要對自己資料的正確性保持懷疑,因此常常要篩選出資料來做觀察確認。

此時,是否熟練的運用dplyr的函數就會影響到我們的工作效率。

distinct

它可以挑出後續多個expression的組合中,不重複的部份。

所以`distinct`能用來回答「資料有多少類」等問題。

distinct(select(flights, year:day))

# A tibble: 365 × 3
year month day
1 2013 1 1
2 2013 1 2
3 2013 1 3
4 2013 1 4
# … with 355 more rows

mutate

我們要更改,或是新增欄位時,就可以使用`mutate`這個欄位了。

舉例來說,如果我們要新增一個欄位`gain`,它是拿arr_delay扣掉dep_delay,代表飛機停留在紐約機場待的時間比原本簡短多少。此時,我們可以用

mutate(flights, gain = arr_delay — dep_delay)

mutate`的功能就類似SQL的UPDATE,但因為可以新增欄位,所以更為廣泛。在`mutate`中,後面的expression可以使用前面expression所新增的欄。

summarise

最後我們介紹`summarise`,它會根據給定的函數,計算出單一的值。

舉例來說:`max`、`min`都可以是這樣的函數,但是`range`就會出問題。因為`max`和`min`的輸出長度為1 ,但是`range`的輸出長度為2。

summarise(flights,mean(dep_delay,na.rm =TRUE))

# A tibble: 1 × 1
`mean(dep_delay, na.rm = TRUE)`
<dbl>
1 12.63907

sample

我們也可以透過`sample_n`或`sample_frac`從`flights`中抽出資料。

這個方法在資料很大,且電腦又不夠力的狀況下很有用。 舉例來說,`sample_n(flights,10)`會從數據中抽10筆資料。 請同學試試看。

sample_n(flights, 10)

sample_frac(flights, 0.01)

當需要取後放回的隨機抽樣時,可以下參數:`replace = TRUE`。 而當希望機率不相等時,可以把機率的比率送到`weight`這個參數。

在認識這些主要的dplyr功能後,同學是不是有注意到,第一個參數總是我們要處理的data.frame

本堂課第二個要點是,我們可以在後面的參數,後續的expression中省略data.frame的變數名稱。

而第三個要點我們前面沒有強調,就是每次產生的data.frame都是新的,我們並沒有更動原本的`flights`。

pipeline operator (%>%)

接下來我們跟同學介紹R 在2014年開始發展的一種寫法,稱作「pipeline operator」。

在剛剛的練習中,同學可能會寫出如`summarise(filter(flights, …))`的程式碼。或是使用大量的暫存變數,如:`a1 <- filter(flights, …)`以及`a2 <- summarise(a1, …)`。

在整理資料的時候,我們常常要對數據做連續的操作(例如:先`filter`再進行`summarise`等)

在dplyr中導入了magrittr在2014年的發明:pipeline operator,`%>%`。

`%>%`會將上一個函數的輸出,放到後面函數的第一個參數。也就是說,上述的程式碼可以改寫成:`filter(flights, …) %>% summarise(…)`。

而`%>%`是可以串接的,所以實務上,我們就可以寫出

filter(flights, …) %>%
select(…) %>%
mutate(…) %>%
summarise`

每一個函數的輸出,都是下一個函數的第一個參數(也就是要進行處理的data.frame)。

所以這段程式碼中,`filter`的輸出就交給`select`處理後,再交給`mutate`,最後給`summarise`。

使用`%>%`寫程式,不只不需要命名大量的變數,如:`a1`、`a2`等,程式碼的看起來也比`summarise(mutate(select(filter(flights, …), …), …),…)`簡單的多了。

剛剛的第一個練習題(answer04.1),我們計算了一月份平均的gain(arr_delay-dep_delay)。

但是如果我們要計算一月、二月到十二月份平均的gain,並且做比較,要怎麼做呢?

dplyr提供了函數`group_by`來解決這樣的問題。它讓我們依照某個欄位,將整個資料切割成若干份。

之後,我們對它(經過`group_by`處理後的data.frame)做的動作都會同步作用在每一份資料中(data.frame)。

舉例來說,我們可以用`df <- group_by(flights , month)`,先用`group_by`標記要根據`month`對flights的資料做分割。並且把這個動作的結果存到`df`變數中。

answer05 <-
group_by(flights, month) %>%
mutate(gain = arr_delay — dep_delay) %>%
summarise(mean(gain, na.rm = TRUE))

> answer05

# A tibble: 12 × 2
month `mean(gain, na.rm = TRUE)`
<int> <dbl>
1 1 -3.855519
2 2 -5.147220
3 3 -7.356713
4 4 -2.673124
5 5 -9.370201
6 6 -4.244284
7 7 -4.810872
8 8 -6.529872
9 9 -10.648649
10 10 -6.400238
11 11 -4.958993
12 12 -1.611806

這裡建議同學一個在實務上撰寫`gruop_by`的思路。

1. 首先,我們針對某個欄位(例如月份)做`filter`,挑出特定的類別。

2. 並對`filter`的結果做不同的操作,回答並解決相對應的問題。

3. 當我們想要把`filter`之後的動作,重複的操作在每一種類別(例如每一個月份)時,只要把`filter`換掉改成`group_by`,後面的步驟照舊即可得到答案。

View(cl_info)

這個練習是要對這個資料做一連串的整理之後,算出【每個月份的銀行房貸放款數量】。

【這個數字會和我國的房地產是否泡沫化有關。】

--

--