数据

数据框 属于 R

1 个练习

关于 数据框

在课程体系的其他部分,我们已经见过各种特性不同的数据类型。

  • 原子向量是一维的,且类型同质。
  • 数组是一维的,元素可以属于不同的类型。
  • 矩阵和数组是多维的,且类型同质。

本概念将探讨如何存储多维、异构的数据。 现实中,_大多数_真实数据都是这样的,所以我们现在正触及 R 在实践中用法的核心。

数据框的几种变体

几十年来,R 陆续加入了多种用于处理表格数据的类型。

本大纲主要聚焦于 tibble,但了解一下其他一些替代方案也很有用。

data.table

tibble(下文会介绍)和 data.table 都是为改进基础 R 中传统 data.frame 而做的大受认可的尝试。 关于哪个“更好”,争论在所难免。 也许下面几点已在某种程度上达成共识(即便它们会被批评为过于简单化)。

  • tibble 主要针对易用性,以及与 Tidyverse 生态的集成做了优化。
  • data.table 主要针对原始性能与可扩展性做了优化,尤其是在处理超大数据集时。

无论如何,data.table在 Exercism 中并不可用,这里提到它只是为了让内容完整。

data.frame

在基础 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。

与基础 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

最简单的方法是,像 data.frame() 那样,用 tibble() 函数把列向量拼接起来。 上一节已经展示过一个这样的例子。

如果按行输入值更方便,对应的函数是 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 作为输出格式。 我们会在以后的概念中再回到这一点。

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

在现代 R 和 Tidyverse 生态中,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

可以组合多个条件,使用布尔运算符 &、| 和 !(与、或、非)。

属于_合法 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 作为输入,或把 tibble 作为输出创建出来。

本概念只做了简要介绍,仅仅是浅尝辄止,远未展现全部可能。

后面的概念会讨论数据框的其他几个方面(受限于 Exercism 的技术限制)。

通过 GitHub 编辑 该链接会在新窗口或标签页中打开

学习 数据框