単語の出現頻度を調べることはよくある。
最近使い始めた Scala で、何気に Ruby でやっていたことをすぐに書くことができなかったので、整理しておく。
Table of Contents
Open Table of Contents
まずは集計 Ruby 編
まずは、Ruby で、以下のようなデータあるとする。
>> gosanke = ["Goro", "Hideki", "Hiromi", "Hideki", "Goro", "Hideki"]
[
[0] "Goro",
[1] "Hideki",
[2] "Hiromi",
[3] "Hideki",
[4] "Goro",
[5] "Hideki"
]
御三家の出現頻度を調べるには、
>> count_by_gosanke = gosanke.inject(Hash.new(0)) { |r, e| r[e] += 1; r }
{
"Hideki" => 3,
"Hiromi" => 1,
"Goro" => 2
}
でいける。
まずは集計 Scala 編
Ruby の Enumerable#inject
にあたるものが、Scala では、Iterable#foldLeft
になる。
この foldLeft
だが、別名として、/:
という絵文字のようなメソッド名が用意されている。
Scala には、:
で終わるメソッドが他にもあり(List の ::
など)、:
で終わるメソッドは、レシーバが逆転する、という仕様がある。
Scala の(変態)仕様に慣れてくると、/:
で書いても悪くないと思えてくるので、ここでは、/:
で表記する。
というところで Scala 版。
先程と同様にまずは御三家のリストを用意する。
scala> val gosanke = List("Goro", "Hideki", "Hiromi", "Hideki", "Goro", "Hideki")
gosanke: List[java.lang.String] = List(Goro, Hideki, Hiromi, Hideki, Goro, Hideki)
御三家の出現頻度を調べる。
scala> val countByGosanke = (Map.empty[String, Int] /: gosanke) { (r, e) =>
| r + (e -> (r.getOrElse(e, 0) + 1))
| }
countByGosanke: scala.collection.immutable.Map[String,Int] = Map((Goro,2), (Hideki,3), (Hiromi,1))
/:
(foldLeft
) メソッドのレシーバは御三家のリスト gosanke
であり、Map.empty[String, Int]
(空の Map)が結果値の初期値となっている。
/:
では無く、foldLeft
で書くと、gosanke.foldLeft(Map.empty[String, Int]) { (r, e) => ...
となる。
見た目が少し違うが、使い方自体は、Ruby の inject
メソッドと似ている。
Scala は、関数型言語としての姿も持っており、標準では、不変な(immutable)オブジェクトを使用することを推奨している。
不変と可変の両方の性質を持つオジェクトでは、標準では不変オブジェクトが使われる。その為、上記のように特に何も指定せずに Map を使用した場合は、scala.collection.immutable.Map
が使われている。
Ruby のように可変な(mutable)オブジェクトでやるとすると、scala.collection.mutable.Map
を使用する。
scala> import scala.collection.mutable
import scala.collection.mutable
scala> val countByGosanke = (mutable.Map.empty[String, Int] /: gosanke) { (r, e) =>
| r += (e -> (r.getOrElse(e, 0) + 1))
| }
countByGosanke: scala.collection.mutable.Map[String,Int] = Map((Hideki,3), (Hiromi,1), (Goro,2))
のような感じか。
先程の違いとしては、mutable な Map を使っている以外では、Map への要素の追加に +
メソッドではなく、+=
メソッドを使っている。
(もっと良い書き方ができるのかな?)
出現頻度順で降順にソート Ruby 編
では、集計ができたところで、出現頻度順(ハッシュの値によるソート)で降順にソートしてみる。
まずは、Ruby で。
>> count_by_gosanke.to_a.sort{|a, b| b[1] <=> a[1]}
[
[0] [
[0] "Hideki",
[1] 3
],
[1] [
[0] "Goro",
[1] 2
],
[2] [
[0] "Hiromi",
[1] 1
]
]
Hideki、Goro、Hiromi の順に並ぶ。
出現頻度順で降順にソート Scala 編
では、Scala で。
scala> countByGosanke.toSeq.sortWith(_._2 > _._2)
res3: Seq[(String, Int)] = List((Hideki,3), (Goro,2), (Hiromi,1))
sortWith
は、Scala 2.8 から追加されたコレクションの機能。
一旦 Map を Seq オブジェクトに変換する。
scala> countByGosanke.toSeq
res4: Seq[(String, Int)] = List((Goro,2), (Hideki,3), (Hiromi,1))
実体としては上記のようにタプルを要素とした List に変換されている。
で、その List の sortWith
メソッドの引数に、_._2 > _._2
と書いているようにタプルの 2 番目の要素、つまり出現頻度の数で降順にソートしている。
ちなみに、sortBy
を使って書くと以下の通り。
scala> countByGosanke.toSeq.sortBy { case (a, b) => -b }
res5: Seq[(String, Int)] = List((Hideki,3), (Goro,2), (Hiromi,1))
さらに、sortBy
では、2 つのキーでのソートも書けるようだ。
scala> countByGosanke.toSeq.sortBy { case (a, b) => (-b, a) }
res6: Seq[(String, Int)] = List((Hideki,3), (Goro,2), (Hiromi,1))
上記の結果ではあまり意味は無いが、第 1 ソートキーとしてタプルの 2 番目の要素である出現頻度の数で降順にソートをかけ、第 2 ソートキーとしてタプルの 1 番目の要素である名前の文字列で昇順にソートしている。
集計と出現頻度によるソートを一気に Scala 編
Scala 2.8 で追加されている groupBy を使った方法を Atsuhiko さんにコメントにて教えて頂いた。ありがとうございます。
scala> gosanke.groupBy(identity).mapValues(_.size).toSeq.sortWith(_._2 > _._2)
res36: Seq[(java.lang.String, Int)] = List((Hideki,3), (Goro,2), (Hiromi,1))
ズバッと。
groupBy をかけた後の結果は以下の通り。両方とも同じ意味になる。
scala> gosanke groupBy identity
res39: scala.collection.immutable.Map[java.lang.String,List[java.lang.String]] = Map((Hideki,List(Hideki, Hideki, Hideki)), (Hiromi,List(Hiromi)), (Goro,List(Goro, Goro)))
scala> gosanke groupBy(e => e)
res40: scala.collection.immutable.Map[java.lang.String,List[java.lang.String]] = Map((Hideki,List(Hideki, Hideki, Hideki)), (Hiromi,List(Hiromi)), (Goro,List(Goro, Goro)))
groupBy とパターンマッチと組み合わせた FizzBuzz の例。