---
title: 【R 語言與統計資料分析】Ch8:用 tidyverse 進行資料處理
image: https://ppt.cc/fnT4xx@.png
---
# 【R 語言與統計資料分析】Ch8:用 tidyverse 進行資料處理
`tidyverse` 是一個 R 語言套件集合,主要用於資料科學與資料分析,內含許多非常好用的套件,協助我們將資料轉換成更易讀、更好分析的樣貌 (tidy data)。
| 套件 | 功能 |
|----------|------|
| `ggplot2` | 資料視覺化,製作圖表 |
| `dplyr` | 資料操作:篩選 (`filter`)、排序 (`arrange`)、新增欄位 (`mutate`)、彙總 (`summarise`) 等|
| `tidyr` | 資料整理:轉換為 tidy data,例如: `pivot_longer`、`pivot_wider` |
| `readr` | 讀取資料檔案(CSV、TSV 等) |
| `purrr` | 函數式程式,對向量或列表進行操作 |
| `tibble` | 改良版資料框(data frame),印出更美觀 |
| `stringr` | 字串操作 |
| `forcats` | 類別型變數(factor)操作 |
`tidyverse` 中的套件是可以分別下載的,正如我們先前使用的 `ggplot2`,若直接下載 `tidyverse` 就會把所有套件打包一起下載。
```r
# 下載 tidyverse 套件
install.packages("tidyverse")
```
若我們執行 `library(tidyverse)`,就會一次性全部執行所有套件。
```r
library(tidyverse)
```

<div style="text-align: center;">
<img src="https://miro.medium.com/v2/resize:fit:4032/format:webp/1*B-cwhqnFgGIbd9lWnzi_mQ.png" width="400">
<br>
<span style="color: gray; font-size: 0.8em;">tidyverse 套件的各種架構</span>
</div>
我們同樣以先前使用的[新北市公共自行車租賃系統(YouBike2.0)](https://data.ntpc.gov.tw/datasets/010e5b15-3823-4b20-b401-b1cf000550c5)」資料作為範例。
```r
# 資料讀取與基本轉換
ubike <- read.csv("C:\\Users\\Public\\R\\新北市公共自行車租賃系統(YouBike2.0)\\010e5b15-3823-4b20-b401-b1cf000550c5-新北市公共自行車租賃系統(YouBike2.0)-7080864958052842756.csv")
# 1. 定義「英文 = 中文」的對照表
# 這樣做的好處是:易於閱讀、方便未來維護
ubike_colnames_map <- c(
scity = "縣市",
scityen = "縣市英文",
sna = "中文場站名稱",
sarea = "中文場站區域",
ar = "中文地址",
snaen = "英文場站名稱",
sareaen = "英文場站區域",
aren = "英文地址",
sno = "站點代號",
tot_quantity = "場站總停車格",
sbi_quantity = "可借車位數",
mday = "資料更新時間",
lat = "緯度",
lng = "經度",
bemp = "可還空位數",
act = "場站是否暫停營運",
yb2_quantity = "YouBike2.0 數量",
eyb_quantity = "電輔 YouBike 數量"
)
# 2. 取得目前 ubike 資料框的原始欄位名稱
current_names <- names(ubike)
# 3. 執行轉換邏輯
# 檢查目前名稱是否存在於對照表中,若有則替換為對應的中文值
new_names <- current_names
matches <- match(current_names, names(ubike_colnames_map))
new_names[!is.na(matches)] <- ubike_colnames_map[matches[!is.na(matches)]]
# 4. 正式更新資料框標題
names(ubike) <- new_names
# 5. 確認結果
head(ubike, 3)
```
# 8.1 管道運算符 pipe operator
**管道運算符 (pipe operator)** `%>%` 是一個源於 `magrittr` 套件的一個語法,其概念為將前面算出來的答案經由**管道** (pipe) 傳送給下一個函數做計算,而此管道可以無限延長下去。
我們給一個很簡單的範例,若我們想要得知 `ubike` 有幾個變數欄位,以傳統的方法,我們需要如此計算:
```r
# 傳統方法
length(colnames(ubike))
```
在撰寫上其實會比較麻煩,撰寫速度上也會較慢,常常會發生括號對不齊的狀況,倘若我們使用 pipe operator,就會變成相當直觀:
```r
library(tidyverse)
# pipe 方法
ubike %>%
colnames() %>%
length()
```
# 8.2 dplyr
`dplyr` 是 tidyverse 核心套件之一,舉凡各式資料整理功能,幾乎都能透過此套件進行處理。`dplyr` 中有許多常用函數,基本上都要配合 pipe operator `%>%` 進行處理為佳。我們將一一介紹 `dplyr` 的常用函數。
| 功能類別 | 函數 | 說明 |
| ----------- | ---------------------------------------------- | ----------------- |
| **選欄** | `select()` | 選擇指定欄位 |
| **篩選列** | `filter()` | 根據條件篩選列 |
| **新增/修改欄位** | `mutate()` | 計算新欄位或修改欄位 |
| **排序** | `arrange()` | 升序 / 降序排序資料 |
| **彙總** | `summarise()` | 計算統計量(平均、總和、最大值等) |
| **分組** | `group_by()` | 將資料依欄位分組,用於彙總 |
| **列操作** | `slice()` | 根據索引選列 |
| **重命名欄位** | `rename()` | 改變欄位名稱 |
| **加入資料** | `left_join()` / `inner_join()` / `full_join()` | SQL 風格的資料合併 |
| **去重** | `distinct()` | 取得唯一值 |
## 8.2.1 `select()` 選取欄位
回到 `ubike` 資料,其中有許多內容是重疊的 (如中英文名稱),我們可以挑選出想要的欄位並且另存成新的 dataframe 再行計算,如此既可以減少資料維度,也不會傷害到原始資料。
讓我們先看看有什麼欄位:
```r
colnames(ubike)
```
```
Output:
[1] "縣市" "縣市英文" "中文場站名稱"
[4] "中文場站區域" "中文地址" "英文場站名稱"
[7] "英文場站區域" "英文地址" "站點代號"
[10] "場站總停車格" "可借車位數" "資料更新時間"
[13] "緯度" "經度" "可還空位數"
[16] "場站是否暫停營運" "YouBike2.0 數量" "電輔 YouBike 數量"
```
我們把感興趣的欄位做取出:
```r
ubike_CN <- ubike %>%
select( "中文場站名稱", "中文場站區域",
"場站總停車格" , "可借車位數",
"場站是否暫停營運" , "YouBike2.0 數量" , "電輔 YouBike 數量")
str(ubike_CN)
```

`select()` 也有進階用法:
- 選取以特定字串開頭的欄位:`starts_with("特定字串")`
- 選取以特定字串結尾的欄位:`ends_with("特定字串")`
- 選取包含特定字串的欄位:`contains("特定字串")`
- 排除欄位:`-欄位名稱`
讓我們用下列程式碼作為示範:
```r
ubike_CN2 <- ubike %>%
select( starts_with("中文"),
"場站總停車格" , "可借車位數","場站是否暫停營運" ,
contains("YouBike"),
-'中文地址')
str(ubike_CN2)
```
## 8.2.2 `filter()` 篩選條件
`filter()` 的概念與之前所使用的 `subset()` 非常雷同,其目的皆是透過條件篩選來取出符合條件的 rows。
```r
ubike_CN %>% filter(中文場站區域 == "永和區", 場站總停車格 >= 50)
```

`filter()`支援邏輯運算符 (`&`、`|`、`!`),在 `()` 內的各條件為交集。此外,也可以搭配 `between()` 進行範圍篩選。(有等號)
```r
ubike_CN %>% filter(中文場站區域 == "永和區", between(場站總停車格, 60, 70) )
```

:::success
💡 **管道一路暢通**
透過 pipe operator,我們可以一路進行各種資料處理,腦袋只需要想著下一步要進行什麼就好!
```r
ubike %>%
select( starts_with("中文"),
"場站總停車格" , "可借車位數","場站是否暫停營運" ,
contains("YouBike"),
-'中文地址') %>%
filter(中文場站區域 == "永和區", 場站總停車格 >= 50) %>%
filter( `電輔 YouBike 數量` >= 3)
```
:::
## 8.2.3 `mutate()` 新增或修改欄位
`mutate()` 的概念十分簡單,就是針對 dataframe 中的欄位資料進行新增或是修改,讓我們直接以程式範例做介紹。
```r
ubike_CN3 <- ubike %>%
select( "中文場站名稱", "中文場站區域",
"場站總停車格" , "可借車位數","可還空位數",
"場站是否暫停營運" , "YouBike2.0 數量" , "電輔 YouBike 數量")%>%
mutate(空位率 = 可還空位數 / 場站總停車格)
str(ubike_CN3)
```

```r
# 也可以同時新增多個欄位,用逗號隔開
ubike_CN4 <- ubike %>%
select( "中文場站名稱", "中文場站區域",
"場站總停車格" , "可借車位數","可還空位數",
"場站是否暫停營運" , "YouBike2.0 數量" , "電輔 YouBike 數量") %>%
mutate(空位率 = 可還空位數 / 場站總停車格,
電輔車比率 = `電輔 YouBike 數量`/(`YouBike2.0 數量`+`電輔 YouBike 數量`))
str(ubike_CN4)
```

除了新增欄位之外,我們也可以透過 `mutate()` 對既存的欄位進行修改。比方說,若我們希望將新創建的「空位率」以及「電輔車比率」四捨五入到小數點後第二位,我們可以這樣執行:
```r
ubike_CN4 <- ubike_CN4 %>%
mutate(空位率 = round(空位率,2),
電輔車比率 = round(電輔車比率,2))
str(ubike_CN4)
```

我們也可以可以在同一個 mutate() 中,即時引用剛算好的新欄位:
```r
ubike_CN5 <- ubike %>%
select( "中文場站名稱", "中文場站區域",
"場站總停車格" , "可借車位數","可還空位數",
"場站是否暫停營運" , "YouBike2.0 數量" , "電輔 YouBike 數量")%>%
mutate(空位率 = 可還空位數 / 場站總停車格,
可用率 = 1-空位率)
str(ubike_CN5)
```

## 8.2.4 `arrange()` 排序資料
我們先前有學習過如何透過 `order()` 函數來針對特定欄位大小值排序資料集,但 `order()` 的寫法相對冗贅,若使用 `dplyr` 套件中的 `arrange()` 即能像點選 Excel 中的「排序與篩選」功能一樣快速且直觀地進行排序。
假設我們想要依據「可借車位數」欄位來看現在哪些站點的車位是充足或是哪些站點的腳踏車已快用罄,我們就可以先透過 `arrange()` 來瞥見一些端倪。
```r
# 由小到大排:
ubike_CN5 %>%
arrange(可借車位數) %>%
head(15) # 用 pipe 取出頭 15 筆資料
```
```r
# 若要由大到小排:
ubike_CN5 %>%
arrange(desc(可借車位數)) %>%
head(15)
```
## 8.2.5 `group_by()` 進行分組運算
在分析複雜資料時,我們常常會希望依據不同的「組別」進行分別運算,比如在 `ubike` 資料中,每一個行政區計算可借車位數的總數。在 Excel 中,我們可以透過「**樞紐分析表**」功能快速完成一些基礎運算,但倘若我們面對到更客製化的運算,或是資料量大到 Excel 無法開啟,就需要借助程式語言的巧手了。
<div style="text-align: center;">
<img src="https://hackmd.io/_uploads/SyGpeZTUuWe.png" width="250">
<br>
<span style="color: gray; font-size: 0.8em;">Excel 的樞紐分析表計算每一行政區的可借單車數量</span>
</div>
要執行類似的功能,就可以使用 `dplyr` 內的 `group_by()`,將資料依據特定欄位 (如:行政區) 分組後,此函數會匯出一個 `tibble` 表格,裡面就已經隱藏了分組資訊。
```r
ubike %>%
group_by(中文場站區域)
```
<div style="text-align: center;">
<img src="https://hackmd.io/_uploads/HJmY7TIdZg.png" width="450">
<br>
<span style="color: gray; font-size: 0.8em;">tibble 表格</span>
</div>
一旦進行 `group_by()` 後,接續的運算就不再是以全體資料集的規模來去執行,而是每組各自彙算後再行呈現。
比如說,我想抓取出所有行政區各自的最大站,可以透過以下方法來執行:
```r
ubike %>%
select( "中文場站名稱", "中文場站區域",
"場站總停車格" , "可借車位數", "可還空位數") %>%
group_by(中文場站區域) %>% # 依照行政區分組
filter(場站總停車格 == max(場站總停車格)) %>%
print(n = Inf) #展開顯示所有行政區
```

:::success
💡 **不都是資料表格嗎?** `dataframe` **vs.** `tibble`
| 特性 | dataframe (`base` R) | tibble (`tidyverse`) |
| :--- | :--- | :--- |
| **預覽顯示** | 一口氣輸出所有資料 | 只顯示前 10 列與視窗放得下的欄位 |
| **資料型態顯示** | 不會顯示 | 欄位下方會註記 `<int>`, `<chr>`, `<dbl>` 等|
| **字串處理** | 有時會自動轉為 Factor | 永遠保持為字串(Character) |
| **子集選取 `[`** | 有時回傳向量,有時回傳資料框 | 永遠回傳 tibble |
:::
`group_by()` 也可以根據分組後資料做運算後,再將這些結果回傳至原資料中。
舉例來說,若我們想要幫每個站點在每一行政區的車位總數做排名,來找出誰是板橋第一大站、誰是永和第五大站等等,就可以透過 `group_by()` 和 `mutate()` 來做呈現。
```r
rank_ubike <- ubike %>%
select( "中文場站名稱", "中文場站區域",
"場站總停車格" , "可借車位數", "可還空位數") %>%
group_by(中文場站區域) %>%
mutate(區域內排名 = min_rank(desc(場站總停車格))) # 由大到小排
print(rank_ubike)
View(rank_ubike)
```

若我們完成分組運算後,希望把 `tibble` 回復成一般狀態,就需要使用 `ungroup()` 來解除分組。
```r
rank_ubike %>% ungroup()
# 會發現 # Groups: 中文場站區域 [29] 不見了
```

目前為止,我們已經認識到 `group_by()` 的強大之處,接著,我們要來介紹另一個常用函數 `summarise()`,以及其與 `group_by()` 的結合運用。
## 8.2.6 `summarise()` 進行統計輸出
當我們想要計算敘述統計時,先前提過可以使用 `summary()` 一次匯出所有欄位的敘述統計,但在使用上不見得好用。此時,我們就可以透過 `dplyr` 中的 `summarise()` 自行設計輸出報表。
比方説我們針對站點內可借的車位可以做以下的統計指標計算:
```r
ubike %>%
summarise(
總站點數 = n(),
總可借車位 = sum(可借車位數, na.rm = TRUE),
平均可借車位 = round(mean(可借車位數, na.rm = TRUE),2)
)
# n() 計算個數
# n_distinct() 計算不重複個數
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/S1bcFC8_Zx.png" width="300">
<br>
</div>
另一個 `summarise()` 的強大功能即為配合 `group_by()` 同步呈現,若我們想依據行政區的不同來研究車位的狀況,就可以先做 `group_by()` 後,再做 `summarise()`。
```r
ubike %>%
group_by(中文場站區域) %>%
summarise(
總站點數 = n(),
總可借車位 = sum(可借車位數, na.rm = TRUE),
平均可借車位 = round(mean(可借車位數, na.rm = TRUE),2)
)
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/B1n6qAIdZg.png" width="350">
<br>
</div>
## 8.2.7 `rename()` 更改欄位名稱
過往在更改欄位名稱時,都需要將 `colnames()` 取出後再行取代,若如今使用 `dplyr` 的 `rename()` 函數,即可更有效率且直觀地進行欄位名稱的修改。
```r
ubike2 <- read.csv("010e5b15-3823-4b20-b401-b1cf000550c5-新北市公共自行車租賃系統(YouBike2.0)-4595947263462708017.csv")
colnames(ubike2)
library(dplyr)
ubike2 <- ubike2 %>%
rename( # 新名稱 = 舊名稱
"縣市" = scity,
"縣市英文" = scityen,
"中文場站名稱" = sna,
"中文場站區域" = sarea,
"中文地址" = ar,
"英文場站名稱" = snaen,
"英文場站區域" = sareaen,
"英文地址" = aren,
"站點代號" = sno,
"場站總停車格" = tot_quantity,
"可借車位數" = sbi_quantity,
"資料更新時間" = mday,
"緯度" = lat,
"經度" = lng,
"可還空位數" = bemp,
"場站是否暫停營運" = act,
"YouBike2.0 數量" = yb2_quantity,
"電輔 YouBike 數量" = eyb_quantity
)
colnames(ubike2)
```
此外,我們也可以針對所有欄位名稱統一加上前綴字:
```r
ubike %>%
rename_with(~ paste0("新北市_", .)) %>%
colnames()
```

## 8.2.8 報表合併
`dplyr` 也可以進行合併資料,就如同我們先前所使用的 `merge()` 函數一樣,讓我們用實際案例來進行解說。我們在此先建立一個虛擬資料集:
```r
# 建立資料框架
df <- data.frame(
ID = c(1, 2, 3, 4, 5),
Sales = c(100, 150, 120, 200, 250),
number = c(10, 30, 23, 49, 15),
pos = c("specialist", "specialist", "manager", "sales", "specialist")
)
# 查看結果
print(df)
```

另外,我們再建立一個參照用的資料集:
```
# 獎金對照表
bonus <- data.frame(
pos = c("specialist", "manager", "director"),
bonus = c(500, 1000, 3000)
)
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/r1WBGSwd-g.png" width="150">
<br>
</div>
#### `left_join()` 以左表為主
我們現在以 `pos` 欄位的職位為參照值,將每個職位所對應的獎金填入 `df`:
```
df %>% left_join(bonus, by = "pos")
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/SkIuMrPubx.png" width="300">
<br>
</div>
可以看到成功合併兩個表格,且沒有對應 `pos` 的 `bonus` 值會被填入 `NA`。
:::info
💡 **合併後列數怎麼變多了?**
若我們現在的獎金資料集改為如下:
```r
bonus2 <- data.frame(
pos = c("specialist", "manager", "manager"),
bonus = c(500, 1000, 3000)
)
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/HkaRmHPubl.png" width="150">
<br>
</div>
也就是 `manager` 職位對應到不只一個薪水,若我們執行以下程式碼,則會輸出:
```r
df %>% left_join(bonus2, by = "pos")
```

會出現警告訊息且告訴我們原因是來自於 key 值不唯一所導致的。此時,我們就應該重新檢視來源資料集。
:::
```
# 新增獎金3資料
bonus3 <- data.frame(
position = c("specialist", "manager", "director"),
bonus = c(500, 1000, 3000)
)
```
若我們遇到兩個資料集的欄位意義相同,但名稱不同,比如我們先增一個 `bonus3` 資料集,其描述職位的欄位名稱不再是 `pos` 而是 `position`,於 `left_join()` 中可執行以下設定:
```
left_join(df, bonus3, by = c("pos" = "position"))
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/ByogUHPO-x.png" width="350">
<br>
</div>
`dplyr` 中還有與 `left_join()` 相對的 `right_join()`,使用方法相同,但因為與大家的資料使用習慣不符,所以較少被使用。
#### `inner_join()` 取表格交集
`inner_join()` 只會合併兩者皆有 key 值的欄位,也就是說前述的 ID4 資料就會被移除:
```r
df %>% inner_join(bonus, by = "pos")
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/B1LQDSP_Zg.png" width="350">
<br>
</div>
#### `full_join()` 取表格聯集
`full_join()` 會將兩張表的內容全部合併,也就是說,`df` 裡的 `sales` 會留下 (獎金為 `NA`),`bonus` 裡的 `director` 也會出現 (`ID`、`Sales` 為 `NA`)。
```r
# 兩張表的內容全部合併
df %>% full_join(bonus, by = "pos")
```
<div style="text-align: left;">
<img src="https://hackmd.io/_uploads/Bk3ZOBvuZg.png" width="350">
<br>
</div>
# 參考資料
1. 陳旭昇(2024),資料分析的統計學基礎:使用R語言,東華書局
2. 林建甫 Jeff Lin(2020),[R 資料科學與統計](https://bookdown.org/jefflinmd38/r4biost)
3. Gareth James, Daniela Witten, Trevor Hastie, Robert Tibshirani. (2017). An Introduction to Statistical Learning: With Applications in R. New York: Springer.
4. 陳基國(2024). 基礎統計與R語言. 台北:五南圖書出版股份有限公司