緣起
感謝台北大學企業管理學系的游擱嘉助理教授讓我去他的金融數據分析課程給一個短講,修課學生的主修是企業管理學系,在金融數據的處理和分析上是以 R 作為程式語言,顧及一學期的課程長度,所以實踐了 Tidyverse first 的教學理念。Tidyverse first 的教學理念主要的發起點還是在於表格式資料(e.g. Data.Frame、Tibble),優點當然是實戰程度高、應用性強、學習歷程短;缺點就是在不具備 R 語言基本資料結構宏觀的理解下,無法善加利用函數輸出結果。因此這個短講我設定的主題是「如何有效率地使用 R 語言處理表格式資料 Manipulating Tabular Data with R Efficiently」,旨在透過 90 分鐘的時間,讓聽眾快速建立對於 R 語言表格式資料結構的概覽。
什麼是表格式資料
表格式資料是具備列(Rows)與欄(Columns)的資料組織型態,每一列所具備的欄數都相同、每一欄所具備的列數也都相同;每一列的欄順序都相同、每一欄的列順序也都相同,這樣的一種對稱外型,讓我們能夠以 (m, n)
來描述外型為 m
列 n
欄的表格式資料。表格式資料中的每一欄各自是同質資料,但不同欄彼此允許異質資料;列資料通常具有某個特定的順序,但順序並不是必備的表格式資料特性。
R 語言常見的表格式資料結構
以科學計算、統計分析為主體應用的程式語言,例如 R、SAS、Stata、Matlab 或 Python Pandas,都會具備處理表格式的資料結構,R 內生的 data.frame
、Python Pandas 的 DataFrame
或 SAS 的 SAS Data Set
。R 語言的表格式資料結構非常多元,常見的有:
matrix
data.frame
tibble
data.table
xts
其中 matrix
與 data.frame
是 R 語言的內建資料結構,tibble
、data.table
與 xts
則是第三方套件 tibble
、data.table
與 xts
分別提供的資料結構,值得注意的是這些資料結構的類別命名與第三方套件的命名是相同的,要避免混淆。
matrix
是二維的數值向量,可以透過 matrix()
函數建立、也可以更新一維數值向量的維度來建立。
A <- matrix(1:6, nrow = 2)
B <- 1:6
dim(B) <- c(2, 3)
A
B
使用函數檢視 matrix
的類別,matrix
屬於 array
的子類別。
class(A)
is.matrix(A)
is.array(A)
使用函數檢視 matrix
的外型。
length(A)
dim(A)
nrow(A)
ncol(A)
data.frame
是一個由相同長度向量所組合而成的 list
資料結構,是 R 語言最常用來儲存資料的格式。使用函數檢視 data.frame
的類別,data.frame
屬於 list
的子類別。
#install.packages("dplyr")
library("dplyr")
class(dplyr::starwars)
is.data.frame(dplyr::starwars)
is.list(dplyr::starwars)
使用函數檢視 data.frame
的外型。
dim(dplyr::starwars)
nrow(dplyr::starwars)
ncol(dplyr::starwars)
使用函數預覽 data.frame
的內容。
head(dplyr::starwars)
tail(dplyr::starwars)
#View(dplyr::starwars) # Works in RStudio only
使用函數取得 data.frame
的列命名、欄命名。
colnames(dplyr::starwars)
dplyr::starwars |>
rownames() |>
as.numeric()
tibble
是「簡約版」的 data.frame
,提供了更簡潔明瞭的顯示外觀,並且是 tidyverse
套件組的通用資料結構,例如使用 tidyverse
套件組之中 readr
套件的 read_csv()
函數載入 CSV 檔案、使用 tidyverse
套件組之中 dplyr
、tidyr
套件的函數處理資料,都會得到 tibble
的輸出。使用函數檢視 tibble
的類別,tibble
屬於 data.frame
與 list
的子類別。
class(dplyr::starwars)
tibble::is_tibble(dplyr::starwars)
is.data.frame(dplyr::starwars)
is.list(dplyr::starwars)
至於處理 tibble
跟處理 data.frame
完全沒有分別,可以百分百移植所有處理資料框的技巧到 tibble
之上。
data.table
是「大數據版」的 data.frame
,提供了更快速的聚合運算、集合運算以及分組運算,例如使用 data.table
套件的 fread()
函數載入 CSV 檔案就會得到 data.table
的輸出。使用函數檢視 data.table
的類別,data.table
屬於 data.frame
與 list
的子類別。
#install.packages("data.table")
library("data.table")
starwars_dt <- data.table::as.data.table(dplyr::starwars)
class(starwars_dt)
data.table::is.data.table(starwars_dt)
is.data.frame(starwars_dt)
is.list(starwars_dt)
基本檢視、預覽 data.table
的函數與 data.frame
完全沒有分別,不過處理 data.table
的語法以及技巧卻是其自成一格的 DT[i, j, by]
,無法完全移植過往處理資料框的技巧。
xts
是 eXtensible Time Series 的縮寫,專門用來儲存與處理時間序列的表格式資料,例如使用 quantmod
套件的 getSymbols()
函數載入個股資訊就會得到 xts
的輸出。使用函數檢視 xts
的類別,xts
屬於 matrix
的子類別。
#install.packages("quantmod")
library("quantmod")
start_date <- "2022-01-01"
AAPL <- getSymbols(Symbols = "AAPL", from = start_date, auto.assign = FALSE)
class(AAPL)
xts::is.xts(AAPL)
is.matrix(AAPL)
解構 xts
可以發現它是由一個日期向量(Date
)與一個 matrix
組合而成。
aapl_index <- zoo::index(AAPL)
aapl_coredata <- zoo::coredata(AAPL)
class(aapl_index)
class(aapl_coredata)
有效率處理表格式資料的訣竅
訣竅一:掌握不同資料結構之間的轉換,像是從表格式資料擷取向量,透過下列函數轉換為特定表格式資料結構。
as.matrix()
as.data.frame()
tibble::as.tibble()
data.table::as.data.table()
xts::as.xts()
訣竅二:同質資料的處理效率高於異質資料,盡可能將所需資料提取為向量或 matrix
再處理,而非以資料框格式處理,例如同樣想要使用 height
與 mass
這兩個欄位,提取為向量或 matrix
的作法比較有效率。
# Good
class(dplyr::starwars[["height"]])
dplyr::starwars[, c("height", "mass")] |>
as.matrix() |>
class()
# Not so good
class(dplyr::starwars[, "height"])
class(dplyr::starwars[, c("height", "mass")])
訣竅三:向量化的處理效率高於函數型程式設計,例如同樣想要使用 height
與 mass
這兩個欄位運算 bmi
,使用向量運算的作法比較有效率(且易懂。)
# Good
bmi <- dplyr::starwars[["mass"]] / (dplyr::starwars[["height"]]*0.01)^2
# Not so good
bmi <- mapply(function(w, h) {w / (h*0.01)^2}, dplyr::starwars[["mass"]], dplyr::starwars[["height"]])
訣竅四:函數型程式設計的處理效率高於迴圈,例如同樣想要使用 height
與 mass
這兩個欄位運算 bmi
,使用函數型程式設計的作法比較有效率。
# Good
bmi <- mapply(function(w, h) {w / (h*0.01)^2}, dplyr::starwars[["mass"]], dplyr::starwars[["height"]])
# Not so good
bmi <- vector("numeric", length = nrow(dplyr::starwars))
for (rowi in 1:nrow(dplyr::starwars)) {
w <- dplyr::starwars[rowi, "mass"][["mass"]]
h <- dplyr::starwars[rowi, "height"][["height"]]
bmi[rowi] <- w/(h*0.01)^2
}
訣竅五:使用迴圈處理時先行定義好輸出的長度與類別效率高於未定義輸出,例如同樣想要使用迴圈輸入 height
與 mass
這兩個欄位運算 bmi
,先行定義好輸出的長度與類別的作法比較有效率。
# Good
bmi <- vector("numeric", length = nrow(dplyr::starwars))
for (rowi in 1:nrow(dplyr::starwars)) {
w <- dplyr::starwars[rowi, "mass"][["mass"]]
h <- dplyr::starwars[rowi, "height"][["height"]]
bmi[rowi] <- w/(h*0.01)^2
}
# Not so good
bmi <- vector()
for (rowi in 1:nrow(dplyr::starwars)) {
w <- dplyr::starwars[rowi, "mass"][["mass"]]
h <- dplyr::starwars[rowi, "height"][["height"]]
bmi <- c(bmi, w/(h*0.01)^2)
}
訣竅六:處理異質資料時 data.table
快於 data.frame
,例如載入龐大的來源資料應優先考慮 data.table::fread()
函數。
file_url <- "https://raw.githubusercontent.com/Rdatatable/data.table/master/vignettes/flights14.csv"
# Good
flights_dt <- data.table::fread(file_url)
# Not so good
flights_df <- utils::read.csv(file_url)
非常感謝台北大學企業管理學系的游擱嘉助理教授邀約,讓我有機會能夠以另一個角度整理 R 語言的相關知識,也希望來聽短講的同學、我的電子報讀者有所收穫。
對於這篇文章有什麼想法呢?喜歡😻、留言🙋♂️或者分享🙌
約維安計畫學員專區
約維安計畫學員可以點選 nbgitpuller 連結取得本篇文章完整的內容,包含範例資料、程式碼、執行結果以及 Jupyter Notebook。在這個環境中,我們可以新增 R 語言為核心的 Jupyter Notebook,執行本篇文章的程式碼。