關卡 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。這樣,甚至是更複雜的結構,是我們在處理文字資料時常常遇到的。

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

R的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->ReopenWithEncoding…->選取: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 <- 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)