この記事ではRubyのデータフレームであるDaruを紹介します。
(編集リクエストは大歓迎します)
Daruとは
Pythonにおけるpandas。Daruはインドのお酒の意味だそうです。
2023年6月時点の開発状況について
幸いにしてRubyのデータフレームには新しい作品がいくつもありますが、Daru自体の開発は事実上休止しています。Daruはあくまで歴史的な文化財として、実際にデータを処理する場合は、下記のいずれかのデータフレームの利用をおすすめします。
↑Pure Ruby製。この中では最も安定していると思われる。
↑今後Pythonなどでデファクトになる可能性が高いPolarsのRuby版。
↑RedArrowを用いたubyとの親和性の高いデータフレーム。
特徴
column(Series)をvectorと呼ぶところ。
青い四角で強調されている部分がVectorです。
関連プロジェクト
-
Daru::View
- Google Charts(GoogleVisualr), Highcharts(lazy_high_charts) による可視化ライブラリ。
-
Daru-IO
- 標準でサポートされていないデータ形式等の読み込み。完成度はそんなに高くないかも。
-
rubyplot
- Daruに搭載されるかもしれないプロッティングライブラリ
クラス
Daru::DataFrame と Daru::Vector を覚えておけばだいたい事足ります。
クラス名 | 説明 |
---|---|
Daru::DataFrame | データフレーム |
Daru::Vector | シリーズ(列) |
Daru::Index | インデックス(軸) |
Daru::DateTimeIndex | 時系列のインデックス |
Daru::MultiIndex | 階層型インデックス |
Daru::Core::GroupBy | |
Daru::Core::MergeFrame |
インストール
RubyGemに登録されているDaruはやや古いのでGithubのmasterからインストールする方がいいでしょう。
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/v1.0.5/pandas/tests/io/data/csv/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?
DataFrame
-
each
map
はaxis:
(: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を返すが破壊的ではないという関係性はわかりにくい。ここに書いてあることも間違っているかもしれない。現状まだ把握しきれていない。
Vector
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
という名前のメソッドもある。
結合する
new_df = df.concat(df2)
時系列インデックス
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 には object
と numeric
の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
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
散布図行列
下記のような関数を作成すれば、散布図行列のようなものを描くことも可能である。
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)
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")
書き出す
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の記事
- Daru のちょっと詳しい説明: Index, Vector, DataFrame のデータ構造について
- Rubyで数値計算(R, Pythonと比較)
- Jupyter Notebook + Ruby で CSV を加工して出力する
- daru チートシート
- Ruby で数値を弄るやーつ -- Daru について --
- Rubyでデータ解析 - Daru入門