Skip to content

Instantly share code, notes, and snippets.

@heronshoes
Last active January 15, 2023 01:45
Show Gist options
  • Save heronshoes/c2a1c9d10d8ef1f8d28da8ad13b6d0fd to your computer and use it in GitHub Desktop.
Save heronshoes/c2a1c9d10d8ef1f8d28da8ad13b6d0fd to your computer and use it in GitHub Desktop.
Rubyアソシエーション開発助成2022 中間報告

Rubyアソシエーション開発助成2022 中間報告 [RedAmber - A simple dataframe library for Ruby]

 Red ArrowをベースとしたデータフレームライブラリであるRed Amberについて、下記の項目で開発に取り組んでいる。ここでは期間前半の取り組みの成果について報告する。

期間前半では下記のリリースを行なった。変更履歴とリリースノートは下記の通り。

活動内容を下記の項目に分けて報告する。

  1. 新機能の実装
  2. Red Arrowプロジェクトへのフィードバック
  3. パフォーマンスの向上
  4. コード品質の向上
  5. ドキュメント整備
  6. 普及活動

1. 新機能の実装

1.1 データフレームの結合(join)関連機能の実装

 DataFrameCombinableモジュールにて、データフレームと他のデータフレームとの結合操作を実装した。

  • v0.2.3(11/16リリース)で大部分を実装
  • v0.3.0(12/18リリース)でハッシュによるキーの指定を追加

  join

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を使って構築した。

dataframe set and binding image

概ねRの語彙に近いが、差演算は#setdiffの代わりにRubyのArrayの差と同じ#differenceを主たるメソッド名として採用した。

列方向に長くする連結に#concatenate、行方向に長くする連結に#mergeを割り当てた。

1.2 列分割/列結合機能

ベクトルの要素を空白文字または任意の文字で分割し、複数のベクトルに分ける、または長さ方向に並べたベクトルを生成する機能(split_*)を実装した。またベクトルの要素またはスカラーを連結した文字列を要素とするベクトルを生成するメソッド(merge)を実装した(v0.3.0で実装)。

split_to_columns(sep = nil, limit = 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

split_to_rows(sep = nil, limit = 0)

  • ベクトルの要素を空白文字または任意の文字で分割し、長さ方向に並べたベクトルを生成する。
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"]

merge(other, sep: ' ')

  • 文字列またはベクトルを要素毎に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"]

1.3 今後の追加予定

  • 欠損値処理、ウィンドウ処理:23年1月予定
  • データエンコーディング:23年2月予定
  • その他:随時

2. Red Arrowプロジェクトへのフィードバック

RedAmberを開発する中で遭遇したバグや機能改善の提案を随時Red Arrowにフィードバックしている。基本的な機能は積極的にRed Arrowに移していきたい。

  • バグ報告
    • CIのhomebrewのバグ報告と修正 (GH-15093):マージ済
  • RedAmberで使用している機能の改善提案
    • Table#column_names (GH-15089):マージ済
    • Table#joinでjoin_keyを省略できる機能(GH-15088):マージ済
    • Table#joinでカラムをマージ、リネームする機能(GH-15287):提案中
  • 機能/改善提案
    • Table#saveでcsvをセーブする際にselfを返すことでREPL環境での待ち時間を減らす( GH-15289):提案中

3. パフォーマンスの向上

 第一段階として、主要メソッドのバージョン間のパフォーマンス比較を行えるように、ベンチマークを作成した(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

chart_2-1_basic_benchmark

chart_2-2_basic_benchmark

chart_2-3_basic_benchmark

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

chart_3_combining_benchmark

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

chart_4_group_benchmark

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

chart_5_reshape_benchmark

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

chart_6_vector_benchmark

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

chart_1_dataframe_benchmark

この総合的な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)に対する立ち位置を明確にする目的で、ライブラリ間の比較も行なっていきたい。

4. コード品質の向上

Test coverageの計測のために simplecovを導入した(v0.2.3)。導入した時点でのカバー率は98.54%であり、43行のカバーされていない行が存在していた。

コードのリファクタリングと共にカバー率の向上にも取組み、v0.3.0で100%のカバーを達成した。今後はカバー率の維持を図る。

5. ドキュメント整備

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のドキュメントを完成させていきたい。

6. 普及活動

以上

Appendix

ベンチマーク比較用のバーチャートの作成は、下記の手順でデータをRedAmberのデータフレームとして読み込み、縦持ちのデータに変換し、Chartyでプロットして作成した。

Appendix_1

Appendix_2

Appendix_3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment