關卡 1

近年來,能源議題是個和大家息息相關的問題。 而透過政府的公開資料,我們是有能力從這些資料更深入的了解能源議題。 又由於能源議題往往和經濟議題息息相關,所以我們希望能夠好好的整理 台灣各行業的耗電量,以及各行業所貢獻的GDP 。 藉由整理出來的結果,來協助我們更深入的思考台灣的能源相關議題。

關卡 2

這次練習中,用電的資料來源是取自:<http://data.gov.tw/node/6064>。 我目前將2015-09-26時下載後的檔案解壓縮,並且把要用到的「歷年行業別.txt」 搬到課程目錄之下。由於中文檔名在跨平台上不方便,所以更改名稱為power.txt。

關卡 3

由於網路在傳輸課程資料時,可能會出錯,所以我們可以先用R 的內建 功能檢查power.txt的內容是不是在安裝過程中毀損。請同學輸入: tools::md5sum(power_path)。完成後課程會在背景自動比對結果。

tools::md5sum(power_path)

關卡 4

這裡的::代表說,md5sum這個函數是來自tools這個套件。 這是近代R 語言新增的一種類似namespace的功能,是個很大的進步。

關卡 5

各行業的GDP 資料則下載自<http://statdb.dgbas.gov.tw/pxweb/dialog/statfile9L.asp>。 檔案路徑已經放入gdp_path之中。請同學再次利用tools::md5sum(gdp_path) 來檢查檔案內容。

tools::md5sum(gdp_path)

關卡 6

首先,就讓我們把資料載入到R 之中吧! 首先我們來一起處理電力資料。 一般來說,在讀中文資料之前,我們要先了解資料的編碼。 由於官方文件上並沒有編碼的資訊,所以在實務上只能一個個嘗試。 通常政府的官方資料如果沒有特別註明,建議就是從BIG-5開始嘗試。 請同學試試看:readLines(file(power_path, encoding = "BIG-5"), n = 10) 這裡的file是R 建立一個檔案連結的方式。file的第一個參數是檔案的路徑, encoding則代表這個檔案內容的編碼。 readLines則是輸出一個字串向量,而這個向量是從檔案中一行一行的把內容讀出來的。 也就是說,檔案裡面的每一行會變成一個向量中的字串。 參數n = 10則代表我們只讀最前面的10行。 這是實務上的一個好習慣:先看一看檔案的一開始。

readLines(file(power_path, encoding = "BIG-5"), n = 10)

關卡 7

如果成功看到中文字,就是猜對Encoding了。

關卡 8

接下來,從螢幕上我們可以看到,這個檔案的分隔符號會是應該是";\t", (還滿少見的)。另外在中間有部分資料的分隔符號只有"\t"。 可惜R 的能接受的分隔符號,通常只能是一個字元。 資料不大,所以我們可以自己切資料吧! 首先,請同學用readLines把所有資料讀出來,並且將資料都存到power這個變數。

power <- readLines(file(power_path, encoding = "BIG-5"))

關卡 9

其實這樣混亂、不好整理的資料,是我們在處理實際資料時 常常遇到的狀況。而且我們也常常犯錯,導致要重頭開始重新整理。

關卡 10

在R 中,我們可以使用strsplit來切割字串。 strsplit的參數x 是要被切割的字串,split是定位分割點的字串。 請同學呼叫strsplit來用";?\t"切割power,並且將結果儲存到 power.split中。 ";?\t"在這邊代表,這個分號;是可有可無的。這是為了讓R 能夠 同時處理分隔符號為";\t""\t"的狀況。

power.split <- strsplit(power, ";?\t")

關卡 11

使用do.call可以讓我們把power.split組裝成一個character matrix。 請同學輸入:power.mat <- do.call(rbind, power.split)power.split是一個R 物件向量,而rbind是吃一個任意參數的函數, 並且把每個參數當成一個row,組裝出一個matrix。 do.call會把在這裡的功能,就等價於:rbind(power.split[[1]], power.split[[2]], ...)power.split拆解後,全部當成參數丟到rbind去做組裝。

power.mat <- do.call(rbind, power.split)

關卡 12

我們差不多把電力資料整理的差不多了,只剩下最後一步:把power.mat 轉換成一個data.frame,並且讓各個column擁有正確的形態。 但是這最後一步卻有許多小步驟。 首先,我們把power.matdata.frame轉換成power.df 這裡要注意的是,data.frame在轉換的時候,這裡有下參數:stringsAsFactors = FALSE, 否則欄位會被轉換成factor,在做後續字串處理時,有時候會出錯。

power.df <- data.frame(power.mat, stringsAsFactors = FALSE)

關卡 13

然後,我們設定power.df的欄位名稱(colnames)依序為: c("id", "name", "year", "power") 請同學使用:colnames(power.df) <- 的語法來指定power.df的 colnames。

colnames(power.df) <- c("id", "name", "year", "power")

關卡 14

接下來,我們設定讓power.df的name欄位是factor。 請使用factor這個函數。

power.df$name <- factor(power.df$name)

關卡 15

接下來,我們設定讓power.df的year欄位是integer。 請使用as.integer這個函數。

power.df$year <- as.integer(power.df$year)

關卡 16

接下來,我們設定讓power.df的power欄位是numeric。 請使用as.numeric這個函數。

power.df$power <- as.numeric(power.df$power)

關卡 17

大功造成了,請輸入head(power.df)來檢視一下我們整理好的 data.frame物件。同時我們也會再檢驗一次power.df看看是否正確。

head(power.df)

關卡 18

由於我們在抓資料的分割點時,很有可能會抓錯。所以保險起見, 在這個時間點我們要檢查資料是否有清理乾淨。 在R 中,如果中間資料有任何錯誤的話,常常會有NA產生。 所以一個方法就是用is.na檢查power.df的四個欄位中間有沒有NA。 all這個函數,則可以檢查一個邏輯向量的所有的值是否為TRUE。 請同學組合is.naall來檢查power.df的欄位是否包含NA。

all(!is.na(power.df))

關卡 19

接下來,我們來運用dplyr和ggplot2這兩個套件將power.df的資料繪製成圖表。 請先安裝dplyr套件。

check_then_install("dplyr", "0.4.3")

關卡 20

請再安裝ggplot2套件

check_then_install("ggplot2", "1.0.1")

關卡 21

使用套件的起手式:載入dplyr

library(dplyr)

關卡 22

這裡的id欄位,是中華民國行業標準分類的代碼。 這個代碼的最大類, 都是用英文字母標示的。所以我們就用regular expression來找出 所有英文開頭的idgrepl("^[A-Z]", input)會判斷input這個字串向量的值,是不是符合 字母開頭。如果是的話,就傳TRUE,否則就是FALSE。 和dplyr套件的filter搭配使用,我們就可以快速取得所有大類的資料。 請同學將這樣的資料存到power.target這個變數之中。

power.target <- filter(power.df, grepl("^[A-Z]", id))

關卡 23

filter 不只可以幫我們挑出主要的行業類別,還可以挑選年度。 所以我們可以繪製每年度,主要行業別的用電比較。 首先,我們先整理出91年度的主要類別,並且存放到power.target之中。

power.target <- filter(power.df, grepl("^[A-Z]", id), year == 91)

關卡 24

我們可以利用power.target來繪製一個barchart來呈現 各種行業別的用電量。首先,還是要先載入ggplot2。

library(ggplot2)

關卡 25

我們先建立ggplot2的底圖物件。 這裡我們會用到aes這個函數,來辨識在power.target的欄位,哪一欄要放到x 軸, 哪一欄,要放到y 軸。x 軸的通常放的是分類,而y 放的是我們想要觀測的數據的值。 請同學修改以下的指令,將ggplot2的底圖物件寫入變數g。 指令是:g <- ggplot(power.target, aes(x = <欄位名稱1>, y = <欄位名稱2>))

g <- ggplot(power.target, aes(x = name, y = power))

關卡 26

建立底圖之後,我們就要選用適當的圖形工具來適當的呈現我們要看的工具。 這裡我們來用geom_bar來呈現我們要資料。 大家可以試試看:g + geom_bar(stat = "identity")

g + geom_bar(stat = "identity")

關卡 27

有些使用者可能會無法在圖上顯示中文。這可能是字形的問題。 如果是蘋果牌的使用者,可以更改預設的字形來顯示中文: theme_set(theme_gray(base_family = "STKaiti"))

關卡 28

另外我們也會發現整個x 軸容納不下我們的說明文字。 這時候可以透過設定xlab的字體方向,來解決這個問題: theme(axis.text.x = element_text(angle = 90)) 請注意,ggplot2在設定類型時,是直接透過在程式碼的最後用+來加上 theme(...)來作處理的。 我建議各位同學先使用上下鍵盤,找出剛剛繪圖的指令,然後在最後加上: + theme(axis.text.x = element_text(angle = 90))

g + geom_bar(stat = "identity") + theme(axis.text.x = element_text(angle = 90))

關卡 29

從圖上顯示,我們可以注意到在民國91年的時候, 在各行業中,製造業還是消耗了最多的電力。

關卡 30

在ggplot2,我們可以把bar chart更改成pie chart。 請同學試試看:ggplot(power.target, aes(x = "", y = power, fill = name)) + geom_bar(stat = "identity") + coord_polar(theta = "y") 如果同學覺得這個指令很冗長的話,可以輸入skip()來跳過。

ggplot(power.target, aes(x = "", y = power, fill = name)) + geom_bar(stat = "identity") + coord_polar(theta = "y")

關卡 31

事實上ggplot2對pie chart的支援並不好,而這是有理由的。 有興趣的同學可以讀一讀<http://www.r-chart.com/2010/07/pie-charts-in-ggplot2.html> 裡面提到一些統計學家對pie chart的批評。

關卡 32

而根據這個pie chart,我們至少可以了解 在民國91年製造業消耗了我們超過一半的電力。

關卡 33

一個我們也許會感興趣的問題是: 我們的GDP是不是大部份也來自於製造業呢?

關卡 34

接著,我們來整理GDP的資料…

關卡 35

透過一樣的要領,我們應該先用readLines先看看gdp_path 的資料內容。請同學檢視gdp_path這個檔案的前20行。

readLines(file(gdp_path, encoding = "BIG-5"), n = 20)

關卡 36

一些不熟悉R 的同學可能會被\"這個符號給搞混了。 由於"在R 中是代表字串的開始和結束,所以R 會在顯示字串時, 在"之前加註\,所以一個\"實際上就是一個"

關卡 37

另一個避免\"干擾的方式,是使用cat這函數。 請同學試試看:cat(readLines(file(gdp_path, encoding = "BIG-5"), n = 20), sep = "\n")

cat(readLines(file(gdp_path, encoding = "BIG-5"), n = 20), sep = "\n")

關卡 38

上一個問題的答案,其實牽涉到了三層函數:

關卡 39

我們先呼叫file(gdp_path, encoding = "BIG-5")建立一個connection。 connection是R 處理檔案的專業術語,如果比較不熟悉程式設計的同學,就先把 connection當成檔案的代理就好。

關卡 40

接著,我們使用readLines處理file回傳的物件。這是第二層。 這裡我們指定參數n=20,代表我們要從connection的最前面讀取前20行。

關卡 41

最後,我們把readLines回傳的20個字串交給cat作處理。 cat會直接把字串內容印到R 的console上,而且我們指定每一個 字串之間要插入一個斷行。

關卡 42

這樣一層一層疊上去的寫法,很容易讓程式變的很複雜。 Stefan Milton Bache推出了一個R 套件很好地解決了這個問題: magrittr。我們在安裝dplyr之後,應該已經安裝了這個套件了。 因此,請同學直接輸入:library(magrittr)

library(magrittr)

關卡 43

回顧一下我們剛剛做的事情: 把file(gdp_path, encodoing = "BIG-5") 得到的物件給 readLines(..., n = 20) 再把得到的物件給 cat(..., sep = "\n")。 這裡的...就代表從上一個函數得到的物件。使用magrittr後, 我們的寫法就變成:file(gdp_path, encoding = "BIG-5") %>% readLines(n = 20) %>% cat(sep = "\n") 請同學試試看

file(gdp_path, encoding = "BIG-5") %>% readLines(n = 20) %>% cat(sep = "\n")

關卡 44

在實務上,我們會在%>%後斷行,增加程式的可讀性。

關卡 45

回歸正傳,我們第一個要看的,是檔案中的分隔符號是什麼。 大家在看過檔案內容之後,應該會同意:分隔符號是, 另外大家也會注意到這個檔案中,字串的內容的前後會加上"。 另外每一列的欄位數目也不一致,所以不能簡單用read.csv解決。 因為資料量不大,我們還是全部用readLines讀進來後再用strsplit 自己切吧!

關卡 46

和剛剛的電力資料一樣,請讓我們先用readLines把資料全部 載入並且存到gdp這個變數之中。

gdp <- file(gdp_path, encoding = "BIG-5") %>% readLines()

關卡 47

接著,我們利用strsplit,用","當分隔符號, 將讀入的gdp資料切成list。請將結果存到gdp.split

gdp.split <- strsplit(gdp, ",")

關卡 48

仔細一點的話,我們能注意到,年份的資料在切割後,長度都是1 。 所以讓我們選取gdp.split這個list中,length為1 的值。這部分,可以 利用sapply這個函數來查詢。請同學試試看: (sapply(gdp.split, length) == 1) %>% which 這裡的sapply(gdp.split, lenght)相當於建立一個向量,並且讓第一個值 為length(gdp.split[[1]]),第二個值為length(gdp.split[[2]]),以此類推。

(sapply(gdp.split, length) == 1) %>% which

關卡 49

我們可以看一下,這些長度為一的行的內容: 請同學輸入:gdp.split[(sapply(gdp.split, length) == 1) %>% which]

gdp.split[(sapply(gdp.split, length) == 1) %>% which]

關卡 50

我們可以看到,只有前7 個index 是真的代表年份。 最後幾個index 其實只有指到這個檔案的一些說明。 所以我們利用head這個函數,拿出前7 個index,並存到:year.index之中。 請同學輸入:year.index <- (sapply(gdp.split, length) == 1) %>% which %>% head(7)

year.index <- (sapply(gdp.split, length) == 1) %>% which %>% head(7)

關卡 51

接著我們可以利用for 迴圈來切出這份資料。這部分請同學 參考路徑在gdp_df_path的這個檔案,修改它,讓我們能得到gdp_df 這個整理乾淨過後的data.frame。 你可以利用browseURL(gdp_df_path)來打開參考檔案。把檔案 的內容複製到gdp_df.R之中,並且再完成之後,輸入:submit()

# 開始的index
year.index.start <- year.index
# 結束的index
## 每一個開始的index接續到下一個開始的index
## 也就是結尾的index就是下一個開始的index
year.index.end <- c(tail(year.index, -1), length(gdp.split))
# 開始撰寫for迴圈
## 我們先建立每一年份整理出來的gdp 資料
## 這些資料會被放到gdp.df.components之中
gdp.df.components <- list()
# 請填寫正確的for 迴圈的範圍
for(i in 1:7) {
  # 針對特定年份做處理的程式碼
  ## 開始的index
  start <- year.index.start[i]
  ## 結束的index
  end <- year.index.end[i]
  ## 年份的資料在第一筆
  year <- gdp.split[[start]] %>%
    ## 將 "2007" ==> 2007,將"用空白替換掉
    gsub(pattern = '"', replacement = '')
  ## 只抽出這次要處理的資料
  target <- gdp.split[start:end]
  ## 挑出長度是3 的,做rbind
  target.mat <- do.call(rbind, 
                        target[sapply(target, length) == 3])
  ## 原本的第一行是空白,我們改成放年份
  target.mat[,1] <- year
  ## 處理第二行中的"
  target.mat[,2] <- gsub('"', '', target.mat[,2])
  ## 將這輪處理的資料,放到gdp.df.components
  gdp.df.components[[i]] <- target.mat
}
gdp.df <- do.call(rbind, gdp.df.components) %>% 
  ## 我們先不把資料轉成factor
  data.frame(stringsAsFactors = FALSE)
colnames(gdp.df) <- c("year", "name", "gdp")
## 把year 轉成integer
gdp.df$year <- as.integer(gdp.df$year)
## 把name 轉成character
gdp.df$name <- as.character(gdp.df$name)
## 把gdp 轉成numeric
gdp.df$gdp <- as.numeric(gdp.df$gdp)
gdp.df <- filter(gdp.df, !is.na(gdp))

關卡 52

接下來我們比對兩者的資料,好計算使用能源的效率。 但是,這不容易… 比對資料最重要的,是兩者的一致性。

關卡 53

我們先看看gdp.df資料中的行業別。請同學輸入:unique(gdp.df$name)

unique(gdp.df$name)

關卡 54

再來看看power.df資料中的行業別。請同學輸入:unique(power.df$id)

unique(power.df$id)

關卡 55

這裡的資料,一邊是英文+中文,一邊是數字或英文。所以我們先把目標 放在英文上就好了。

關卡 56

請同學輸入:gdp.df %<>% mutate(id = strsplit(name, ".", fixed = TRUE) %>% sapply("[", 1)) 這裡的 %<>% 表示R 會把gdp.df 放到 mutate的第一個參數,然後把輸出結果回存到 變數 gdp.df 。這裡的動作是用 “.” 來切割 gdp.df$name ,然後將“.”之前的結果 放到欄位id

gdp.df %<>% mutate(id = strsplit(name, ".", fixed = TRUE) %>% sapply("[", 1))

關卡 57

我們輸入:unique(gdp.df$id)看看整理後的結果。

unique(gdp.df$id)

關卡 58

中間混雜了一些中英文,並且有些是子類別。例如:JA、JB喊JC就是J的子類別。

關卡 59

請輸入:gdp.df2 <- filter(gdp.df, nchar(id) == 1) 來只挑出那些 id 為單一文字的資料

gdp.df2 <- filter(gdp.df, nchar(id) == 1)

關卡 60

我們輸入:unique(gdp.df2$id)看看整理後的結果,是不是全英文

unique(gdp.df2$id)

關卡 61

接著我們把目標轉到power.df。請同學利用上述所學,挑選出 id欄位為英文的data.frame,將year轉為西元,並將結果存到 power.df2中。

#' 提示:
#' 我會用到filter + grepl, 來挑出英文的id
#'    ps. "^[A-Z]" 代表要尋找英文大寫字母開頭的字串
#' mutate 來將年度從民國變成西元
#' 最後用 mutate + gsub 把 id 中的"."給去除

power.df2 <- 
  filter(power.df, grepl("^[A-Z]", id)) %>%
  mutate(year = year + 1911, id = gsub(".", "", id, fixed = TRUE))

stopifnot(nrow(power.df2) == 204)
stopifnot(sum(power.df2$year) == 409224)

關卡 62

接下來,就可以用id與year來比對資料了…嗎?

關卡 63

請同學輸入:distinct(power.df2, id, name)再檢查一下 用電量資料裡的id所代表的意義

distinct(power.df2, id, name)

關卡 64

再請同學輸入:distinct(gdp.df2, id, name)再檢查一下 國民生產毛額的資料裡的id所代表的意義

distinct(gdp.df2, id, name)

關卡 65

同學有沒有注意到,兩者的K,代表的意義不一致?

關卡 66

這裡我們幫同學簡略地整理一個對應表,放在變數:translation, 請同學看一看。這是一個多對多的對應關係。有些行業別我們不知道 如何對應,等等就先略過。

translation

關卡 67

我們要先把translation展開。利用translation做一個對應表。 請同學手動建立一個文字向量:名字就剛好是 c("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K") 代表power.df2中的id;值是則是c(1, 2, 3, …),代表 該值出現在translation中的位置。最後將結果存到變數: translation.power

translation.power <- local({
  retval <- c(1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 10)
  names(retval) <- c("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K")
  retval
})

關卡 68

請用同樣的要領建立變數:translation.gdp

translation.gdp <- local({
  translation.gdp <- c(1, 2, 3, 4, 4, 5, 6, 7, 6, 7, 8, 8, 9, 9, 10, 9, 9, 9, 9)
  names(translation.gdp) <- head(LETTERS, length(translation.gdp))
  translation.gdp
})

關卡 69

接著,透過translation,我們就可以將兩邊的資料比對成一個data.frame。 請同學挑戰看看。

power.df3 <-
  power.df2 %>%
  mutate(id2 = translation.power[id]) %>%
  filter(!is.na(id2)) %>%
  group_by(year, id2) %>%
  summarise(power = sum(power), name = paste(name, collapse = ","))

gdp.df3 <- 
  gdp.df2 %>%
  mutate(id2 = translation.gdp[id]) %>%
  group_by(year, id2) %>%
  summarise(gdp = sum(gdp))

power.gdp <- inner_join(power.df3, gdp.df3, c("year", "id2")) %>%
  mutate(eff = gdp / power) %>%
  as.data.frame()

關卡 70

恭喜同學完成比對各行業的用電資料與貢獻的國民生產毛額。 我們接下來嘗試利用power.gdp來建立一些圖表看看資料說說 故事。