關卡 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)