關卡 1
前半段的課程,大部分著重於從資料中萃取出資訊,並且整理成一個data.frame(結構化)。 這門課程則是要開始討論,當資訊已經被結構化之後,我們要如何整理結構化的資料。
關卡 2
R 有許多解決這類問題的函數,例如:melt
、subset
等等,族繁不及備載。 這些函數的名稱不容易記憶,而且效能並不是很好。
關卡 3
dplyr套件是Hadley Wickham和Romain Francois在2014上架的一個套件,是目前我認為在R 中做資料整理上最完善的套件之一。 dplyr提供了直觀的函數,並且能夠和SQL expression做對應,效能也被Romain透過C++進行優化過,上述的優勢讓我決定跳過傳統R 整理資料的工具,直接教大家dplyr。
關卡 4
請大家先安裝dplyr
套件。
check_then_install("dplyr", "0.4.3")
關卡 5
接著請大家載入dplyr套件。
library(dplyr)
關卡 6
請同學輸入學套件的起手式:vignette(package = "dplyr")
。
vignette(package = "dplyr")
關卡 7
Hadley等人都是開發R 套件界的大大,他們對vignette是非常重視的,所以我們會看到許多vignette。 說實話,我個人寫的dplyr套件的介紹絕對也不會比Hadley大大他們寫的精彩。 不過透過swirl的好處,就是我們可以把整個dplyr的功能都經歷過一遍。
關卡 8
請同學執行vignette("introduction", package = "dplyr")
,打開Introduction。
vignette("introduction", package = "dplyr")
關卡 9
簡單起見,我們直接跟著vignette,拿nycflights13
中的資料集flights
做練習。 請同學先安裝nycflights13
套件。
check_then_install("nycflights13", "0.1")
關卡 10
接著載入nycflights13
套件。
library(nycflights13)
關卡 11
這個套件提供的flights資料集合,內容為所有於2013年在紐約起降的飛機資料。
關卡 12
Vignette中宣稱flights共有336776筆資料。 我們進行驗證一下,也順便讓同學複習data.frame的操作。 請同學用指令查詢flights共有多少資料。
nrow(flights)
關卡 13
在dplyr中,處理data.frame的函數共有:filter
、slice
、arrange
、select
、distinct
、mutate
、summarise
和sample_n
。 接下來的課程中,我們一邊操作,一邊講解。
關卡 14
filter
的目的是用來做列方向的過濾,所以經過filter
處理後,資料的個數(nrow
)會下降。 filter
函數的用法為,第一個參數放我們要處理的data.frame,後面接著不同的過濾條件。 例如:filter(flights, month == 1, day == 1)
,請同學試試看。
filter(flights, month == 1, day == 1)
關卡 15
同學應該會注意到,輸出結果中month
和day
都是1 了。 因為filter
會將資料一筆一筆的對後面的語句(month == 1
和day == 1
)做檢查。 R 在解析這些語句時,會自動將這些變數對應到flights
的欄位。 所以在filter
中的month
就等同於flights$month
,day
就等同於flights$day
。
關卡 16
熟悉SQL expression的同學請注意,filter
就是SQL中的WHERE
。
關卡 17
如果要使用RBasic系列所使用的語法,要如何得到同樣的結果呢? 請同學試著寫寫看。 提示:兩個布林比較和一個[
。 這題請在檔案:RDataEngineer-05-01.R中作答,完成後請存檔並切換到console中輸入:submit()。
# local函數只會輸出最後一個expression的結果
# 所以中間建立的變數不會污染到外部
answer01 <- local({
# 提示:先拿flights$month 和 1 比較
month_is_1 <- flights$month == 1
# 再拿flights$day 和 1 比較
day_is_1 <- flights$day == 1
# 拿上面兩個比較結果做 & 後丟到中括號的第一個參數。
is_target <- month_is_1 & day_is_1
flights[is_target,]
})
關卡 18
為了降低難度,我們在scripts中讓大家是一步一步的做操作。 實務上,熟悉R 的使用者會直接寫一行:flights[flights$month == 1 & flights$day == 1,]
,這是一種程式碼的壓縮。
關卡 19
程式碼的壓縮,讓使用者可以減少打字及減少設定暫存變數,如剛剛寫的month_is_1
、day_is_1
和is_target
等等。 但是即使如此,dplyr提供的filter
可以用更簡潔程式碼達到一樣的效果。 背後的原因就在於flights$month
和flights$day
的flights
被省略了。
關卡 20
一般來說,filter
的第一個參數,代表著要做處理的data.frame,而後面的參數皆都為條件。 每個條件就是一個expression,並且輸出布林向量。 filter
則只會回傳那些滿足所有條件的資料。
關卡 21
這堂課程介紹的dplyr的函數,都具有一樣的性質:第一個參數是要處理的data.frame,而其餘的參數是不同的expression,且這些expression中的變數名稱都會優先對應到data.frame 中的欄位。
關卡 22
請同學試著從flights
中找出flights$month
為 1,flights$day
為2的資料。
filter(flights, month == 1, day == 2)
關卡 23
filter也會自動過濾掉結果為 NA 的條件。 在下一個題目,同學可以趁機摸索看看。
關卡 24
請問同學,在2013年的紐約,共有多少班次的飛機起飛延誤呢?(dep_delay > 0
) 請同學在視窗開啟的Script中編寫,並且在完成後輸入submit()
。
answer02 <- local({
# 請從flights篩選出dep_delay > 0的資料
target <- filter(flights, dep_delay > 0)
nrow(target)
})
關卡 25
上一題的答案也是可以做程式碼的壓縮的。 同學可以先用filter(flights, dep_delay > 0)
篩選出資料集之後,直接把結果的值傳到nrow
的第一個參數,也就是:nrow(filter(flights, dep_delay > 0))
。
關卡 26
有時候我們希望找出符合特定條件的文字。 舉例來說,如果我們要找出tailnum
中包含有"AA"
的文字,要怎麼做呢? R 內建有一個grepl
的函數可以解決這個問題。 請同學先輸入?grepl
打開它的說明文件。
?grepl
關卡 27
grepl
的參數很多,但是這裡我們只要學三個參數:pattern
、x
和、fixed
。 第一個參數pattern
,代表的是我們要找的模式。 舉例來說,如果我們要找"AA"
,pattern就會是"AA"
。 但是如果是在fixed = FALSE
狀態下,R 會使用「正則表示式」(Regular Expression)來處理pattern
,有時候會有預期外的結果。 這部份在同學學會正則表示式之前,都請先設定fixed = TRUE
比較單純。 x
就是我們要搜尋文字。 在這範例中,就是flights$tailnum
。
關卡 28
我們想輸出的程式碼是:filter(flights, grepl(pattern = <1>, x = <2>, fixed = TRUE))
, 這裡透過適當的設定grepl
的參數,就可以獲得我們想要的比對結果,也就是「tailnum中是不是有包含"AA"
這樣的文字」的結果。 而filter
再利用這樣的結果對資料做篩選。 請問同學,<1>應該要填寫什麼呢?
“AA”
關卡 29
我們想輸出的程式碼是:filter(flights, grepl(pattern = <1>, x = <2>, fixed = TRUE))
。 請問同學,<2>應該要填寫什麼?
flights$tailnum
關卡 30
再請問同學,filter(flights, grepl(pattern = "AA", x = tailnum, fixed = TRUE))
是不是也成立呢?
Yes
關卡 31
最後,請同學試試看:filter(flights, grepl(pattern = "AA", x = tailnum, fixed = TRUE))
。
filter(flights, grepl(pattern = "AA", x = tailnum, fixed = TRUE))
關卡 32
針對filter
已經介紹得差不多了。接下來我們會介紹slice
。 slice
很單純,slice(flights, 1:6)
就等價於flights[1:6,]
。
關卡 33
讓我們直接進入練習吧!「用肌肉記憶」。 請同學選出flights
的第10000 到 第20000筆資料。
slice(flights, 10000:20000)
關卡 34
接下來,我們練習arrange
。 arrange
會把data.frame的資料,根據後面的expression做排序。 請同學試試看:arrange(flights, month, day, dep_time)
。 給熟悉SQL的同學,arrange
的用法很類似SQL的ORDER BY
。
arrange(flights, month, day, dep_time)
關卡 35
我們仔細觀察上一題的輸出。 arrange
會先比較month
的值,當month
平手時就比較day
的值,以此類推。 所以我們看到的輸出中,最前面的結果就是month
、day
皆為最小的資料中,dep_time
值最小的資料。
關卡 36
所以我們能不能根據上面的結果判斷:dep_time
的最小值是517呢?(第一列的dep_time)
No
關卡 37
我們是不能確定這件事情的,因為有些資料可能有更小的dep_time
,但是他們的day
和month
太大,所以被排到後面去了。 我們來找找看dep_time
的最小值吧。 請同學輸入:min(flights$dep_time)
。
min(flights$dep_time)
關卡 38
我們注意到dep_time的資料有missing data。 導致上一題的輸出為NA
。 我們要怎麼忽略NA
呢? 請同學試試看:min(flights$dep_time, na.rm = TRUE)
。
min(flights$dep_time, na.rm = TRUE)
關卡 39
我們也可以把比較的順序從由小到大改成由大到小,只要加上desc
就行了。 舉例來說:arrange(flights, desc(month), desc(day), desc(dep_time))
,請同學試試看。
arrange(flights, desc(month), desc(day), desc(dep_time))
關卡 40
接下來我們來介紹select
。 通常一個很大的data.frame中,我們一次只會對少數幾欄資料有興趣,select
可以讓我們挑出我們有興趣的欄位。 請同學試試看:select(flights, year, month, day)
。 給熟SQL的同學,select
就是SQL中的SELECT
。
select(flights, year, month, day)
關卡 41
我們先檢查flights
的欄位名稱。 請同學複習一下之前所學的data.frame的操作。
colnames(flights)
關卡 42
我們也可以用select(flights, year:day)
來選取year
和day
之間的欄位。 請同學試試看。
select(flights, year:day)
關卡 43
如果是要反面選取,剃除掉year
到day
之間的欄位,只要使用:select(flights, -(year:day))
。 請同學試試看。
select(flights, -(year:day))
關卡 44
進行到這裡,出一個小練習給同學。 請同學選出flights
中dep_time
為NA的資料,並只挑出year
、month
和day
這三欄。 請同學在視窗開啟的Script中編寫,並且在完成後輸入submit()
。
answer03 <- local({
# 請用filter挑出dep_time為NA的資料
# 你可以用is.na
result1 <- dplyr::filter(flights, is.na(dep_time))
# 請用select從result1挑出year, month 和day
select(result1, year:day)
})
# 完成後,請回到console輸入submit()
關卡 45
上一題的目的是想看看dep_time為“NA”是不是有異常的模式。 例如,都聚集在某一天。 實務上在做資料分析時,常常需要對自己資料的正確性保持懷疑,因此常常要篩選出資料來做觀察確認。 此時,是否熟練的運用dplyr的函數就會影響到我們的工作效率。
關卡 46
接著我們介紹distinct
。 它可以挑出後續多個expression的組合中,不重複的部份。 所以distinct
能用來回答「資料有多少類」等問題。 給熟SQL的同學,distinct就是SQL的DISTINCT
。
關卡 47
請同學試試看:distinct(select(flights, year:day))
。 一年有365天,是不是每天都有資料呢?
distinct(select(flights, year:day))
關卡 48
當我們要更改,或是新增欄位時,就可以使用mutate
這個欄位了。 舉例來說,如果我們要新增一個欄位gain
,它是拿arr_delay扣掉dep_delay,代表飛機停留在紐約機場待的時間比原本簡短多少。 此時,我們可以用:mutate(flights, gain = arr_delay - dep_delay)
。
mutate(flights, gain = arr_delay - dep_delay)
關卡 49
mutate
的功能就類似SQL的UPDATE,但因為可以新增欄位,所以更為廣泛。 在mutate
中,後面的expression可以使用前面expression所新增的欄。
關卡 50
最後我們介紹summarise
,它會根據給定的函數,計算出單一的值。 舉例來說:max
、min
都可以是這樣的函數,但是range
就會出問題。 因為max
和min
的輸出長度為1 ,但是range
的輸出長度為2。
關卡 51
請同學輸入summarise(flights, mean(dep_delay, na.rm = TRUE))
來計算平均的誤點時間。
summarise(flights, mean(dep_delay, na.rm = TRUE))
關卡 52
我們也可以透過sample_n
或sample_frac
從flights
中抽出資料。 這個方法在資料很大,且電腦又不夠力的狀況下很有用。 舉例來說,sample_n(flights, 10)
會從數據中抽10筆資料。 請同學試試看。
sample_n(flights, 10)
關卡 53
也請同學試試看:sample_frac(flights, 0.01)
。
sample_frac(flights, 0.01)
關卡 54
當需要取後放回的隨機抽樣時,可以下參數:replace = TRUE
。 而當希望機率不相等時,可以把機率的比率送到weight
這個參數。
關卡 55
在認識這些主要的dplyr功能後,同學是不是有注意到,第一個參數總是我們要處理的data.frame呢?
關卡 56
本堂課第二個要點是,我們可以在後面的參數,後續的expression中省略data.frame的變數名稱。
關卡 57
而第三個要點我們前面沒有強調,就是每次產生的data.frame都是新的,我們並沒有更動原本的flights
。
關卡 58
有了這些工具之後,就可以組合出複雜的邏輯。
關卡 59
這裡請同學做一些小練習。 請在完成之後存檔,並輸入submit()
來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen。
# 我們定義gain為arr_delay - dep_delay
# 請算出1 月份平均的gain
answer04.1 <- local({
#' 以下的答案有使用`[[`這個函數。道理是這樣:
#' 在R之中,中括號等符號的背後也是函數。
#' 例如 tmp[[1]] 等同 `[[`(tmp, 1)
#' ps. 這裡需要在console中輸入中括號兩邊的反引號(`),告訴R 這裡的[[代表的是函數
#' 我是期待同學寫出:
#' tmp <- filter(...) %>% ...
#' tmp[[1]]
#' 的答案,但是這裡的參考答案用了上述知識與`%>%`做搭配搭配
filter(flights, month == 1) %>%
mutate(gain = arr_delay - dep_delay) %>%
summarise(mean(gain, na.rm = TRUE)) %>%
`[[`(1)
})
stopifnot(class(answer04.1) == "numeric")
stopifnot(length(answer04.1) == 1)
# 請問carrier為AA的飛機,是不是tailnum都有AA字眼?
answer04.2 <- local({
# 請填寫你的程式碼
# 請給出你的答案: TRUE or FALSE
retval <-
filter(flights, carrier == "AA", !grepl("AA", tailnum)) %>%
nrow
retval == 0
})
stopifnot(class(answer04.2) == "logical")
stopifnot(length(answer04.2) == 1)
# 請問dep_time介於 2301至2400之間的平均dep_delay為何
answer04.3 <- local({
# 請填寫你的程式碼
retval <-
filter(flights, 2301 <= dep_time, dep_time <= 2400) %>%
summarise(mean(dep_delay, na.rm = TRUE))
retval[[1]]
})
stopifnot(class(answer04.3) == "numeric")
stopifnot(length(answer04.3) == 1)
# 完成後請存檔,並回到console輸入`submit()`
關卡 60
接下來我們跟同學介紹R 在2014年開始發展的一種寫法,稱作「pipeline operator」。
關卡 61
在剛剛的練習中,同學可能會寫出如:summarise(filter(flights, ...))
的程式碼。 或是使用大量的暫存變數,如:a1 <- filter(flights, ...)
以及a2 <- summarise(a1, ...)
。 在整理資料的時候,我們常常要對數據做連續的操作(例如:先filter
再進行summarise
等)
關卡 62
這時後,我們可能只能建立大量的暫存變數,如a1
,a2
,或者是寫出不好讀的程式碼, 如:summarise(filter(flights, ...))
關卡 63
在dplyr中導入了magrittr在2014年的發明:pipeline operator,%>%
。 %>%
會將上一個函數的輸出,放到後面函數的第一個參數。 也就是說,上述的程式碼可以改寫成:filter(flights, ...) %>% summarise(...)
。
關卡 64
而%>%
是可以串接的,所以實務上,我們就可以寫出:filter(flights, ...) %>% select(...) %>% mutate(...) %>% summarise
。 每一個函數的輸出,都是下一個函數的第一個參數(也就是要進行處理的data.frame)。 所以這段程式碼中,filter
的輸出就交給select
處理後,再交給mutate
,最後給summarise
。 各位同學從這邊,就可以看出dplyr在設計函數時所下的苦心。
關卡 65
使用%>%
寫程式,不只不需要命名大量的變數,如:a1
、a2
等, 程式碼的看起來也比summarise(mutate(select(filter(flights, ...), ...), ...), ...)
簡單的多了。
關卡 66
請同學將剛剛做的檔案:RDataEngineer-05-04.R 再讀一次。 我們需要再做一次這一個練習。
關卡 67
請同學用%>%
完成剛剛的 RDataEngineer-05-04.R。 請在完成之後存檔,並輸入submit()
來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen。
# 我們定義gain為arr_delay - dep_delay
# 請算出1 月份平均的gain
answer04.1 <- local({
#' 以下的答案有使用`[[`這個函數。道理是這樣:
#' 在R之中,中括號等符號的背後也是函數。
#' 例如 tmp[[1]] 等同 `[[`(tmp, 1)
#' ps. 這裡需要在console中輸入中括號兩邊的反引號(`),告訴R 這裡的[[代表的是函數
#' 我是期待同學寫出:
#' tmp <- filter(...) %>% ...
#' tmp[[1]]
#' 的答案,但是這裡的參考答案用了上述知識與`%>%`做搭配搭配
filter(flights, month == 1) %>%
mutate(gain = arr_delay - dep_delay) %>%
summarise(mean(gain, na.rm = TRUE)) %>%
`[[`(1)
})
stopifnot(class(answer04.1) == "numeric")
stopifnot(length(answer04.1) == 1)
# 請問carrier為AA的飛機,是不是tailnum都有AA字眼?
answer04.2 <- local({
# 請填寫你的程式碼
# 請給出你的答案: TRUE or FALSE
retval <-
filter(flights, carrier == "AA", !grepl("AA", tailnum)) %>%
nrow
retval == 0
})
stopifnot(class(answer04.2) == "logical")
stopifnot(length(answer04.2) == 1)
# 請問dep_time介於 2301至2400之間的平均dep_delay為何
answer04.3 <- local({
# 請填寫你的程式碼
retval <-
filter(flights, 2301 <= dep_time, dep_time <= 2400) %>%
summarise(mean(dep_delay, na.rm = TRUE))
retval[[1]]
})
stopifnot(class(answer04.3) == "numeric")
stopifnot(length(answer04.3) == 1)
# 完成後請存檔,並回到console輸入`submit()`
關卡 68
剛剛的第一個練習題(answer04.1),我們計算了一月份平均的gain(arr_delay - dep_delay)。 但是如果我們要計算一月、二月到十二月份平均的gain,並且做比較,要怎麼做呢?
關卡 69
同學可能會想利用lapply
、sapply
甚至是寫迴圈來解決這個問題。
關卡 70
dplyr提供了函數group_by
來解決這樣的問題。 它讓我們依照某個欄位,將整個資料切割成若干份。 之後,我們對它(經過group_by
處理後的data.frame)做的動作都會同步作用在每一份資料中(data.frame)。
關卡 71
舉例來說,我們可以用df <- group_by(flights, month)
,先用group_by
標記要根據month
對flights的資料做分割。 並且把這個動作的結果存到df
變數中。 請同學試試看。
df <- group_by(flights, month)
關卡 72
接著,我們將剛剛解答answer04.1
的動作中,扣掉filter
的部份,再做一次。 請同學看看改過之後的結果,輸入submit()
即可。這題不用對程式進行修改。
# 這是一種計算answer04.1的方式
# answer04.1 <- local({
# retval <-
# filter(flights, month == 1) %>%
# mutate(gain = arr_delay - dep_delay) %>%
# summarise(mean(gain, na.rm = TRUE))
# retval[[1]]
# })
answer05 <-
# 為了清楚起見,再寫一次df的定義
group_by(flights, month) %>%
# mutate 和 summarise 的部份就照抄
mutate(gain = arr_delay - dep_delay) %>%
summarise(mean(gain, na.rm = TRUE))
# 這裡的answer05是一個data.frame
關卡 73
讓我們看看結果吧。請同學輸入:answer05
。
answer05
關卡 74
是不是很輕鬆就可以算出每個月份的gain的平均呢?
關卡 75
這裡建議同學一個在實務上撰寫gruop_by
的思路。 首先,我們針對某個欄位(例如月份)做filter
,挑出特定的類別。 並對filter
的結果做不同的操作,回答並解決相對應的問題。 當我們想要把filter
之後的動作,重複的操作在每一種類別(例如每一個月份)時,只要把filter
換掉改成group_by
,後面的步驟照舊即可得到答案。
關卡 76
以上的內容就是這門課程對dplyr的介紹。 為了下一個課程,我們要做最後一個練習。
關卡 77
變數cl_info
記載著從金管會銀行局的一般銀行及信用合作社消費者貸款業務項目。 同學可以使用View(cl_info)
看一看資料。
View(cl_info)
關卡 78
這個練習是要對這個資料做一連串的整理之後,算出每個月份的銀行房貸放款數量。 這個數字會和我國的房地產是否泡沫化有關。
關卡 79
請在完成之後存檔,並輸入submit()
來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen
cl_info2 <- local({
# 請填寫你的程式碼
mutate(cl_info, year_month = substring(data_dt, 1, 7)) %>%
select(year_month, mortgage_bal)
})
stopifnot(class(cl_info2$year_month)[1] == "character")
stopifnot(ncol(cl_info2) == 2)
stopifnot(!is.null(cl_info2$mortgage_bal))
cl_info3 <- local({
# 請填寫你的程式碼
group_by(cl_info2, year_month) %>%
summarise(mortgage_total_bal = sum(mortgage_bal)) %>%
arrange(year_month)
})
stopifnot(nrow(cl_info3) == 98)
stopifnot(ncol(cl_info3) == 2)
stopifnot(!is.unsorted(cl_info3$year_month))
#' 這個資料集只要能和GDP做比較,就是一個我國房地產泡沫化的指標