關卡 1

這門課程要介紹的是,資料科學團隊在剛成立時一定要經常使用的技術:資料比對。

關卡 2

現代資料科學有很豐富的內涵。 每天都有新的統計方法、新的演算法發明,讓人類在處理資料的能力上更進一步。

關卡 3

但是在企業中,或是實際應用中,最重要的是「價值」,而不是「處理資料的手法」。

關卡 4

而這之中最重要的「價值」來自於我們能夠完成過去辦不到的事情。 資料科學也是如此。 在企業內就是要能夠在「有限的資源中」做出「過去做不到的事情」。 而一個資料科學團隊,所擁有的最特別的資產,就是檢視各種不同資料源的能力。

關卡 5

當我們取得一個來源的資料之後,前面的課程協助我們從中萃取出有用的資訊(01~04)、並且能夠轉換成結構化資料(05)。 本堂課目的,是要讓大家可以將複數資料源的知識彙整起來。

關卡 6

就我個人的經驗,這是資料科學團隊為企業帶來價值的捷徑。

關卡 7

首先,請同學安裝套件dplyr。 已經裝過的同學請輸入skip()略過。

check_then_install("dplyr", "0.4.3")

關卡 8

接著,請同學載入dplyr。

library(dplyr)

關卡 9

再來,請同學打開:vignette("two-table", package = "dplyr")

vignette("two-table", package = "dplyr")

關卡 10

這份文件很清楚的說明,在整合兩個data.frame時,dplyr所提供的不同功能。 接下來的課程中,我們就依照這份文件的脈落,用肌肉記住它們。

關卡 11

請同學安裝nycflights13的資料。已經載入的同學可以輸入skip()

check_then_install("nycflights13", "0.1")

關卡 12

請同學載入套件:nycflights13

library(nycflights13)

關卡 13

nycflights13的flights中,在carrier欄位中有航空公司的簡稱。 而airlines中則有航空公司的全名。 上一堂課中,我們已經看過flights的資料了。 現在請同學輸入:View(airlines)看一下航空公司的資料。

View(airlines)

關卡 14

dplyr所提供的left_join可以讓我們把航空公司的全名貼到flights資料上。 但是在貼資料的時候,我們一定要有一個依據。 我們先看一下flights的第一筆資料,請同學用slice作答。

slice(flights, 1)

關卡 15

參考airlines的資料後,我們要貼哪一筆航空公司的全名到flights的第一筆資料中呢?

United Air Lines Inc.(UA)

關卡 16

在同學剛剛的思考過程應該是這樣的。 flights的第一筆資料的航空公司簡稱是UA,而United Air Lines Inc.的簡稱也是UA,所以可以把United Air Lines Inc.當成第一筆資料的航空公司全名。

關卡 17

上述的例子透過航空公司簡稱(carrier欄位)進行兩方的比對,當兩者相同的時候,才宣佈比對成功並貼上資料。 接下來,我們請同學打開left_join的說明文件。

?left_join

關卡 18

請問以下哪一個「不是」left_join的參數呢?

key

關卡 19

我們來講解left_join的參數意義。 xy在這邊扮演的角色就是兩個data.frame。 x是我們關心的、主要的data.frame,我們的目的是想把y的資料貼到x之上。 所以在剛剛航空公司名稱的範例中,xflightsy則是airlines

關卡 20

by則代表「黏貼規則」的欄位。 回憶一下航空公司的範例,我們是透過比對「航空公司簡稱(carrier)」 才能完成比對,所以by在這裡的填入值就是該欄的名稱,也就是"carrier"。 在慣例中,當我們提到依據「xxx」來比對xy兩張表格時,通常我們就是指要作上述的動作,而「xxx」扮演的角色就是本案例中「航空公司簡稱」所扮演的角色。

關卡 21

接著請同學從flights中「依序」選出year:day, hour, origin, dest, tailnum, carrier等欄位,再抽出前100筆記資料,最後和airlinesleft_join

answer01 <- local({
  # 請在此填寫你的程式碼
  slice(flights, 1:100) %>%
    select(year:day, hour, origin, dest, tailnum, carrier) %>%
    left_join(y = airlines, by = "carrier")
})
# 結束之後請回到console輸入`submit()`

關卡 22

weather資料中也包含了天氣的資訊。 所以我們可以依據時間和地點,把天氣的資料貼到answer01中。 請同學輸入:View(weather)先看一下天氣資料。

View(weather)

關卡 23

接著請同學輸入:answer02 <- left_join(answer01, weather),請注意到我們沒有給任何by的資訊。

answer02 <- left_join(answer01, weather)

關卡 24

請輸入View(answer02),我們看一下answer02的資料黏貼後的結果。

View(answer02)

關卡 25

剛剛我們為什麼省略by之後,left_join還是仍然正常運作呢?它是依據什麼欄位來比對資料的呢?

關卡 26

根據文件,在沒有給定by的狀況下,left_join會比對兩個data.frame的欄位名稱(colnames),並且用重複的欄位名稱做比對的依據。 請同學輸入:intersect(colnames(answer01), colnames(weather))

intersect(colnames(answer01), colnames(weather))

關卡 27

螢幕上顯示的四個欄位:"year" "month" "day" "hour" "origin"就是left_join比對answer01weather的依據。 這代表著當這五個欄位全部都一模一樣時,left_join才會把對應的天氣資料黏貼到answer01上。 另外同學應該也可以了解,上一課所學的select搭配重新命名的機制在此是很有用的。

關卡 28

課程進行至今,我們開始會跳過一些字面意義上很簡單的函數,例如intersect。 同學可以自行打開說明文件查詢這些函數的用法。 如果覺得太難需要說明,歡迎同學到<https://github.com/wush978/DataScienceAndR/issues>(輸入issue()) 或是聊天室<https://gitter.im/wush978/DataScienceAndR>(輸入:chat())進行回報。

關卡 29

另外,在剛剛的比對過程中,R 有發出警告,這是因為我們在比較一個字串向量與一個factor。 了解RBasic系列的同學應該知道,這兩者的本質是非常類似的,所以left_join就會自動把factor轉換成字串向量後再進行比對。

關卡 30

接著,我們看一下airports這個資料集。 請同學輸入:View(airports),可以看到裡面有不少關於機場的資訊(如:經緯度、簡稱等等)。

View(airports)

關卡 31

這裡我們要比對的依據是地點,但在flights的資料裡面,有兩個關於地點的欄位:origindest。 這兩個欄位都是記載著美國機場的代號。而在airports資料中機場的代號是faa。 問題來了,請問我們應該要怎麼告訴left_join,讓它進行比對呢?

關卡 32

可以透過by參數來指定比對的方法。 by的參數除了是一般的字串向量外,也可以是有名字的字串向量。 舉例來說:c("origin" = "faa")就是一個有名字的字串向量。 請同學輸入:c("origin" = "faa")看看。

c("origin" = "faa")

關卡 33

接著,我們透過left_join來比對這兩筆資料。 比對的依據分別是answer02$originairports$faa。 請同學試試看執行answer03 <- left_join(answer02, airports, by = c("origin" = "faa"))

answer03 <- left_join(answer02, airports, by = c("origin" = "faa"))

關卡 34

現在我們輸入View(answer03),看看這份包含航班資訊、天氣資訊以及出發機場地理位置資訊的資料。

View(answer03)

關卡 35

最後,我們再將answer03airports做一次整合,但是這次比對的是answer03$destairports$faa。 請同學寫寫看,並且把結果存到變數answer04中。

answer04 <- left_join(answer03, airports, by = c("dest" = "faa"))

關卡 36

請同學輸入colnames(answer04),看一下比對後的名稱。

colnames(answer04)

關卡 37

在最後,是不是有看到lat.x或lat.y 這些欄位名稱呢? 因為answer03airports進行比對時,answer03$latairports$lat撞名,也不在by之內,所以產生的資料就會有兩種latleft_join在這邊的處理方式,就是把來自參數xlat標記為lat.x,而來自參數y(比對機場代碼後被貼上去的lat)則取名為lat.y

關卡 38

以上的內容已經將left_join的功能與應用上可能發生的狀況細解一遍。 接著我們來練習解決一個問題:起飛機場的風速對抵達時間的延遲是不是有影響。 請同學在完成之後存檔,並輸入submit()來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen。

# 請同學使用`left_join`將`weather`的資訊貼到`flights`資料中
#   合併的過程請使用 year:day, hour, origin 當成合併的key
#   提示:請檢查weather與flights的欄位名稱重複的部分
# 比對完畢後,請先做資料的清理:
# - 結果只包含wind_speed與arr_delay這兩個欄位
# - 並且這些欄位中都不應該有NA或NaN
answer02.1 <- local({
  select(flights, year:day, hour, origin, dest, tailnum, carrier, arr_delay) %>%
    left_join(weather) %>%
    select(wind_speed, arr_delay) %>%
    filter(!is.na(wind_speed), !is.na(arr_delay))
})

stopifnot(nrow(answer02.1) == switch(packageVersion("nycflights13"), "0.1" = 116774, "0.2.0" = 326116, stop("Invalid nycflights13 version")))
stopifnot(sum(is.na(answer02.1$wind_speed)) == 0)
stopifnot(sum(is.nan(answer02.1$wind_speed)) == 0)
stopifnot(sum(is.na(answer02.1$arr_delay)) == 0)
stopifnot(sum(is.nan(answer02.1$arr_delay)) == 0)

# 接著我們要把風速(wind_speed)做分級。
# 由於我們對氣象的數據沒有背景知識,所以最好的分類方法就是透過數據的比率來抓。
# quantile函數會抓出一個數值向量中的百分位數,也就是說:
# answer02.2[1]會超過 0% 的 answer02.2
# answer02.2[2]會超過 25% 的 answer02.2
# answer02.2[3]會超過 50% 的 answer02.2
# answer02.2[4]會超過 75% 的 answer02.2
# answer02.2[5]會超過 100% 的 answer02.2
answer02.2 <- quantile(answer02.1$wind_speed, seq(0, 1, by = 0.25))
stopifnot(length(answer02.2) == 5)
stopifnot(answer02.2[1] == 0)
stopifnot(answer02.2[5] == max(answer02.2))

# 最後,我們利用`cut`與`answer02.2`對原始的wind_speed做分類。
# 介於 answer02.2[1]至answer02.2[2]的風速,會被歸類為等級1
# 介於 answer02.2[2]至answer02.2[3]的風速,會被歸類為等級2
# 介於 answer02.2[3]至answer02.2[4]的風速,會被歸類為等級3
# 介於 answer02.2[4]至answer02.2[5]的風速,會被歸類為等級4
# 接著,我們計算arr_delay在每一種分類中的平均數
answer02.3 <- local({
  mutate(answer02.1, wind_speed = cut(wind_speed, breaks = c(answer02.2[1]-1e-5, tail(answer02.2, -1)))) %>%
    group_by(wind_speed) %>%
    summarise(mean(arr_delay))
})
stopifnot(nrow(answer02.3) == 4)
stopifnot(colnames(answer02.3) == c("wind_speed", "mean(arr_delay)"))
if (packageVersion("nycflights13") < package_version("0.2.0")) stopifnot(answer02.3[[2]] > 4)
stopifnot(answer02.3[[2]] < 16)
# 請同學完成後回到console輸入`submit()`做檢查

關卡 39

請同學用View(answer02.3),觀察我們在上一題得到的結果。

View(answer02.3)

關卡 40

這樣的數據顯示,當起飛機場的風速越大,飛機在降落時的延誤時間就越大。

關卡 41

接著,我們來講解dplyr中的多種合併表格的方法。

關卡 42

除了left_join之外,dplyr還提供了6 種join函數。 他們的參數都非常類似:xyby,而且在用法和比對的方法上也一模一樣。 他們的差異,在於當比對結束之後,會產生不同資料範圍的data.frame。

關卡 43

為了等下的說明,我們仿照vignette,建立了兩個變數:df1df2。 請同學輸入View(df1)來查看df1的資料。

View(df1)

關卡 44

請同學輸入View(df2)來查看df2的資料。

View(df2)

關卡 45

我們先拿left_join進行解說。 做完left_join之後,最後的data.frame,就是x中所有的資料。 而y的資料只有在和x比對成功之後,才會貼到輸出的欄位中。否則就是NA。

關卡 46

請同學輸入:wiki_join()來打開這六種JOIN的圖示。

wiki_join()

關卡 47

wiki_join()中的第一張圖中,淺藍色的部份就代表最後輸出的data.frame,剛剛好就是原本x的資料。

關卡 48

請問同學,如果輸入:left_join(df1, df2)的話,left_join會以哪一個欄位名稱為基準作比對呢?(提示:df1和df2共有的名稱)

x

關卡 49

請同學輸入:left_join(df1, df2)

left_join(df1, df2)

關卡 50

我們會看到df1資料都有保留下來。(結果中的x欄數值為c(1,2)) 而結果中的第二筆資料,因為df2中沒有x = 2的資料,所以ab的欄位都是NA。

關卡 51

wiki_join()中,第二張圖中淺藍色的部份就代表right_join最後輸出的data.frame,剛剛好就是原本y的資料。 事實上,right_join(x, y)就等價於left_join(y, x),差別只在欄位排序的不同。

關卡 52

請同學輸入:right_join(df2, df1)。請注意,我們這次是x = df2

right_join(df2, df1)

關卡 53

left_join(df1, df2)相比,x欄位的值一樣是c(1,2),但是欄位順序不同了。

關卡 54

wiki_join()中,第三張圖中淺藍色的部份就代表inner_join最後輸出的data.frame,資料必須要同時存在於xy之中。

關卡 55

請同學輸入:inner_join(df1, df2)

inner_join(df1, df2)

關卡 56

結果出現x欄位只剩下1的狀況了,df1df2中都有x = 1的資料。

關卡 57

接續著的full_joinanti_join,都是非常類似的。 我們練習一下,用肌肉作記憶。

關卡 58

請同學輸入:full_join(df1, df2)

full_join(df1, df2)

關卡 59

輸出的結果的x欄位為c(1,2,3),為df1和df2的聯集。

關卡 60

請同學輸入:anti_join(df1, df2)

anti_join(df1, df2)

關卡 61

結果只列出df1中和df2比對失敗的資料。 這是目前為止,唯一欄位只剩下x的欄位,所產生的JOIN結果。

關卡 62

在講解semi_join之前,我們要先提醒同學一個狀況:當比對的欄位,值不唯一的時候,JOIN要如何作操作呢?

關卡 63

舉例來說,我們已經建立了兩個data.frame: df3df4。 他們的x欄位都有兩個1。 請同學輸入View(df3)看看df3

View(df3)

關卡 64

請同學輸入View(df4)看看df4

View(df4)

關卡 65

接著,我們輸入inner_join(df3, df4)看看結果有多少列。

inner_join(df3, df4)

關卡 66

上述的這些join都會窮舉出所有可能排列組合。 因為df3x有兩個1, df4x有兩個1, 所有可能的排列組合就有:2 * 2 = 4種 因此inner_join就回傳4列。

關卡 67

最後一個semi_join非常類似inner_join,但是當比對的資料不是唯一時, 它不會窮舉所有的可能性,而只是將有比對成功的x的資料給保留下來。

關卡 68

請同學輸入:semi_join(df3, df4)看看結果。

semi_join(df3, df4)

關卡 69

結果幾乎等價於df3[1:2,],原因是只有前兩列資料有和df4比對成功。

關卡 70

這次的大魔王要延續在RDataEngineer-05-Data-Manipulation最後所使用的cl_info資料。

關卡 71

同時,我們也準備了從央行抓取的GDP資料。但是這份資料請同學自己想辦法讀出來。

關卡 72

我們的目標是要比對房貸餘額是否超過GDP的40%。 這是一種拿來評估房地產是否會泡沫化的指標。

關卡 73

就請同學多多加油了! 利用從RBasic到RDataEngineer系列所學到的知識,完成這個挑戰。 請同學在完成之後存檔,並輸入submit()來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen。

#' 請用各種方式讀取`gdp_path`的資料、整理資料,並把最後的結果存到變數`gdp`。
#' 提示:`gdp_path`中的第一欄數據是年/季、第二欄數據是該季的GDP(百萬)
#' 結果應該要有兩欄的數據,第一欄是年,第二欄是我國每年的GDP
#' 具體細節請參考最後的`stopifnot`的檢查事項
#' 提示:拿掉數據中間的逗號,請用:`gsub(pattern = ",", replacement = "", x = <你的字串向量>)`
gdp <- local({
  # 請填寫你的程式碼
  read.table(gdp_path, skip = 4, header = FALSE, sep = ",") %>%
    slice(1:132) %>%
    select(season = V1, gdp = V2) %>%
    mutate(
      season = as.character(season),
      year = substring(season, 1, 4),
      gdp = gsub(pattern = ",", replacement = "", x = gdp), 
      gdp = as.numeric(gdp) * 1000000) %>%
    group_by(year) %>%
    summarise(gdp = sum(gdp))
})
stopifnot(is.data.frame(gdp))
stopifnot(colnames(gdp) == c("year", "gdp"))
stopifnot(class(gdp$year) == "character")
stopifnot(class(gdp$gdp) == "numeric")
stopifnot(nrow(gdp) == 33)
stopifnot(range(gdp$year) == c("1981", "2013"))
stopifnot(range(gdp$gdp) == c(1810829,14564242) * 1000000)

#' cl_info的資料包含各家銀行的房貸餘額(mortgage_bal)資訊與資料的時間(data_dt)。
#' 請用各種方法整理cl_info的資料,把最後的結果整理至`cl_info_year`
#' 結果應該要有兩欄的數據,第一欄是年,第二欄是每年房貸餘額的值(請以每年的一月份資料為準)
#' 具體細節請參考最後的`stopifnot`檢查事項
cl_info_year <- local({
  select(cl_info, data_dt, mortgage_bal) %>%
    mutate(month = substring(data_dt,1,7)) %>%
    group_by(month) %>%
    summarise(mortgage_total_bal = sum(mortgage_bal, na.rm = TRUE)) %>%
    mutate(year = substring(month, 1, 4)) %>%
    group_by(year) %>%
    arrange(month) %>%
    summarise(month = head(month, 1), mortgage_total_bal = head(mortgage_total_bal, 1)) %>%
    select(year, mortgage_total_bal)
})

stopifnot(is.data.frame(cl_info_year))
stopifnot(colnames(cl_info_year) == c("year", "mortgage_total_bal"))
stopifnot(class(cl_info_year$year) == "character")
stopifnot(class(cl_info_year$mortgage_total_bal) == "numeric")
stopifnot(nrow(cl_info_year) == 9)
stopifnot(range(cl_info_year$year) == c("2006", "2014"))
stopifnot(range(cl_info_year$mortgage_total_bal) == c(3.79632e+12, 5.726784e+12))

#' 最後請同學用這門課程所學的技術整合`gdp`與`cl_info`的資料,
#' 計算出房貸餘額與gdp的比率(mortgage_total_bal / gdp)。
#' 請將結果輸出到一個data.frame,第一攔是年份,第二欄是房貸餘額的GDP佔有比率。
#' 細節請參考`stopifnot`的檢查
answerHW <- local({
  # 請在這邊填寫你的程式碼
  inner_join(cl_info_year, gdp, by = "year") %>%
    mutate(index = mortgage_total_bal / gdp) %>%
    select(year, index)
})

stopifnot(is.data.frame(answerHW))
stopifnot(nrow(answerHW) == 8)
stopifnot(colnames(answerHW) == c("year", "index"))
stopifnot(class(answerHW$year) == "character")
stopifnot(class(answerHW$index) == "numeric")
stopifnot(min(answerHW$index) > 0.3)
stopifnot(max(answerHW$index) < 0.4)