轨道
/
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

在现代 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      

# 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),它接收一个数据框和一个数值参数。该函数应返回一个新的数据框,其中新增两列 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),它接收一个数据框作为输入。它应返回一个新的数据框,把 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) 函数。返回一份客户版数据框,包含 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,全部免费。