學習軌道
/
R
R
/
練習
/
Trees & Tibble-ations
Trees & Tibble-ations

Trees & Tibble-ations

學習練習

簡介

在課程大綱的其他部分,我們看過各種特性不同的資料型態。

  • 原子向量是一維的,且型態同質。
  • 串列是一維的,但元素可以是異質型態。
  • 矩陣與陣列是多維的,且同質。

這個概念要來看如何儲存多維、異質的資料。 實務上,_大多數_真實世界的資料都是這樣,所以我們現在要觸及 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

這裡有一欄字串、一欄數字,還有一欄布林值。 當資料量放大時,這是用來表示許多真實世界資料集合的直覺方式。

tibble

data.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

tibble 是 Tidyverse 的核心部分,可以用library(tibble)或library(tidyverse)載入。

建立 tibble

最簡單的方式是用tibble()函式把欄向量組合起來,就跟data.frame()一樣。 前面小節已經示範過例子。

如果逐列輸入值比較方便,對應的函式是tribble()。

實務上,建立 tibble 的方法有幾十種,因為許多 Tidyverse 函式的預設輸出格式就是 tibble。

操控 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

先提醒一點:_修改時複製_的預設行為意味著原本的 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     

說明

大型櫻桃果園通常會有少量剩餘的樹木庫存,他們想將這些樹木作為木材出售。這有助於維持果園的健康,同時還能帶來額外的收入。他們請你協助撰寫一些功能,以便在潛在客戶來電時協助挑選樹木。

Note

內建的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來處理。

1. 重新命名欄位

很遺憾地,有個從未修正的文書錯誤:資料集中有一個欄位命名為 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

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

3. 果園用的資料集副本

針對每位潛在客戶,果園會保留一份特別的資料集副本,以協助銷售。 這份資料集會把最相關的欄位 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

4. 客戶用的資料集副本

每位潛在客戶都會根據自己的偏好,取得一份專屬的資料集版本。 為此,客戶可以指定:最小高度、最大高度,以及任何單一樹木的最大重量。

定義 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
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
R Exercism

準備好開始 Trees & Tibble-ations 了嗎?

註冊 Exercism,透過 21 個概念111 個練習 和真人引導來學習並精通 R,全部免費。