關卡 1

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

關卡 2

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

關卡 3

dplyr套件是HadleyWickham和RomainFrancois在2014上架的一個套件,是目前我認為是在R做資料整理上最完善的套件之一。dplyr提供了直觀的函數,並且能夠和SQLexpression做對應,效能也被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

熟悉SQLexpression的同學請注意,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會使用「正則表示式」(RegularExpression)來處理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的ORDERBY

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的資料有missingdata。導致上一題的輸出為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_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({
  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年開始發展的一種寫法,稱作「pipelineoperator」。

關卡 61

在剛剛的練習中,同學可能會寫出如:summarise(filter(flights,...))的程式碼。或是使用大量的暫存變數,如:a1<-filter(flights,...)然後a2<-summarise(a1,...)在整理資料的時候,我們常常要對數據做連續的操作(例如先filtersummarise

關卡 62

這時後,我們可能只能建立大量的暫存變數,如a1a2,或者是寫出不好讀的程式碼,如:summarise(filter(flights,...))

關卡 63

而在dplyr中,導入了magrittr在2014年的發明:pipelineoperator:%>%%>%會把上一個前面函數的輸出,放到後面函數的第一個參數。也就是說,我們可以把剛剛的程式碼寫成: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({
  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做比較,就是一個我國房地產泡沫化的指標