]
---
layout: true
background-image: url(data:image/png;base64,#https://www.agec.ntu.edu.tw/uploads/asset/data/675f864c74ccc38a768f8254/%E6%9C%AA%E4%BE%86%E5%B1%95%E6%9C%9B%E5%9C%96%E7%89%87.png)
background-position: 98% 2%
background-size: 5%
---
## Recall pipeline
- ` %>% ` 是 tidy 家族中常用且使程式碼更易閱讀的運算子
- 使用 pipeline 建立工作流程(workflow)有效建立操作邏輯
``` r
library(dplyr)
```
- 預設是將左方 R object 帶入右方指令的第一個參數
.pull-left[
f(x)
``` r
sum(c(1:5))
```
```
## [1] 15
```
]
.pull-right[
x %>% f()
``` r
c(1:5) %>% sum()
```
```
## [1] 15
```
]
---
## Recall pipeline
**penguins** 資料集中包含企鵝品種、居住島、喙長寬與體重等等變數。
``` r
head(penguins)
```
```
## species island bill_len bill_dep flipper_len body_mass sex year
## 1 Adelie Torgersen 39.1 18.7 181 3750 male 2007
## 2 Adelie Torgersen 39.5 17.4 186 3800 female 2007
## 3 Adelie Torgersen 40.3 18.0 195 3250 female 2007
## 4 Adelie Torgersen NA NA NA NA <NA> 2007
## 5 Adelie Torgersen 36.7 19.3 193 3450 female 2007
## 6 Adelie Torgersen 39.3 20.6 190 3650 male 2007
```
---
## Recall pipeline
使用 dplyr 指令可以讓工作流程更清晰,例如:統計不同品種、性別間的企鵝嘴喙的長寬比最小值是多少。
.panelset[
.panel[.panel-name[w/o dplyr]
``` r
summarize(select(group_by(mutate(drop_na(
penguins), bill_ratio = bill_len/ bill_dep), species, sex), species, sex, bill_ratio), min_bill_ratio = min(bill_ratio), .groups = "drop")
```
```
## # A tibble: 6 × 3
## species sex min_bill_ratio
## <fct> <fct> <dbl>
## 1 Adelie female 1.76
## 2 Adelie male 1.64
## 3 Chinstrap female 2.35
## 4 Chinstrap male 2.48
## 5 Gentoo female 2.84
## 6 Gentoo male 2.57
```
]
.panel[.panel-name[w/ dplyr]
``` r
penguins %>%
drop_na() %>%
mutate(bill_ratio = bill_len/ bill_dep) %>%
group_by(species, sex) %>%
select(species, sex, bill_ratio) %>%
summarize(min_bill_ratio = min(bill_ratio), .groups = 'drop')
```
```
## # A tibble: 6 × 3
## species sex min_bill_ratio
## <fct> <fct> <dbl>
## 1 Adelie female 1.76
## 2 Adelie male 1.64
## 3 Chinstrap female 2.35
## 4 Chinstrap male 2.48
## 5 Gentoo female 2.84
## 6 Gentoo male 2.57
```
]
]
---
## Recall pipeline
<img src="data:image/png;base64,#./Pipe_baking_magrittr_backAssign.gif" width="55%" style="display: block; margin: auto;" />
<p style="text-align: center;">圖片來源:<a href="https://github.com/arthurwelle/VIS/blob/master/Pipe_Cake/Pipe_baking_magrittr_backAssign.gif">Arthur Welle</a></p>
---
## Merge
通常在整理資料的過程中,需要透過結合各個不同類型或是時間段的資料並加以分析,這時就必須將多個資料進行合併。實務上,資料合併大致可分為:
- 垂直合併:通常在合併相同但**時間不同**的資料集,或是合併**不同公司或單位**的財務報表或股價資料,這些資料集通常都會有相同的欄位(column)名稱與數量,整體資料寬度不會改變,例如:將一月至十二月的天氣資料垂直合併。
- 水平合併:通常在合併有關連的資料集,這類資料通常主題或調查製作單位不一樣,兩個資料集的行數或欄位皆可以不盡相同但是必須**具有一個或多個索引鍵(key value)**使兩個資料集有依據的水平合併起來,例如:將國家總經資料與社經資料依照國家名稱水平合併。
---
## Vertically Merge
將具有相同欄位數量與名稱(非必要)的資料以上下疊加的方式垂直合併。
.panelset[
.panel[.panel-name[rbind]
.pull-left[
``` r
d1 <- tribble(
~id, ~name, ~score,
1, "Chloe", 95,
2, "Leo" , 60,
3, "Sandy", 82
)
d2 <- tibble(
id = c(4:6),
name = c("Tim", "Irene", "Cindy"),
score = id * 10 + 15
)
```
]
.pull-right[
``` r
rbind(d1, d2)
```
```
## # A tibble: 6 × 3
## id name score
## <dbl> <chr> <dbl>
## 1 1 Chloe 95
## 2 2 Leo 60
## 3 3 Sandy 82
## 4 4 Tim 55
## 5 5 Irene 65
## 6 6 Cindy 75
```
]
]
.panel[.panel-name[difference column]
當資料集中存在不同變數名稱時,未合併到的部分將為 NA。
.pull-left[
``` r
d3 <- tribble(
~id, ~name, ~math,
1, "Chloe", 95,
2, "Leo" , 60,
3, "Sandy", 82
)
d4 <- tibble(
id = c(4:6),
name = c("Tim", "Irene", "Cindy"),
english = id * 10 + 15
)
```
]
.pull-right[
``` r
d3 %>% bind_rows(d4)
```
```
## # A tibble: 6 × 4
## id name math english
## <dbl> <chr> <dbl> <dbl>
## 1 1 Chloe 95 NA
## 2 2 Leo 60 NA
## 3 3 Sandy 82 NA
## 4 4 Tim NA 55
## 5 5 Irene NA 65
## 6 6 Cindy NA 75
```
]
]
]
---
## Vertically Merge
如果兩個資料集的變數名稱不同時,需要先將變數名稱修改一致,再進行垂直合併。
.panelset[
.panel[.panel-name[data]
.pull-left[
``` r
d5 <- tribble(
~order, ~name, ~math,
1, "Chloe", 95,
2, "Leo" , 60,
3, "Sandy", 82
)
```
```
## # A tibble: 3 × 3
## order name math
## <dbl> <chr> <dbl>
## 1 1 Chloe 95
## 2 2 Leo 60
## 3 3 Sandy 82
```
]
.pull-right[
``` r
d6 <- tibble(
id = c(4:6),
name = c("Tim", "Irene", "Cindy"),
english = id * 10 + 15
)
```
```
## # A tibble: 3 × 3
## id name english
## <int> <chr> <dbl>
## 1 4 Tim 55
## 2 5 Irene 65
## 3 6 Cindy 75
```
]
]
.panel[.panel-name[rename]
``` r
d5 %>% rename(id = order) %>% bind_rows(d6)
```
```
## # A tibble: 6 × 4
## id name math english
## <dbl> <chr> <dbl> <dbl>
## 1 1 Chloe 95 NA
## 2 2 Leo 60 NA
## 3 3 Sandy 82 NA
## 4 4 Tim NA 55
## 5 5 Irene NA 65
## 6 6 Cindy NA 75
```
]
]
---
## Horizontally Merge
通常在整理資料的過程中,需要結合各個不同類型的資料並加以分析,這時需要依據**索引鍵(key value)**將不同資料水平合併,tidyverse 中有幾種 join 方式大致可分為:
.left-column[
- Full Join
- Left Join
- Right Join
- Inner Join
- Semi Join
]
.right-column[
<img src="data:image/png;base64,#./tidyversrejoin.png" width="75%" style="display: block; margin: auto;" />
]
---
## Horizontally Merge
有兩個資料集,資料集 people 中包含個人的 id, name 兩個變數;資料集 school 中包含個人 id, school 兩個變數,我們可以藉由索引鍵 **id** 來水平合併兩個資料集。
``` r
people <- tibble(
id = c(1, 2, 3, 4, 6),
name = c("Tim", "Irene", "Cindy", "Chloe", "Sandy")
)
school <- tribble(
~id, ~school,
3, "NTU",
4, "NTNU",
5, "NCCU",
7, "NTU"
)
```
---
## Horizontally Merge
依據一或多個索引鍵(key value)對兩個或以上的資料集進行水平合併, `by` argument 可以提供實際要使用的索引鍵,沒設定的話會自動依據被合併的資料集做選擇。
.panelset[
.panel[.panel-name[Full]
``` r
people %>% full_join(school, by = "id")
```
```
## # A tibble: 7 × 3
## id name school
## <dbl> <chr> <chr>
## 1 1 Tim <NA>
## 2 2 Irene <NA>
## 3 3 Cindy NTU
## 4 4 Chloe NTNU
## 5 6 Sandy <NA>
## 6 5 <NA> NCCU
## 7 7 <NA> NTU
```
]
.panel[.panel-name[Left]
以左表 people 為主表,將右表 school 向左水平合併。
``` r
people %>% left_join(school, by = "id")
```
```
## # A tibble: 5 × 3
## id name school
## <dbl> <chr> <chr>
## 1 1 Tim <NA>
## 2 2 Irene <NA>
## 3 3 Cindy NTU
## 4 4 Chloe NTNU
## 5 6 Sandy <NA>
```
]
.panel[.panel-name[Right]
以右表 school 為主表,將左表 people 向右水平合併。
``` r
people %>% right_join(school, by = "id")
```
```
## # A tibble: 4 × 3
## id name school
## <dbl> <chr> <chr>
## 1 3 Cindy NTU
## 2 4 Chloe NTNU
## 3 5 <NA> NCCU
## 4 7 <NA> NTU
```
]
.panel[.panel-name[Inner]
沒設定 by 什麼索引鍵的話,則自動依據被合併的資料集做選擇。
``` r
people %>% inner_join(school)
```
```
## Joining with `by = join_by(id)`
```
```
## # A tibble: 2 × 3
## id name school
## <dbl> <chr> <chr>
## 1 3 Cindy NTU
## 2 4 Chloe NTNU
```
]
]
---
## Horizontally Merge
`semi_join()` 與 `inner_join()` 差別在於 `semi_join()` 不會保留右表資料。
.pull-left[
<img src="data:image/png;base64,#./semi-join.gif" width="100%" style="display: block; margin: auto;" />
]
.pull-right[
<img src="data:image/png;base64,#./inner-join.gif" width="100%" style="display: block; margin: auto;" />
]
<p style="text-align: center;">圖片來源:<a href="https://github.com/gadenbuie/tidyexplain">Garrick Aden-Buie tidyexplain</a></p>
---
## Horizontally Merge
`semi_join(x, y)` 是當遇到一種情況:舉行交換學生行前說明會之前,想從學生名單中挑出**有錄取的交換學生**寄通知信。此時我只需要用錄取名單中的 `id` 來篩選左表,而不需要學校、國家、學期等欄位資訊。
``` r
people <- tibble(
id = c(1, 2, 3, 4, 6),
name = c("Tim", "Irene", "Cindy", "Chloe", "Sandy"),
score = c(88, 76, 95, 60, 82)
)
exchange <- tribble(
~id, ~school, ~country, ~semester,
1, "NTU", "Japan", "2025 Fall",
3, "NTNU", "Germany", "2025 Fall",
6, "NCCU", "American", "2026 Spring",
7, "NTU", "American", "2026 Spring"
)
```
---
## Horizontally Merge
.panelset[
.panel[.panel-name[Semi]
從左表 people 中篩選出 id 也在右表 exchange 的資料,篩選完後只保留左表資料。
``` r
people %>% semi_join(exchange, by = "id")
```
```
## # A tibble: 3 × 3
## id name score
## <dbl> <chr> <dbl>
## 1 1 Tim 88
## 2 3 Cindy 95
## 3 6 Sandy 82
```
]
.panel[.panel-name[Anti]
從左表 people 中篩選出 id 不在右表 exchange 的資料,篩選完後只保留左表資料。
``` r
people %>% anti_join(exchange, by = "id")
```
```
## # A tibble: 2 × 3
## id name score
## <dbl> <chr> <dbl>
## 1 2 Irene 76
## 2 4 Chloe 60
```
]
]
---
## Horizontally Merge
如果兩個資料集的索引鍵(key value)名稱不同時,在 `by` argument 中手動指定左右表的索引鍵,寫法為 `by = c("左表索引鍵" = "右表索引鍵")`。
``` r
people <- tibble(
ID = c(1, 2, 3, 4, 6),
name = c("Tim", "Irene", "Cindy", "Chloe", "Sandy"))
school <- tribble(
~id, ~school,
3, "NTU",
4, "NTNU",
5, "NCCU",
7, "NTU")
people %>% inner_join(school, by = c("ID" = "id"))
```
```
## # A tibble: 2 × 3
## ID name school
## <dbl> <chr> <chr>
## 1 3 Cindy NTU
## 2 4 Chloe NTNU
```
---
## Horizontally Merge
有時候單一索引鍵無法唯一辨別一筆資料。例如 `score` 記錄學生**每一年**的學期成績,`grant` 記錄每個學生**每一年**領到的獎學金金額——同一個 id 在兩張表中都出現多次,此時需同時使用 id, year 作為索引鍵才能辨別唯一資料。
``` r
score <- tribble(
~id, ~year, ~name, ~score,
1, 2024, "Tim", 88,
1, 2025, "Tim", 91,
2, 2024, "Irene", 76,
2, 2025, "Irene", 82,
3, 2024, "Cindy", 95
)
grant <- tribble(
~id, ~year, ~scholarship,
1, 2024, 8000,
1, 2025, 12000,
2, 2024, 5000,
3, 2025, 10000
)
```
---
## Horizontally Merge
.panelset[
.panel[.panel-name[only key: id]
``` r
score %>% left_join(grant, by = "id")
```
```
## Warning in left_join(., grant, by = "id"): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 1 of `x` matches multiple rows in `y`.
## ℹ Row 1 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
## "many-to-many"` to silence this warning.
```
```
## # A tibble: 7 × 6
## id year.x name score year.y scholarship
## <dbl> <dbl> <chr> <dbl> <dbl> <dbl>
## 1 1 2024 Tim 88 2024 8000
## 2 1 2024 Tim 88 2025 12000
## 3 1 2025 Tim 91 2024 8000
## 4 1 2025 Tim 91 2025 12000
## 5 2 2024 Irene 76 2024 5000
## 6 2 2025 Irene 82 2024 5000
## 7 3 2024 Cindy 95 2025 10000
```
]
.panel[.panel-name[both of keys]
將 id, year 一起放進 `by`,**兩個條件同時符合**才算配對成功,資料維持 5 筆。
``` r
score %>% left_join(grant, by = c("id", "year"))
```
```
## # A tibble: 5 × 5
## id year name score scholarship
## <dbl> <dbl> <chr> <dbl> <dbl>
## 1 1 2024 Tim 88 8000
## 2 1 2025 Tim 91 12000
## 3 2 2024 Irene 76 5000
## 4 2 2025 Irene 82 NA
## 5 3 2024 Cindy 95 NA
```
沒領獎學金的學生年度(Irene 2025、Cindy 2024)`amount` 為 `NA`。
]
.panel[.panel-name[one difference key name]
一個索引鍵名稱相同、另一個不同,可以**混合寫法**。
``` r
grant2 <- grant %>% rename(yr = year)
score %>% left_join(grant2, by = c("id", "year" = "yr"))
```
```
## # A tibble: 5 × 5
## id year name score scholarship
## <dbl> <dbl> <chr> <dbl> <dbl>
## 1 1 2024 Tim 88 8000
## 2 1 2025 Tim 91 12000
## 3 2 2024 Irene 76 5000
## 4 2 2025 Irene 82 NA
## 5 3 2024 Cindy 95 NA
```
]
]
---
## Horizontally Merge
前面的範例中 `id` 都是唯一值,所以合併前後筆數不變;但只要有**一邊的索引鍵重複,合併後的資料筆數就會增加**。
``` r
people <- tibble(
id = c(1, 2, 3, 4, 6),
name = c("Tim", "Irene", "Cindy", "Chloe", "Sandy"),
score = c(88, 76, 95, 60, 82)
)
club <- tribble( # 一個人可以參加多個社團
~id, ~club,
1, "Basketball",
1, "Photography",
2, "Guitar",
3, "Basketball",
3, "Debate",
3, "Volleyball"
)
```
---
## Horizontally Merge
`people` 原本 5 筆(一人一筆),合併後變成 8 筆:Cindy 參加 3 個社團,會複製成 3 列。
``` r
people %>% left_join(club, by = "id")
```
```
## # A tibble: 8 × 4
## id name score club
## <dbl> <chr> <dbl> <chr>
## 1 1 Tim 88 Basketball
## 2 1 Tim 88 Photography
## 3 2 Irene 76 Guitar
## 4 3 Cindy 95 Basketball
## 5 3 Cindy 95 Debate
## 6 3 Cindy 95 Volleyball
## 7 4 Chloe 60 <NA>
## 8 6 Sandy 82 <NA>
```
---
## Practice:Vertically Merge
甲班與乙班的期中考成績分別存在兩張表,乙班因為有補考,所以多了一個 `makeup` 欄位,而且學號欄位的名稱也不一樣。
``` r
class_a <- tribble(
~id, ~name, ~midterm,
1, "Tim", 88,
2, "Irene", 76,
3, "Cindy", 95
)
class_b <- tribble(
~stu_id, ~name, ~midterm, ~makeup,
4, "Chloe", 55, 70,
5, "Sandy", 82, NA,
6, "Leo", 48, 65
)
```
---
## Practice:Vertically Merge
.panelset[
.panel[.panel-name[question]
#### 1. 直接執行 `rbind(class_a, class_b)` 會發生什麼事?為什麼?
#### 2. 把兩個班級合併成一張完整的成績表 `grade`。
#### 3. 合併後 `makeup` 欄位有幾個 `NA`?分別是誰?
]
.panel[.panel-name[answer1]
``` r
rbind(class_a, class_b)
```
```
## Error in rbind(deparse.level, ...): numbers of columns of arguments do not match
```
`rbind()` 要求欄位數量完全相同。改用 `bind_rows()` 雖然不會報錯,但名稱不一致的欄位會被當成兩個不同變數:
``` r
class_a %>% bind_rows(class_b)
```
```
## # A tibble: 6 × 5
## id name midterm stu_id makeup
## <dbl> <chr> <dbl> <dbl> <dbl>
## 1 1 Tim 88 NA NA
## 2 2 Irene 76 NA NA
## 3 3 Cindy 95 NA NA
## 4 NA Chloe 55 4 70
## 5 NA Sandy 82 5 NA
## 6 NA Leo 48 6 65
```
]
.panel[.panel-name[answer2]
``` r
grade <- class_a %>% bind_rows(class_b %>% rename(id = stu_id))
grade
```
```
## # A tibble: 6 × 4
## id name midterm makeup
## <dbl> <chr> <dbl> <dbl>
## 1 1 Tim 88 NA
## 2 2 Irene 76 NA
## 3 3 Cindy 95 NA
## 4 4 Chloe 55 70
## 5 5 Sandy 82 NA
## 6 6 Leo 48 65
```
** 先 `rename()` 讓索引欄位名稱一致,再 `bind_rows()`。**
]
.panel[.panel-name[answer3]
``` r
sum(is.na(grade$makeup))
```
```
## [1] 4
```
``` r
grade %>% filter(is.na(makeup))
```
```
## # A tibble: 4 × 4
## id name midterm makeup
## <dbl> <chr> <dbl> <dbl>
## 1 1 Tim 88 NA
## 2 2 Irene 76 NA
## 3 3 Cindy 95 NA
## 4 5 Sandy 82 NA
```
]
]
---
## Practice:Horizontally Merge
`students` 是系上學生名單,`enroll` 是選課紀錄。
``` r
students <- tibble(
id = c(1, 2, 3, 4, 5),
name = c("Tim", "Irene", "Cindy", "Chloe", "Sandy"),
dept = c("AGEC", "ECON", "AGEC", "STAT", "AGEC")
)
enroll <- tribble(
~id, ~course, ~credit,
1, "Econometrics", 3,
1, "Statistics", 3,
2, "Econometrics", 3,
3, "Calculus", 4,
6, "Statistics", 3
)
```
---
## Practice:Horizontally Merge
.panelset[
.panel[.panel-name[question]
#### 1. 保留所有學生資料,將 `enroll` 合併進來存成 `df`。
#### 2. 哪些學生沒有修任何一門課?
#### 3. 列出有修課的學生名單,只保留學生基本資料(不要課程欄位)。
#### 4. 每位學生總共修了幾學分?(沒修課的學生也要出現在結果中)
]
.panel[.panel-name[answer1]
``` r
df <- students %>% left_join(enroll, by = "id"); df
```
```
## # A tibble: 6 × 5
## id name dept course credit
## <dbl> <chr> <chr> <chr> <dbl>
## 1 1 Tim AGEC Econometrics 3
## 2 1 Tim AGEC Statistics 3
## 3 2 Irene ECON Econometrics 3
## 4 3 Cindy AGEC Calculus 4
## 5 4 Chloe STAT <NA> NA
## 6 5 Sandy AGEC <NA> NA
```
#### 從 5 筆變成 6 筆:Tim 修了兩門課被複製成 2 列。另外 `enroll` 中 id = 6 不在名單裡,`left_join` 直接忽略。
]
.panel[.panel-name[answer2]
``` r
students %>% anti_join(enroll, by = "id")
```
```
## # A tibble: 2 × 3
## id name dept
## <dbl> <chr> <chr>
## 1 4 Chloe STAT
## 2 5 Sandy AGEC
```
]
.panel[.panel-name[answer3]
``` r
students %>% semi_join(enroll, by = "id")
```
```
## # A tibble: 3 × 3
## id name dept
## <dbl> <chr> <chr>
## 1 1 Tim AGEC
## 2 2 Irene ECON
## 3 3 Cindy AGEC
```
** 用 `semi_join()` 而不是 `inner_join()`:Tim 修兩門課也只會出現一次。**
]
.panel[.panel-name[answer4]
``` r
df %>% group_by(id, name) %>%
summarise(total_credit = sum(credit, na.rm = TRUE), .groups = "drop")
```
```
## # A tibble: 5 × 3
## id name total_credit
## <dbl> <chr> <dbl>
## 1 1 Tim 6
## 2 2 Irene 3
## 3 3 Cindy 4
## 4 4 Chloe 0
## 5 5 Sandy 0
```
** 要從 `df`(left_join 的結果)算起,沒修課的 Chloe、Sandy 才會留下來變成 0。**
]
]
---
class: center, middle, inverse
### 謝謝
#### 下周見