在課程大綱的其他部分,我們看過各種特性不同的資料型態。
這個概念要來看如何儲存多維、異質的資料。 實務上,_大部分_真實世界的資料就是這樣,所以我們現在要進入 R(大致上)在實務中如何被使用的核心。
幾十年來,R 陸續加入了多種資料型態來處理表格資料。
這份課程大綱主要聚焦在 tibble,不過認識一些替代方案也很有用。
data.tabletibble(下面會介紹)和 data.table 都是備受推崇的嘗試,用來改良 Base R 中傳統的 data.frame。
關於哪一個「比較好」,免不了有很多爭論。
對於以下幾點,或許某種程度上有些共識(即使這些說法會被批評為過度簡化)。
tibble 主要針對易用性,以及與 Tidyverse 生態系的整合來最佳化。data.table 主要針對原始效能與可擴充性來最佳化,尤其在處理非常龐大的資料集時。無論如何,data.table 在 Exercism 裡無法使用,這裡只是為了完整起見才提到它。
data.frame在 Base R 裡,data.frame 是由長度相同的 vectors 組成的 list。
可以把它想成一張長方形的資料表,其中每一欄都是同質的,但每一列可以(而且通常確實會)包含不同型別的資料。
用一個例子來說明:
# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)
# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#> languages created has.syllabus
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
# look at the structure
str(df)
#> 'data.frame': 4 obs. of 3 variables:
#> $ languages : chr "Fortran" "R" "Python" "Julia"
#> $ created : num 1957 1993 1991 2012
#> $ has.syllabus: logi FALSE TRUE TRUE TRUE
我們有一個字串欄、一個數字欄,還有一個布林欄。 規模放大之後,這是表示許多真實世界資料集合的直覺做法。
tibbledata.frame 的設計已經_很老_了。
累積數十年的經驗,加上 R 使用方式的改變,促成了重新設計,在 Tidyverse 中創造出一個現代化的替代方案:tibble。
相較於 Base R,tibble 有以下特點:
簡而言之,tibble 的目標是「做得更少,抱怨更多」,也有人形容它「懶散又壞脾氣」。
這兩種型別通常可以互換:任何接受 data.frame 的函式也會接受 tibble,反之亦然。
兩種型別之間的轉換很簡單,用 as_tibble(df) 和 as.data.frame(tbl) 就行。
開發新東西時,使用 tibble 大概能幫你寫出更強健的程式碼。
不過,R 的世界裡有非常多舊有的程式碼和資料,所以 data.frame 很可能在很長一段時間內依然很常見。
# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#> $ languages : chr [1:4] "Fortran" "R" "Python" "Julia"
#> $ created : num [1:4] 1957 1993 1991 2012
#> $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE
注意預設的列印格式:帶有維度的註解行會自動列印出來,欄的型別也會顯示。
tibble 是 Tidyverse 的核心部分,所以用 library(tibble) 或 library(tidyverse) 載入它們。
文件相當豐富,走 Tidyverse 的風格:
最簡單的方式,是用 tibble() 函式把各欄向量接起來,就跟 data.frame() 一樣。
前面一節已經示範過一個例子。
如果以列為單位輸入值比較方便,對應的函式是 tribble()。
tbl_r <- tribble(
# column names marked with tilde prefix
~languages, ~created, ~has.syllabus,
"Fortran", 1957, FALSE,
"R", 1993, TRUE,
"Python", 1991, TRUE,
"Julia", 2012, TRUE
)
tbl_r
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
實務上,建立 tibble 的方法有幾十種,因為它們是各種 Tidyverse 函式的預設輸出格式。 之後的概念會再回來談這點。
函式程式設計概念談過用 purrr 函式庫來操作向量和清單(一維的資料結構)。
至於資料框(不論是傳統的還是 tibble),對應要使用的函式庫是 dplyr。
我們之前在 Switch 概念中介紹過 dplyr。
那時只用了幾個工具函式,現在我們可以開始探索這個大型函式庫的其他部分。
資料框(包括 tibble)可以當成由各欄向量組成的清單,所以用清單的索引方式就能取出指定的欄。
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl$created
#> [1] 1957 1993 1991 2012
資料框也可以用矩陣風格的索引方式來取用。
tbl[c(2, 4), 1:2]
# A tibble: 2 × 2
#> languages created
#> <chr> <dbl>
#> 1 R 1993
#> 2 Julia 2012
在搭配 Tidyverse 生態系的現代 R 裡,dplyr的函式通常更有彈性也更方便,接下來這個概念會把重點放在它們身上。
用 pull()加上欄名或順序編號(負數表示從右邊數回來)來取得單一欄。
tbl |> pull(created)
#> [1] 1957 1993 1991 2012
結果和 tbl$created 相同,但用的是適合搭配管線的函式。
要取得多個欄,合適的函式是 select(),它非常有彈性。
依照欄名或型別的性質來取得(或排除)欄。
# Range with position and/or name
tbl |> select(1:created)
# A tibble: 4 × 2
#> languages created
#> <chr> <dbl>
#> 1 Fortran 1957
#> 2 R 1993
#> 3 Python 1991
#> 4 Julia 2012
# Exclude a column
tbl |> select(!created)
# A tibble: 4 × 2
#> languages has.syllabus
#> <chr> <lgl>
#> 1 Fortran FALSE
#> 2 R TRUE
#> 3 Python TRUE
#> 4 Julia TRUE
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
可以同時使用多個條件,透過布林運算子 &、| 和 !(and、or、not)。
是_合法的 R 識別字_的欄名,在 select()裡不需要加引號。
不合法的名稱(例如包含空白的)可以用反引號包起來,不過把它們重新命名可能更好。
select()函式可以搭配一系列輔助函式來挑選欄名:starts_with、contains、num_range 以及其他好幾種。
matches則可以進行完整的正規表示式比對。
詳情請看文件。
拿我們那個語言的玩具資料框來試這種威力,看起來有點滑稽。
幸好 dplyr 裡附了 starwars 這個 tibble,讓我們有更大的東西可以練習。
# limit display to top 3 rows of non-list columns
starwars |>
select(!where(is.list)) |>
head(3)
# A tibble: 3 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
# pick a subset of columns
starwars |>
select(name | ends_with("color")) |>
head(5)
# A tibble: 5 × 4
#> name hair_color skin_color eye_color
#> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker blond fair blue
#> 2 C-3PO NA gold yellow
#> 3 R2-D2 NA white, blue red
#> 4 Darth Vader none white yellow
#> 5 Leia Organa brown light brown
顯然,dplyr 提供了強大的方法可以依欄名選取欄。
我們也能對列名做類似的事嗎?
不行!
傳統的 R 資料框可以有列名,但是(在經歷一連串的 bug 和效能問題之後)tibbles 裡_不允許_有列名。
如果你想要名稱,就把它們放進一個 <chr> 欄(通常是第 1 欄),像其他欄一樣使用。
像 as_tibble()這類匯入函式,在匯入帶有具名列的資料時會自動建立這個欄。
如果這個列名的限制看起來限制得莫名其妙,記得大多數大型資料庫系統處理資料表的方式也一樣:Oracle、SQL Server、PostgreSQL、MySQL……
用 filter()取得符合某些條件的列,或用 filter_out() 排除它們。
starwars |>
select(name:mass) |>
filter(between(height, 150, 165) & !is.na(mass))
# A tibble: 4 × 3
#> name height mass
#> <chr> <int> <dbl>
#> 1 Leia Organa 150 49
#> 2 Beru Whitesun Lars 165 75
#> 3 Nien Nunb 160 68
#> 4 Ben Quadinaros 163 65
篩選條件可以任意複雜,但永遠是根據列的_內容_。
如果知道列的_編號_,我們可以用各種 slice() 函式。
starwars |>
select(name | homeworld) |>
slice(20:25)
# A tibble: 6 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Palpatine Naboo
#> 2 Boba Fett Kamino
#> 3 IG-88 NA
#> 4 Bossk Trandosha
#> 5 Lando Calrissian Socorro
#> 6 Lobot Bespin
# random sample of rows
starwars |>
select(name | homeworld) |>
slice_sample(n = 4)
# A tibble: 4 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Shaak Ti Shili
#> 2 Luminara Unduli Mirial
#> 3 Grievous Kalee
#> 4 Palpatine Naboo
要移除重複的列,使用 distinct()。
先說一個注意事項:修改時複製這個預設行為,意味著原本的 tibble 通常會保持不變。
大多數修改都是以欄為單位來套用。
欄名可以用 rename(newname = oldname) 來更改,或用 rename_with() 套用一個函式。
典型的用法是清理匯入的名稱,讓它們在 R 裡更容易處理,做法是移除空白,並讓相關的名稱採用一致的格式。
注意 rename() 裡的語法。
欄 oldname 的_內容_會被_綁定_到名稱 newname,順序就是這樣來的。
欄的順序可以用 relocate() 來改變。
指定的欄預設會移到最左邊的位置,不過可以用 .before 或 .after 引數做更精細的定位。
sw <- starwars |> select(name:species) |> slice(1:4)
sw
# A tibble: 4 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
#> 4 Darth Vader 202 136 none white yellow 41.9 male masculine Tatooine Human
sw |> relocate(c(species, homeworld), .after = name)
# A tibble: 4 × 11
#> name species homeworld height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <chr> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke Skywalker Human Tatooine 172 77 blond fair blue 19 male masculine
#> 2 C-3PO Droid Tatooine 167 75 NA gold yellow 112 none masculine
#> 3 R2-D2 Droid Naboo 96 32 NA white, blue red 33 none masculine
#> 4 Darth Vader Human Tatooine 202 136 none white yellow 41.9 male masculine
要做比較大的變動,mutate() 可以讓你:
NULL 來刪除該欄顯然,mutate() 很強大,也可能讓人困惑,更是我們該非常感謝修改時複製的理由。
沒什麼明顯的理由要在意星際大戰角色的身體質量指數,不過以防萬一:
starwars |>
select(c(name, species, height, mass)) |>
mutate(BMI = mass / (height / 100)^2) |>
head(4)
# A tibble: 4 × 5
#> name species height mass BMI
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 Luke Skywalker Human 172 77 26.0
#> 2 C-3PO Droid 167 75 26.9
#> 3 R2-D2 Droid 96 32 34.7
#> 4 Darth Vader Human 202 136 33.3
當你想用 mutate() 之類的函式對一部分欄進行操作時,上面例子裡的 select() |> mutate() 這種寫法是選項之一。
結果裡只會有所選的欄。
另一種做法,是在 mutate() 呼叫_內部_使用 pick(),有時這樣很方便:
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.
Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:
```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
# A tibble: 4 × 15
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <lis> <list>
#> 1 Luke Skywal… 172 77 blond fair blue 19 male mascu… Tatooine Human <chr> <chr>
#> 2 C-3PO 167 75 NA gold yellow 112 none mascu… Tatooine Droid <chr> <chr>
#> 3 R2-D2 96 32 NA white, bl… red 33 none mascu… Naboo Droid <chr> <chr>
#> 4 Darth Vader 202 136 none white yellow 41.9 male mascu… Tatooine Human <chr> <chr>
# ℹ 2 more variables: starships <list>, BMI <dbl>
只有 pick 選中的欄會參與這次修改,但所有欄都會回傳。
逐列操作在修改單一 tibble 時比較少見(合併多個 tibble 會在之後的概念討論)。
有一個例外:arrange() 會依一個或多個欄的值來排序列。
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl |> arrange(languages)
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 Julia 2012 TRUE
#> 3 Python 1991 TRUE
#> 4 R 1993 TRUE
資料框,不論是傳統的還是 tibble,都是現代 R 典型用法的核心。
大多數 Tidyverse 函式(不只是 dplyr)都以 tibble 作為輸入,或把它們當成輸出產生,或兩者皆是。
這個概念只做了簡短的介紹,才剛剛碰觸到各種可能性的表面。
之後的概念會討論資料框的其他幾個面向(在 Exercism 的技術限制之內)。