在课程体系的其他部分,我们已经见过各种特性不同的数据类型。
本概念将探讨如何存储多维、异构的数据。 现实中,_大多数_真实数据都是这样的,所以我们现在正触及 R 在实践中用法的核心。
几十年来,R 陆续加入了多种用于处理表格数据的类型。
本大纲主要聚焦于 tibble,但了解一下其他一些替代方案也很有用。
data.tabletibble(下文会介绍)和 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
这里有一列字符串、一列数字和一列布尔值。 扩大到更大规模时,这是表示许多现实世界数据集合的一种直观方式。
tibbledata.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 是 Tidyverse 的核心部分,用 library(tibble) 或 library(tidyverse) 加载即可。
文档相当丰富,延续了 Tidyverse 的风格:
最简单的方法是,像 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 作为输出格式。 我们会在以后的概念中再回到这一点。
函数式编程概念讨论了用 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
显然,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 通常保持不变。
大多数修改都是按列进行的。
列名可以用 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 的技术限制)。