軌道
/
R
R
/
課程大綱
/
資料框
資料

資料框 在 R

1 個練習

關於 資料框

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

  • 原子向量是一維的,而且型別同質。
  • 清單是一維的,元素可以是異質的型別。
  • 矩陣與陣列是多維的,而且同質。

這個概念要來看如何儲存多維、異質的資料。 實務上,_大部分_真實世界的資料就是這樣,所以我們現在要進入 R(大致上)在實務中如何被使用的核心。

資料框的各種變體

幾十年來,R 陸續加入了多種資料型態來處理表格資料。

這份課程大綱主要聚焦在 tibble,不過認識一些替代方案也很有用。

data.table

tibble(下面會介紹)和 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

我們有一個字串欄、一個數字欄,還有一個布林欄。 規模放大之後,這是表示許多真實世界資料集合的直覺做法。

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

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

文件相當豐富,走 Tidyverse 的風格:

建立 tibble

最簡單的方式,是用 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 函式的預設輸出格式。 之後的概念會再回來談這點。

操作 tibble

函式程式設計概念談過用 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    

逐列操作

Note

顯然,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

先說一個注意事項:修改時複製這個預設行為,意味著原本的 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 的技術限制之內)。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟

學習 資料框