歩いたら休め

なんでこんな模様をしているのですか?

【Ruby】RubyでRのdplyrっぽいメソッドチェーン

今日は、RubyでDBの戻り値を集計する部分を触っていました。

DBからの戻り値は例えばこんな感じです。ここから、各アイドルグループ(soleil, luminas)ごとに平均年齢を出したいとします。

# スターライト学園のアイドルの名簿 
name_list = [
  {'name' => 'ichigo', 'age' => 16, 'group' => 'soleil'},
  {'name' => 'aoi', 'age' => 17, 'group' => 'soleil'},
  {'name' => 'ran', 'age' => 17, 'group' => 'soleil'},
  {'name' => 'akari', 'age' => 13, 'group' => 'luminas'},
  {'name' => 'sumire', 'age' => 14, 'group' => 'luminas'},
  {'name' => 'hinaki', 'age' => 14, 'group' => 'luminas'},
]

元々はザ・手続き型言語みたいな実装が取られており、(本当はもっとちょっと複雑な箇所だったので)ちょっとややこしくて意味を読み取るのが大変でした。

result = []
age_sum, num = 0, 0
name_list.size.times do |i|
  raw = name_list[i]
  next_raw = name_list[i + 1]
  age_sum += raw['age']
  num += 1
  if next_raw && raw['group'] == next_raw['group']
    next
  else
    result.push({raw['group'] => age_sum/num.to_f})
    age_sum, num = 0, 0
  end
end

p result
# [{"soleil"=>16.666666666666668}, {"luminas"=>13.666666666666666}]

「もしRなら、DBの戻り値はデータフレームに格納され、dplyrを使って簡単に集計することがでるのになー」と思いながらモヤモヤしていたのですが、

library(dplyr)

name_list %>%
    dplyr::group_by(group) %>%
    dplyr::summarise(mean = mean(age))

うまいやり方がないか調べていると、Rubyにも配列にgroup_byメソッドがあり、似た感覚で集計できることがわかりました。

# Pythonプログラマーは関数をちゃんと定義するのが好きです
def col_mean(list, colname)
  list.reduce(0){|x, y| x + y[colname]} / list.size.to_f
end

p name_list.
    group_by{|raw| raw['group']}.
    map{|k, v| {k => col_mean(v, 'age')}}
# [{"soleil"=>16.666666666666668}, {"luminas"=>13.666666666666666}]

group_byすると、ブロック内で指定した要素(raw['group'])がキーになったハッシュになり、それを更にmapで集計している感じですね。

p name_list.group_by{|raw| raw['group']}
# {
#   "soleil"=>[{"name"=>"ichigo", "age"=>16, "group"=>"soleil"}, {"name"=>"aoi", "age"=>17, "group"=>"soleil"}, {"name"=>"ran", "age"=>17, #"group"=>"soleil"}], 
#   "luminas"=>[{"name"=>"akari", "age"=>13, "group"=>"luminas"}, {"name"=>"sumire", "age"=>14, "group"=>"luminas"}, {"name"=>"hinaki", # "age"=>14, "group"=>"luminas"}]
# }

simanman.hatenablog.com

同様に、dplyrっぽい操作をメソッドチェーンで試して遊んでみました。

# name_list %>% dplyr::filter(group == 'soleil')
p name_list.
    select{|raw| raw['group'] == 'soleil'}

# name_list %>% dplyr::arrange(age)
p name_list.
    sort{|x, y| x['age'] <=> y['age']}

# name_list %>% dplyr::select(name, age)
p name_list.
    map{|raw| raw.select{|x| ['name', 'age'].include?(x)}}

qiita.com

Rubyは他の言語で学んだことが活かせるので楽しいですね。

Pythonでは、標準モジュールのitertoolsgroupbyという関数があるようです。リストではなくジェネレーターで返すので、ちょっとクセがあるかも。

# Python 3.4.3
from itertools import groupby

name_list = [
  {'name': 'ichigo', 'age': 16, 'group': 'soleil'},
  {'name': 'aoi', 'age': 17, 'group': 'soleil'},
  {'name': 'ran', 'age': 17, 'group': 'soleil'},
  {'name': 'akari', 'age': 13, 'group': 'luminas'},
  {'name': 'sumire', 'age': 14, 'group': 'luminas'},
  {'name': 'hinaki', 'age': 14, 'group': 'luminas'},
]

groupby_list = groupby(name_list, key = lambda x: x['group'])

print(groupby_list)
# <itertools.groupby object at 0x10054d138>

# Rubyのハッシュに対応する辞書に変換
print({x: list(y) for x, y in groupby_list})
# {'soleil': [{'age': 16, 'name': 'ichigo', 'group': 'soleil'}, {'age': 17, 'name': 'aoi', 'group': 'soleil'}, {'age': 17, 'name': 'ran', 'group': 'soleil'}], 'luminas': [{'age': 13, 'name': 'akari', 'group': 'luminas'}, {'age': 14, 'name': 'sumire', 'group': 'luminas'}, {'age': 14, 'name': 'hinaki', 'group': 'luminas'}]}

個人的にはRubyメソッドチェーンのほうが読みやすいです。

ただ、(今回の記事内容そのものとは関係ありませんが)Rubyは合計値のためのsumメソッドが無く、畳み込みのためのメソッドarray.inject(:+)としなければいけなかったり、数値計算的な処理はちょっと読みづらい気がします。Pythonとnumpyで実装したい…。