關卡 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的參數意義。 x和y在這邊扮演的角色就是兩個data.frame。 x是我們關心的、主要的data.frame,我們的目的是想把y的資料貼到x之上。 所以在剛剛航空公司名稱的範例中,x是flights,y則是airlines。
關卡 20
by則代表「黏貼規則」的欄位。 回憶一下航空公司的範例,我們是透過比對「航空公司簡稱(carrier)」 才能完成比對,所以by在這裡的填入值就是該欄的名稱,也就是"carrier"。 在慣例中,當我們提到依據「xxx」來比對x和y兩張表格時,通常我們就是指要作上述的動作,而「xxx」扮演的角色就是本案例中「航空公司簡稱」所扮演的角色。
關卡 21
接著請同學從flights中「依序」選出year:day, hour, origin, dest, tailnum, carrier等欄位,再抽出前100筆記資料,最後和airlines做left_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比對answer01與weather的依據。 這代表著當這五個欄位全部都一模一樣時,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的資料裡面,有兩個關於地點的欄位:origin與dest。 這兩個欄位都是記載著美國機場的代號。而在airports資料中機場的代號是faa。 問題來了,請問我們應該要怎麼告訴left_join,讓它進行比對呢?
關卡 32
可以透過by參數來指定比對的方法。 by的參數除了是一般的字串向量外,也可以是有名字的字串向量。 舉例來說:c("origin" = "faa")就是一個有名字的字串向量。 請同學輸入:c("origin" = "faa")看看。
c("origin" = "faa")關卡 33
接著,我們透過left_join來比對這兩筆資料。 比對的依據分別是answer02$origin與airports$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
最後,我們再將answer03與airports做一次整合,但是這次比對的是answer03$dest與airports$faa。 請同學寫寫看,並且把結果存到變數answer04中。
answer04 <- left_join(answer03, airports, by = c("dest" = "faa"))關卡 36
請同學輸入colnames(answer04),看一下比對後的名稱。
colnames(answer04)關卡 37
在最後,是不是有看到lat.x或lat.y 這些欄位名稱呢? 因為answer03和airports進行比對時,answer03$lat與airports$lat撞名,也不在by之內,所以產生的資料就會有兩種lat。 left_join在這邊的處理方式,就是把來自參數x的lat標記為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函數。 他們的參數都非常類似:x、y和by,而且在用法和比對的方法上也一模一樣。 他們的差異,在於當比對結束之後,會產生不同資料範圍的data.frame。
關卡 43
為了等下的說明,我們仿照vignette,建立了兩個變數:df1與df2。 請同學輸入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的資料,所以a與b的欄位都是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,資料必須要同時存在於x和y之中。
關卡 55
請同學輸入:inner_join(df1, df2)。
inner_join(df1, df2)關卡 56
結果出現x欄位只剩下1的狀況了,df1 和 df2中都有x = 1的資料。
關卡 57
接續著的full_join與anti_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: df3與df4。 他們的x欄位都有兩個1。 請同學輸入View(df3)看看df3。
View(df3)關卡 64
請同學輸入View(df4)看看df4。
View(df4)關卡 65
接著,我們輸入inner_join(df3, df4)看看結果有多少列。
inner_join(df3, df4)關卡 66
上述的這些join都會窮舉出所有可能排列組合。 因為df3的x有兩個1, df4的x有兩個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)