關卡 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.tablefile兩個函數的組合,並適當的設定sepheaderencoding等參數,將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]的操作。 根據上述的解釋,lapplyX參數應該要填入什麼呢?

tmp

關卡 33

lapplyFUN參數,也就是對每個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

到目前為止,同學學到XFUN這兩個參數在lapply中扮演的角色。 但是最後一個...的參數是什麼意思呢? 因為lapply並不清楚執行FUN需要什麼樣的參數,所以使用者可以在指定XFUN之後,放入任意的參數, 而這些參數並不是由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型態對於後續的處理還是比較麻煩的。 因此在輸出時,通常會希望能夠將轉換為字串向量。 其中一種做法是透過unlistlapply所輸出的list拆開。 請同學試試看先輸入:tmp2 <- lapply(tmp, "[", 1),並將結果儲存到tmp2這個變數。

tmp2 <- lapply(tmp, "[", 1)

關卡 44

接著,我們可以使用unlist(tmp2)取得字串向量了。 請同學試試看。

unlist(tmp2)

關卡 45

另外一種方式是使用sapply這個函數。 sapplylapply幾乎一樣,差別在於sapply最後會嘗試重新整理輸出的格式,將list轉成array。 請同學試試sapply(tmp, "[", 1)

sapply(tmp, "[", 1)

關卡 46

以上示範的技巧在實務的應用中很常被使用到。 當我們想要從文字中擷取出資訊時,都可以優先考慮運用substringstrsplit來擷取資訊。 而當R 將資訊轉成非結構化的list物件後,可以運用lapplysapply做資料的整理。

關卡 47

事實上,如果同學能夠撰寫R 的函數,就可以適當的將lapplysapply作組合,更能有效率的整理資料。

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