跳至内容

Apache Arrow 是一个软件开发平台,用于构建高性能应用程序,这些应用程序处理和传输大型数据集。它旨在提高数据分析方法的性能,并提高将数据从一个系统或编程语言移动到另一个系统的效率。

arrow 包提供了一种使用 Apache Arrow 的标准方法。它提供了一个到 Arrow C++ 库 的低级接口,以及一些高级工具,用于以一种对 R 用户来说感觉自然的方式使用它。本文概述了各个部分如何组合在一起,并描述了类和方法在 R 中遵循的约定。

包约定

arrow R 包构建在 Arrow C++ 库之上,而 C++ 是一种面向对象的语言。因此,Arrow C++ 库的核心逻辑封装在类和方法中。在 arrow R 包中,这些被实现为 R6 类,这些类都采用“标题大小写”命名约定。以下是一些示例:

  • 二维表格数据结构,例如 TableRecordBatchDataset
  • 一维向量式数据结构,例如 ArrayChunkedArray
  • 用于读取、写入和流式传输数据的类,例如 ParquetFileReaderCsvTableReader

此低级接口允许您以非常灵活的方式与 Arrow C++ 库交互,但在许多常见情况下,您可能根本不需要使用它,因为 arrow 还提供了一个高级接口,使用遵循“蛇形大小写”命名约定的函数。以下是一些示例:

  • arrow_table() 允许您在不直接使用 Table 对象的情况下创建 Arrow 表
  • read_parquet() 允许您在不直接使用 ParquetFileReader 对象的情况下打开 Parquet 文件

本文中使用的所有示例都依赖于此高级接口。

对于有兴趣了解有关包结构的更多信息的开发人员,请参阅 开发者指南

Arrow 中的表格数据

Apache Arrow 的一个关键组件是其内存中列式格式,这是一个标准化的、与语言无关的规范,用于表示内存中的结构化表格式数据集。在 arrow R 包中,Table 类用于存储这些对象。表格大致类似于数据帧,并具有类似的行为。arrow_table() 函数允许您以与使用 data.frame() 创建新数据帧的方式非常相似的方式生成新的 Arrow 表

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 表中的各个列表示为分块数组,它们是 Arrow 中的一维数据结构,大致类似于 R 中的向量。

表格是使用 Arrow 在内存中表示矩形数据的首要方式,但它们不是 Arrow C++ 库使用的唯一矩形数据结构:还有数据集,用于存储在磁盘上而不是内存中的数据,以及记录批次,它们是基本构建块,但通常不用于数据分析。

要了解有关 arrow 中不同数据对象类的更多信息,请参阅关于 数据对象 的文章。

将表格转换为数据帧

表格是用于表示 Arrow C++ 库分配的内存中矩形数据的 data structure,但可以使用 as.data.frame() 将它们强制转换为本机 R 数据帧(或 tibble)。

##   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 包还支持多文件数据集,其中单个矩形数据集存储在多个文件中。

单个文件

当目标是将单个数据文件读入内存时,您可以使用以下几种函数

在除了 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 列的每个值。为此,我们首先指定一个文件夹的路径,我们将把数据文件写入其中

dataset_path <- file.path(tempdir(), "random_data")

然后,我们可以使用来自 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"

每个 Parquet 文件都可以使用 read_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("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 开发人员相关的主题,但用户极不可能需要这些主题。

有关开发流程概述以及开发人员相关文章列表,请参阅 开发者指南