第 4 章 Operation on vector and list

4.1 選取元素

4.1.1 選「一個元素」

4.1.1.1 用位置選[[.]]

  • 用位置選:object_name[[i]]
範例:
vectorExample <- c("小明", "小英", "大雄")

# 有多少位置可選:
length(vectorExample)
vectorExample[[1]]
vectorExample[[3]]
listExample <- list(student = "小明", 學期 = list(`108-1` = list(list(name = "個體經濟學", 
    teacher = "Alice", grade = 85)), `108-2` = list(list(name = "總體經濟學", 
    teacher = "Mark", grade = 78))))
# 有多少位置可選:
length(listExample)
listExample[[1]]
listExample[[2]]
  `.台北市預報元素值` <- 
  list(
    city="台北市",
    high=25,
    low=18
  )
`.新北市預報元素值` <- 
  list(
    city="新北市",
    high=24,
    low=15
  )

library(lubridate)
`今明天氣` <-
  list(
    `今天日期`=ymd("2020-03-31"),
    `明天天氣`=ymd("2020-04-01"),
    `各地預報`=list(
      `.台北市預報元素值`,
      `.新北市預報元素值`
      )

  )

4.1.1.2 用名字選$.

# 有多少名字可選:
names(listExample)
listExample$student
listExample$學期  # 特殊命名依然要用反頓點呼叫

其實也可以用[["名字"]]來選,只是名字要以字串「值」的方式來寫,也就是要用引號一對"…", 不用反頓點一對`…`。

listExample[["student"]]
listExample$student

listExample[["學期"]]  # 小心不是反頓點喔。
listExample$學期

4.1.2 選「多個元素」

4.1.2.1 用位置選[c(...)]

vectorExample
vectorExample[c(1, 3)]
# 顯示到第一層
str(listExample, max.level = 1)

listExample[c(1, 2)]

4.1.2.2 用名字選[c("name1","name2",...)]

# 顯示到第一層
str(listExample, max.level = 1)

listExample[c("學期")]
listExample[c("student", "學期")]

4.1.2.3 用「要/不要」邏輯向量選[c(T,T,F,...)]

vectorExample
vectorExample[c(T, T, F)]  # 要,要,不要
str(listExample[c(F, T)])  # 不要,要
str(listExample[c(T, T)])  # 要,要
str(listExample)
listExample: length=2, list class
  |
  |--student: length=1, character class
  |
  |--學期: length=3, list class

以下兩個都是表面都是選取“學期”:

get1 <- listExample[["學期"]]
get2 <- listExample["學期"]
  • [[]]: 拿出某個物件值。listExample[["學期"]]將listExample裡的“學期”物件值拿出來
get1  # 學期拿出來,有3個元素的list
str(get1)
  • []: 留下某個物件值。listExample["學期"]只留下」listExample裡的“學期”物件值。
get2  # listExample 只留下 學期元素,是個只有一個元素的list
str(get2)

4.1.3 連鎖選取

numVector <- c(2, 3, 6, -1, 4, 2, 6)
select1 <- numVector[c(1, 4)]
select1
select2 <- select1[[1]]
select2

# 相當於
numVector[c(1, 4)][[1]]
select1 <- numVector[c(T, T, F, T, F, F, T)]
select1
select2 <- select1[c(1, 4)]
select2

# 相當於
numVector[c(T, T, F, T, F, F, T)][c(1, 4)]
majors <- c("經濟學系", "經濟學系", "社會學系", "社工系", "經濟學系")
names <- c("小英", "小美", "大雄", "阿華", "阿尼")
gender <- c("F", "F", "M", "F", "M")
  • 創造出只要“經濟學系”學生的邏輯向量,econOnly。
  • 選出econOnly的names與gender。
  • 在econOnly的gender下創造出只要“F”的邏輯向量, econFemaleOnly。
  • 選出names中為“經濟學系”且“F”的姓名。

在前面討論使用$及[ ]取一個元素時我們創造了get1與get2兩個物件,請分別由get1, get2取出108-1學期個體經濟學教師姓名。


list應用

今明兩日台北市、新北市氣溫:

today <- list(list("台北市", c(highest_temp = 25), c(lowest_temp = 18)), list("新北市", 
    c(highest_temp = 24), c(lowest_temp = 15)))
tomorrow <- list(list("台北市", c(highest_temp = 25), c(lowest_temp = 18)), list("新北市", 
    c(highest_temp = 24), c(lowest_temp = 15)))

weather <- list(today, tomorrow)

print(weather)

選出新北市今明最高溫

weather[[1]][[2]][[2]]  # 今天/新北市/highest_temp
weather[[2]][[2]][[2]]  # 明天/新北市/highest_temp

以下選法行得通嗎?

weather[[1]][[2]]$highest_temp  # 今天/新北市/highest_temp
weather[[2]][[2]]$highest_temp  # 明天/新北市/highest_temp

如果不行,請把weather修改成可以那樣選。

4.2 新增/替換/刪除元素

元素提取[.],[[.]],$.也可用來進行元素「值」的修改與新增, 使用方法為:

obj[.] <- value
obj[[.]] <- value
obj$. <- value
  • .所指定的元素不存在,則為「新增」。

  • .所指定的元素存在,則為「修改」。

範例

a <- c("1", "b", "TRUE")
a
a[[2]] <- "c"  # 元素存在: 替換
a[[4]] <- "7"  # 元素不存在: 增加
a[c(5, 6)] <- c("J", "K")
a
  • 增加一個“Johnson”使用[[.]]<-

  • 使用前一章的向量疊代(recursive vector concatenate)法,新增一個“Mary”。

library(lubridate)
list1 <- list(list(name = "Jack", birthday = ymd("1998-03-21"), status = c(height = 177, 
    weight = 80)))

str(list1)
# 更改日期
list1[[1]]$birthday <- ymd("1997-03-21")

# 新增資料
list1[[2]] <- list(name = "Mary", birthday = ymd("1998-08-24"))

str(list1)
  • 替Mary依Jack的記錄方式增加身高163,體重45。

  • 將Jack的身高改成176。



使用[.] <-

由於[.]會保留母層結構,所以<-右側要使用和母層相同的型態設定:

  • 母層若為list,則需用list(...)方式增加。

  • 母層若為atomic vector,則用c(...)方式增加。

list1[[1]][["age"]] <- 21
list1[[2]]["age"] <- list(21)

# 改變「一個」時,使用[[ ]]比較不會錯。
str(list1)
list1[[1]][c("bloodType", "registered")] <- list("AB", TRUE)

str(list1)

進行以下任務:

  • 108-1新增一個“產業經濟學”。

  • 產業經濟學,同時加上教師Wen及成績88。


刪除可以使用[- c(數字位置)]

  • 只能「一個」中括號([[.]]不能接受負數值)
  • 只能用負數值,不能用元素名稱。
print(a)
a[-c(1, 3)]
a[c(-2)]

print(a)
a  # 要回存才算真的刪除
 <- a[-c(1, 3)]
自先前
  • 刪除Jack的status.

  • 刪除Mary的status, blookType.

list元素要刪除時也可以用[.]<-NULL, [[.]]<-NULL

str(list1)
list1[[2]][c(1, 2)] <- NULL
str(list1)

以下資料來自 新北市公共自行車租賃系統(YouBike)

library(jsonlite)
newTaipeiYouBike <- fromJSON("https://data.ntpc.gov.tw/api/datasets/71CD1490-A2DF-4198-BEF1-318479775E8A/json", 
    simplifyDataFrame = F)

請自行對它做內容更動練習。

4.3 On numeric class

加、減、乘、除: +, -, *, /

a <- c(2, 3, 5)
b <- c(4, -1, 3)
a + b
a - b
a * b
a/b

餘數:%%

次方:**^

a%%b
# 相當於
c(2%%4, 3%%(-1), 5%%3)

a^b
奇、偶數判斷
sequenceNums <- c(11, 6, 8, 11, 12, 11, 3, 7, 10, 8)
print(sequenceNums)

sequenceNums%%2  # 餘數為1則是奇數,0則是偶數

在多數時候R向量間的運算都是elementwise(個別元素)的運算:

  • 所有向量一一取出各自對應相同位置的元素值進行運算。
# a+b 即等於
c(2 + 4, 3 + (-1), 5 + 3)
# a**b 即等於
c(2^4, 3^(-1), 5^3)

當向量間不等長度時,R則對短的向量進行Recycle的動作(即Python的Broadcast):

  • 將其中較短的向量反複堆疊到可以長得跟最長的向量一樣長度
5 * c(1, 3, 4) + 7
# 其實是
c(5) * c(1, 3, 4) + c(7)

## 對向量5,向量7進行recycle:
c(5, 5, 5) * c(1, 3, 4) + c(7, 7, 7)
## Recycle等長後才進行elementwise operation:
c(5 * 1 + 7, 5 * 3 + 7, 5 * 4 + 7)

當運算的兩物件內容長度不同時,則將其中較短的一個反複堆疊到可以長得跟另一個一樣高時才來運算,稱為recycling。

# 狀況一: 堆疊一定倍數剛好一樣長
c(2, 3)/c(-2, -13, 10, 22)
c(2, 3, 2, 3)/c(-2, -13, 10, 22)
# 狀況二: 倍數堆疊一定會超過,只好截斷
c(2, 3)/c(-2, -13, 10)
c(2, 3, 2)/c(-2, -13, 10)

Recycling不只用在數值class, 只要向量間的處理要等長度才合理時,recycling通常也會用在其他的class。

paste0(c("我叫"), c("小明", "小美"))

也等於是

paste0(c("我叫", "我叫"), c("小明", "小美"))
paste0(c("他叫", "我叫"), c("小明", "小美", "大雄"))

會出現什麼?

4.4 Relational Operators

這節在介紹產生「要/不要」向量的常見手法。

example <- list(name = c("小明", "小花", "小新", "美美"), height = c(175, 
    166, 170, 160), weight = c(77, NA, 60, 43), birthday = lubridate::ymd(c("1998-03-11", 
    "1999-12-22", "1995-08-22", "2001-10-10")), hobby = c("美食 旅遊", "旅遊 時尚", 
    "3C 美食", "音樂 旅遊"), residence = c("Taipei", "New Taipei", "Taichung", 
    "Kaohsiung"), allowance = factor(c("0-1000", "1001-2000", "2000+", "1001-2000")), 
    bloodType = c("A", "B", "B", "O"))

4.4.1 比較

>,<,<=,>=: 分別為大於、小於、小於等於、大於等於

  • 數字比較

  • 時間比較

  • 可排序類別資料比較


數字比較
example裡誰的身高大於等於170
example$name[c(T, F, T, F)]
str(example[c("name", "height")])

pick_above170 <- example$height >= 170
example$height
c(175, 166, 170, 160) >= 170

example$name[pick_above170]
不同屆入學學生在2年級的學業表現
source("https://www.dropbox.com/s/qsrw069n94k61lj/transcript100to103_list.R?dl=1")
str(transcript100to103)

分析情境:

# 各學屆2年級人數
table(transcript100to103$學屆)
# 各學屆2年級成績大於85年數
table(transcript100to103$學屆[pick_above85])
選成績大於85分
# 只要成績大於85的
pick_above85 <- transcript100to103$成績 > 85
# 各學屆2年級人數
table(transcript100to103$學屆)
# 各學屆2年級成績大於85人數
table(transcript100to103$學屆[pick_above85])

時間比較:
example裡誰1998年(含)以後出生
example$birthday
class(example$birthday)
typeof(example$birthday)
print(example[c("name", "birthday")])

pick_after98 <- example$birthday >= lubridate::ymd("1998-01-01")
example$name[pick_after98]
美元匯率
source("https://www.dropbox.com/s/16h6ggua6qtydci/exchangeRate.R?dl=1")
str(exchangeRate)
## List of 3
##  $ 期間: Date[1:3525], format: "1960-01-01" "1960-02-01" ...
##  $ 幣別: chr [1:3525] "歐元USD/EUR" "歐元USD/EUR" "歐元USD/EUR" "歐元USD/EUR" ...
##  $ 匯率: num [1:3525] NA NA NA NA NA NA NA NA NA NA ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   期間 = col_date(format = ""),
##   ..   幣別 = col_character(),
##   ..   匯率 = col_double()
##   .. )

情境:

exchangeRate_after98 <-
  list(
    `期間`=exchangeRate$`期間`[pick_after98_01],
    `幣別`=exchangeRate$`幣別`[pick_after98_01],
    `匯率`=exchangeRate$`匯率`[pick_after98_01]
    
  )
選1998年1月(含)以後的匯率
# 只要1998年1月(含)以後的
library(lubridate)
pick_after98_01 <- exchangeRate$期間 >= ymd("1998-01-01")
選出1998年1月(含)以後的匯率資料
exchangeRate_after98 <- list(期間 = exchangeRate$期間[pick_after98_01], 幣別 = exchangeRate$幣別[pick_after98_01], 
    匯率 = exchangeRate$匯率[pick_after98_01])

可排序類別資料比較:
example裡誰零用錢大於1000:
print(example[c("name", "allowance")])

pick_allowanceOver1000 <- example$allowance >= "1001-2000"
example$name[pick_allowanceOver1000]

factor資料可進一步分成可排序,與不可排序的,如:
* 可排序: 以年齡層區分的類別,以所得級距區分的類別等。
* 不排序: 性別,學系等。

factor的設定在不調整時內定為不可排序資料,如要改成可排序類別資料,以先前已處理好的example$allowance 為例:

example$allowance <- ordered(example$allowance)

或在設定為factor時即把levels排好,並ordered=T:

example$allowance <- factor(example$allowance, levels = c("0-1000", "1001-2000", 
    "2000+"), ordered = T  # 設定為可排序factor
)
pick_allowanceOver1000 <- example$allowance >= "1001-2000"
example$name[pick_allowanceOver1000]
刑事案件被害者人數

https://data.gov.tw/dataset/36240

list_victimAges_female <- jsonlite::fromJSON("https://www.dropbox.com/s/3uijub7xheib405/list_victimAges_female.json?dl=1", 
    simplifyDataFrame = F)
str(list_victimAges_female)
View(list_victimAges_female$數目)
sum(list_victimAges_female$數目)
sum(list_victimAges_female$數目, na.rm = T)
請將list_victimAges_female各元素的class做合理設定。
list_victimAges_female$數目 <- as.integer(list_victimAges_female$數目)
list_victimAges_female$年齡層 <- as.factor(list_victimAges_female$年齡層)
levels_ages <- levels(list_victimAges_female$年齡層)
print(levels_ages)
將levels順序改成: 不詳,總計,0_5歲,12_17歲,…,70歲以上。
levels_new <- c(levels_ages[c(12, 13, 1, 8, 2:7, 9:11)])
levels(list_victimAges_female$年齡層) <- levels_new

情境

sum(list_victimAges_female$數目, na.rm = T)
sum(list_victimAges_female$數目[pick_above30], na.rm = T)
可選出「30_39歲以上受害者」的「要/不要」向量:
# 先將類別資料設定成可排序類別資料
list_victimAges_female$年齡層 <- ordered(list_victimAges_female$年齡層)
pick_above30 <- list_victimAges_female$年齡層 >= "30_39歲"
選出「30_39歲以上受害者」的數目
sum(list_victimAges_female$數目, na.rm = T)
sum(list_victimAges_female$數目[pick_above30], na.rm = T)

4.4.2 相等,屬於

==: 等於

!=: 不等於

==!=可使用於字串

example裡誰血型B型
print(example[c("name", "bloodType")])

pick_bloodB <- example$bloodType == "B"
example$name[pick_bloodB]
sequenceNums <- c(11, 6, 8, 11, 12, 11, 3, 7, 10, 8)

pick_evens <- (sequenceNums%%2) == 0
sequenceNums[pick_evens]

創造可留下偶數的「要/不要」向量pick_evens。


還有一個常用的關聯運算:

%in%: 屬於

  • 左邊元素「一一」檢視是不是屬於右邊元素集合。
x <- c(1, 5, 8)
y <- c(5, 8)

# x裡的元素值是不是屬於y集合
x %in% y
example裡誰來自大台北
print(example[c("name", "residence")])

set_TaipeiMetro <- c("Taipei", "New Taipei")
pick_fromTaipeiMetro <- example$residence %in% set_TaipeiMetro
example$name[pick_fromTaipeiMetro]
str(transcript100to103)

創造 可選出來自法商學院的「要/不要」向量,pick_lawBusiness。

4.4.3 Negation(否定用法)

在「要/不要」向量前加上!會成為否定句的「要/不要」向量,元素裡的TRUE會轉成FALSE, FALSE則轉成TRUE。

pick_not_fromTaipeiMetro <- !pick_fromTaipeiMetro
# 或
pick_not_fromTaipeiMetro <- !(example$residence %in% set_TaipeiMetro)  # (..) 裡面會先運算完才做外面!的處理
print(example[c("name", "residence")])

example$name[pick_fromTaipeiMetro]
example$name[pick_not_fromTaipeiMetro]

4.4.4 資料狀態

  • is.na: 有缺失

  • is.{class/type name}: is.integer, is.character, is.factor … etc

有時資料有缺失,在R裡會記成NA(即not available)如下例:

x2 <- c(1, NA, 10)
y2 <- c(-1, NA, 20)

x3 <- c(NA, "小花")
y3 <- c(NA, "小華")

前述的關係判斷遇到NA時,結果都會是NA——即無法判斷。要知道向量內各元素值是否NA,可使用is.na():

x2
is.na(x2)

缺失資料的判斷一定要用is.na不能用==NA來判斷:

a <- c(22, NA, 18)
a == NA
is.na(a)
example裡誰沒有體重資料
print(example[c("name", "weight")])

pick_na <- is.na(example$weight)
example$name[pick_na]

R還有一種特別的缺失資料NaN (即not a number),出現在沒有定義的數學運算上,如:

0/0
list_victimAges_female$數目 <- as.integer(list_victimAges_female$數目)
創立 可選出缺失資料的「要/不要」向量pick_na, 並計算有多少筆缺失。
pick_na <- is.na(list_victimAges_female$數目)
total_na <- sum(pick_na)
print(total_na)

4.4.5 字元偵測

  • stringr::str_detect()
example裡誰喜歡美食
print(example[c("name", "hobby")])

pick_loveFood <- stringr::str_detect(example$hobby, "美食")
example$name[pick_loveFood]

常有同學會有以下錯誤寫法:

pick_loveFood <- example$hobby == "美食"
pick_loveFood

新增一個假設的hobby2:

example[["hobby2"]] <- c("美食", "時尚", "美食", "旅遊")

print(example[c("name", "hobby2")])
pick_loveFood2 <- example$hobby2 == "美食"
example$name[pick_loveFood2]
  • == 字串內容一模一樣。

  • str_detect 字串內容有關鍵字。

EDC3AD26-8AE7-4916-A00B-BC6048D19BF8
以下資料為新北市垃圾車路線

garbageRoutes <- jsonlite::fromJSON("https://data.ntpc.gov.tw/api/datasets/EDC3AD26-8AE7-4916-A00B-BC6048D19BF8/json")
# 1 用typeof()函數查詢電腦實質上如何看待garbageRoutes。
typeof(garbageRoutes)
# 2 用class()函數查詢電腦把它能進行的操作運算歸屬於哪一類型。
class(garbageRoutes)

由於garbageRoutes的本質是list,所以我們可以使用list所有操作手法,而class為data frame表示它有比典型list的運作多了些工具與變化(後面章節會提)。

# 由linename元素判斷垃圾車有幾條路線。
garbageRoutes$linename <- factor(garbageRoutes$linename)
levels(garbageRoutes$linename)

# 由linename創造: 可篩選出下午路線的「要/不要」向量pick_afternoonRoutes。
pick_afternoonRoutes <- stringr::str_detect(garbageRoutes$linename, "下午")
garbageRoutes$linename[pick_afternoonRoutes]

4.4.5.1 閱讀函數說明

`?`(str_detect)
  • Title, Description, Usage, Arguments, Value, Examples
Title

Detect the presence or absence of a pattern in a string.

Description

Vectorised over string and pattern. Equivalent to grepl(pattern, x). See str_which() for an equivalent to grep(pattern, x).

Usage
str_detect(string, pattern, negate = FALSE)
Arguments

string:
input character vector. Either a character vector, or something coercible to one.

pattern:
Pattern to look for. The default interpretation is a regular expression, as described in stringi::stringi-search-regex. Control options with regex(). Match a fixed string (i.e. by comparing only bytes), using fixed(). This is fast, but approximate. Generally, for matching human text, you’ll want coll() which respects character matching rules for the specified locale. Match character, word, line and sentence boundaries with boundary(). An empty pattern, "“, is equivalent to boundary(”character").

negate:
If TRUE, return non-matching elements.

Value

A logical vector.

Examples
fruit <- c("apple", "banana", "pear", "pinapple")
str_detect(fruit, "a")
str_detect(fruit, "^a")
str_detect(fruit, "a$")
str_detect(fruit, "b")
str_detect(fruit, "[aeiou]")

# Also vectorised over pattern
str_detect("aecfg", letters)

# Returns TRUE if the pattern do NOT match
str_detect(fruit, "^p", negate = TRUE)

疾病管制署傳染病答問集

CDC_chatbox <- readr::read_csv("http://od.cdc.gov.tw/pr/CDC_chatbox.csv")
找出問題中包含“肺炎”字眼的問題。
pick_pneumonia <- stringr::str_detect(CDC_chatbox$Question, "肺炎")
CDC_chatbox$Question[pick_pneumonia]

4.5 On logical class

邏輯向量間(即「要/不要」向量)的操作主要是以下幾個:

  • &: AND

  • |: OR

str(example)
pick_above170 <- example$height >= 170
pick_bloodB <- example$bloodType == "B"
pick_loveFood <- stringr::str_detect(example$hobby, "美食")
pick_na <- is.na(example$weight)
pick_after98 <- example$birthday >= lubridate::ymd("1998-01-01")
set_TaipeiMetro <- c("Taipei", "New Taipei")
pick_fromTaipeiMetro <- example$residence %in% set_TaipeiMetro
誰喜歡美食且血型為B型
str(example[c("name", "hobby", "bloodType")])

example$name[pick_loveFood & pick_bloodB]
誰1998以後(含)出生或不住大台北
example[c("name", "birthday", "residence")]

example$name[pick_after98 | !pick_fromTaipeiMetro]
誰1998以後(含)出生且住大台北且血型B型
example$name[pick_after98 & pick_fromTaipeiMetro & pick_bloodB]

以上邏輯向量間的運算結果還是邏輯向量——依然是「要/不要」向量,所以還是可以用來取出元素。但有時我們要做的運算是希望得到一個T/F元素值,而非一串元素值,如:

  • 是不是所有的人都來自大台北?

  • 有任何人喜歡美食嗎?

這種運算我們留到流程控制時再講。

4.6 On character class

這節使用stringr套件,同時也有一小部份用到glue套件,請先下載安裝。

library(stringr)
library(glue)

對character vector每個元素進行部份資訊粹取:

  • 學號“410873002”,其中108是入學年,73是學系碼。 對100學年以後學生學系碼是固定在「第5、6位元」。
str_sub("410873002", 5, 6)
  • 地址幾號: “大學號151號”,只知道號碼在「號」這字前面,但它會是第幾個位元不知道。

    • 使用Regular Expression (regex, 正規表示式)

如何讀入“民國108年12月5日”、“民國98年10月5日”?

lubridate::ymd(c("民國108年12月5日", "民國98年10月5日"))
  • lubridate對年只有西元年的概念。

要能想辦法把上面的“108”,“98”取出加上1911。

lubridate裡的字串日期時間判斷採取的策略是「忽略非數字文字」只以「數字組合」(還允許各地習慣使用的文字,如March, Mar都是3月的意思)判斷時間,所以以下幾個都可以產生「西元」“2019-12-05”:

lubridate::ymd(c("民國2019年12月05日", "馬丁2019的12課05muydifícil", "助教2019Emily12解說05真棒", 
    "2019R12課程05すごい", "R程式2019的12期中考05とても変態"))

然而即使忽略非數字文字,三個數字的組合誰是年?月?日?依然受每個國家日期書寫習慣影響,所以有時還是可能會有誤判。

4.6.1 Regular expression(regex)

請自RStudio Cheatsheets下載「Work with Strings Cheatsheet」pdf檔

一種廣義描述所要字串規律的表示法,分成兩部份:

  • target pattern: 指對取出字串的描述。如“108”,“98”是我們想取出的字串。我們可以說目標是,
    • 「0-9的字元2-3個」: [:digit:]{2,3}[0-9]{2,3}
  • target location description:
    指target位置的情境描述。如“民國108年”,“民國98年”,我們可以說,
    • target前面是「民國」後面是「年」: (?<=民國)target_pattern(?=年)
  • 合成regex:(?<=民國)[0-9]{2,3}(?=年)
str_extract_all(c("民國108年12月5日", "民國98年10月5日"), "(?<=民國)[0-9]{2,3}(?=年)")  # 回傳list 

str_extract(c("民國108年12月5日", "民國98年10月5日"), "(?<=民國)[0-9]{2,3}(?=年)")  # 回傳vector 

參見RStudio stringr cheat sheet:

4.6.2 組合alternate

用來製做一台吃角子老虎,它能夠包含你要的所有可能target出象。吃角子老虎由數條reel strip組成,每一條strip可以有以下幾種設計

  • 固定字元/組:單寫一個字組或字元,表示那個strip固定不動只有一個出象,例如:2表示只有“2”一個出象,櫻桃只有“櫻桃”一個出象。
heartSutra <- c("舍利子,色不異空,空不異色;色即是空,空即是色。")
str_view_all(heartSutra, "色")  # 檢視
str_count(heartSutra, "色")  # 計數
  • 字「群」組合(..|..|..): strip上的出象會出現一組字時用,如“AB|ab”, 可以產生“AB”或“ab”兩種出象。

    • 字群組合規則若很單純前後沒有接其他描述則( )可不寫
studentIds <- c("410873029", "410773005", "410772035", "410562123")
# 取出學號中的入學屆為107或108
str_view_all(studentIds, "(107|108)")

str_view_all(studentIds, "107|108")  # 可不寫()

# 是否來自107或108學屆
str_detect(studentIds, "107|108")
  • 字「元」組合[]: strip上的出象「均」是一個字「元」時用,如[af78]可以產生“a”,“f”,“7”,“8”四種字元出象。
    • 幾種常見的字元模版:[0-9](或[:digit:]),[a-z](或[:lower:]),[A-Z](或[:upper:]),[0-9a-zA-Z](或[:alnum:])
Ids <- c("A283743192", "B829103720", "A10920402", "C291022384")

str_view_all(Ids, "[AC]")
str_detect(Ids, "[AC]")  # 偵測 出現A、C

str_extract(Ids, "[A-Z]")  # 取出 A-Z的字元
  • 將設定好的reel strips依你要的順序排好就是你的regex slot machine, 它可以用來驗證字串裡頭是否出現來自你regex slot machine的某些組合。
strip1 <- "[0-9]"
strip2 <- "(櫻桃|777|紅心)"  # 字群組合strip因為等下放中間所以用()括起來
strip3 <- "[A-Z]"
myRegexSlotMachine <- paste0(strip1, strip2, strip3)
print(myRegexSlotMachine)
claimA <- "我得到A檸檬99"
claimB <- "我的是7777Z"
claimC <- "我玩了兩次,一次是7蘋果H,另一次是5紅心J"
# 顯示吻合的出象
str_view(c(claimA, claimB, claimC), pattern = myRegexSlotMachine)
# 是否有吻合的出象
str_detect(c(claimA, claimB, claimC), pattern = myRegexSlotMachine)
  • 以否定法定義的字「元」組合[^ ]: “[^趙錢孫李]”不能有趙錢孫李任何一個

範例

.wrap {
  word-wrap: break-word;
}
headline <- "資科系的謝陳官乃飛鳶書院三大名師,其中謝無雙的策略運算,陳大器的數理資科學,及官求敗的運算成本更是打通演算思維任督二脈的三大好課。書院目前有陳一、陳二、陳三四這三名學生。"
# 顯示所有符合規則的
str_view_all(headline, "謝陳官")  # 固定字組
str_count(headline, "謝陳官")

str_view_all(headline, "[謝陳官]")  # 有1個strip: 出象有3個可能字元 
str_count(headline, "[謝陳官]")

str_view_all(headline, "謝無雙|官求敗")  # 有1個strip,出象有2個字組 
str_count(headline, "謝無雙|官求敗")

str_view_all(headline, "陳[^官]")  # 固定字元+有1個strip: 出象為排官的字元
str_count(headline, "陳[^官]")

請使用str_extract函數粹取出每個地址的“xx號”包含“號”字。

addresses <- c("臺中市后里區公館里尾社路385號", "新竹市東區科學園路125號1樓")
strip1 <- strip2 <- strip3 <- "[0-9]"
strip4 <- "號"
myRegexSlotMachine <- paste0(strip1, strip2, strip3, strip4)
print(myRegexSlotMachine)
str_extract(addresses, myRegexSlotMachine)

4.6.3 複製次數Quantifiers:

(..|..|...)[...]只是一條reel strip, 前者是寬的(每個出象可以是多字元)後者是窄的(每個出象只是一個字元)。有時我們同樣的strip想連放很多條,可以在(..|..|...)[...]後面加:

  • {n}: 放n條strip,n是數字。
  • +: 放1條或以上(多少不限)。
  • {n,m}: 放n到m條strip,n及m都是數字。

將以下地址的號數取出(含“號”字)

addresses <- c("臺中市后里區公館里尾社路385號", "新竹市東區科學園路125號1樓", 
    "新竹市北區竹光路99號", "金門縣金城鎮賢庵里山前33號", "臺中市南屯區工業區二十四路23號1樓")
myRegexSlotMachine <- "[0-9]+號"
print(myRegexSlotMachine)
str_view_all(addresses, myRegexSlotMachine)
str_extract(addresses, myRegexSlotMachine)

str_view_all(addresses, "[0-9]+")  # 樓號也會選到

了解如何regex形式的描述你的target後,接下來我們進一步學習如何更準確描述target在整個字串的位置。

4.6.4 頭尾定位 Anchors:

  • 在開頭:^target_regex

  • 在結尾:target_regex$

phrase <- c("唧唧复唧唧")
# 目標是前面的唧唧
target_regex <- "唧唧"
pattern <- glue::glue("^{target_regex}")
print(pattern)
str_view(phrase, pattern)

glue套件裡的glue函數會把字串中的{物值名稱}換成目前Environment裡該物件的值。在上面{target_regex}會被換成“唧唧”。

# 目標是後面的唧唧
target_regex2 <- "唧唧"
pattern <- glue::glue("{target_regex2}$")
print(pattern)
str_view(phrase, pattern)

4.6.5 前後鄰居描述Look around:

  • target後面是B:target_regex(?=B_regex): target_pattern後面是年

  • target前面是A:(?<=A_regex)target_regex:target_pattern前面是民國

  • target前有A後有B:(?<=A_regex)target_regex(?=B_regex)

Cheat sheet上還有更多字串處理情境。

民國年月日存成date class:

twDate <-  c("民國108年12月5日","民國98年10月5日")

library(stringr)
# 準備regex: 
## 取出:「前有」民國,「後有」年的「數字」「們」
## "(?<={A_regex}){target_regex}(?={B_regex})"

target_regex <- "[0-9]+"
A_regex <- "民國"
B_regex <- "年"

regex_pattern <- glue::glue(
  "(?<={A_regex}){target_regex}(?={B_regex})"
  )

print(regex_pattern)

## 如果同學已經很熟就可以直接寫
regex_pattern <-
  "(?<=民國)[0-9]+(?=年)"

# 取出民國年,計算西元年
year <- 
  str_extract(
    twDate,
    regex_pattern)
westernYear <- as.integer(year)+1911

# 替換民國xx年的xx成西元年數字
str_replace(
  twDate,
  regex_pattern,  # 要換掉的文字
  as.character(westernYear) # 要替換的內容
) -> twDate_reformat
print(twDate_reformat)


lubridate::ymd(twDate_reformat)

康熙元年是西元1662年,請將“康熙23年5月6日”轉成date class。(假設月日已是西曆月日)

之前我們使用str_extract函數粹取出以下地址的“xx號”包含“號”字,如果不要取到「號」字只取出數字,你會怎麼做?

addresses <- c("臺中市后里區公館里尾社路385號", "新竹市東區科學園路125號1樓")
str_extract(addresses, "[0-9]+(?=號)")

4.6.6 字元模版

  • [:graph:] 包山包海,還包中文字元。

  • [\u4E00-\u9FFF] 代表所有中日韓文字字元。

str_extract_all("我的名字8293叫17380小abce明", "[一-\u9fff]+")

4.6.7 綜合練習

學期初在輸入Github inclass-practice-repo網址時,它要長得像

https://github.com/...../108-2-XX-inclass-practice

其中XX只可以是56或78,以下是課程資訊表單學生填答的可能形式,請寫下正確格式的regex,並用str_detect判斷那幾個人沒有正確填寫:

studentGithubURLs <- c("https://github.com/student1/108-2-78-inclass-practice", "github.com/student2 / 108-2-56-inclass-practice", 
    "https://github.com/student3/108-2-56-inclass-practice", "student4 / 108-2-56-inclass-practice", 
    "student5")
target_regex <- "^https://github.com/[:graph:]+/108-2-(56|78)-inclass-practice"
str_detect(studentGithubURLs, target_regex)

[難度5星] 期中考檔案命名出現以下幾種寫法:

midtermFilename <- c("exam1-410773888.rmd", "exam1410882888.Rmd", "410682999第一次期中考.Rmd", 
    "期中考310573888.R")

請「只」以str_extract完成以下任務:
1. 取出9位數的學號(4或3開頭的數字)。
2. 系號是學號的第5-6兩位數字, 如410773082的系號是73。
3. 如果多了兩位99年入學但休學多年的學生(學號只有8位數49975013及49977012,系號是第4-5位數),他們的檔案名稱分別是“exam149975013.Rmd”,“499770121stExam.Rmd”,執行以下程序更新midterFilename:

midtermFilename <- c(midtermFilename, "exam149975013.Rmd", "499770121stExam.Rmd")
你的前兩題答案會不會成功?若不會,要怎麼修正。
# 1.
pattern <- "[43][0-9]{8}"
str_extract(midtermFilename, pattern)

# 2.
target_regex <- "[0-9]{2}"
A_regex <- "[43](107|108|106|105)"
pattern <- glue::glue("(?<={A_regex}){target_regex}")
str_extract(midtermFilename, pattern)

# 3.
midtermFilename <- c(midtermFilename, "exam149975013.Rmd", "499770121stExam.Rmd")
# 3.1
pattern <- "[43](99|107|108|106|105)[0-9]{5}"
str_extract(midtermFilename, pattern)

# 3.2 target pattern: 2位數字
target_regex <- "[0-9]{2}"
## target location: 前面鄰居 4或3,接99,105-108
A_regex <- "[43](99|107|108|106|105)"
pattern <- glue::glue("(?<={A_regex}){target_regex}")

pattern
str_extract(midtermFilename, pattern)

4.7 On factor class

  • 內定levels順序是根據電腦內字元排序(collating sequence)依辭典序列方式決定排序,很多時候沒有意義。
char1 <- c("Wild", "Apple", "Donald", "May", "Avocada")
factor1 <- factor(char1)
levels(factor1)
char2 <- c("蔡英文", "習近平", "Donald Trump", "蔡頭")
factor2 <- factor(char2)
levels(factor2)

sort(str_sub(char2, 1, 1))
sort(str_sub(char2, 2, 2))

查詢你的collating sequence設定:

Sys.getlocale("LC_COLLATE")

由於不同電腦、不同作業系統的collating sequence不同,如果levels順序在你的分析是重要的,一定要在設定factor時自行設定好。


嚴重特殊傳染性肺炎

covid19 <- jsonlite::fromJSON("https://od.cdc.gov.tw/eic/Day_Confirmation_Age_County_Gender_19CoV.json")

# 不可排序類別:性別、縣市、是否境外移入
covid19$縣市 <- factor(covid19$縣市)
covid19$性別 <- factor(covid19$性別)
covid19$是否為境外移入 <- factor(covid19$是否為境外移入)
# 可排序類別資料:年齡層
covid19$年齡層 <- factor(covid19$年齡層, ordered = TRUE)
levels(covid19$縣市)
levels(covid19$性別)
levels(covid19$是否為境外移入)
covid19$年齡層
levels(covid19$年齡層)

4.7.1 levels重新排列

重新再定義factor一次

factor(目前的factor向量, levels = 自訂排序)
  • levels放第二格時, levels= 可省略。

  • 目前的類別向量的可不可排序性質依然會保留,不用特意去設ordered=T/F

使用函數時,若argument input的位置就是原help說明裡定義該argument位置時,「argument_name =」部份可以不寫。


手動輸入排法:由女男改男女

levels(covid19$性別)
covid19$性別 <- factor(covid19$性別, c("男", "女"))

依與levels相關的其他數據排列:

  • covid19$縣市的levels依其人口由大排到小。如“新北市”人口最多,它就要排levels的第一個,依此類推。

演算思維:

  1. 下載台灣各縣市人口資料:
population <- jsonlite::fromJSON("https://www.dropbox.com/s/jckqryeh5zeat5w/regionPopulation.json?dl=1")
population <- unlist(population)

資料科學心法一:拿到新的資料先檢視「內容」、「class/type」

print(population)
class(population)  # 判斷出是named integer vector
names(population)  # 元素名稱是台灣各縣市
  1. 留下levels中有的popluation元素
levels(covid19$縣市)  # 是名稱字串向量

# 由於population元素值有names,可以用`[名稱字串向量]`取出所要的
levelsInData <- levels(covid19$縣市)
population <- population[levelsInData]
  1. 將population由大排到小(sort(x, decreasing=T)可將x numeric vector由大排到小。)
population <- sort(population, decreasing = T)
population

依數據重排類別小技巧,善用named vector:

  1. 排完後population第一個元素值最大,它的名稱即是人口最多的縣市,第二元素值次大,它名稱是人口第二多縣市,依此類推。我們要的是population排完後的元素名稱。
newLevels <- names(population)
newLevels
  1. 重設levels排序
covid19$縣市 <- factor(covid19$縣市, levels = newLevels)

levels(covid19$縣市)

將covid19$縣市 依縣市目前個案數目由大排到小。(hint: 可使用table()來得到個案數統計, 它會自然產生named integer vector)


年齡應該用年齡區間起始數字排序

levels(covid19$年齡層)
  1. 取出levels中各年齡層的起始數字,存在startingAges
level_names <- levels(covid19$年齡層)
startingAges <- stringr::str_extract(level_names, "^[0-9]+"  # regex:開頭的一串數字
)
  1. 將startingAges變成named integer vector
# 要numerical class排的才一定對
startingAges <- as.integer(startingAges)
names(startingAges) <- level_names
  1. 將startingAges從小排到大。
startingAges <- sort(startingAges)
  1. 重設levels排序
covid19$年齡層 <- factor(covid19$年齡層, names(startingAges))
levels(covid19$年齡層)

4.7.2 levels改名

levels(x) <- 

在R裡,若你看到函數f說明在Usage同時寫上f() <-的用法時,它表示此函數除了一般用來產生結果以外,也可以用來設定結果。

  • levels: 一般用來顯示類別

  • levels <- : 用來設定類別

其他常用到設定用法的還有names(),attr()等等

factor1 <- factor(c("A", "A", "C", "A", "D", "F", "D", "D"))
factor1

levels(factor1) <- c("優", "佳", "尚可", "普")

factor1

也可改levels其中幾個利用元素取代概念

levels(factor1)[[1]] <- "Excellent"
levels(factor1)[c(3, 4)] <- c("C", "D")
factor1

由於factor內容的呈現是依循levels對照表走,所以levels內容一變,factor的內容呈現也變了。

4.7.3 levels整併

  • 原本levels是台灣各縣市,你想改成北/中/南/東部四類。

  • 原本年齡層levels是每5歲一個間距,你想改成10歲一個間距。

範例一:

factor1 <- factor(c("新北市", "台北市", "台中市", "彰化市", "基隆市", 
    "苗栗縣", "苗栗縣"))
factor1
levels(factor1) <- c("中部", "北部", "北部", "中部", "北部", "中部")
factor1

範例二:

factor1 <- factor(c("0-4歲", "5-9歲", "10歲以上", "0-4歲", "5-9歲", "5-9歲", 
    "10歲以上"))
factor1
levels(factor1) <- c("0-9歲", "10歲以上", "0-9歲")
factor1

由covid19$縣市建立一個地區變數,值為北部、中部、南部、東部,其中:

  • 北部:“宜蘭縣、基隆市、台/臺北市、新北市、桃園市、新竹市、新竹縣”

  • 中部:“苗栗縣、台/臺中市、彰化縣、南投縣、雲林縣”

  • 南部:“嘉義市、嘉義縣、台/臺南縣、台/臺南市、高雄市、屏東縣、澎湖縣”

  • 東部:“花蓮縣、台東縣”

covid19$地區 <- covid19$縣市
levels(covid19$地區)
currentLevels <- levels(covid19$地區)

# 整併
north_regex <- "宜蘭縣|基隆市|[台臺]北市|新北市|桃園市|新竹市|新竹縣"
middle_regex <- "苗栗縣|[台臺]中市|彰化縣|南投縣|雲林縣"
south_regex <- "嘉義市|嘉義縣|[台臺]南[縣市]|高雄市|屏東縣|澎湖縣"
east_regex <- "花蓮縣|台東縣"

currentLevels <- str_replace(currentLevels, north_regex, "北部")
currentLevels <- str_replace(currentLevels, middle_regex, "中部")
currentLevels <- str_replace(currentLevels, south_regex, "南部")
newLevels <- str_replace(currentLevels, east_regex, "東部")


levels(covid19$地區) <- newLevels
covid19$地區

將covid19$年齡層,由5歲間距成10歲間距,使改完後的levels為4 < 5-14 < 15-24 < …< 55-64 < 65+

levels(covid19$年齡層)

# 創造10歲間距的所有可能
start <- seq(5, 55, by = 10)  # 由5到55,每個值差10的數值向量
end <- seq(14, 64, by = 10)
middleLevels <- rep(paste0(start, "-", end), each = 2)  # 每個新間距要2個
newLevels <- c("4", middleLevels, "65+", "65+")

levels(covid19$年齡層) <- newLevels
covid19$年齡層

4.7.4 levels擴充

levels <-右邊值的部份包含原本的外,還有想新增的類別。

factor2 <- factor(c("好", "好", "普通", "普通", "好"))
levels(factor2)

加一筆新資料“差”(之前沒有這類)

factor2[[6]] <- "差"
factor2  # 元素6變NA

先擴大levels, 再加新資料

factor2 <- factor(c("好", "好", "普通", "普通", "好"))
levels(factor2)

# 先擴大levels
levels(factor2) <- c(levels(factor2), "差")
levels(factor2)
factor2[[6]] <- "差"
factor2

factor無法用c(...)把兩個factor vector串接在一起:

# non factor
char1 <- c("小明", "小美")
char2 <- c("大雄", "技安")
c(char1, char2)

# factor
factor1 <- factor(c("好", "壞", "壞"))
factor2 <- factor(c("壞", "好", "好"))
c(factor1, factor2)

因為The output type is determined from the highest type of the components in the hierarchy NULL < raw < logical < integer < double < complex < character < list < expression. factor是integer type所以會被當integer後才串接在一起。

正確接法:

factor1[4:6] <- factor2
factor1

或使用套件forcats裡的fct_c函數

forcats::fct_c(factor1, factor2)

c(...)只會回傳logical, integer, double, character, list等可能的class,所以:

factor1 <- factor(c("a", "b", "c"))
c(factor1)
as.integer(factor1)

中的c(factor1)會相當於as.integer(factor1)

4.7.5 綜合練習

1

將covid19$性別的levels名稱男、女改Male、Female。

2

以下資料為新北市垃圾車路線

garbageRoutes <- jsonlite::fromJSON("https://data.ntpc.gov.tw/api/datasets/EDC3AD26-8AE7-4916-A00B-BC6048D19BF8/json")
  1. 在garbageRoutes新增route元素,其元素值為對應garbageRoutes$linename裡粹取出的“XXX路線”字眼,如原本“A11路線(一、四)下午”的linename, 它的route值為“A11路線” 。(garbageRoutes$route為factor class)

  2. 請設定garbageRoutes$route的levels依其路線停靠總站數由小排到大陳列。

  3. linename的“路線”字眼後面接的是營運時間,如“A11路線(一、四)下午”的linename, 它的營運時間為“(一、四)下午”。若沒寫時間的路線,它的營運時間為“全天”。請在garbageRoutes新增time元素,其元素值為對應linename所隱含的營運時間。(garbageRoutes$time為factor class, levels會有“(一、四)下午”, “下午”, “下午(1.4版)”, “晚上”, 及“全天”)

  4. garbageRoutes$time的levels中的“下午(1.4版)”改成“下午”。

4.8 On list

library(purrr)

4.8.1 每個元素相同運算

在之前的example物件
example <- list(name = c("小明", "小花", "小新", "美美"), height = c(175, 
    166, 170, 160), weight = c(77, NA, 60, 43), birthday = lubridate::ymd(c("1998-03-11", 
    "1999-12-22", "1995-08-22", "2001-10-10")), hobby = c("美食 旅遊", "旅遊 時尚", 
    "3C 美食", "音樂 旅遊"), residence = c("Taipei", "New Taipei", "Taichung", 
    "Kaohsiung"), allowance = factor(c("0-1000", "1001-2000", "2000+", "1001-2000")), 
    bloodType = c("A", "B", "B", "O"))

我們有可能想要拿出其中屬於小新的資料:

pick_小新 <- example$name == "小新"
data_selected <- list(name = example$name[pick_小新], weight = example$weight[pick_小新], 
    height = example$height[pick_小新], hobby = example$hobby[pick_小新], residence = example$residence[pick_小新], 
    allowance = example$allowance[pick_小新], bloodType = example$bloodType[pick_小新])

data_selected的產生,相當於進行

example[[1]][pick_小新]
example[[2]][pick_小新]
example[[3]][pick_小新]
example[[4]][pick_小新]
example[[5]][pick_小新]
example[[6]][pick_小新]
example[[7]][pick_小新]

再存放在list()裡。也就是我們想要:

  • example底下的每個元素進行

  • [pick_小新]的動作

purrr底下的map()允許我們

  • 使用.x來代表每個元素,即[[1]], [[2]], ..., [[7]]

所以

  • 對每個元素進行[pick_小新]

  • 可以寫成.x[pick_小新]

由於.x[pick_小新]是個公式化的動作,在R,~代表要形成一個公式(formula),所以

  • 要寫成~ .x[pick_小新]

完整的map()用法為:

map(對象物件, ~對每個元素作的動作)

以一開始的例子為例:

  • example 底下的每個元素進行

  • .x[pick_小新] 的動作

data_selected <- map(example, ~.x[pick_小新])

data_selected

範例:舞蹈表演

舞蹈表演資訊

dance <- jsonlite::fromJSON("https://www.dropbox.com/s/6252gbdnv9owljm/dance.json?dl=1", 
    simplifyDataFrame = F)
# 第一個dance表演名稱
dance[[1]]$title
# 第二個dance表演名稱
dance[[2]]$title

若想得到所有表演的名稱,我們得:

  • 對 dance 每一個元素(.x表示)進行

  • .x$title的動作

list_titles <- map(dance, ~.x$title)

View(list_titles)

每個表演有時不止演出一場,每個表演的場次總數:

# 記載第一個表演的所有場次訊息
dance[[1]]$showInfo
str(dance[[1]]$showInfo)
# 計算第一個表演有幾個場次
length(dance[[1]]$showInfo)
# 計算第二個表演有幾個場次
length(dance[[2]]$showInfo)
  • 對 dance 每個元素進行

  • length(.x$showInfo)的動作

list_numberOfShows <- map(dance, ~length(.x$showInfo))

View(list_numberOfShows)

第4個表演有6個場次,想知道它們的日期時間:

str(dance[[4]]$showInfo)
# 這個表演第1個場次的訊息
dance[[4]]$showInfo[[1]]$time
# 這個表演第2個場次的訊息
dance[[4]]$showInfo[[2]]$time
  • 對 dance[[4]]$showInfo 的每個元素(.x)表示

  • 進行 .x$time 動作

 <- map(dance[[4]]$showInfo, 
    ~.x$time)
list_showTimes_dance4

找出第8個表演的所有場次時間(time)和地點(location):

4.8.2 綜合練習

1.

新北市公車路線清單

busRoutes <- jsonlite::fromJSON("https://www.dropbox.com/s/5nozcipa3rzrrmy/busRoutes.json?dl=1", 
    simplifyDataFrame = F)

請抓出每條路線的Id(路線代碼)、providerId(業者代碼)、providerName(業者中文名稱)、nameZh(中文名稱)。

2.

一般天氣預報-今明36小時天氣預報:

weather_next36hours <- jsonlite::fromJSON("https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-C0032-001?Authorization=rdec-key-123-45678-011121314&format=JSON")

2.1 找出有氣象預報資訊的縣市名稱(locationName欄位)

2.2 氣象局針對每個縣市提供了不同氣象因子在未來三個時段的預報。每個都市的氣象因子預報有那些,以第一個都市為例,它存在

weather_next36hours$cwbopendata$dataset$location$weatherElement[[1]]$elementName

請取出每個縣市有提供的氣象預報因子名稱。(個別名稱的意思要去看欄位說明文件)

2.3 請抓出每縣市在第一個預告時段的

  • 預告開始時間

  • 預告結束時間

  • 這段時間的最高氣溫