さっそく時間を作って読んでいるわけですが，コードが Python なんですよね．僕は Ruby の人なので Ruby でやりたいなと思っていまして，読みながらその場で同時通訳しています．その過程で Ruby のコードができるわけですが，せっかくなのでココで紹介しますね．現状，2.5 の最後 (p.20) まで読みましたので，それを載っけましょう．ファイル名は recommendations.rb です．

まずは p.8 のデータセットです．recommendations.rb は load で読み込むことを前提にしています．Ruby はファイルスコープを持っていて，ローカル変数はファイルの外からは見えません*1．そのため本文中の変数 critics は，Ruby では手っ取り早くグローバル変数にしました．

$critics = { ' Lisa Rose ' => { ' Lady in the Water ' => 2.5 , ' Snakes on a Plane ' => 3.5 , ' Just My Luck ' => 3.0 , ' Superman Returns ' => 3.5 , ' You, Me and Dupree ' => 2.5 , ' The Night Listener ' => 3.0 , }, ' Gene Seymour ' => { ' Lady in the Water ' => 3.0 , ' Snakes on a Plane ' => 3.5 , ' Just My Luck ' => 1.5 , ' Superman Returns ' => 5.0 , ' The Night Listener ' => 3.0 , ' You, Me and Dupree ' => 3.5 , }, ' Michael Phillips ' => { ' Lady in the Water ' => 2.5 , ' Snakes on a Plane ' => 3.0 , ' Superman Returns ' => 3.5 , ' The Night Listener ' => 4.0 , }, ' Claudia Puig ' => { ' Snakes on a Plane ' => 3.5 , ' Just My Luck ' => 3.0 , ' The Night Listener ' => 4.5 , ' Superman Returns ' => 4.0 , ' You, Me and Dupree ' => 2.5 , }, ' Mick LaSalle ' => { ' Lady in the Water ' => 3.0 , ' Snakes on a Plane ' => 4.0 , ' Just My Luck ' => 2.0 , ' Superman Returns ' => 3.0 , ' The Night Listener ' => 3.0 , ' You, Me and Dupree ' => 2.0 , }, ' Jack Matthews ' => { ' Lady in the Water ' => 3.0 , ' Snakes on a Plane ' => 4.0 , ' The Night Listener ' => 3.0 , ' Superman Returns ' => 5.0 , ' You, Me and Dupree ' => 3.5 , }, ' Toby ' => { ' Snakes on a Plane ' => 4.5 , ' You, Me and Dupree ' => 1.0 , ' Superman Returns ' => 4.0 , }, }

このコードに対する p. 9 の実行例は次のようになります．irb を使います．

muraken#mrkn-macbook:~/study/books/collective_intelligence_ja/ruby$ irb >> load 'recommendations.rb' => true >> $critics['Lisa Rose']['Lady in the Water'] => 2.5 >> $critics['Toby']['Snakes on a Plane'] = 4.5 => 4.5 >> $critics['Toby'] => {"Superman Returns"=>4.0, "Snakes on a Plane"=>4.5, "You, Me and Dupree"=>1.0}

ちなみに，本文の実行例の最後の行は間違っていて，上記ように Superman Returns も出てくるのが正しいです．

次に p.11，ユークリッド距離に基づく類似性スコアを求める関数です．Ruby では以下のように Recommendations モジュールの中で，モジュール関数として定義しました．

module Recommendations def self . sim_distance (prefs, person1, person2) si = prefs[person1].keys & prefs[person2].keys return 0.0 if si.empty? sum_of_squares = si.inject( 0.0 ) {| acc , item | acc += (prefs[person1][item] - prefs[person2][item])** 2 } return 1.0 / ( 1.0 + sum_of_squares) end end

Array の集合演算を使うと小気味良いコードになりますね．差の平方の総和ではもちろん inject を使います JK．実行例は以下のとおりです．

>> load 'recommendations.rb' => true >> Recommendations.sim_distance($critics, 'Lisa Rose', 'Gene Seymour') => 0.148148148148148

今度は Pearson の積率相関係数に基づく類似性スコアを算出する関数です．ユークリッド距離との違いは，データの分布関数の位置 (平均値の値) が影響するか否かです．ユークリッド距離は位置が影響します．Pearson の積率相関係数は位置は影響せず，分布関数の形だけが重要になります．

module Recommendations def self . sim_pearson (prefs, p1, p2) si = prefs[p1].keys & prefs[p2].keys n = si.length return 0.0 if si.empty? sum1 = si.inject( 0.0 ) {| acc , item | acc += prefs[p1][item] } sum2 = si.inject( 0.0 ) {| acc , item | acc += prefs[p2][item] } sum1sq = si.inject( 0.0 ) {| acc , item | acc += prefs[p1][item]** 2 } sum2sq = si.inject( 0.0 ) {| acc , item | acc += prefs[p2][item]** 2 } psum = si.inject( 0.0 ) {| acc , item | acc += prefs[p1][item]*prefs[p2][item] } num = psum - (sum1*sum2/n) den = Math .sqrt((sum1sq - sum1** 2 /n)*(sum2sq - sum2** 2 /n)) return 0 if den == 0.0 r = num / den return r end end

これらのコードは，本文のコードのコメントをそのまま残す形で翻訳しています．上のコードの sum1，sum2，sum1sq，sum2sq を求める4つの inject は次のようにまとめることができて，こっちのほうがループが1/4回になるので，データのサンプル数が多いときに有利です．

sum1, sum2, sum1sq, sum2sq = si.inject([ 0.0 ] * 4 ) {| accs , item | accs[ 0 ] += prefs[p1][item] accs[ 1 ] += prefs[p2][item] accs[ 3 ] += prefs[p1][item]** 2 accs[ 4 ] += prefs[p2][item]** 2 }

実行例は以下のようになります．

>> load 'recommendations.rb' => true >> Recommendations.sim_pearson($critics, 'Lisa Rose', 'Gene Seymour') => 0.39605901719067

さて，どんどん行きましょう．次は p.15 の topMatches 関数です．Ruby へ翻訳するときは，関数名と変数名を snake_case に変更しています．

module Recommendations def self . top_matches (prefs, person, n= 5 , &similarity) scores = (prefs.keys - [person]).collect {| other | [similarity[prefs, person, other], other] } scores.sort! scores.reverse! return scores[ 0 , n] end end

このコードでも Array の集合演算がとてもキレイにはまってる気がします．実行例は以下のとおりです．

>> load 'recommendations.rb' => true >> Recommendations.top_matches($critics, 'Toby', n=3) {|prefs, p1, p2| Recommendations.sim_pearson(prefs, p1, p2) } => [[0.99124070716193, "Lisa Rose"], [0.924473451641905, "Mick LaSalle"], [0.893405147441565, "Claudia Puig"]]

今回はあと2つです．推薦を算出する関数は次のようになりました．

module Recommendations def self . get_recommendations (prefs, person, &similarlity) totals = {} sim_sums = {} for other, items in prefs next if other == person sim = similarlity[prefs, person, other] next if sim <= 0.0 for item, value in items if !prefs[person].has_key?(item) || prefs[person][item] == 0.0 totals[item] ||= 0.0 totals[item] += value * sim sim_sums[item] ||= 0.0 sim_sums[item] += sim end end end rankings = totals.keys.collect {| item | [totals[item] / sim_sums[item], item] } rankings.sort! rankings.reverse! return rankings end end

totals と sim_sums を初期化するところで付値している {} を Hash.new(0.0) へ書き換えて

totals = Hash .new( 0.0 ) sim_sums = Hash .new( 0.0 )

このようにすると，

totals[item] ||= 0.0

と

sim_sums[item] ||= 0.0

の2行が不要になります．最初から初期値が 0.0 だと分かってる場合は，このようにすると良いでしょう．ところで，私はあまり for を使わない人なんです．しかし，今回翻訳するにあたって Python 版が for を使っていたので試しに for で書いてみたのですが，以外とすっきりしていてビックリです．ただし，for はスコープを汚すので，それが嫌な場合はイテレータを使うことになります．

for item, value in items

この部分をそのまま

items.each do | item , value |

と書き換えれば良いと思います．実行例はこうなります．

>> load 'recommendations.rb' => true >> Recommendations.get_recommendations($critics, 'Toby') {|prefs, p1, p2| Recommendations.sim_pearson(prefs, p1, p2) } => [[3.3477895267131, "The Night Listener"], [2.83254991826416, "Lady in the Water"], [2.53098070376556, "Just My Luck"]] >> Recommendations.get_recommendations($critics, 'Toby') {|prefs, p1, p2| Recommendations.sim_distance(prefs, p1, p2) } => [[3.50024784014159, "The Night Listener"], [2.75612429399594, "Lady in the Water"], [2.46198848607437, "Just My Luck"]]

さて，今回の最後の関数です．critics を転置するための関数ですね．次のようになりました．

module Recommendations def self . transform_prefs (prefs) result = {} for person, items in prefs for item, value in items result[item] ||= {} result[item][person] = value end end return result end end

result へ最初に付値するハッシュを {} ではなく

result = Hash .new {| h , k | h[k] = {} }

このように書き換えると，次の1行が不要になります．

result[item] ||= {}

また，このコードの for もイテレータに置き換えられます．

for person, items in prefs for item, value in items

これが

prefs.each do | person , items | items.each do | item , value |

こうなります．お好きな方を使えば良いと思います*2．私は後者が好みです．最後の実行例は次のとおりです．

>> load 'recommendations.rb' => true >> movies = Recommendations.transform_prefs($critics) => {"The Night Listener"=>{"Jack Matthews"=>3.0, "Gene Seymour"=>3.0, "Mick LaSalle"=>3.0, "Lisa Rose"=>3.0, "Claudia Puig"=>4.5, "Michael Phillips"=>4.0}, "Superman Returns"=>{"Jack Matthews"=>5.0, "Gene Seymour"=>5.0, "Mick LaSalle"=>3.0, "Toby"=>4.0, "Lisa Rose"=>3.5, "Claudia Puig"=>4.0, "Michael Phillips"=>3.5}, "Lady in the Water"=>{"Jack Matthews"=>3.0, "Gene Seymour"=>3.0, "Mick LaSalle"=>3.0, "Lisa Rose"=>2.5, "Michael Phillips"=>2.5}, "Snakes on a Plane"=>{"Jack Matthews"=>4.0, "Gene Seymour"=>3.5, "Mick LaSalle"=>4.0, "Toby"=>4.5, "Lisa Rose"=>3.5, "Claudia Puig"=>3.5, "Michael Phillips"=>3.0}, "Just My Luck"=>{"Gene Seymour"=>1.5, "Mick LaSalle"=>2.0, "Lisa Rose"=>3.0, "Claudia Puig"=>3.0}, "You, Me and Dupree"=>{"Jack Matthews"=>3.5, "Gene Seymour"=>3.5, "Mick LaSalle"=>2.0, "Toby"=>1.0, "Lisa Rose"=>2.5, "Claudia Puig"=>2.5}} >> Recommendations.top_matches(movies, 'Superman Returns') {|prefs, p1, p2| Recommendations.sim_pearson(prefs, p1, p2) } => [[0.657951694959769, "You, Me and Dupree"], [0.487950036474269, "Lady in the Water"], [0.111803398874989, "Snakes on a Plane"], [-0.179847194799054, "The Night Listener"], [-0.422890031611031, "Just My Luck"]] >> Recommendations.get_recommendations(movies, 'Just My Luck') {|prefs, p1, p2| Recommendations.sim_pearson(prefs, p1, p2) } => [[4.0, "Michael Phillips"], [3.0, "Jack Matthews"]]

来週くらいまでに2章を終えて3章へ突入したいなと思っています．

(追記 2008-08-22 12:42)

既に Ruby への翻訳をやっている方がいらっしゃいましたので，リンク貼っておきます：

翻訳の仕方がちょっとずつ違っていて面白いですね．