[C++][ADAPT]データ分析、処理ライブラリADAPTの更新情報(3)。

ほぼ10ヶ月ぶりの更新情報。と言っても放置していたわけではなく、Gnuplotライブラリを取り込んで整備するなど数多くの変更を行っていた。Gnuplotライブラリの詳細は別記事で投稿済みなので省略するとして、それ以外の追加機能に関するまとめである。そろそろ記事にしておかないと自分自身が忘れてしまいそうなので、備忘録を兼ねて。

ADAPTについての説明はこちらへ。
ADAPTのGitHubリポジトリはこちらへ。

SHist/DHistの追加

N次元ヒストグラムに似た構造のコンテナであるSHistとDHistを追加した。階層構造を持つという点ではSTree/DTreeと共通しているが、こちらの0層はヒストグラムの各ビンに相当し、1層にはそれらのビンに属す各要素の一覧が含まれる。
Placeholder、Traverser、Lambda関数などの使い方は他のコンテナと共通しているが、要素追加やアクセスの仕方でヒストグラムの性質に基づいた手法が用意されている。 詳細な使い方はquickstart_dhist.cppquickstart_shist.cppを参照。日本語ドキュメントを整備するのに疲れたので英語のみである。

    //1次元ヒストグラム様のDHistを作る例。
    using enum adapt::FieldType;
    // x軸を数学の点数とし、ビン幅10とする。ここでは省略しているが、ビン中心も指定できる。
    std::vector<adapt::AxisAttr> axis{ { "math", 10. } };
    std::vector<std::pair<std::string, adapt::FieldType>> additional_fields{ { "name", Str }, { "eng", F64 } { "jpn", F64 } };
    adapt::DHist h;
    h.SetAxesAttr(axis, additional_fields);

    // 第1引数は数学の点数で、この値とビン幅、ビン中心により格納されるビンが自動判定される。
    // 計算式は以下の通り。
    // bin_index[axis] = static_cast<int32_t>(std::floor((value[axis] - bin_center[axis]) / bin_width[axis]));
    // ヒストグラムの範囲はAppendによって追加された要素の軸値から自動的に必要な大きさまで拡大される。
    h.Append(std::forward_as_tuple(10), std::forward_as_tuple("濃伊田美衣子", 32, 46));
    h.Append(std::forward_as_tuple(91), std::forward_as_tuple("角兎野誠人", 76, 88));
    h.Append(std::forward_as_tuple(71), std::forward_as_tuple("子虚烏有花", 98, 85));

    ADAPT_GET_PLACEHOLDERS(h, math, name)
    adapt::Bin1D bin{ 1 };
    auto dummy_dummy_ko = h[bin][0];// bin{ 1 }には濃伊田美衣子のみが属す。
    // 各軸はF64(doulbe)型になっていることに注意。
    std::cout << dummy_dummy_ko[name].str() << ", " << dummy_dummy_ko[math].f64() << "点" << std::endl;
    // 濃伊田美衣子, 10点

なおSHist/DHistは他のコンテナから生成することもできる。どちらかと言えばこちらの使い方を主として想定している。ここで使用されているHist(ないしそのヘルパーであるADAPT_HIST)はマルチスレッド処理に対応している。

    adapt::DTree t;
    // 各生徒の試験点数が格納されたDTree tがあるとして、
    // x軸に数学、y軸に英語の点数を取った2次元ヒストグラムを生成する。
    ADAPT_GET_PLACEHOLDERS(t, math, eng, jpn)
    auto fmath = cast_f64(math);// 軸に取る値はF64(double)型である必要があるため、事前にキャストする。
    auto feng = cast_f64(eng);
    // jpnは各軸には使用されず、単に追加のフィールドとしてDHistに保持される。
    adapt::DHist h = t | Filter(...) | Hist(fmath, 10., feng, 10., jpn);

SHist/DHistにはもう少しいろいろな機能を追加するつもりだ。

  • 格納するビンの計算式の外部指定。例えば対数軸にしたい場合などに。
  • BinJoinの実装。つまり、特定のビンに含まれる要素へのJoin。
  • N次元(N>=2)のヒストグラムをある軸方向に射影してN-1次元以下に圧縮するProjection関数。
  • いくつかの軸でスライスする関数。
  • 軸方向に沿って走査するTraverser。
  • SHist/DHistからヒストグラムをプロットする機能。

上記はいずれも既存機能で代替できないことはないので、とりあえず後回しとなった。しかし汎用機能を使用する場合、どうしてもパフォーマンスで劣ったりコード記述量が増えたりするので、将来的には用意したいところ。

CrossJoitの追加

ADAPTのコンテナ類は2個以上のコンテナを連結するJoinをサポートしているが、新たにCrossJoinを追加した。簡単に言えば、複数のコンテナ要素の直積、つまり全ての要素の組み合わせを作るJoinである。 詳細な使い方はquickstart_join.cppQuickstartCrossJoin関数にまとめてある。

    adapt::DTree t1, t2;
    auto jt = Join(t0, 1_layer, -1_layer, t1);// t0の1層要素とt1要素とで直積を作る。
    jt.SetCrossJoint<1>();

Show関数をフォーマット可能に

簡易的な要素の表示用に用意していたShow関数を、std::formatと概ね同等のフォーマット指定に対応させた。前回の記事でstd::variant用のstd::formatterを作っていたが、実は本当の目的はこちらで、コンパイル時型情報を持たないRttiPlaceholderやRtti Lambdasを介したstd::formatを実現するために検証していたのだ。

    adapt::DTree t;
    ADAPT_GET_PLACEHOLDERS(t, name, math);
    t | Filter(...) | Show("{:>10} {:>6.1f}", name, mean(math));

ちなみに副産物であるが、std::formatterを用意したことにより、std::formatの引数にフィールドや計算結果を与えるとき、str()i32()のように型を明示する必要がなくなった。

    adapt::DTree t;
    ADAPT_GET_PLACEHOLDERS(t, name, math);
    auto ref = t[1][3];
    auto mean_math = mean(cast_f64(math));
    // 以前はref[name].str()やmean_math(t).f64()とする必要があった。
    // 今はformatter内で自動識別するので、与えなくてもよい。
    std::cout << std::format("{:>10} {:>6.1f}\n", ref[name], mean_math(t));

複数のコンテナの要素結合

同一の階層構造を持つコンテナが2個あったとする。例えば、あるクラスAの試験成績のデータaと、別のクラスBのデータbなどである。これらは階層構造もフィールドも全て共通した構造を持つが、格納されているデータは異なる。これらをまとめて1個のコンテナにするためのConcat関数を用意した。

    adapt::STree<...> a, b;
    a.Concat(b);// aの末尾にbの要素が全てコピーされる。
    a.MoveConcat(std::move(b));// bの要素をコピーではなくムーブしたい場合はこちら。bの要素は空になる(階層構造自体は保持される)。

様々なマクロを追加

私自身がADAPTに感じていた不満の一つ、階層構造定義やExtract関数呼び出しの面倒臭さを少しでも軽減するために、マクロを用意した。

    adapt::DTree dt;
    ADAPT_D_SET_TOP_LAYER(dt, nation, Str, capital, Str);
    ADAPT_D_ADD_LAYER(dt, state, Str, state_capital, Str);
    ADAPT_D_ADD_LAYER(dt, county, Str, county_seat, Str);
    ADAPT_D_ADD_LAYER(dt, city, Str, population, I32, area, F64);
    dt.VerifyStructure();

    using TopLayer = ADAPT_S_DEFINE_LAYER(nation, std::string, capital, std::string);
    using Layer0 = ADAPT_S_DEFINE_LAYER(state, std::string, state_capital, std::string);
    using Layer1 = ADAPT_S_DEFINE_LAYER(county, std::string, county_seat, std::string);
    using Layer2 = ADAPT_S_DEFINE_LAYER(city, std::string, population, int32_t, area, double);
    using STree_ = adapt::STree<TopLayer, Layer0, Layer1, Layer2>;
    STree_ st;

    ADAPT_GET_PLACEHOLDERS(dt, state, county, city, population, area);
    auto population_density = population / area;
    auto e = dt | Filter(population > 100000) | ADAPT_EXTRACT(state, city, population_density);
    auto h = dt | ADAPT_HIST(population, 500000., area, 200., city);

以前は次のように書く必要があった。

    adapt::DTree dt;
    dt.SetTopLayer({ { "nation", adapt::FieldType::Str }, { "capital", adapt::FieldType::Str } });
    dt.AddLayer({ { "state", adapt::FieldType::Str }, { "state_capital", adapt::FieldType::Str } });
    dt.AddLayer({ { "county", adapt::FieldType::Str }, { "county_seat", adapt::FieldType::Str }});
    dt.AddLayer({ { "city", adapt::FieldType::Str }, { "population", adapt::FieldType::I32 }, { "area", adapt::FieldType::F64 } });
    dt.VerifyStructure();

    using TopLayer = adapt::NamedTuple<adapt::Named<"nation", std::string>, adapt::Named<"capital", std::string>>;
    using Layer0 = adapt::NamedTuple<adapt::Named<"state", std::string>, adapt::Named<"state_capital", std::string>>;
    using Layer1 = adapt::NamedTuple<adapt::Named<"county", std::string>, adapt::Named<"county_seat", std::string>>;
    using Layer2 = adapt::NamedTuple<adapt::Named<"city", std::string>, adapt::Named<"population", int32_t>, adapt::Named<"area", double>>;
    using STree_ = adapt::STree<TopLayer, Layer0, Layer1, Layer2>;
    STree_ st;

    auto [state, county, city] = dt.GetPlaceholders("state"_fld, "county"_fld, "city"_fld, "population"_fld, "area"_fld);
    auto population_density = population / area;
    auto e = dt | Filter(population > 100000) | Extract(state.named("state"_fld), city.named("city"_fld), population_density.named("population_density"_fld));
    auto h = dt | Hist(population.named("population"_fld), 500000., area.named("area"_fld), 200., city.named("city"_fld));

フィールドの数が少ないのでそれほど短縮されたように見えないが、各層が10個とか20個とかフィールドを持つような巨大なデータを扱う場合は我ながら嫌気が差す記述量になっていたので、せめてもの軽減策である。

なお予め#define ADAPT_OMIT_MACRO_PREFIXとしておくことで、各マクロの接頭辞のADAPT_を省略しD_ADD_LAYER(...)のように書けるようになるが、EXTRACTとかHISTのようなごく短い名前が定義されてしまうことに注意。どうしても面倒くさい人向け。推奨はしない。

閑話

他にも色々と更新はしたはずだし、細々とした不具合修正なども行っているが、正直多すぎて覚えていない。今回もGitの履歴を眺めて思い返しながらまとめている。もっと丁寧に記事にまとめておくべきなのだが、誰も読まないであろう記事を更新するのは結構な気力を要するので、本業が非常に忙しかったここ半年ほどはちょっと難しかった。

というわけで、記事が上がらなかったからと言って更新意欲がなくなったわけではないのである。でもSIMD対応の方はちょっと放置気味。かなり動くようになったのだが、一部の仕様がちょっと難しくて悩んでいる。いっそC++26が実装されるまでは放置しても良いんじゃないかと思い始めていたりいなかったり。

……しかし、これほど色々な更新作業をしているにも関わらず、新しいC++知識を取り込んでいないせいか記事更新のネタが出てこないのが残念でならない。昔は月に2回とか更新してたのになぁ。コルーチンとかモジュールとかをしっかり勉強したいと思いつつも、なかなかまとまった時間が取れず実現していない(なおお盆期間中は新型コロナに苦しめられていた)。知識がどんどん時代遅れになっていく……。