C++でparquetファイルを作る
Apache Arrow, Parquet のC++パッケージをインストール
C++でparquetファイルを作るにはこちらの[https://arrow.apache.org/install/] C++ and GLib (C) Packages for... の部分を参考にしてまずは Apache ArrowとParquetのパッケージをインストールする。
筆者の場合Ubuntu 22.04を使っているので、
sudo apt update
sudo apt install -y -V ca-certificates lsb-release wget
wget https://apache.jfrog.io/artifactory/arrow/$(lsb_release --id --short | tr 'A-Z' 'a-z')/apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
sudo apt install -y -V ./apache-arrow-apt-source-latest-$(lsb_release --codename --short).deb
sudo apt update
sudo apt install -y -V libarrow-dev
sudo apt install -y -V libparquet-dev
最低限のサンプルコード
まずは"column1"という名前のdouble型の列に数値を詰めてtest.parquetに書き出すシンプルなプログラム。
#include <iostream>
#include <arrow/api.h>
#include <arrow/io/api.h>
#include <parquet/arrow/writer.h>
int main(int argc, char **argv)
{
auto pool = arrow::default_memory_pool();
// Define ArrayBuilder
arrow::DoubleBuilder builder;
// Append data to builders
PARQUET_THROW_NOT_OK(builder.Append(1.0));
PARQUET_THROW_NOT_OK(builder.Append(2.0));
PARQUET_THROW_NOT_OK(builder.Append(3.0));
// Fill finalized data to arrays
arrow::ArrayVector arrayVec;
std::shared_ptr<arrow::Array> array;
PARQUET_THROW_NOT_OK(builder.Finish(&array));
arrayVec.emplace_back(array);
// Create schema for the table
arrow::FieldVector fieldVec;
fieldVec.emplace_back(std::make_shared<arrow::Field>("column1", arrow::float64()));
auto schema = arrow::schema(fieldVec);
// Create arrow::Table from finalized arrays
auto table = arrow::Table::Make(schema, arrayVec);
// Open output parquet file
std::shared_ptr<arrow::io::FileOutputStream> outfile;
PARQUET_ASSIGN_OR_THROW(
outfile,
arrow::io::FileOutputStream::Open("test.parquet"));
PARQUET_THROW_NOT_OK(parquet::arrow::WriteTable(*table, pool, outfile));
return 0;
}
基本的にデータ型に対応したarrow::ArrayBuilder(DoubleBuilder, Int32Builder, ListBuilder, StructBuilderなど)を定義して値をAppend()していく。
最後にarrow::Arrayオブジェクトを定義してFinish(&array)を呼ぶことでファイナライズされたArrayができる。最終的にparquetファイルに書き出すのはarrow::Tableなので、それに必要なschema情報を定義する。
schemaはarrow::FieldVectorから作る。これはarrow::Fieldのvectorで、arrow::Fieldはカラム名とそのデータ型の定義が含まれている。Tableはarrow::Table::Make(スキーマ, ファイナライズされたアレイのvector)で作成する。
最後にファイルを開いてWriteTable()して完了。
確認のためpandasで開いてみる。
pip install pandas
してpythonを起動
Python 3.12.0 (main, Apr 15 2024, 01:05:02) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>> pd.read_parquet("test.parquet")
column1
0 1.0
1 2.0
2 3.0
>>>
カラムにアレイを詰める場合
アレイを詰める場合、ListBuilderと値を詰めるNumericBuilderを定義する。
ListBuilderのAppend()を呼んでから、アレイの長さ分NumericBuilderのAppend(value)を呼ぶ
auto double_builder = std::make_shared<arrow::DoubleBuilder>;
auto list_builder = std::make_shared<arrow::ListBuilder>(pool, double_builder);
PARQUET_THROW_NOT_OK(list_builder->Append());
PARQUET_THROW_NOT_OK(double_builder->Append(1.0));
PARQUET_THROW_NOT_OK(double_builder->Append(2.0));
PARQUET_THROW_NOT_OK(list_builder->Append());
PARQUET_THROW_NOT_OK(double_builder->Append(3.0));
PARQUET_THROW_NOT_OK(double_builder->Append(4.0));
スキーマの定義はarrow::list(arrow::type)
fieldVec.emplace_back(std::make_shared<arrow::Field>("column1", arrow::list(arrow::float64())));
出力parquetファイルはこんな感じになる。
Python 3.12.0 (main, Apr 15 2024, 01:05:02) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import pandas as pd
>>> pd.read_parquet("test.parquet")
column1
0 [1.0, 2.0]
1 [3.0, 4.0]
>>>
Structure を詰める場合
Structure を詰める場合、まずはstruct内のfieldを定義してしまう。
auto int_field = arrow::field("intColumn", arrow::int32());
auto list_field = arrow::field("listColumn", arrow::list(arrow::float64()));
auto struct_type = arrow::struct_({int_field, list_field});
struct_type
がstructureに対応する型。
今回は試しに int型とdouble型のアレイの二つを含むStructureにする。Builderに関しては、structure内部の値のbuilderを定義したのち、それらをstd::vectorに詰めて arrow::StructBuilderのコンストラクタにstruct_typeと共に渡す。
// Define ArrayBuilder
auto int_builder = std::make_shared<arrow::Int32Builder>();
auto double_builder = std::make_shared<arrow::DoubleBuilder>();
auto list_builder = std::make_shared<arrow::ListBuilder>(pool, double_builder);
std::vector<std::shared_ptr<arrow::ArrayBuilder>> builder_vec = {int_builder, list_builder};
auto struct_builder = std::make_shared<arrow::StructBuilder>(struct_type, pool, builder_vec);
データを詰める部分はこんな感じ
// Append data to builders
PARQUET_THROW_NOT_OK(struct_builder->Append());
PARQUET_THROW_NOT_OK(int_builder->Append(10));
PARQUET_THROW_NOT_OK(list_builder->Append());
PARQUET_THROW_NOT_OK(double_builder->Append(1.0));
PARQUET_THROW_NOT_OK(double_builder->Append(2.0));
PARQUET_THROW_NOT_OK(struct_builder->Append());
PARQUET_THROW_NOT_OK(int_builder->Append(20));
PARQUET_THROW_NOT_OK(list_builder->Append());
PARQUET_THROW_NOT_OK(double_builder->Append(3.0));
PARQUET_THROW_NOT_OK(double_builder->Append(4.0));
最後にstruct_builder->Finish()とschemaの定義をしてTableに書く部分はあまり変わらず。
// Fill finalized data to arrays
arrow::ArrayVector arrayVec;
std::shared_ptr<arrow::Array> array;
PARQUET_THROW_NOT_OK(struct_builder->Finish(&array));
arrayVec.emplace_back(array);
// Create schema for the table
arrow::FieldVector fieldVec;
fieldVec.emplace_back(std::make_shared<arrow::Field>("column1", struct_type));
auto schema = arrow::schema(fieldVec);
結果
>>> import pandas as pd
>>> pd.read_parquet("test.parquet")
column1
0 {'intColumn': 10, 'listColumn': [1.0, 2.0]}
1 {'intColumn': 20, 'listColumn': [3.0, 4.0]}
column1にStructureが詰められた。
ちょっと面倒くさいけどもこれで基本的なデータは大抵詰めることができるはず。
これでC++ ベースのCERN ROOTのTTreeもparquetにできそう。
Discussion