關卡 1
這門課程將會帶大家走過一遍R 所提供的字串處理功能。
關卡 2
在實務上,數據來源經常是隱含某些規則的文字檔案。 例如:伺服器依據工程師擬定的規則所產生的資料。 這個規則可能是工程師自己訂的,也可能是符合眾人智慧所訂定的規範。
關卡 3
但是不管是工程師自己的想法,或是眾人的智慧,R 都能從文字中萃取出資訊。 這樣的技術就稱作Parsing。 同學上一堂課程所學到的read.table
,其實就是Parsing的其中一種功能。
關卡 4
這個課程中,我們事先從政府的公開資料平台抓取了血清白蛋白(Albumin)檢查比率的資料。 資料已經存放於hospital_path
了。 有興趣的同學,可以參考網址:<http://data.gov.tw/node/25511>閱讀關於這個資料集的故事。
關卡 5
請同學用上一堂課所學的方法,先用readBin
來檢查這個檔案的BOM。
readBin(hospital_path, "raw", n = 3L)
關卡 6
請問同學,根據這三個byte,他們的BOM可能是什麼呢?
Unknown
關卡 7
當BOM無法被判斷的時候,我們只好用不同Encoding讀取資料最初幾行試試看。 由於政府公開資料網宣稱這個檔案為UTF-8編碼的,我們先使用UTF-8讀讀看。 請同學輸入:readLines(file(hospital_path, encoding = "UTF-8"), n = 6)
readLines(file(hospital_path, encoding = "UTF-8"), n = 6)
關卡 8
接著我們再使用readLines(file(hospital_path, encoding = "BIG5"), n = 6)
試試看。 如果看到警告(warning)訊息,即代表這份檔案並非UTF-8編碼,因而導致的小錯誤。
readLines(file(hospital_path, encoding = "BIG5"), n = 6)
關卡 9
透過上面的測試,請問同學,這個檔案的編碼是什麼呢?
BIG5
關卡 10
像是這類編碼與文件不符的狀況,在實務的例子中是很常見的。
關卡 11
接著請同學依照在RBasic-07中所學到的技巧,用read.table
和file
兩個函數的組合,並適當的設定sep
、header
和encoding
等參數,將hospital_path
的內容存到hospital
之中。
hospital <- .read.table.big5(hospital_path, header = TRUE, sep = ",")
關卡 12
如果希望從欄位YEARYY中擷取出資料的年份。 我們先看看名稱為YEARYY這欄,請同學自hospital中選出YEARYY欄位。 (同學可以使用[[
、或$
搭配名稱或是欄位順序)
hospital$YEARYY
關卡 13
有時候,觀察到太大量的資料可能會對R 、電腦帶來大量的負荷,同時也對我們沒有意義。 因為我們能同時處理的資料量是有限的。 這部份建議同學可以用head
這個函數篩選出資料的前6 列做觀察。
關卡 14
請問同學,hospital
中YEARYY欄位的型態為何?
factor
關卡 15
Levels上的資訊顯示,年份的資訊,可能位於前兩個或前三個數字。 如果都是兩個數字的話,我們可以透過substring
函數直接擷取字串中的段落。 請同學輸入:?substring
先看看substring函數的說明文件。
?substring
關卡 16
請問下列哪一個「不是」substring
的參數?
x
關卡 17
R 的substring
函數可以將text
參數代表的字串中,依照字符的位置,擷取出中間的段落。 舉例來說,substring("abc", 1, 2)
就會擷取出“abc”中第1 個字母到第2 個字母的段落,也就是“ab”。 而substring
也是向量式的函數。 請同學輸入substring(head(hospital$YEARYY), 1, 3)
看看前6 筆數據經過R 的substring
函數處理之後的結果。
substring(head(hospital$YEARYY), 1, 3)
關卡 18
有時候這樣的作法就夠了,可惜在現在的狀況不適合。 因為年度可能是2 位數也可能是3 位數。
關卡 19
另外一種想法則是拿"Q"
當做定位點。 我們如果利用"Q"
把字串分割成兩部份,第一部份就是我們需要的年份了。 在R 中,可以運用函數strsplit
達到這個目的。 請同學打開strsplit
的說明文件。
?strsplit
關卡 20
請問同學,下列哪一個「不是」strsplit
的參數?
str
關卡 21
根據說明文件,strsplit
會利用split
參數來切割x
字串,並回傳一個list
。 因為x
的長度可能超過1 ,而strsplit
會用split
去切割每一個x
的元素。 而切割出來的結果,第一個元素可能切出兩段,但是第二個元素可能只切出一段。 所以R 使用list
這個結構來處理。 但是strsplit
並不接受factor參數,只接受字串向量。 因此請同學用:yearyy <- as.character(hospital$YEARYY)
將資料存到yearyy
這個變數中。
yearyy <- as.character(hospital$YEARYY)
關卡 22
接著請同學輸入:tmp <- strsplit(yearyy, "Q")
,把切割的結果儲存到tmp
這個變數。
tmp <- strsplit(yearyy, "Q")
關卡 23
請同學輸入:head(tmp)
看看結果。
head(tmp)
關卡 24
這時,同學會看到裝著許多字串向量的list。 這樣的list,甚至是更複雜的結構,都是在處理文字資料時經常遇到的。
關卡 25
由於我們需要的是在"Q"
之前的文字,而在經過strsplit
處理後,這些文字會在每個list元素的第一個。 請同學先用[[
拿出tmp的第一個字串向量元素,然後用[
拿出這個字串向量的第一個值。 該值即為第一筆資料的年度。
tmp[[1]][1]
關卡 26
同樣的要領,如果要取出第二筆資料的年度,只要把上一題tmp[[1]][1]
的語法改成tmp[[2]][1]
即可取得。
關卡 27
請問同學,tmp
中總共有多少筆資料呢?
length(tmp)
關卡 28
顯然一個一個處理是不可行的。而R 中當然有針對這種狀況的解法。 首先請先觀察看看tmp[[1]][1]
、tmp[[2]][1]
、tmp[[3]][1]
有什麼共通性? 一種觀點是,我們需要對tmp
的每一個元素進行[1]
的操作。
關卡 29
這樣的需求,在parsing時是十分常見的。 所以R 提供了lapply
這個函數。 請同學先打開lapply
的說明文件。
?lapply
關卡 30
R 的lapply
視同第一個接觸到的進階函數。 請問同學,下列哪一個不是lapply
的參數?
Y
關卡 31
lapply
的第一個參數X
通常是一個vector。 第二個參數FUN
則是代表一種「動作」。 lapply
會對每一個X
的元素進行FUN
所定義的動作,並且把結果彙整回R 之中。
關卡 32
如果想要對tmp
的每一個元素進行[1]
的操作。 根據上述的解釋,lapply
的X
參數應該要填入什麼呢?
tmp
關卡 33
而lapply
的FUN
參數,也就是對每個X
的元素所進行的動作,應該是以下哪一個呢?
[1]
關卡 34
課程進行至此,再解釋一遍給還沒完全理解的同學。 最一開始我們希望產生的結果,會等於:c(tmp[[1]][1], tmp[[2]][1], tmp[[3]][1], ...)
, 在這樣的運算中,第一個[[1]]
、[[2]]
、[[3]]
是變動的,代表著從tmp
中拿出第一個元素、第二個元素和第三個元素等等。 而[1]
則是不變的動作。
關卡 35
因此lapply
的第一個參數X
放的是tmp
,代表著:c(FUN(tmp[[1]]), FUN(tmp[[2]]), FUN(tmp[[3]]), ...)
,而此刻我們希望FUN
作出[1]
的動作。 然而,lapply(tmp, [1])
這種語法是不合邏輯的。
關卡 36
在R 中,所有的動作都是函數。同理,[1]
在R 中其實也是一個名叫[
的函數。
關卡 37
因為這個函數比較特別,所以我們使用lapply(tmp, "[", 1)
這個語法來取出tmp
中每個元素(字串向量)的第一個欄位。 請同學試試看。
lapply(tmp, "[", 1)
關卡 38
如果我們要取出tmp
中每個元素(字串向量)的第二個欄位,可以用:lapply(tmp, "[", 2)
取得,請同學試試看。
lapply(tmp, "[", 2)
關卡 39
到目前為止,同學學到X
和FUN
這兩個參數在lapply
中扮演的角色。 但是最後一個...
的參數是什麼意思呢? 因為lapply
並不清楚執行FUN
需要什麼樣的參數,所以使用者可以在指定X
和FUN
之後,放入任意的參數, 而這些參數並不是由lapply
所使用,而是由FUN
所使用。
關卡 40
所以lapply(tmp, "[", 1)
中的第三個參數1
就會透過lapply
轉交給[
。 如此一來,R 就可以知道要從tmp
的每個元素中拿出第1個元素。
關卡 41
同理,lapply(tmp, "[", 2)
的第三個參數2
透過lapply
轉交給[
後,R 就會知道要從tmp
的每個元素中拿出第2個元素。
關卡 42
接著請教同學,lapply(tmp, "[", 1)
的輸出結果是什麼型態呢?
list
關卡 43
list型態對於後續的處理還是比較麻煩的。 因此在輸出時,通常會希望能夠將轉換為字串向量。 其中一種做法是透過unlist
把lapply
所輸出的list
拆開。 請同學試試看先輸入:tmp2 <- lapply(tmp, "[", 1)
,並將結果儲存到tmp2
這個變數。
tmp2 <- lapply(tmp, "[", 1)
關卡 44
接著,我們可以使用unlist(tmp2)
取得字串向量了。 請同學試試看。
unlist(tmp2)
關卡 45
另外一種方式是使用sapply
這個函數。 sapply
和lapply
幾乎一樣,差別在於sapply
最後會嘗試重新整理輸出的格式,將list轉成array。 請同學試試sapply(tmp, "[", 1)
。
sapply(tmp, "[", 1)
關卡 46
以上示範的技巧在實務的應用中很常被使用到。 當我們想要從文字中擷取出資訊時,都可以優先考慮運用substring
或strsplit
來擷取資訊。 而當R 將資訊轉成非結構化的list物件後,可以運用lapply
或sapply
做資料的整理。
關卡 47
事實上,如果同學能夠撰寫R 的函數,就可以適當的將lapply
與sapply
作組合,更能有效率的整理資料。
關卡 48
最後,還是要請同學利用這次所學的內容,做一個小練習。 請同學在完成之後存檔,並輸入submit()
來檢查結果是否符合預期。 如果同學在檔案中看到亂碼,請使用Rstudio 左上角的File -> Reopen With Encoding… -> 選取:UTF-8
# 這是從 <http://data.gov.tw/node/7769> 下載的海盜通報資料
# 由於這份文件並沒有遵循任何已知的常見格式
# 所以我們必須要利用這章所學的技巧
# 才能從中翠取出資訊
# 首先,我們把該檔案載入到R 之中
pirate_info <- readLines(file(pirate_path, encoding = "BIG5"))
# 接著我們要把經緯度從這份資料中萃取出來
# 這份資料的格式,基本上可以用`:`分割出資料的欄位與內容
# 請同學利用`strsplit`將`pirate_info`做切割
# 並把結果儲存到`pirate_info_key_value`之中
pirate_info_key_value <- {
# 請在這邊填寫你的程式碼
# 這個程式碼可以多行
# 這裡的.delim其實是要從原始資料中取出冒號
# 為了要讓正確答案跨平台,所以只能這樣寫
.delim <- strsplit(pirate_info[2], "")[[1]][3]
strsplit(pirate_info, .delim)
}
# 我們需要的欄位名稱是「經緯度」
# 請同學先把`pirate_info_key_value`中每個元素(這些元素均為字串向量)的第一個值取出
# 你的答案鷹該要是字串向量
pirate_info_key <- {
# 請在這邊填寫你的程式碼
# 這個程式碼可以多行
sapply(pirate_info_key_value, "[", 1)
}
# 確保你的結果是字串向量,否則答案會出錯
stopifnot(class(pirate_info_key) == "character")
# 我們將`pirate_info_key`和`"經緯度"`做比較後,把結果存到變數`pirate_is_coordinate`
# 結果應該為一個布林向量
pirate_is_coordinate <- {
# 請在這邊填寫你的程式碼
# 這個程式碼可以多行
pirate_info_key == pirate_info_key[8]
}
# 確保你的結果是布林向量,否則答案會出錯
stopifnot(class(pirate_is_coordinate) == "logical")
# 應該總共有11件海盜通報事件
stopifnot(sum(pirate_is_coordinate) == 11)
# 接著我們可以利用`pirate_is_coordinate`和`pirate_info_key_value`
# 找出所有的經緯度資料
# 請把這個資料存到變數`pirate_coordinate_raw`中,並且是個長度為11的字串向量
pirate_coordinate_raw <- {
.tmp <- sapply(pirate_info_key_value, "[", 2)
.tmp[pirate_is_coordinate]
}
stopifnot(class(pirate_coordinate_raw) == "character")
stopifnot(length(pirate_coordinate_raw) == 11)
# 我們接著可以使用`substring`抓出經緯度的數字
# 請先抓出緯度並忽略「分」的部份
# 結果應該是整數(請用as.integer轉換)
pirate_coordinate_latitude <- {
# 請在這邊填寫你的程式碼
# 這個程式碼可以多行
as.integer(substring(pirate_coordinate_raw, 3, 4))
}
stopifnot(class(pirate_coordinate_latitude) == "integer")
stopifnot(length(pirate_coordinate_latitude) == 11)
# 請用同樣的要領取出經度並忽略「分」的部份
# 結果同樣應該是整數
pirate_coordinate_longitude <- {
# 請在這邊填寫你的程式碼
# 這個程式碼可以多行
as.integer(substring(pirate_coordinate_raw, 12, 14))
}
stopifnot(class(pirate_coordinate_longitude) == "integer")
stopifnot(length(pirate_coordinate_longitude) == 11)
stopifnot(sum(pirate_coordinate_longitude) == 1151)
pirate_df <- data.frame(
latitude = pirate_coordinate_latitude,
longitude = pirate_coordinate_longitude
)
stopifnot(is.data.frame(pirate_df))
stopifnot(nrow(pirate_df) == 11)
stopifnot(ncol(pirate_df) == 2)
stopifnot(class(pirate_df$latitude) == "integer")
stopifnot(class(pirate_df$longitude) == "integer")
stopifnot(sum(pirate_df$latitude) == 43)
stopifnot(sum(pirate_df$longitude) == 1151)