Scala と Ruby で単語の出現頻度を調べて多い順にソートする

Written by @dr_taka_n at 2010/08/20 23:51:25 [, ]

単語の出現頻度を調べることはよくある。

最近使い始めた Scala で、何気に Ruby でやっていたことをすぐに書くことができなかったので、整理しておく。

まずは集計 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 の例。

blog comments powered by Disqus