Ruby
sciruby
daru

Daruの基本操作

この記事ではRubyのデータフレームであるDaruを紹介します。

(編集リクエストは大歓迎します)


Daruとは

Pythonにおけるpandas。Daruはインドのお酒の意味だそうです。

  image.png   image.png


特徴

column(Series)をvectorと呼ぶところ。

image.png

青い四角で強調されている部分がVectorです。


関連プロジェクト



  • Daru::View


    • Google Charts(GoogleVisualr), Highcharts(lazy_high_charts) による可視化ライブラリ。




  • Daru-IO


    • 標準でサポートされていないデータ形式等の読み込み。完成度はそんなに高くないかも。




  • Nyaplot


    • Daru標準のプロッティングライブラリ。綺麗だが古いのでおすすめしにくい。




クラス

クラス名
説明

Daru::DataFrame
データフレーム

Daru::Vector
シリーズ(列)

Daru::Index
インデックス(軸)

Daru::DateTimeIndex
時系列のインデックス

Daru::MultiIndex
階層型インデックス

Daru::Core::GroupBy

Daru::Core::MergeFrame


インストール

gem install specific_install

gem specific_install https://github.com/SciRuby/daru

require 'daru'

Jupyter Notebook + IRuby環境が推奨される。


データを読み込む


CSVファイル

Daru::DataFrame.from_csv "filepath"

インターネット上のCSVファイル

require 'open-uri'

url = 'https://raw.githubusercontent.com/pandas-dev/pandas/master/pandas/tests/data/iris.csv'
df = Daru::DataFrame.from_csv(url)

一般的なもの

Daru::DataFrame.new a: [1,2,3,4,5], b: [2,3,4,5,6]

Daru::DataFrame.rows 配列
# 数値がStringになる場合あり、何らかの改善の余地

RのデータセットでDaruをためすとき。

require 'rdatasets'

df = RDatasets.load :datasets, :iris

そのほか

from_activerecord from_excel from_html from_plaintext from_sql など

Daru-ioを見ると色々ある。(完成度は高くない印象)


  • Importers



    • ActiveRecord, Avro, CSV, Excel, Excelx, HTML, JSON, Mongo, Plaintext, RData, RDS, Redis, SQL



  • Exporters



    • Avro, CSV, Excel, JSON, RData, RDS, SQL




データの状態を確認する

先頭の5行を表示する

df.head 5

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

0
5.1
3.5
1.4
0.2
Iris-setosa

1
4.9
3.0
1.4
0.2
Iris-setosa

2
4.7
3.2
1.3
0.2
Iris-setosa

3
4.6
3.1
1.5
0.2
Iris-setosa

4
5.0
3.6
1.4
0.2
Iris-setosa

最後尾の5行を表示する

df.tail 5

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

145
6.7
3.0
5.2
2.3
Iris-virginica[150, 5]

146
6.3
2.5
5.0
1.9
Iris-virginica

147
6.5
3.0
5.2
2.0
Iris-virginica

148
6.2
3.4
5.4
2.3
Iris-virginica

149
5.9
3.0
5.1
1.8
Iris-virginica

行数・列数を確認する

df.shape # 形状 [150, 5]

df.ncols # 列数 5
df.nrows # 行数 150
df.size # 行数 150

列名の取得

df.vectors

#<Daru::Index(5): {SepalLength, SepalWidth, PetalLength, PetalWidth, Name}>

返却されるのはIndexクラスのオブジェクト。Vectorや配列ではない。


データを選択する

以下、特に断りがない場合は .head(5) している場合があります。


Ⅲ 列 Ⅲ

[] でVectorの名前を指定する

df['vector1', 'vector2']

df.vector1

df['SepalLength']

df.SepalLength

SepalLength

0
5.1

1
4.9

2
4.7

3
4.6

4
5.0

at 列数で指定

df.at 0, 1

SepalLength
SepalWidth

0
5.1
3.5

1
4.9
3.0

2
4.7
3.2

3
4.6
3.1

4
5.0
3.6

[列数] は返却されたデータフレームのベクトルの名前も数になる

df[0, 1]

0
1

0
5.1
3.5

1
4.9
3.0

2
4.7
3.2

3
4.6
3.1

4
5.0
3.6


≡ 行 ≡

row[] でindexを指定(行数ではないので注意)

df.row[100] # df.row 100 はエラー df.row [100] もエラーになる

100

SepalLength
6.3

SepalWidth
3.3

PetalLength
6.0

PetalWidth
2.5

Name
Iris-virginica

範囲

df.row[100..105]

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

100
6.3
3.3
6.0
2.5
Iris-virginica

101
5.8
2.7
5.1
1.9
Iris-virginica

102
7.1
3.0
5.9
2.1
Iris-virginica

103
6.3
2.9
5.6
1.8
Iris-virginica

104
6.5
3.0
5.8
2.2
Iris-virginica

105
7.6
3.0
6.6
2.1
Iris-virginica

複数

df.row[40,90,140]

# indexは数値とは限らない
# df.row[:a]

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

40
5.0
3.5
1.3
0.3
Iris-setosa

90
5.5
2.6
4.4
1.2
Iris-versicolor

140
6.7
3.1
5.6
2.4
Iris-virginica

row_at で行数を指定

df.row_at 100          # Vector が返される

df.row_at 100..105 # DataFrame
df.row_at 100, 102 # DataFrame

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

100
6.3
3.3
6.0
2.5
Iris-virginica

102
7.1
3.0
5.9
2.1
Iris-virginica

df.row[100] 等として、取り出すことは可能だが、取り出したデータフレームのベクトルの名前も100になる。また、1行だけ取り出す場合は、データフレームではなくベクトルが返却される。

df = Daru::DataFrame.new({a:[1,2,3], b:[4,5,6]}, index: [:a,:b,:c])

a
b

a
1
4

b
2
5

c
3
6

df.row[:b]

Daru::Vector(2)

b

a
2

b
5

df.row[1]

Daru::Vector(2)

1

a
2

b
5

df.row_at 1

Daru::Vector(2)

a
2

b
5


列と行を同時に選択する

ベクトルを先に指定する

df['SepalLength'][3]       # 4.6


where で抽出

真偽値インデックス

df.where(df.col1.eq "hoge")

df.where(df.col1 > 10 )
df.where(df.col1 > 10 | df.col2 < 10)
df.where((df.col1 > 10 | df.col2 < 10 ) & df.col3.eq 10)

# and or
df.where(((df.col1 > 10).or df.col2 < 10).and df.col3.eq 10)

eq not_eq

mt lt mteq lteq

gt

& | && || and or など

この領域はDaruは改善の余地がありそうです

Numo::NArray など他のライブラリとの統一感があまりない

データフレーム全体を真偽値でマスクする方法はないかもしれない

df.where(df.SepalLength > 7.5)

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

105
7.6
3.0
6.6
2.1
Iris-virginica

117
7.7
3.8
6.7
2.2
Iris-virginica

118
7.7
2.6
6.9
2.3
Iris-virginica

122
7.7
2.8
6.7
2.0
Iris-virginica

131
7.9
3.8
6.4
2.0
Iris-virginica

135
7.7
3.0
6.1
2.3
Iris-virginica


filter で抽出

whereよりも記述が長くなるのですが、filterの方が書いていてRubyっぽくて楽だなあと感じることがあります。

df2 = df.filter do |vector|

vector.type == :numeric and vector.median < 50
end

df2 = df.filter(:row) do |row|
row[:a] + row[:d] < 100
end

mdt df.filter(:row){|row| row["SepalLength"] < 4.6}

SepalLength
SepalWidth
PetalLength
PetalWidth
Name

8
4.4
2.9
1.4
0.2
Iris-setosa

13
4.3
3.0
1.1
0.1
Iris-setosa

38
4.4
3.0
1.3
0.2
Iris-setosa

41
4.5
2.3
1.3
0.3
Iris-setosa

42
4.4
3.2
1.3
0.2
Iris-setosa


keep_row_if  keep_vector_if

破壊的メソッド

df.keep_row_if do |row|

row[:a] > 5
end
# return [index, index, ...]


要素をカウントする

vector.value_counts

df.Name.value_counts

Iris-setosa
50

Iris-versicolor
50

Iris-virginica
50

Uniq

vector.uniq

df.Name.uniq

Name

0
Iris-setosa

50
Iris-versicolor

100
Iris-virginica


欠損値を扱う

df.has_missing_data?

# nil, Float::NAN に反応する. 文字列 "NA" などには反応しない。
# df.reject_values "NA"
# 下記を使うためには df.replace_values "NA", nil などとする

df.include_values? nil
vector.count_values nil
df.filter_rows{|r| r.include? nil, Float::NAN}
df.dup_only_valid
df.clone_only_valid # 'shallow' copy
df.collect{|v| v.count_values nil}

df.replace_values nil, Float::NAN

例えば次のようにして、欠損値があるベクトルと、その個数の一覧を表示できる。

require 'rdatasets'

df = RDatasets.Stat2Data.Hawks
df.replace_values "NA", nil
df.collect{|v| v.count_values nil}

Month
0

Day
0

Year
0

CaptureTime
0

ReleaseTime
0

BandNumber
0

Species
0

Age
0

Sex
0

Wing
1

Weight
10

Culmen
7

Hallux
6

Tail
0

StandardTail
337

Tarsus
833

WingPitFat
831

KeelFat
341

Crop
343

ベクトルには replace_nils! メソッドを使うことができる。

vector.replace_nils!

df["vname"].replace_nils!(0)


補間

現状ではほとんどできないと思われる。

df.rolling_fillna

df.rolling_rillna!


Rubyのイテレーターを使う

鬼門です。ものすごくわかりにくいです。

もしかすると、DaruのAPI自体をアップデートして改善した方がよいかもしれません。

Daruの評判の悪さの原因の大半はここにあると思います。ひょっとすると、このわかりにくさはRubyそのものにも遠因があるかもしれません。


 map collect reduce inject all? any?



  • each mapaxis:(:row :vector :column)を指定する。all? any? も同様


  • each_vector, each_row, map_vectors, map_rows


  • each_vector_with_index, each_row_with_index, map_vectors_with_index, map_rows_with_index

  • collect の挙動は map と違い、vectorになる。

  • map は配列を返却する。

  • map! は破壊的でdataframeになる

  • recode は破壊的ではないが、dfを返却する


  • inject はない。

  • collect_matrix


  • clone / dup


  • transpose


名前の違うメソッドは、それぞれ微妙に挙動が違う傾向がある。mapは配列を返し、collectはVectorを返し、map!はdfを返し破壊的、recodeもdataframeを返すが破壊的ではないという関係性はわかりにくい。ここに書いてあることも間違っているかもしれない。現状まだ把握しきれていない。


apply_method

引数はsymbol。procも引数に取る。破壊的ではない

df.apply_method(:some_nice_method)

df.apply_method(->(x){x*10})
# df * 10


aggregate

Rubyではブロックがあるため lambda のダッシュロケット記法はそんなに見かけないが、Daruでは使うシーンがあるだろうか?

df.aggregate(num_100_times: ->(df) { (df.num*100).first })

df.aggregate(num: :mean)


列名の変更

rename_vectors

df.rename_vectors :a => :alpha, :b => :beta

単なるrenameはデータフレームの名称変更を意味するので、'name' という名前のベクトルがある時に注意

df.name # データフレームの名前


列の並び替え

df.order = ["col2", "col1"]

df.order = Daru::Index.new["col2", "col1"]


行の並び替え

df.reindex Daru::Index.new([1,3,2,4])


特定の列をindexにする

df.set_index "vector"


ソートする

df.sort ["col1"]

df.sort ["col1", "col2"]
df.sort ["col1"], assending: false
df.sort! ["col1"]

df.sort [:b], by: {b: lambda { |a| a.length } }, handle_nils: true


マージする

Daruでは join というメソッドを使う。

how:inner :outer :right :left などを指定。

new_df = df.join(df2, how: :inner, on: [:name])

一方で merge という名前のメソッドもある。


時系列インデックス


DateTimeIndex

from_csv => datetime => set_index

vecotor['2014']

month などを利用して df["month"] = df.index.month

と行を追加して、`group_by("month") などとすれば月別の集計もできるかも。(現状は既知のバグあり)


統計量

df.max

df.min
df.mean
df.median
df.std
df.variance
df.rolling_XXX #XXXにはmin, max, meanなどが入る
df.standardize
# etc

表示が見にくいときはroundが使える。

df.group_by("Species").mean.round(3)


describe

df.describe.round(2)

SepalLength
SepalWidth
PetalLength
PetalWidth

count
150
150
150
150

mean
5.84
3.05
3.76
1.2

std
0.83
0.43
1.76
0.76

min
4.3
2.0
1.0
0.1

max
7.9
4.4
6.9
2.5

summary はterminal出力用


group_by

gb = df.group_by [:col1, :col2]

gb.mean
gb.get_group :col1

gbは Daru::Core::GroupBy クラスのオブジェクト。

df.group_by("Name").mean.round(3)

SepalLength
SepalWidth
PetalLength
PetalWidth

Iris-setosa
5.006
3.418
1.464
0.244

Iris-versicolor
5.936
2.77
4.26
1.326

Iris-virginica
6.588
2.974
5.552
2.026


pivot

df = Daru::DataFrame.new({

a: ['foo' , 'foo', 'foo', 'foo', 'foo', 'bar', 'bar', 'bar', 'bar'],
b: ['one' , 'one', 'one', 'two', 'two', 'one', 'one', 'two', 'two'],
c: ['small','large','large','small','small','large','small','large','small'],
d: [1,2,2,3,3,4,5,6,7],
e: [2,4,4,6,6,8,10,12,14]
})

a
b
c
d
e

0
foo
one
small
1
2

1
foo
one
large
2
4

2
foo
one
large
2
4

3
foo
two
small
3
6

4
foo
two
small
3
6

5
bar
one
large
4
8

6
bar
one
small
5
10

7
bar
two
large
6
12

8
bar
two
small
7
14

hoge = df.pivot_table(index: [:a, :c], vectors: [:b], agg: :mean, values: :e)

one
two

bar
large
8.0
12.0

small
10.0
14.0

foo
large
4.0

small
2.0
6.0


行や列の追加

最後の列に追加するとき

df["new_name"] = Vector.new

add_vector add_row などで列数・行数を指定できる。


行や列の削除

delete_vector delete_vectors delete_row


only_numerics

type には objectnumeric の2種類がある。

only_numerics は便利で使用頻度が高い

df.only_numerics # 数値だけのデータフレーム


カテゴリー

df.to_category "Name"

df.Name.type # :category

dfs = df.split_by_category "Name"

dfs[0].head(3)
dfs[1].head(3)
dfs[2].head(3)

SepalLength
SepalWidth
PetalLength
PetalWidth

0
5.1
3.5
1.4
0.2

1
4.9
3.0
1.4
0.2

2
4.7
3.2
1.3
0.2

SepalLength
SepalWidth
PetalLength
PetalWidth

50
7.0
3.2
4.7
1.4

51
6.4
3.2
4.5
1.5

52
6.9
3.1
4.9
1.5

SepalLength
SepalWidth
PetalLength
PetalWidth

100
6.3
3.3
6.0
2.5

101
5.8
2.7
5.1
1.9

102
7.1
3.0
5.9
2.1


可視化ライブラリ


Nyaplot

標準はNyaplotである。もし使いたい場合は erector もインストールする。

Nyaplotはかっこいい。しかしメンテナンスが事実上停止しており、今後の開発は期待しにくい。

Jupyter Labでは動作しないかもしれない。オフラインでも動作しない。

散布図、棒グラフ、箱ひげ図、折れ線、ヒストグラムなどがある。

現在RubyPlotの開発が進められているが、おそらくNyaplotの置き換えを視野に入れていると思われる。


Daru::View

現状では実用性で Daru::View が頭一つ抜ける。Google Chartsは使いやすい。

require 'daru/view'

require 'rdatasets'

df = RDatasets.load :datasets, :USArrests
df.head 5

Murder
Assault
UrbanPop
Rape

0
Alabama
13.2
236
58
21.2

1
Alaska
10
263
48
44.5

2
Arizona
8.1
294
80
31

3
Arkansas
8.8
190
50
19.5

4
California
9
276
91
40.6

Daru::View::Plot.new(df,

title: "USArrests dataset",
width: 900,
height: 560,
isStacked: true,
hAxis: {slantedTextAngle: 90, textStyle: {fontSize: 9}},
type: :column,
adapter: :googlecharts
).show_in_iruby

image.png

Daru::View::Plot.new(df,

title: "USArrests Dataset",
titleTextStyle: {fontSize: 16},
width: 840,
height: 560,
hAxis: {title: "Murder"},
vAxis: {title: "Assault"},
colors: ["yellow", "red"],
type: :bubble,
adapter: :googlecharts
).show_in_iruby

image.png


散布図行列

下記のような関数を作成すれば、散布図行列のようなものを描くことも可能である。

def pair_plot(df)

vector_names = df.only_numerics.vectors.to_a

opts = {
chartArea: { left: 5, right: 5, top: 5, bottom: 5 },
height: 100,
width: 100,
hAxis: { viewWindowMode: 'maximized' },
vAxis: { viewWindowMode: 'maximized' },
backgroundColor: '#f0f0f0',
gridlineColor: 'white',
colors: ['#4784BF']
}

vector_names.each do |row|
plots = vector_names.map do |column|
if row != column

Daru::View::Plot.new(
df[column, row], opts.merge(type: :scatter,
pointSize: 1)
)
else
Daru::View::Plot.new(
df[row], opts.merge(title: row,
titlePosition: 'in',
type: :histogram)
)
end
end
IRuby.display Daru::View::PlotList.new(plots).show_in_iruby
end
end

pair_plot(df)

image.png


numo-gnuplot

gnuplotの文法に慣れていれば使いやすいかもしれない。

【Ruby】Daru + gnuplot で散布図を描く


PyCall + Python のライブラリ

ややトリッキーだが、PyCallを利用してPythonのライブラリを使用する方法がある。仕事で他人に見せるなど、本格的なグラフが必要なときの手段として有力。

require 'daru'

require 'rdatasets'

iris = RDatasets.load :datasets, :iris
iris.write_csv "iris.csv"

require 'pycall'

require 'pycall/import'
include PyCall::Import
require 'matplotlib/iruby'
Matplotlib::IRuby.activate
Matplotlib.interactive(false) # これがないと描画がとても遅くなる

pyimport :pandas, as: :pd
pyimport :seaborn, as: :sns

df = pd.read_csv("iris.csv")
df.head(3)

sns.pairplot(df, hue: "Species")

sns.png


書き出す

write_csv


おわりに


Rails関連

Rails と jupyter の連携をとても簡単にする railtie の gem を書いた


Daruの歩き方

困ったら yard server -g でドキュメントを読む。

IRuby上で ri でドキュメントを見たり、 show-source や でソースコードに当たってみる。Pryを活用する。

show-source Daru::DataFrame#from_csv

とすれば、該当部分のソースがJupyterに表示される。

ソースコードは平易で、詳しくない人でもまあまあ読める。怪しいと思ったら、ソースコード全体をセルにコピペして、オープンクラスの仕組みを使ってデバッグ。バグを踏んだらぜひissueに投稿してほしい。

ri Daru::DataFrame.some_nice_method


Daruからの声

下記のポエムを頭の片隅におくと、Daruのメソッドの名付け方が理解しやすいかもしれない。


…きこえますか…きこえますか…ルビイストよ…Daruです。いいですか、データは大事です。データとは、単なる数字の列ではありません。それは誰かが膨大な時間をかけて収集した貴重な資料です。たとえば…パン10個、ゾウ10頭、10光年、10年、Version10…。いずれも同じ数字の 10 ですが意味はだいぶ違うでしょ? 実はデータには、数字以上の情報が含まれているのですよ!君は、その数字の列が本当は何を意味しているのか理解しなければなりません…。

…ベクトルをいくつか集めたのがデータフレームです。ベクトルの「名前」には、データを記述した人間が観測した目的や意図が、名前という形で刻印されています。データフレームとは、単なる行列計算の道具ではないのです。それは、数字に付与された人間の残存思念を、ベクトルの名称という形でサポートするツールなのです。だから、ベクトルは番号ではなく、なるべく「名前」で呼んであげなければなりません。Daruの世界ではベクトルはRow(行)などよりずっと偉いのです!


(※Daruの公式見解ではなく筆者がDaruをみて感じた感想です)


QiitaにおけるDaruの記事


参考資料(pandas)