在教学大纲的其他部分,我们已经见过各种特性不同的数据类型。
本概念将探讨存储多维、异构数据的方法。 在实践中,_大多数_真实世界的数据都是这样的,所以我们现在正触及 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
在现代 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 通常保持不变。
大多数修改都是按列进行的。
可以用 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),它接收一个数据框和一个数值参数。该函数应返回一个新的数据框,其中新增两列 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),它接收一个数据框作为输入。它应返回一个新的数据框,把 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) 函数。返回一份客户版数据框,包含 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