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
类用于存储这些对象。表大致类似于数据框,并具有类似的行为。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 数组,这是 Arrow 中的一维数据结构,大致类似于 R 中的向量。
表是使用 Arrow 在内存中表示矩形数据的主要方式,但它们不是 Arrow C++ 库使用的唯一矩形数据结构:还有用于存储在磁盘上而不是内存中的数据的 Dataset,以及 Record Batches,它们是基本构建块,但通常不用于数据分析。
要了解有关 arrow 中不同数据对象类的更多信息,请参阅关于数据对象的文章。
将表转换为数据框
表是一种用于表示 Arrow C++ 库分配的内存中矩形数据的数据结构,但可以使用 as.data.frame()
将它们强制转换为原生 R 数据框(或 tibble)
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 接口,这是一种读取、写入和分析大于内存的单个数据文件和多文件数据集的便捷方法。
为了说明这些概念,我们将创建一个包含 100000 行的无意义数据集,可以将其拆分为 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 数据
可以使用 dplyr 语法分析 Arrow 表和数据集。这是可能的,因为 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("voltrondata-labs-datasets/nyc-taxi")
nyc_taxi <- open_dataset(bucket)
要了解有关 arrow 中云服务支持的更多信息,请参阅云存储文章。
R 和 Python 之间高效的数据交换
reticulate 包提供了一个接口,允许您从 R 中调用 Python 代码。arrow 包设计为可与 reticulate 互操作。如果 Python 环境安装了 pyarrow 库(与 arrow 包等效的 Python 库),则可以使用 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 开发人员相关的主题,但用户不太可能需要这些主题。
有关开发过程的概述以及开发人员相关文章的列表,请参阅开发人员指南。