今日は、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"}] # }
同様に、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)}}
Rubyは他の言語で学んだことが活かせるので楽しいですね。
Pythonでは、標準モジュールのitertools
にgroupby
という関数があるようです。リストではなくジェネレーターで返すので、ちょっとクセがあるかも。
# 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で実装したい…。