arrow系列5---arrow的基本数据结构(Basic Arrow Data Structures)
作者:yunjinqi   类别:    日期:2023-10-14 12:11:06    阅读:495 次   消耗积分:0 分    

Apache Arrow提供了用于表示数据的基本数据结构:Array、ChunkedArray、RecordBatch和Table。本文将演示如何从基本数据类型构造这些数据结构;具体而言,我们将使用不同大小的整数表示天、月和年。我们将使用它们来创建以下数据结构:

  • Arrow Arrays

  • ChunkedArrays

  • 从Arrays创建的RecordBatch

  • 从ChunkedArrays创建的Table

先决条件 

在继续之前,请确保已经具备以下条件:

  • 安装了Arrow,您可以在这里设置:在您自己的项目中使用Arrow C++

  • 了解如何使用基本的C++数据结构

  • 了解基本的C++数据类型

设置 

在尝试Arrow之前,我们需要填补一些空白:

我们需要包括必要的头文件。

需要一个main()函数来将一切粘合在一起。

包含 

首先,像往常一样,我们需要一些包含。我们将获取输出的iostream,然后从api.h中导入Arrow的基本功能,如下所示:

#include <arrow/api.h>

#include <iostream>
主函数 

接下来,我们需要一个main()函数 - Arrow的常见模式如下:

int main() {
  arrow::Status st = RunMain();
  if (!st.ok()) {
    std::cerr << st << std::endl;
    return 1;
  }
  return 0;
}

这允许我们轻松使用Arrow的错误处理宏,如果发生失败,它将返回一个arrow::Status对象,然后这个main()函数将报告错误。请注意,这意味着Arrow从不引发异常,而是依赖于返回Status。关于这一点的更多信息,请阅读这里:Conventions。

为配合这个main()函数,我们有一个RunMain()函数,任何Status对象都可以从中返回 - 这是我们将编写程序的地方:

arrow::Status RunMain() {
    // code
    return arrow::Status::OK();
}


创建Arrow Array 

构建int8 Arrays 

考虑到我们在标准的C++数组中有一些数据,希望使用Arrow,我们需要将数据从这些数组中移动到Arrow数组中。在Array中,我们仍然保证内存的连续性,因此使用Array与C++数组相比不会导致性能损失。构建Array的最简单方法是使用ArrayBuilder。

以下代码初始化了一个ArrayBuilder,该Array将保存8位整数。具体来说,它使用AppendValues()方法,该方法存在于具体的arrow::ArrayBuilder子类中,以填充ArrayBuilder的内容。请注意使用ARROW_RETURN_NOT_OK。如果AppendValues()失败,该宏将返回到main(),main()将打印出失败的原因。

// Builders是在Arrow中从现有值创建Arrays的主要方法,这些值不是在磁盘上的。
// 在这种情况下,我们将创建一个简单的数组,并将其传递给它。
// 数据类型仍然很重要,每种兼容类型都有一个Builder;
// 在这种情况下是int8。
arrow::Int8Builder int8builder;  
int8_t days_raw[5] = {1, 12, 17, 23, 28};  // 如其名称所示,AppendValues将days_raw中的5个值放入我们的Builder对象。
ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw, 5));

一旦ArrayBuilder中具有我们希望在Array中的值,我们可以使用ArrayBuilder::Finish()将最终结构输出到Array,具体而言,输出为std::shared_ptrarrow::Array。请注意在以下代码中使用的ARROW_ASSIGN_OR_RAISE。Finish()输出一个arrow::Result对象,ARROW_ASSIGN_OR_RAISE可以处理该对象。如果该方法失败,它将返回到main(),并解释出了什么问题。如果成功,它将将最终输出分配给左侧变量。

// 我们只有一个Builder,不是一个Array -- 以下代码将构建好的数据输出到一个适当的Array中。
std::shared_ptr<arrow::Array> days;  
ARROW_ASSIGN_OR_RAISE(days, int8builder.Finish());

一旦ArrayBuilder调用了其Finish方法,其状态将重置,因此它可以再次使用,就像它是全新的一样。因此,我们可以重复上面的过程,为我们的第二个数组做同样的操作:

// 由于Builder在填充Array时清除其状态,因此如果类型相同,我们可以重用Builder。
// 我们在这里重用Builder以获取月份值。
int8_t months_raw[5] = {1, 3, 5, 7, 1};  
ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw, 5));
std::shared_ptr<arrow

构建int16数组 

一旦在声明时指定了ArrayBuilder的类型,就无法更改其类型。当我们切换到需要至少16位整数的年份数据时,我们必须创建一个新的ArrayBuilder。当然,Arrow提供了相应的ArrayBuilder。它使用完全相同的方法,但具有新的数据类型:

  // 现在我们切换到int16,我们使用该数据类型的Builder。
  arrow::Int16Builder int16builder;
  int16_t years_raw[5] = {1990, 2000, 1995, 2000, 1995};
  ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw, 5));
  std::shared_ptr<arrow::Array> years;
  ARROW_ASSIGN_OR_RAISE(years, int16builder.Finish());

现在,我们有三个Arrow数组,类型上有一些差异。

创建RecordBatch 

只有当您有一个表时,列式数据格式才真正发挥作用。因此,让我们创建一个。我们首先要制作的是RecordBatch - 这在内部使用Arrays,这意味着每个列内的所有数据将是连续的,但任何附加或连接都需要复制。制作RecordBatch有两个步骤,给定现有的Arrays:

  • 定义模式 

  • 将模式和Arrays加载到构造函数中

定义模式 

为了开始制作RecordBatch,我们首先需要定义列的特征,每个特征由一个Field实例表示。每个Field包含其关联列的名称和数据类型;然后,一个Schema将它们组合在一起,并设置列的顺序,如下所示:

  // 现在,我们想要一个RecordBatch,其中包含列和列的标签。
  // 这会得到我们在Arrow中想要的2D数据结构。
  // 这些由模式定义,模式有字段 - 在这里我们准备好这两种对象类型。
  std::shared_ptr<arrow::Field> field_day, field_month, field_year;
  std::shared_ptr<arrow::Schema> schema;

  // 每个字段都需要其名称和数据类型。
  field_day = arrow::field("Day", arrow::int8());
  field_month = arrow::field("Month", arrow::int8());
  field_year = arrow::field("Year", arrow::int16());

  // 可以从字段的矢量构建模式,我们在这里这样做。
  schema = arrow::schema({field_day, field_month, field_year});
构建RecordBatch 

具有来自上一节的Arrays中的数据以及先前步骤中模式的情况下,我们可以制作RecordBatch。请注意,列的长度是必需的,并且所有列共享相同的长度。

  // 有了模式和充满数据的Arrays,我们就可以制作我们的RecordBatch!在这里,每列在内部是连续的。
  // 这与我们将在下一节看到的Tables相反。
  std::shared_ptr<arrow::RecordBatch> rbatch;
  // RecordBatch需要模式,列的长度,它们都必须匹配,
  // 以及实际的数据本身。
  rbatch = arrow::RecordBatch::Make(schema, days->length(), {days, months, years});

  std::cout << rbatch->ToString();

现在,我们将数据以漂亮的表格形式安全地放在RecordBatch中。我们将在后续的教程中讨论如何处理这些数据。


制作ChunkedArray 


假设我们想要一个由子数组组成的数组,因为这可以在连接时避免数据复制,用于并行化工作,将每个块可爱地切分到缓存中,或者超过标准Arrow Array的2,147,483,647行限制。为此,Arrow提供了ChunkedArray,它可以由单独的Arrow Arrays组成。在此示例中,我们可以重用之前制作的数组的部分来组成我们的Chunked数组,从而可以扩展它们而无需复制数据。因此,让我们使用相同的构建器建立几个新的Arrays:

  // 现在,让我们获取一些新的数组!它们的数据类型与上面相同,因此我们重用构建器。
  int8_t days_raw2[5] = {6, 12, 3, 30, 22};
  ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw2, 5));
  std.shared_ptr<arrow::Array> days2;
  ARROW_ASSIGN_OR_RAISE(days2, int8builder.Finish());

  int8_t months_raw2[5] = {5, 4, 11, 3, 2};
  ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw2, 5));
  std.shared_ptr<arrow::Array> months2;
  ARROW_ASSIGN_OR_RAISE(months2, int8builder.Finish());

  int16_t years_raw2[5] = {1980, 2001, 1915, 2020, 1996};
  ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw2, 5));
  std.shared_ptr<arrow::Array> years2;
  ARROW_ASSIGN_OR_RAISE(years2, int16builder.Finish());

为了支持在ChunkedArray的构建中具有任意数量的Arrays,Arrow提供了ArrayVector。这为Arrays提供了一个向量,我们将在这里使用它来准备制作ChunkedArray:

// ChunkedArrays允许我们拥有一系列数组,它们之间不是连续的。
// 首先,我们获取一个数组向量。
arrow::ArrayVector day_vecs{days, days2};

为了充分利用Arrow,我们需要采取最后一步,进入ChunkedArray:

// 然后,我们使用它来初始化一个ChunkedArray,可以与Arrow中的其他函数一起使用!这很好,因为使用普通数组向量无法做到这一点。
std::shared_ptr<arrow::ChunkedArray> day_chunks =
    std::make_shared<arrow::ChunkedArray>(day_vecs);

对于我们的day值,现在我们只需要重复相同的过程来处理月份和年份数据:

// 重复处理月份。
arrow::ArrayVector month_vecs{months, months2};
std::shared_ptr<arrow::ChunkedArray> month_chunks =
    std.make_shared<arrow::ChunkedArray>(month_vecs);

// 重复处理年份。
arrow::ArrayVector year_vecs{years, years2};
std::shared_ptr<arrow::ChunkedArray> year_chunks =
    std.make_shared<arrow::ChunkedArray>(year_vecs);

通过这样做,我们留下了三个不同类型的ChunkedArray。

创建Table 

我们可以使用前一节中的ChunkedArrays创建Tables。与RecordBatch类似,Table存储表格数据。但是,由于它由ChunkedArrays组成,Table不保证连续性。这对于逻辑、并行化工作、将块适应缓存或超出Array和因此RecordBatch中的2,147,483,647行限制可能很有用。

如果您阅读了RecordBatch的部分,您可能会注意到以下代码中的Table构造函数实际上是相同的,只是将列的长度放在位置3,并创建一个Table。我们重用了之前的模式,并制作了我们的Table:

// Table是我们需要的结构,用于这些非连续的列,并将它们都放在一个地方,以便我们可以像它们是正常数组一样使用它们。
std::shared_ptr<arrow::Table> table;
table = arrow::Table::Make(schema, {day_chunks, month_chunks, year_chunks}, 10);

std::cout << table->ToString();

现在,我们将数据以漂亮的表格形式安全地放在Table中。我们将在后续的教程中讨论如何处理这些数据。

结束程序 最后,我们只需返回Status::OK(),这样main()函数就知道我们已经完成了,一切都很好。

return arrow::Status::OK();

总结 通过这样做,您已经创建了Arrow中的基本数据结构,并可以继续在下一篇文章中使用文件I/O将它们输入和输出到程序中。


完整代码:

#include <arrow/api.h>

#include <iostream>

arrow::Status RunMain() {
  // Builder 是从不在磁盘上的现有值创建 Arrow 数组的主要方法。在这种情况下,我们将创建一个简单的数组,并将其提供。
  // 数据类型仍然很重要,每个兼容类型都有一个 Builder;
  // 在这种情况下,是 int8。
  arrow::Int8Builder int8builder;
  int8_t days_raw[5] = {1, 12, 17, 23, 28};
  // 正如所调用的 AppendValues 一样,将来自 days_raw 的 5 个值放入我们的 Builder 对象中。
  ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw, 5));
  // 但是我们只有一个 Builder,没有数组 —— 以下代码将构建好的数据输出到正规的数组中。
  std::shared_ptr<arrow::Array> days;
  ARROW_ASSIGN_OR_RAISE(days, int8builder.Finish());
  // 每次填充数组后,Builder 会清除其状态,因此如果类型相同,我们可以重复使用该 Builder。
  // 在这里,我们用于月份值。
  int8_t months_raw[5] = {1, 3, 5, 7, 1};
  ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw, 5));
  std.shared_ptr<arrow::Array> months;
  ARROW_ASSIGN_OR_RAISE(months, int8builder.Finish());
  // 现在,我们切换到 int16,使用该数据类型的 Builder。
  arrow::Int16Builder int16builder;
  int16_t years_raw[5] = {1990, 2000, 1995, 2000, 1995};
  ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw, 5));
  std.shared_ptr<arrow::Array> years;
  ARROW_ASSIGN_OR_RAISE(years, int16builder.Finish());
  // 现在,我们想要一个 RecordBatch,它具有列和相应列的标签。
  // 这使我们得到了 Arrow 中想要的二维数据结构。
  // 这些由模式定义,模式具有字段 —— 在这里我们准备好了这两种对象。
  std.shared_ptr<arrow::Field> field_day, field_month, field_year;
  std.shared_ptr<arrow::Schema> schema;

  // 每个字段都需要其名称和数据类型。
  field_day = arrow::field("Day", arrow::int8());
  field_month = arrow::field("Month", arrow::int8());
  field_year = arrow::field("Year", arrow::int16());

  // 模式可以从字段向量构建,我们在这里这样做。
  schema = arrow::schema({field_day, field_month, field_year});
  // 使用模式和充满数据的数组,我们可以制作 RecordBatch!在这里,
  // 每个列在内部是连续的。这与 Tables 相反,我们将会看到下一步。
  std.shared_ptr<arrow::RecordBatch> rbatch;
  // RecordBatch 需要模式、列的长度,所有这些必须匹配,
  // 以及实际数据本身。
  rbatch = arrow::RecordBatch::Make(schema, days->length(), {days, months, years});

  std::cout << rbatch->ToString();
  // 现在,让我们获得一些新的数组!数据类型与上面相同,因此我们可以重复使用生成器。
  int8_t days_raw2[5] = {6, 12, 3, 30, 22};
  ARROW_RETURN_NOT_OK(int8builder.AppendValues(days_raw2, 5));
  std::shared_ptr<arrow::Array> days2;
  ARROW_ASSIGN_OR_RAISE(days2, int8builder.Finish());

  int8_t months_raw2[5] = {5, 4, 11, 3, 2};
  ARROW_RETURN_NOT_OK(int8builder.AppendValues(months_raw2, 5));
  std::shared_ptr<arrow::Array> months2;
  ARROW_ASSIGN_OR_RAISE(months2, int8builder.Finish());

  int16_t years_raw2[5] = {1980, 2001, 1915, 2020, 1996};
  ARROW_RETURN_NOT_OK(int16builder.AppendValues(years_raw2, 5));
  std::shared_ptr<arrow::Array> years2;
  ARROW_ASSIGN_OR_RAISE(years2, int16builder.Finish());
  // ChunkedArrays 允许我们拥有一组不连续的数组,它们之间没有紧密排列。
  // 首先,我们获取一个数组的向量。
  arrow::ArrayVector day_vecs{days, days2};
  // 然后,我们使用它来初始化一个 ChunkedArray,可以与 Arrow 中的其他函数一起使用!
  // 这非常有用,因为拥有普通的数组向量不会让我们走太远。
  std::shared_ptr<arrow::ChunkedArray> day_chunks =
      std::make_shared<arrow::ChunkedArray>(day_vecs);
  // 重复处理月份。
  arrow::ArrayVector month_vecs{months, months2};
  std::shared_ptr<arrow::ChunkedArray> month_chunks =
      std::make_shared<arrow::ChunkedArray>(month_vecs);

  // 重复处理年份。
  arrow::ArrayVector year_vecs{years, years2};
  std::shared_ptr<arrow::ChunkedArray> year_chunks =
      std::make_shared<arrow::ChunkedArray>(year_vecs);

  // 表是我们需要的结构,用于处理这些不连续的列,并将它们保存在一个地方,以便我们可以像处理普通数组一样使用它们。
  std::shared_ptr<arrow::Table> table;
  table = arrow::Table::Make(schema, {day_chunks, month_chunks, year_chunks}, 10);

  std::cout << table->ToString();


  return arrow::Status::OK();
}

int main() {
  arrow::Status st = RunMain();
  if (!st.ok()) {
    std::cerr << st << std::endl;
    return 1;
  }
  return 0;
}


版权所有,转载本站文章请注明出处:云子量化, http://www.woniunote.com/article/338
上一篇:arrow系列4---arrow API规范(Conventions)
下一篇:arrow系列6 --- arrow读写文件(Arrow File I/O)