Ruby
daru
RubyDay 10

Daru のちょっと詳しい説明: Index, Vector, DataFrame のデータ構造について

この記事は Ruby Advent Calendar 2018 の10日の記事です。

イントロ

最近仕事で使ってみて、便利でもっと使う人が増えればいいと思ったので、実際の Usecase 的な記事を書いてみることにした。

説明: Daru とは

一言でいってしまうと pandas の ruby 実装。表計算的なデータをよろしく取り扱いたいときに利用する。

Ruby/Rails を触っていると基本 RDB がいて ActiveRecord が即座に利用できるので、どうして使う必要があるのか、という気分になるが、 DB を経由せずに集計演算を行いたい時に便利

実例: たとえば、勘定系のシステムのようなものを作っていた場合。

# たとえば、こんなテーブルがあったとする
class Shiwake < ApplicationRecord
  def karikatas_as_vector
    Daru::Vector.new(karikatas)
  end

  def kashikatas_as_vector
    Daru::Vector.new(kashikatas)
  end
end

# 実際、こんな値が取れたとする
[37] pry(main)> shiwake1 = Shiwake.find(1)
  Shiwake Load (0.4ms)  SELECT  `shiwakes`.* FROM `shiwakes` WHERE `shiwakes`.`id` = 1 LIMIT 1
=> #<Shiwake:0x00007f992c8f6db8
 id: 1,
 date: Mon, 10 Dec 2018,
 tekiyo: "帰宅",
 karikatas: {"交通費"=>210},
 kashikatas: {"現金"=>210},
 created_at: Sun, 09 Dec 2018 15:50:53 UTC +00:00,
 updated_at: Sun, 09 Dec 2018 15:50:53 UTC +00:00>
# 借方を vector で取得する
[38] pry(main)> shiwake1.karikatas_as_vector
=> #<Daru::Vector(1)>
 交通費 210

# 二つ目の仕分けレコードはこんな感じ
[39] pry(main)> shiwake2 = Shiwake.find(2)
  Shiwake Load (0.4ms)  SELECT  `shiwakes`.* FROM `shiwakes` WHERE `shiwakes`.`id` = 2 LIMIT 1
=> #<Shiwake:0x00007f9929ec2600
 id: 2,
 date: Mon, 10 Dec 2018,
 tekiyo: "PC備品",
 karikatas: {"消耗品費"=>1000},
 kashikatas: {"現金"=>1000},
 created_at: Sun, 09 Dec 2018 15:58:42 UTC +00:00,
 updated_at: Sun, 09 Dec 2018 15:58:42 UTC +00:00>
# vector を足し合わせる
[40] pry(main)> shiwake1.kashikatas_as_vector + shiwake2.kashikatas_as_vector
=> #<Daru::Vector(1)>
   現金 1210

# 悲しいことに、これはうまくいかない。理由は、相手がいない値は nil が引数になり、それが伝搬するから。
[41] pry(main)> shiwake1.karikatas_as_vector + shiwake2.karikatas_as_vector
=> #<Daru::Vector(2)>
  交通費  nil
 消耗品費  nil

# 一旦、 DataFrame を経由すればいける。
[42] pry(main)> df = Daru::DataFrame.new([shiwake1.karikatas_as_vector, shiwake2.karikatas_as_vector])
=> #<Daru::DataFrame(2x2)>
         0    1
  交通費  210  nil
 消耗品費  nil 1000
[43] pry(main)> df.replace_values(nil, 0)
=> #<Daru::DataFrame(2x2)>
         0    1
  交通費  210    0
 消耗品費    0 1000
[44] pry(main)> current_karikata = df.map.reduce(&:+)
=> #<Daru::Vector(2)>
         0
  交通費  210
 消耗品費 1000

# たとえば、このような別の vector があったときに
[50] pry(main)> other = Daru::Vector.new('現金' => 1000, '交通費' => 500)
=> #<Daru::Vector(2)>
   現金 1000
  交通費  500

# これまでの vector と足し合わせる
[52] pry(main)> sum = Daru::DataFrame.new([current_karikata, other]).replace_values(nil, 0).map.reduce(&:+)
=> #<Daru::Vector(3)>
         0
  交通費  710
 消耗品費 1000
   現金 1000

# なんとなく、2倍してみる
[54] pry(main)> sum * 2
=> #<Daru::Vector(3)>
         0
  交通費 1420
 消耗品費 2000
   現金 2000

データ構造解説

Vector

上でみてきたように、「ラベル付の1次元データ」を表す。なので、データ構造的には、ほぼほぼ Hash のようなもの。 Hash と違うのは、データの中身どうしの演算を、もろもろ実現するための機能がいろいろあること。また、1次元データとして、ユーザーフレンドリーな関数がいくつかある。

  • pretty_print (inspect): いい感じにベクトルっぽく表示する
  • オペレーターが、勝手にベクトル演算っぽくなる。
    • ベクトル同士ならば同じラベルの要素が、相手がただのスカラーっぽいものであれば、いわゆるスカラー倍のような挙動
  • 配列としての振る舞い (#each, #map, etc)

Index

キー => 中身データ配列上の番地を表すデータ構造。今回の例では、勘定科目がインデックスで表されていた。何も指定しなければ、 vector 上の番地がそのままキーとして取り扱われた Index になる。

[55] pry(main)> Daru::Vector.new(%w[a b c])
=> #<Daru::Vector(3)>
   0   a
   1   b
   2   c

ちなみに、 Index を Vector に直したいときは、 DataFrame を経由して #reset_index を使う。
(最近マージされたばかりなので、 master ブランチから直接利用する必要あり。まだ地味に使いづらいが。。)

[61] pry(main)> sum_df = sum.to_df; sum_df.index.name = '勘定科目'; sum_df.vectors= Daru::Index.new(['値'])
[69] pry(main)> sum_df
=> #<Daru::DataFrame: 0 (3x1)>
 勘定科目    
  交通費  710
 消耗品費 1000
   現金 1000

[70] pry(main)> sum_df.reset_index
=> #<Daru::DataFrame(3x2)>
      勘定科目    
    0  交通費  710
    1 消耗品費 1000
    2   現金 1000

DataFrame

内部構造的には、 Vector の Vector 、という構造が一番近い。ただし、それぞれの内側の Vector の Index は、一つの Index に統一される。

端的にいうと、エクセルの表っぽい何か。

列を Vector として取り足したり削除したり、行ごとに演算したり、などといったメソッドが定義されている。

csv で書き出す、というような操作も一発でかけるのが地味にありがたかったり。

[72] pry(main)> sum_df.write_csv('summary.csv')
=> nil
[74] pry(main)> puts File.read('summary.csv')
勘定科目,
交通費,710
消耗品費,1000
現金,1000
=> nil

データ構造的には

  • Index == Array をひっくりかえして Hash にしたようなもの +α
  • Vector == Index + 配列 +α
  • DataFrame == Vector の Vector +α、ただし中の Vector の Index は共通化されている

おわりに

Daru の Index/Vector/DataFrame がどんな感じなのかをみてきた。正直、まだゴリゴリの数値計算で利用できるようなライブラリではないし、多分、そこがメインフォーカスではないように思われる。

どちらかというと、それよりは Hash や Array や Set などといった、日常使える便利な基本的データ構造の延長線上に Index/Vector/DataFrame が定義されるイメージ。

なので、 model の中で数値の集合的なものを取り回す際に、 Hash や Array で返して、ゴリゴリとその集計処理を書いていくより、 Daru のオブジェクトを返しておいて、うまい感じで Daru 上の演算処理で華麗に数値演算を行う、などをすると良さそう。