Red ArrowをベースとしたデータフレームライブラリであるRed Amberについて、下記の項目で開発に取り組んでいる。ここでは期間前半の取り組みの成果について報告する。
期間前半では下記のリリースを行なった。変更履歴とリリースノートは下記の通り。
活動内容を下記の項目に分けて報告する。
- 新機能の実装
- Red Arrowプロジェクトへのフィードバック
- パフォーマンスの向上
- コード品質の向上
- ドキュメント整備
- 普及活動
DataFrameCombinableモジュールにて、データフレームと他のデータフレームとの結合操作を実装した。
- v0.2.3(11/16リリース)で大部分を実装
- v0.3.0(12/18リリース)でハッシュによるキーの指定を追加
Red ArrowのTable#joinを用い、:typeオプションをプリセットした#left_join等のメソッドを実装する形で、Rに似たスタイルの結合機能を構築した。
Red Arrowでは左右のカラムを残す仕様であるので、left_outputs: 及び right_outputs: オプションを活用して、他のデータフレームと同様に必要なカラムだけを残し、必要ならばマージして一つのカラムを残すようにした。
Red Arrowでは重複したカラム名が許容されるが、データフレームやRDBのテーブルでは一般的にカラム名(キー)の重複は不可である。RedAmberでも重複したキーはサフィックスをつけてリネームする機能を実装した。サフィックスのデフォルトは suffix: '.1'
とし、otherのデータフレームのカラム名だけをリネームし、それでも重複する場合は succする仕様とした。これはselfとother両方のリネームは過剰であることと、Rubyではselfに対するメソッド呼び出しであるためselfの内容は優先的に保持されるべきと言う考えに基づく。
join_key
を省略した場合、自動的に共通するカラム名を使ってjoinする機能(Natural join)を実装した。これは Red ArrowのTableにも提案しマージされた(GH-15088)。
データフレームの集合的な演算も同様にRed Arrowの Table#joinを使って構築した。
概ねRの語彙に近いが、差演算は#setdiff
の代わりにRubyのArrayの差と同じ#difference
を主たるメソッド名として採用した。
列方向に長くする連結に#concatenate
、行方向に長くする連結に#merge
を割り当てた。
ベクトルの要素を空白文字または任意の文字で分割し、複数のベクトルに分ける、または長さ方向に並べたベクトルを生成する機能(split_*)を実装した。またベクトルの要素またはスカラーを連結した文字列を要素とするベクトルを生成するメソッド(merge)を実装した(v0.3.0で実装)。
- ベクトルの要素を空白文字または任意の文字で分割し、複数のベクトルに分ける。
vector = RedAmber::Vector.new(['a b', 'c d', 'e f'])
vector
#=>
#<RedAmber::Vector(:string, size=3):0x0000000000050014>
["a b", "c d", "e f"]
vector.split_to_columns
#=>
[#<RedAmber::Vector(:string, size=3):0x0000000000058cc8>
["a", "c", "e"]
,
#<RedAmber::Vector(:string, size=3):0x0000000000058cdc>
["b", "d", "f"]
]
このメソッドはデータフレームの列を特定の文字で分割する用途で使える。
RedAmber::DataFrame.new(year_month: %w[2023-01 2023-02 2023-03])
.assign(:year, :month) { year_month.split_to_columns('-') }
#=>
#<RedAmber::DataFrame : 3 x 3 Vectors, 0x0000000000078ed8>
year_month year month
<string> <string> <string>
0 2023-01 2023 01
1 2023-02 2023 02
2 2023-03 2023 03
このメソッドはsepを省略した場合、Arrowの ascii_split_whitespace()
を使いArrow::StringArrayの要素を空白文字で高速に分割する。一方sepを指定した場合は RubyのString#sep
を使うので例えば正規表現を指定して柔軟な分割を行うことができる。
RedAmber::DataFrame.new(yearmonth: %w[202301 202302 202303])
.assign(:year, :month) { yearmonth.split_to_columns(/(?=..$)/) }
#=>
#<RedAmber::DataFrame : 3 x 3 Vectors, 0x0000000000078eec>
yearmonth year month
<string> <string> <string>
0 202301 2023 01
1 202302 2023 02
2 202303 2023 03
- ベクトルの要素を空白文字または任意の文字で分割し、長さ方向に並べたベクトルを生成する。
vector
#=>
#<RedAmber::Vector(:string, size=3):0x0000000000050014>
["a b", "c d", "e f"]
vector.split_to_rows
#=>
#<RedAmber::Vector(:string, size=6):0x00000000000809d0>
["a", "b", "c", "d", "e", "f"]
- 文字列またはベクトルを要素毎にselfに連結した文字列を要素とするベクトルを生成する。
vector = RedAmber::Vector.new(%w[a c e])
other = RedAmber::Vector.new(%w[b d f])
vector.merge(other)
#=>
#<RedAmber::Vector(:string, size=3):0x00000000000a530c>
["a b", "c d", "e f"]
vector.merge('x', sep: '')
#=>
#<RedAmber::Vector(:string, size=3):0x00000000000b1008>
["ax", "cx", "ex"]
- 欠損値処理、ウィンドウ処理:23年1月予定
- データエンコーディング:23年2月予定
- その他:随時
RedAmberを開発する中で遭遇したバグや機能改善の提案を随時Red Arrowにフィードバックしている。基本的な機能は積極的にRed Arrowに移していきたい。
- バグ報告
- CIのhomebrewのバグ報告と修正 (GH-15093):マージ済
- RedAmberで使用している機能の改善提案
- 機能/改善提案
- Table#saveでcsvをセーブする際にselfを返すことでREPL環境での待ち時間を減らす( GH-15289):提案中
第一段階として、主要メソッドのバージョン間のパフォーマンス比較を行えるように、ベンチマークを作成した(v0.2.3)。ベンチマークは benchmark_driver を使い、データは主としてRDatasetのうち比較的データサイズが大きい nycflights13 データセットを使用した。
第二段階として、コードの全面的な見直しを行い、速い処理への置き換え、処理の順番の変更、不要な処理の削除等のリファクタリングを行い処理速度を向上させた。下記にバージョン毎の比較結果を示す。v0.3.0がリファクタリング後のバージョン、v0.2.3はほぼ機能が同じである直前のバージョン、v0.2.0は開発助成期間前の基準となるバージョンである。
計測は下記の環境で行なった。
- distro: Ubuntu 20.04.5 LTS on Windows 11 x86_64
- kernel: 5.15.79.1-microsoft-standard-WSL2
- cpu: Intel i7-8700K (12) @ 3.695GHz
- memory: 30085MiB
- Ruby: ruby 3.2.0 (2022-12-25 revision a528908271) +YJIT [x86_64-linux]
- Arrow: 10.0.0
Basicベンチマーク: データフレームの基本的な操作に対するテスト
Iteration per second (i/s):
# | Benchmark name | 0.3.0 | 0.2.3 | 0.2.0 | 0.1.5 |
---|---|---|---|---|---|
B01 | Pick([]) by a key name | 434,783 | 8,759 | 9,357 | 202,703 |
B02a | Pick([]) by key names | 2,530 | 897 | 1,898 | 2,276 |
B03 | Pick by key names | 2,783 | 653 | 4,374 | 2,311 |
B04 | Drop by key names | 694 | 352 | 761 | 675 |
B05 | Pick by booleans | 792 | 383 | 1,094 | 1,005 |
B06 | Pick by a block | 920 | 386 | 1,346 | 1,091 |
B07 | Slice([]) by a index | 597 | 445 | 798 | 1,934 |
B08 | Slice by indeces | 51.4 | 47.1 | 51.7 | 56.2 |
B09 | Slice([]) by booleans | 54.7 | 2.3 | 2.3 | 0.3 |
B10 | Slice by booleans | 103.3 | 2.3 | 2.2 | 3.0 |
B11 | Remove by booleans | 78.6 | 2.2 | 2.4 | 2.7 |
B12 | Slice by a block | 100.9 | 2.4 | 2.3 | 3.0 |
B13 | Rename by Hash | 804 | 508 | 853 | 737 |
B14 | Assign an existing variable | 3.2 | 3.2 | 3.3 | 3.4 |
B15 | Assign a new variable | 3.3 | 3.4 | 3.3 | 3.5 |
B16 | Sort by a key | 18.5 | 19.3 | 20.0 | 18.4 |
B17 | Sort by keys | 11.8 | 11.6 | 12.0 | 12.1 |
B18 | Convert to a Hash | 2.8 | 2.3 | 2.4 | 2.3 |
B19 | Output in TDR style | 1.3 | 1.3 | 1.3 | 1.3 |
B20 | Inspect | 17.0 | 14.7 | 16.6 | 1.7 |
Combineベンチマーク: データフレームの結合操作に対するテスト
Iteration per second (i/s):
# | Benchmark name | 0.3.0 | 0.2.3 |
---|---|---|---|
C01 | Inner join on flights_Q1 by carrier | 106.3 | 0.9 |
C02 | Full join on flights_Q1 by planes | 0.9 | 0.6 |
C03 | Left join on flights_Q1 by planes | 70.6 | 0.6 |
C04 | Semi join on flights_Q1 by planes | 103.9 | 100.5 |
C05 | Anti join on flights_Q1 by planes | 244.2 | 230.4 |
C06 | Intersection of flights_1_2 and flights_1_3 | 46.8 | 0.2 |
C07 | Union of flights_1_2 and flights_1_3 | 0.07 | 0.07 |
C08 | Difference between flights_1_2 and flights_1_3 | 51.5 | 53.1 |
C09 | Concatenate flight_Q1 on flight_Q2 | 7,393 | 2,903 |
C10 | Merge flights_Q1_right on flights_Q1_left | 0.6 | 0.6 |
Groupベンチマーク: Group関連の操作に関するテスト
Iteration per second (i/s):
# | Benchmark name | 0.3.0 | 0.2.3 | 0.2.2 |
---|---|---|---|---|
G01 | sum distance by destination | 119.9 | 122.5 | 120.3 |
G02 | sum arr_delay by month and day | 168.4 | 155.8 | 140.8 |
G03 | sum arr_delay, mean distance by flight | 29.6 | 25.6 | 27.8 |
G04 | mean air_time, distance by flight | 110.5 | 102.0 | 102.9 |
G05 | sum dep_delay, arr_delay by carrer | 123.6 | 121.3 | 111.0 |
Reshapeベンチマーク: Reshape関連の操作に関するテスト
Iteration per second (i/s):
# | Benchmark name | 0.3.0 | 0.2.3 | 0.2.2 |
---|---|---|---|---|
R01 | Transpose a DataFrame | 3.8 | 3.4 | 3.7 |
R02 | Reshape to longer DataFrame | 1.5 | 1.6 | 1.6 |
R03 | Reshape to wider DataFrame | 0.7 | 0.6 | 0.7 |
Vectorベンチマーク: Vectorの操作に関するテスト
Iteration per second (i/s):
# | Benchmark name | 0.3.0 | 0.2.3 | 0.2.0 |
---|---|---|---|---|
V01 | Vector.new from integer Array | 7.2 | 6.0 | 6.4 |
V02 | Vector.new from string Array | 1.6 | 1.7 | 1.7 |
V03 | Vector.new from boolean Vector | 1,220,000 | 6.6 | 6.7 |
V04 | Vector#sum | 11,256 | 11,624 | 10,823 |
V05 | Vector#* | 1,397 | 1,527 | 1,466 |
V06 | Vector#[booleans] | 4.8 | 6.8 | 6.8 |
V07 | Vector#[boolean_vector] | 22.2 | 6.6 | 6.7 |
V08 | Vector#[index_vector] | 22.0 | 28.0 | 27.6 |
V09 | Vector#replace | 0.4 | 0.4 | 0.4 |
V10 | Vector#replace with broad casting | 0.4 | 0.4 | 0.4 |
DataFrameベンチマーク: データフレームの一連の操作に対する総合的なパフォーマンスのテスト
Iteration per second (i/s):
# | Benchmark name | 0.3.0 | 0.2.3 | 0.2.0 |
---|---|---|---|---|
D01 | Diamonds test | 189.8 | 14.5 | 14.5 |
D02 | Starwars test | 143.6 | 78.8 | 107.0 |
D03 | Import cars test | 141.4 | 141.9 | 125.6 |
D04 | Simpsons paradox test | 45.4 | 3.1 | 3.1 |
- Diamonds test : RedAmberのREADMEで使用例として使っているもの
- Starwars test : RedAmberのREADMEで使用例として使っているもの
- Import cars test : RedAmberのドキュメント DataFrame.mdで使用例として使っているものをアレンジ
- Simpsons paradox test : Qiitaの記事 「RedAmber - Rubyのデータフレームライブラリ」で紹介
この総合的な4つのテストのイテレーション回数(毎秒)を実行時間に変換して合計の実行時間を求め、実行速度の変化率を求める。
require 'red_amber'
df = RedAmber::DataFrame.load(Arrow::Buffer.new(<<CSV), format: :csv)
test_name,0.3.0,0.2.3,0.2.0
D01: Diamonds test,189.817,14.531,14.540
D02: Starwars test,143.570,78.772,107.044
D03: Inport cars test,141.395,141.861,125.560
D04: Simpsons paradox test,45.353,3.105,3.133
CSV
df
#=>
#<RedAmber::DataFrame : 4 x 4 Vectors, 0x000000000007e8d8>
test_name 0.3.0 0.2.3 0.2.0
<string> <double> <double> <double>
0 D01: Diamonds test 189.82 14.53 14.54
1 D02: Starwars test 143.57 78.77 107.04
2 D03: Inport cars test 141.4 141.86 125.56
3 D04: Simpsons paradox test 45.35 3.11 3.13
versions = df.keys[1..]
#=> [:"0.3.0", :"0.2.3", :"0.2.0"]
versions.map { |ver| (1 / df[ver]).sum } => a
#=> [0.04135511938110967, 0.41062359984495833, 0.4052649554075024]
a[2] / a[0]
#=>
9.799632100508957
以上のことから、基本的な一連のデータフレーム操作をベンチマークを対象として 、当初目標のv0.2.0比 20% のパフォーマンス向上の目標を大幅に超えて、v0.2.0比で 979% の速度向上を達成した。
比較的遅いマシンではさらに高い比率で向上しており、
- OS: macOS 11.7.2 20G1020 x86_64
- Machine: MacBookPro11,1 (Retina, 13-inch, Late 2013)
- CPU: Intel i5-4258U (4) @ 2.40GHz
- Memory: 5554MiB / 8192MiB
の環境では、v0.2.0比で 1175% の向上であった。
スケーラビリティの評価にも使える大規模で一般的なデータセットとして、過去にデータベースの評価に使われてきた'Wisconsin Benchmark'の機械合成データセットを試行中。
また、他の言語のデータフレームライブラリ(pandasやR)に対する立ち位置を明確にする目的で、ライブラリ間の比較も行なっていきたい。
Test coverageの計測のために simplecovを導入した(v0.2.3)。導入した時点でのカバー率は98.54%であり、43行のカバーされていない行が存在していた。
コードのリファクタリングと共にカバー率の向上にも取組み、v0.3.0で100%のカバーを達成した。今後はカバー率の維持を図る。
YARDドキュメントカバー率は現在73.1%である。
コンポーネント | メソッドの説明 | 引数 | Example | |
---|---|---|---|---|
✓ | DataFrame combinable | ✓ 完了 | ✓ 完了 | ✓ 完了 |
DataFrame displayable | △ 作成中 | _ 未着手 | _ 未着手 | |
DataFrame indexable | △ 作成中 | △ 作成中 | _ 未着手 | |
DataFrame loadsave | △ 作成中 | △ 作成中 | _ 未着手 | |
DataFrame reshaping | △ 作成中 | △ 作成中 | _ 未着手 | |
DataFrame selectable | △ 作成中 | △ 作成中 | _ 未着手 | |
DataFrame variable_operation | △ 作成中 | △ 作成中 | _ 未着手 | |
DataFrame | △ 作成中 | △ 作成中 | _ 未着手 | |
Group | △ 作成中 | △ 作成中 | _ 未着手 | |
Refinements | △ 作成中 | _ 未着手 | _ 未着手 | |
Vector functions | △ 作成中 | _ 未着手 | _ 未着手 | |
Vector selectable | △ 作成中 | _ 未着手 | _ 未着手 | |
Vector updatable | △ 作成中 | _ 未着手 | _ 未着手 | |
Vector | △ 作成中 | _ 未着手 | _ 未着手 |
YARDドキュメントカバー率100%が23年3月の目標である。全てのメソッドについてmarkdown形式のドキュメントとしてDataFrame.mdとVector.mdは完成済みであるので、@exampleはそれらを反映する形でYARDのドキュメントを完成させていきたい。
- webへの記事投稿
- RedAmber - Rubyの新しいデータフレームライブラリ (2022年11月28日, note)
- RedAmber - Rubyのデータフレームライブラリ, (2022年12月04日, Qiita)
- RedAmberでやろうとしていること, (2022年12月18日, Qiita)
- オンライン配信
- Red-data-toolsの月例配信に11月の回から質問者として参加中
以上
ベンチマーク比較用のバーチャートの作成は、下記の手順でデータをRedAmberのデータフレームとして読み込み、縦持ちのデータに変換し、Chartyでプロットして作成した。