關卡 1

前半段的課程,大部分著重於從資料中萃取出資訊,並且整理成一個data.frame(結構化)。 這門課程則是要開始討論,當資訊已經被結構化之後,我們要如何整理結構化的資料。

關卡 2

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

關卡 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的函數共有:filterslicearrangeselectdistinctmutatesummarisesample_n。 接下來的課程中,我們一邊操作,一邊講解。

關卡 14

filter的目的是用來做列方向的過濾,所以經過filter處理後,資料的個數(nrow)會下降。 filter函數的用法為,第一個參數放我們要處理的data.frame,後面接著不同的過濾條件。 例如:filter(flights, month == 1, day == 1),請同學試試看。

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

關卡 15

同學應該會注意到,輸出結果中monthday都是1 了。 因為filter會將資料一筆一筆的對後面的語句(month == 1day == 1)做檢查。 R 在解析這些語句時,會自動將這些變數對應到flights的欄位。 所以在filter中的month就等同於flights$monthday就等同於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_1day_is_1is_target等等。 但是即使如此,dplyr提供的filter可以用更簡潔程式碼達到一樣的效果。 背後的原因就在於flights$monthflights$dayflights被省略了。

關卡 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的參數很多,但是這裡我們只要學三個參數:patternx和、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已經介紹得差不多了。接下來我們會介紹sliceslice很單純,slice(flights, 1:6)就等價於flights[1:6,]

關卡 33

讓我們直接進入練習吧!「用肌肉記憶」。 請同學選出flights的第10000 到 第20000筆資料。

slice(flights, 10000:20000)

關卡 34

接下來,我們練習arrangearrange會把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的值,以此類推。 所以我們看到的輸出中,最前面的結果就是monthday皆為最小的資料中,dep_time值最小的資料。

關卡 36

所以我們能不能根據上面的結果判斷:dep_time的最小值是517呢?(第一列的dep_time)

No

關卡 37

我們是不能確定這件事情的,因為有些資料可能有更小的dep_time,但是他們的daymonth太大,所以被排到後面去了。 我們來找找看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)來選取yearday之間的欄位。 請同學試試看。

select(flights, year:day)

關卡 43

如果是要反面選取,剃除掉yearday之間的欄位,只要使用:select(flights, -(year:day))。 請同學試試看。

select(flights, -(year:day))

關卡 44

進行到這裡,出一個小練習給同學。 請同學選出flightsdep_time為NA的資料,並只挑出yearmonthday這三欄。 請同學在視窗開啟的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,它會根據給定的函數,計算出單一的值。 舉例來說:maxmin都可以是這樣的函數,但是range就會出問題。 因為maxmin的輸出長度為1 ,但是range的輸出長度為2。

關卡 51

請同學輸入summarise(flights, mean(dep_delay, na.rm = TRUE))來計算平均的誤點時間。

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

關卡 52

我們也可以透過sample_nsample_fracflights中抽出資料。 這個方法在資料很大,且電腦又不夠力的狀況下很有用。 舉例來說,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

這時後,我們可能只能建立大量的暫存變數,如a1a2,或者是寫出不好讀的程式碼, 如: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

使用%>%寫程式,不只不需要命名大量的變數,如:a1a2等, 程式碼的看起來也比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

同學可能會想利用lapplysapply甚至是寫迴圈來解決這個問題。

關卡 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做比較,就是一個我國房地產泡沫化的指標