Rubyアドベントカレンダー2022の18日目の記事です。
タイトルにあるRedAmberとは何かについては、それぞれ違う例を使いながらqiitaとnoteに記事を書いたので良かったら見てみてください。
とってもざっくりと言うと、Pythonにおけるpandas、Rにおけるdata.frame, dplyr/tidyrがやるようなことをRubyでできるようにするライブラリです。
2022年度Rubyアソシエーション開発助成に採択されたプロジェクトとして開発を進めています。
今日は、RedAmberで私がやろうとしていることをお話しします。
1. Rubyでデータサイエンスしたい
一番の目的はこれです。Rubyはデータ処理の分野が弱いです。それでも書きやすいRubyでデータサイエンスっぽいことをしてみたいです。そんな中、Red Data Toolsプロジェクトを始めとしていくつかのライブラリが頑張っています。特に、Apache Arrowプロジェクトの中にはRuby実装としてRed Arrowがあり、データサイエンスの分野でRubyを使う環境は少しずつ整ってきています。Ruby本体もRuby3になって速くなりましたし、もうすぐ3.2も出ます。
RedAmberはそのような環境の中でRed Arrowよりもユーザー寄りなレイヤーに当たるデータフレームを使いやすく提供する目的で作られています。
2. PythonやRのデータフレームよりも使いやすいツールを作りたい
私も (仕方なく) RのdplyrやPythonのpandasを使っていましたが、Rubyならこんな感じに書けそうだな、とか、ここが使いにくいと思うことがいくつかありました。(例えばRのパイプ、pandasのloc/iloc/at/iatのあたりなど)
なにより、データ分析の手前のデータの下準備のようなところでは、Rubyの記述力を生かせるはずなのでRubyで使えるツールは必要なはずだと思っていました。こう言った経験を踏まえてRedAmberでやろうとしていることは次のようなことです。
2.1 Ruby汎用のクラスを生かしたシンプルなAPI
Rubyの組み込みクラスは非常に強力なので、これを生かすAPIができれば少ないメソッドの組み合わせで様々な表現ができると考えました。
RedAmberのデータフレームの基本操作は、
- pick/drop で列単位の操作
- slice/remove で行単位の操作
- rename でキーの名前の変更
- assign で列データの変更/新設
ができます。図で書くとこんな感じ。
行と列のアクセスメソッドが分けられているため、(行, 列)のような形式でインデックスを指定する必要がありません。代わりに行の指定を長くして、slice(0, 3, 5..10, -5..-1)
のように書くこともできます。
データフレームの列ラベルの一覧は、DataFrame#keys
で取れますが、これはSymbolのArrayです。RedAmberではインデックスクラスやラベルクラスのようなクラスを作る代わりにRuby標準のコレクションクラスを多用しています。これはRubyのクラスは十分に強力であるためであり、ルールをシンプルにして組み合わせで豊富な機能を実現したい、という思想に基づいているからです。
2.2. ブロックを使うこと
ブロックはRuby最大の特徴ではないでしょうか。ブロックで詳細な動作をカスタマイズできるような使い方がRubyらしい書き方につながると思います。
RedAmberでは上の図のメソッドはブロックも受け付けて処理をカスタマイズできます。例えばslice
はインデックスまたはブーリアン配列を返すようなブロックを取ることができます。
penguins.slice do |df|
length = df.bill_length_mm
min = length.mean - length.std
max = length.mean + length.std
(length >= min) & (bill_length <= max)
end
#=> bill_length_mmが2σの範囲にあるレコードをデータフレームで返す
ブロックの中ではVector(列)が受け付ける関数的なメソッド(上で言うと#mean, #std, #>=, #&, #<= が該当)を使いながらRubyの式として複雑な処理が書けます。Vectorのメソッドは Apache ArrowのC++実装由来の豊富な関数 の多くを使えるようにしてあります。
2.3. 暗黙のルールを駆使する
データフレームの変形(Reshaping)は、例えばmessyとtidyの変換(ワイドとロングの変換)がありますが期待される動作と必要なオプションは覚えにくいと思います。
Rubyでは明示的な指定よりも、直観に反しない範囲で暗黙的に行われる動作が好まれる文化があると思います。RedAmberでは暗黙の動作で使いやすくなるようにReshapingの操作を設計しました。
ここで左側にあるtransposeは行/列を入れ替える操作ですが、何も指定しなければ一番左にある列を新しいキーとみなして入れ替えを行います
uri = URI("https://raw.githubusercontent.com/heronshoes/red_amber/master/test/entity/import_cars.tsv")
import_cars = RedAmber::DataFrame.load(uri)
#=>
#<RedAmber::DataFrame : 5 x 6 Vectors, 0x000000000000f230>
Year Audi BMW BMW_MINI Mercedes-Benz VW
<int64> <int64> <int64> <int64> <int64> <int64>
0 2017 28336 52527 25427 68221 49040
1 2018 26473 50982 25984 67554 51961
2 2019 24222 46814 23813 66553 46794
3 2020 22304 35712 20196 57041 36576
4 2021 22535 35905 18211 51722 35215
このデータに対して、transposeを引数無しで適用すると、一番左の列が新しいキーになり、元のキーが一番左の列になります。一番左の列の名前は、指定がないので既定値のNAME
となります。
import_cars.transpose
#=>
#<RedAmber::DataFrame : 5 x 6 Vectors, 0x000000000000f244>
NAME 2017 2018 2019 2020 2021
<string> <uint32> <uint32> <uint32> <uint16> <uint16>
0 Audi 28336 26473 24222 22304 22535
1 BMW 52527 50982 46814 35712 35905
2 BMW_MINI 25427 25984 23813 20196 18211
3 Mercedes-Benz 68221 67554 66553 57041 51722
4 VW 49040 51961 46794 36576 35215
実は、transposeは既定値:NAMEと同じ名前の:nameオプションを受け付けて、新しくできる名前の設定ができるように作ってあるので、オプション名を覚えていなくても思い出して使えるようになっています。
import_cars.transpose(name: :Manufacturer)
#=>
#<RedAmber::DataFrame : 5 x 6 Vectors, 0x000000000000f258>
Manufacturer 2017 2018 2019 2020 2021
<string> <uint32> <uint32> <uint32> <uint16> <uint16>
0 Audi 28336 26473 24222 22304 22535
1 BMW 52527 50982 46814 35712 35905
2 BMW_MINI 25427 25984 23813 20196 18211
3 Mercedes-Benz 68221 67554 66553 57041 51722
4 VW 49040 51961 46794 36576 35215
この辺りはRやpandasの使用例を眺めながら、RedAmberではどのようにするのが使いやすいか自問しながら作っています。
3. コミットメッセージの絵文字を普及させる
RedAmberでやろうとしていることの3番目はこれです。
RedAmberではコミットメッセージの先頭に絵文字を使って分類しています。
例えば新メソッドの追加では (zap)、リファクタリングは (recycle)、ドキュメントの改善は (memo)、テストの改善は (white_check_mark)などとしています。独自なのは バグフィックスで 🦋 (butterfly)を使うことです。
バグフィックスで文字通り (bug)を使うのは良くと思うあるのですが、バグ治った感が出せると思うので🦋を広めていきたいです。
ちなみにmacでは青くてきれいな蝶なのですがWindowsでは赤い蛾のような絵文字なのでちょっと残念です。またQiitaでは :butterfly: って出ないんですね。
絵文字は、changelogをまとめる際に種類別に分類しやすくなるので便利、という長所もあります。私が使っている絵文字の一覧は、gistにまとめてあるのでご覧ください。VS codeのgit graphで出てこない絵文字を追加する設定も置いています。
4. 使う側から作る側の活動へ
私はこれまでユーザーとしてRubyを使ってきましたが、今年になって初めてオープンソースソフトウェアの活動に参加しました。今は特にRubyのメタプログラミングの部分で技術不足を感じていますが、成果物がこうして出来つつありますのでやってみてよかったと思っています。
Rubyを使うだけという立場の方は多いと思うので、Rubyの世界をもっと広げていくために私の経験を生かしていけるような機会を作っていきたいと思っています。
おわりに
今日、RedAmber 0.3.0 をリリースしました。前バージョンの0.2.3までは機能追加するたびにだんだん遅くなっていましたが、ベンチマークを入れてコードを全面的に見直し、qiitaとnoteの紹介記事で例に挙げた処理をベンチマークとして 11.7倍の高速化を実現しました。
それでもまだまだ遅いですが、コードの書き味には特徴あると思いますので試してみていただけると嬉しいです。
また、テストカバレッジをsimplecovで測定して100%まで引き上げました。修正の中でテストが書かれていないraiseがいくつかみつかりましたし、後置ifで1行だったためカバーされているように見えてしまっていたraiseもありました(RubocopのLineLengthを90に下げたので見つかった)。今後はカバレッジを維持していきます。
最後にRedAmberに関するリソースを紹介します。
-
GitHubのdiscussionsで基本的な操作をプレゼンスタイルで解説しています。
https://github.com/heronshoes/red_amber/discussions/96