Apache Arrowというデータ分析に特化したインメモリーのデータ操作ライブラリーがあります。
Apache Arrowは多くのシステムで使われることを目指しています。そうなることで各システム間でデータ交換しやすくなるからです。現時点では、Java、C++、JavaScriptは同じ仕様をそれぞれの言語で実装しています。Python、RubyにはC++のバインディングがあり、C++での実装をPython、Rubyで使えます。たとえば、Rubyでの使い方は名古屋Ruby会議03:Apache ArrowのRubyバインディングをGObject Introspectionでを参照してください。
このようにApache Arrowにはいくつかの実装がありますが、ここでは何回かにわけてC++実装の使い方を紹介します。
配列
Apache Arrowはカラム指向のデータ構造になっています。カラム指向の方が高速にデータ分析処理できるからです。
カラム指向のデータ構造というのは、1つのレコード(レコードには複数のカラムがある)をレコード単位で持つのではなく、カラム単位で持つデータ構造です。Apache Arrowのサイトの「Performance Advantage of Columnar In-Memory」に図があるのでピンとこない人はその図を参照してください。
カラム指向のデータ構造にするために、Apache Arrowでは配列が主なデータ構造になります。32bit整数のカラムも配列で表現しますし、文字列のカラムも辞書のカラム(Pythonで言えばdict
、Rubyで言えばHash
で、Apache Arrowは辞書のような複合型もサポートしている)も配列で表現します。
32bit整数のように固定長データのカラムを配列で表現するのはイメージしやすいでしょう。単に固定長(32bit整数なら4バイト)の領域をレコード数分だけ連続して確保してそこに値を詰めていけばよいです。
一方、文字列のような可変長データや辞書のような複合型をどのように配列で表現するかはイメージしにくいかもしれません。詳細はおいおい紹介しますが、たとえば、文字列は2本の配列で表現します。1つ目の配列には文字列が連続して詰まっています。もう1つの配列にはどこからどこまでが1つの文字列かという位置情報が詰まっています。たとえば、["abc", "d", "ef"]
という文字列カラムは次のように表現します。
data = ["a", "b", "c", "d", "e", "f"]
offsets = [
0, # 0番目のカラムの値の開始オフセット→data[0] == "a"
3, # 0番目のカラムの値の長さ→data[0] + data[1] + data[2] == "abc"
3, # 1番目のカラムの値の開始オフセット→data[3] == "d"
1, # 1番目のカラムの値の長さ→data[3] == "d"
4, # 2番目のカラムの値の開始オフセット→data[4] == "e"
5, # 2番目のカラムの値の長さ→data[4] + data[5] == "ef"
]
このような構造になっていると定数時間で目的のカラムの値にアクセスできますし、データが局所化されているので連続してアクセスするときも高速です。(データ分析時は集計処理やフィルター処理をしますが、そのようなときは連続してアクセスします。)
このようにApache Arrowはいろいろな型のカラムを配列で表現します。配列は変更できないという制約があります。この制約があるため、少しずつ配列を作っていくということはできません。作るときは一気に作らないといけません。(この制約があるので、配列間でデータを共有でき、サブ配列の作成コストが安くなるというメリットがあるのですが、そのあたりはおいおい紹介します。)
配列を作るのが大変そうと感じるかもしれませんが、配列を作ることを支援するために配列ビルダーが提供されているので、それを使えばそれほど大変ではありません。
配列ビルダー
配列ビルダーとは配列を作るオブジェクトです。配列を作るときは配列ビルダーを使うのが便利です。
配列ビルダーを使う基本的な流れは次の通りです。
-
配列ビルダーを作る。
-
Append()
で要素を追加する。(要素がNULLの場合はAppendNull()
で要素を追加する。) -
Finish()
でそれまで追加した要素で配列を作る。
たとえば、真偽値の配列を作るには次のようにarrow::BooleanBuilder
を使います。
#include <iostream>
#include <arrow/api.h>
int
main(int argc, char **argv)
{
std::shared_ptr<arrow::Array> array;
{
// 真偽値の配列データを確保する領域
auto memory_pool = arrow::default_memory_pool();
// 真偽値の配列を作成するビルダー
arrow::BooleanBuilder builder(memory_pool);
// 1つ目の要素はtrue
builder.Append(true);
// 2つ目の要素はfalse
builder.Append(false);
// 3つ目の要素はtrue
builder.Append(true);
// 3要素の配列を作成
builder.Finish(&array);
}
// 内容を確認
// 出力:
// 0:true
// 1:false
// 2:true
for (int64_t i = 0; i < array->length(); ++i) {
auto boolean_array = static_cast<arrow::BooleanArray *>(array.get());
std::cout << i << ":";
// i番目の値を出力
std::cout << (boolean_array->Value(i) ? "true" : "false");
std::cout << std::endl;
}
return 0;
}
Apache Arrowはpkg-configに対応しているのでビルド方法は次の通りです。
% g++ -o boolean-array boolean-array.cpp $(pkg-config --cflags --libs arrow)
実行すると次のように[true, false, true]
という配列を作れたことを確認できます。
% ./boolean-array
0:true
1:false
2:true
NULL配列や共用体配列のようにすぐに作れる配列には配列ビルダーはありませんが、基本的には配列ビルダーを使って配列を作ります。
まとめ
Apache Arrowでの配列の位置付けと配列ビルダーを使った配列の作り方を紹介しました。