Apache Arrow 是一个软件开发平台,用于构建处理和传输大型数据集的高性能应用程序。它的设计旨在提高数据分析方法的性能,并提高将数据从一个系统或编程语言移动到另一个系统的效率。
arrow 包提供了一种在 R 中使用 Apache Arrow 的标准方式。它为 Arrow C++ 库提供了一个底层接口,并提供了一些更高级的工具,以便以对 R 用户自然的方式使用它。本文概述了各部分是如何结合在一起的,并描述了 R 中类和方法所遵循的约定。
包约定
arrow R 包构建在 Arrow C++ 库之上,而 C++ 是一种面向对象的语言。因此,Arrow C++ 库的核心逻辑封装在类和方法中。在 arrow R 包中,这些实现为 R6 类,它们都采用“TitleCase”(大驼峰)命名约定。以下是一些示例:
- 二维表格数据结构,如
Table、RecordBatch和Dataset - 一维向量类数据结构,如
Array和ChunkedArray - 用于读取、写入和流式传输数据的类,如
ParquetFileReader和CsvTableReader
这种底层接口允许你以非常灵活的方式与 Arrow C++ 库交互,但在许多常见情况下,你可能根本不需要使用它,因为 arrow 还提供了一个使用遵循“snake_case”(小写下划线)命名约定的函数的高级接口。以下是一些示例:
-
arrow_table()允许你创建 Arrow 表,而无需直接使用Table对象 -
read_parquet()允许你打开 Parquet 文件,而无需直接使用ParquetFileReader对象
本文中使用的所有示例都依赖于这种高级接口。
对于有兴趣了解更多关于包结构的开发者,请参阅开发者指南。
Arrow 中的表格数据
Apache Arrow 的一个关键组件是其内存中列式格式,这是一种标准化的、与语言无关的规范,用于在内存中表示结构化的表格类数据集。在 arrow R 包中,Table 类用于存储这些对象。表与数据框(data frame)大致相似,并且具有类似的行为。arrow_table() 函数允许你生成新的 Arrow 表,其方式与使用 data.frame() 创建新数据框的方式非常相似。
library(arrow, warn.conflicts = FALSE)
dat <- arrow_table(x = 1:3, y = c("a", "b", "c"))
dat## Table
## 3 rows x 2 columns
## $x <int32>
## $y <string>
你可以像处理数据框一样,使用 [ 指定 Arrow 表的子集
dat[1:2, 1:2]## Table
## 2 rows x 2 columns
## $x <int32>
## $y <string>
同样,$ 运算符可用于提取命名列
dat$y## ChunkedArray
## <string>
## [
## [
## "a",
## "b",
## "c"
## ]
## ]
请注意输出:Arrow 表中的每一列都表示为分块数组(Chunked Array),它们是 Arrow 中的一维数据结构,与 R 中的向量大致相似。
表是使用 Arrow 在内存中表示矩形数据的主要方式,但它们并不是 Arrow C++ 库使用的唯一矩形数据结构:还有用于存储在磁盘上而不是内存中的数据集(Datasets),以及作为基础构建块但不常用于数据分析的记录批次(Record Batches)。
要了解有关 arrow 中不同数据对象类的更多信息,请参阅关于数据对象的文章。
将表转换为数据框
表是一种用于表示由 Arrow C++ 库分配的内存中矩形数据的数据结构,但可以使用 as.data.frame() 将其强制转换为原生 R 数据框(或 tibbles)。
as.data.frame(dat)## x y
## 1 1 a
## 2 2 b
## 3 3 c
当发生这种强制转换时,原始 Arrow 表中的每一列都必须转换为原生 R 数据对象。例如,在 dat 表中,dat$x 存储为从 C++ 继承的 Arrow 数据类型 int32,当调用 as.data.frame() 时,它会变成 R 的整数类型。
可以对这个转换过程进行细粒度控制。要了解有关不同类型及其转换方式的更多信息,请参阅数据类型文章。
数据的读取与写入
使用 arrow 的主要方式之一是以几种常见格式读取和写入数据文件。arrow 包提供了极快的 CSV 读取和写入功能,此外还支持 Parquet 和 Arrow(也称为 Feather)等其他包中不支持的数据格式。此外,arrow 包支持多文件数据集,其中单个矩形数据集跨多个文件存储。
单个文件
当目标是将单个数据文件读入内存时,可以使用以下几种函数:
-
read_parquet():读取 Parquet 格式的文件 -
read_feather():读取 Arrow/Feather 格式的文件 -
read_delim_arrow():读取带分隔符的文本文件 -
read_csv_arrow():读取逗号分隔值 (CSV) 文件 -
read_tsv_arrow():读取制表符分隔值 (TSV) 文件 -
read_json_arrow():读取 JSON 数据文件
除 JSON 外,每种情况都有对应的 write_*() 函数,允许你以适当的格式写入数据文件。
默认情况下,read_*() 函数将返回数据框或 tibble,但你也可以使用它们将数据读入 Arrow 表。为此,你需要将 as_data_frame 参数设置为 FALSE。
在下面的示例中,我们获取由 dplyr 包提供的 starwars 数据,并使用 write_parquet() 将其写入 Parquet 文件。
library(dplyr, warn.conflicts = FALSE)
file_path <- tempfile(fileext = ".parquet")
write_parquet(starwars, file_path)然后,我们可以使用 read_parquet() 从该文件加载数据。如下所示,默认行为是返回一个数据框 (sw_frame),但当我们设置 as_data_frame = FALSE 时,数据将作为 Arrow 表 (sw_table) 读取。
sw_frame <- read_parquet(file_path)
sw_table <- read_parquet(file_path, as_data_frame = FALSE)
sw_table## Table
## 87 rows x 14 columns
## $name <string>
## $height <int32>
## $mass <double>
## $hair_color <string>
## $skin_color <string>
## $eye_color <string>
## $birth_year <double>
## $sex <string>
## $gender <string>
## $homeworld <string>
## $species <string>
## $films: list<element <string>>
## $vehicles: list<element <string>>
## $starships: list<element <string>>
要了解有关读取和写入单个数据文件的更多信息,请参阅读取/写入文章。
多文件数据集
当表格数据集变得很大时,通常的做法是将数据分区为有意义的子集,并将每个子集存储在单独的文件中。除其他优点外,这意味着如果只有数据的某个子集与分析相关,则只需读取一个(较小的)文件。arrow 包提供了 Dataset 接口,这是一种读取、写入和分析大于内存的单数据文件以及多文件数据集的便捷方式。
为了说明这些概念,我们将创建一个包含 100,000 行的伪造数据集,并将其拆分为 10 个子集。
set.seed(1234)
nrows <- 100000
random_data <- data.frame(
x = rnorm(nrows),
y = rnorm(nrows),
subset = sample(10, nrows, replace = TRUE)
)我们想要做的是对这些数据进行分区,然后将其写入 10 个单独的 Parquet 文件中,每个文件对应 subset 列的一个值。为此,我们首先指定一个文件夹路径,我们将把数据文件写入该文件夹中。
然后,我们可以使用 dplyr 中的 group_by() 函数指定数据将使用 subset 列进行分区,然后将分组后的数据传递给 write_dataset()。
random_data |>
group_by(subset) |>
write_dataset(dataset_path)这将创建一组 10 个文件,每个子集一个。这些文件按照如下所示的“Hive 分区”格式命名。
list.files(dataset_path, recursive = TRUE)## [1] "subset=1/part-0.parquet" "subset=10/part-0.parquet"
## [3] "subset=2/part-0.parquet" "subset=3/part-0.parquet"
## [5] "subset=4/part-0.parquet" "subset=5/part-0.parquet"
## [7] "subset=6/part-0.parquet" "subset=7/part-0.parquet"
## [9] "subset=8/part-0.parquet" "subset=9/part-0.parquet"
可以使用 read_parquet() 单独打开每个 Parquet 文件,但扫描文件夹并“连接”到数据集而不将其加载到内存中通常更方便(尤其是对于非常大的数据集)。我们可以使用 open_dataset() 来做到这一点。
dset <- open_dataset(dataset_path)
dset## FileSystemDataset with 10 Parquet files
## 3 columns
## x: double
## y: double
## subset: int32
这个 dset 对象不会将数据存储在内存中,只存储一些元数据。然而,正如下一节所讨论的那样,可以像加载数据一样分析 dset 所引用的数据。
要了解有关 Arrow 数据集的更多信息,请参阅数据集文章。
使用 dplyr 分析 Arrow 数据
Arrow 表和数据集可以使用 dplyr 语法进行分析。这是可能的,因为 arrow R 包提供了一个后端,将 dplyr 谓词转换为 Arrow C++ 库可以理解的命令,并将类似地转换出现在 dplyr 谓词调用中的 R 表达式。例如,尽管 dset 数据集不是数据框(并且不在内存中存储数据值),但你仍然可以将其传递给如下所示的 dplyr 管道。
dset |>
group_by(subset) |>
summarize(mean_x = mean(x), min_y = min(y)) |>
filter(mean_x > 0) |>
arrange(subset) |>
collect()## # A tibble: 6 x 3
## subset mean_x min_y
## <int> <dbl> <dbl>
## 1 2 0.00486 -4.00
## 2 3 0.00440 -3.86
## 3 4 0.0125 -3.65
## 4 6 0.0234 -3.88
## 5 7 0.00477 -4.65
## 6 9 0.00557 -3.50
注意我们在管道末尾调用了 collect()。在调用 collect()(或相关的 compute() 函数)之前,不会执行任何实际计算。这种“惰性求值”使 Arrow C++ 计算引擎能够优化计算执行方式。
要了解有关分析 Arrow 数据的更多信息,请参阅数据整理文章。dplyr 查询中可用函数列表页面也可能很有用。
连接到云存储
arrow R 包的另一个用途是读取、写入和分析远程存储在云服务上的数据集。该包目前支持 Amazon Simple Storage Service (S3) 和 Google Cloud Storage (GCS)。下面的示例说明了如何使用 s3_bucket() 引用 S3 存储桶,并使用 open_dataset() 连接到存储在那里的数据集。
bucket <- s3_bucket("arrow-datasets/nyc-taxi")
nyc_taxi <- open_dataset(bucket)要了解有关 arrow 中对云服务支持的更多信息,请参阅云存储文章。
R 与 Python 之间的高效数据交换
reticulate 包提供了一个接口,允许你从 R 调用 Python 代码。arrow 包旨在与 reticulate 实现互操作。如果 Python 环境中安装了 pyarrow 库(Python 等价于 arrow 包),你可以使用 reticulate 中的 r_to_py() 函数将 Arrow 表从 R 传递给 Python,如下所示。
library(reticulate)
sw_table_python <- r_to_py(sw_table)sw_table_python 对象现在存储为 pyarrow 表:Table 类的 Python 等价物。当你打印对象时可以看到这一点。
sw_table_python## pyarrow.Table
## name: string
## height: int32
## mass: double
## hair_color: string
## skin_color: string
## eye_color: string
## birth_year: double
## sex: string
## gender: string
## homeworld: string
## species: string
## films: list<element: string>
## child 0, element: string
## vehicles: list<element: string>
## child 0, element: string
## starships: list<element: string>
## child 0, element: string
## ----
## name: [["Luke Skywalker","C-3PO","R2-D2","Darth Vader","Leia Organa",...,"Finn","Rey","Poe Dameron","BB8","Captain Phasma"]]
## height: [[172,167,96,202,150,...,null,null,null,null,null]]
## mass: [[77,75,32,136,49,...,null,null,null,null,null]]
## hair_color: [["blond",null,null,"none","brown",...,"black","brown","brown","none","none"]]
## skin_color: [["fair","gold","white, blue","white","light",...,"dark","light","light","none","none"]]
## eye_color: [["blue","yellow","red","yellow","brown",...,"dark","hazel","brown","black","unknown"]]
## birth_year: [[19,112,33,41.9,19,...,null,null,null,null,null]]
## sex: [["male","none","none","male","female",...,"male","female","male","none","female"]]
## gender: [["masculine","masculine","masculine","masculine","feminine",...,"masculine","feminine","masculine","masculine","feminine"]]
## homeworld: [["Tatooine","Tatooine","Naboo","Tatooine","Alderaan",...,null,null,null,null,null]]
## ...
必须认识到,当发生这种传输时,只有 C++ 指针(即指向 Arrow C++ 库存储的底层数据对象的元数据)被复制。数据值本身位于内存中的相同位置。因此,将 Arrow 表从 R 传递到 Python 比将 R 中的数据框复制到 Python 中的 Pandas DataFrame 要快得多。
要了解有关在 R 和 Python 之间传递 Arrow 数据的更多信息,请参阅关于Python 集成的文章。
访问 Arrow 消息、缓冲区和流
arrow 包还提供了许多指向 C++ 库的底层绑定,使你能够访问和操作 Arrow 对象。你可以使用这些绑定来构建连接器,以连接到使用 Arrow 的其他应用程序和服务。一个例子是 Spark:sparklyr 包支持使用 Arrow 在 Spark 之间来回移动数据,从而产生显著的性能提升。
为 arrow 做贡献
Apache Arrow 是一个跨多种语言的庞大项目,而 arrow R 包只是这个大型项目的一部分。因此,对于希望为该包做出贡献的开发者来说,有许多特殊注意事项。为了帮助简化此过程,arrow 文档中有几篇文章讨论了与 arrow 开发者相关的主题,但普通用户极不可能用到它们。
有关开发过程的概述以及开发者相关文章列表,请参阅开发者指南。