在課程大綱的其他部分,我們看過各種特性不同的資料型態。
這個概念要來看如何儲存多維、異質的資料。 實務上,_大多數_真實世界的資料都是這樣,所以我們現在要觸及 R(大致上)在實務中如何被使用的核心。
幾十年來,R 陸續加入了多種資料型態來處理表格式資料。
這份課程大綱主要聚焦在 tibble,但了解其他替代方案也很有用。
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
這裡有一欄字串、一欄數字,還有一欄布林值。 當資料量放大時,這是用來表示許多真實世界資料集合的直覺方式。
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
注意預設的列印格式:帶有維度的那行註解會自動印出,欄的型態也會一併顯示。
tibble 是 Tidyverse 的核心部分,可以用library(tibble)或library(tidyverse)載入。
最簡單的方式是用tibble()函式把欄向量組合起來,就跟data.frame()一樣。
前面小節已經示範過例子。
如果逐列輸入值比較方便,對應的函式是tribble()。
實務上,建立 tibble 的方法有幾十種,因為許多 Tidyverse 函式的預設輸出格式就是 tibble。
函式程式設計概念談過用purrr套件來操控向量與串列(一維資料結構)。
對於資料框(無論是傳統的還是 tibble),對應要使用的套件是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
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
可以同時使用多個條件,搭配布林運算子&、|和!(也就是且、或、非)。
拿我們那個語言玩具資料框來玩這麼強大的功能,好像有點誇張。
還好dplyr內建了starwars這個 tibble,讓我們有更大的資料可以練習喔。
# 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
用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
先提醒一點:_修改時複製_的預設行為意味著原本的 tibble 通常維持不變。
大多數修改都是逐欄進行的。
欄名可以用rename(newname = oldname)更改,或用rename_with()套用函式。
注意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
逐列操作在修改單一 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
大型櫻桃果園通常會有少量剩餘的樹木庫存,他們想將這些樹木作為木材出售。這有助於維持果園的健康,同時還能帶來額外的收入。他們請你協助撰寫一些功能,以便在潛在客戶來電時協助挑選樹木。
內建的datasets套件內含許多範例資料集,而這個練習使用的是trees資料集。
要取用這個資料集,只要使用trees這個名稱即可。
library(datasets) # loading is optional
trees |> head(3)
#> A data.frame: 3 × 3
#> Girth Height Volume
#> <dbl> <dbl> <dbl>
#> 1 8.3 70 10.3
#> 2 8.6 65 10.3
#> 3 8.8 63 10.2
請注意這三個欄位:Girth、Height 和Volume
這個資料集一開始是data.frame的形式,但你可能會想改用tibble來處理。
很遺憾地,有個從未修正的文書錯誤:資料集中有一個欄位命名為 Girth,但它其實是 Diameter。
定義一個名為 tree_data 的 data.frame/tibble,將 trees 中 Girth 欄位的名稱更正為 Diameter。
head(tree_data, 3)
#> A tibble: 3 × 3
#> Diameter Height Volume
#> <dbl> <dbl> <dbl>
#> 8.3 70 10.3
#> 8.6 65 10.3
#> 8.8 63 10.2
樹圍(也就是圓周)仍然是有用的測量值,木材的重量也是。
樹圍可以用 πD 輕鬆算出,其中 D 是直徑。
所有木材的重量可以用 ρV 計算,其中 ρ = 35 是櫻桃木的密度,而 V 是木材的體積。
定義函式 girth_n_weight(data, rnd_digits),它接受 dataframe 與數值引數。
這個函式應回傳一個新的 dataframe,並包含兩個額外的欄位:Girth 和 Weight,兩者都四捨五入到 rnd_digits 指定的位數。
girth_n_weight(tree_data, 1) |> head(3)
#> A tibble: 3 × 5
#> Diameter Height Volume Girth Weight
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 8.3 70 10.3 26.1 360.5
#> 8.6 65 10.3 27.0 360.5
#> 8.8 63 10.2 27.6 357.0
針對每位潛在客戶,果園會保留一份特別的資料集副本,以協助銷售。
這份資料集會把最相關的欄位 Weight 和 Height 移到最前面,並依 Weight 排序。
定義函式 orchard_copy(data, important_cols),它接受一個 dataframe 作為輸入。
這個函式應回傳一個新的 dataframe,其中 Weight 和 Height 已移到最前面,且資料列依 Weight 排序。
tree_data |> girth_n_weight() |> orchard_copy() |> head(3)
#> A tibble: 3 × 5
#> Weight Height Diameter Volume Girth
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 357 63 9 10 28
#> 360 70 8 10 26
#> 360 65 9 10 27
每位潛在客戶都會根據自己的偏好,取得一份專屬的資料集版本。 為此,客戶可以指定:最小高度、最大高度,以及任何單一樹木的最大重量。
定義 customer_copy(data, min_height, max_height, max_weight) 函式。
回傳一份客戶版的 dataframe,包含 Height、Weight、Diameter 和 Girth 欄位,且只保留符合限制條件的資料列。
tree_data |>
girth_n_weight(1) |>
orchard_copy() |>
customer_copy(65, 75, 1500) |>
head(3)
#> A tibble: 3 × 4
#> Height Weight Diameter Girth
#> <dbl> <dbl> <dbl> <dbl>
#> 70 360.5 8.3 26.1
#> 65 360.5 8.6 27.0
#> 66 546.0 11.0 34.6