🐡

C++でparquetファイルを作る

2024/08/26に公開

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