LoginSignup
14
14

Daruの基本操作

Last updated at Posted at 2019-02-26

この記事ではRubyのデータフレームであるDaruを紹介します。
(編集リクエストは大歓迎します)

Daruとは

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

  image.png   image.png

2023年6月時点の開発状況について

幸いにしてRubyのデータフレームには新しい作品がいくつもありますが、Daru自体の開発は事実上休止しています。Daruはあくまで歴史的な文化財として、実際にデータを処理する場合は、下記のいずれかのデータフレームの利用をおすすめします。

↑Pure Ruby製。この中では最も安定していると思われる。

↑今後Pythonなどでデファクトになる可能性が高いPolarsのRuby版。

↑RedArrowを用いたubyとの親和性の高いデータフレーム。

特徴

column(Series)をvectorと呼ぶところ。
image.png
青い四角で強調されている部分が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 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を返すが破壊的ではないという関係性はわかりにくい。ここに書いてあることも間違っているかもしれない。現状まだ把握しきれていない。

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 には 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)

14
14
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
14