recommendation 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fc32f50776319a3b9cc96b83a3a5eab90696f393
4
- data.tar.gz: 6dfcb3869f168e4e8165bc3924a537ebd395d9ca
3
+ metadata.gz: cd59a543a20951969a1025319682cad97420a730
4
+ data.tar.gz: 79cf33ccaea0af435a7dccf2a3720895e1ad592d
5
5
  SHA512:
6
- metadata.gz: 0ea965bd5b976ee775be4417131538b0c49213e744369d901795ef54ebd12cda31aa34b25dd3cc83107537c919cc14c85a40537f9cc2fe5874ae2b35cedb6d44
7
- data.tar.gz: be255225fc628bfcd52b870bcdabc513c96725601141bc197cf805230a00ba52fcb8df604d3d24ee096ae61c40b1828ac28830d1e9c3b6eb5e85b28a9bb680e2
6
+ metadata.gz: 083fd844755e173b827dabf723fadd9d0065e0edc0d2a263ac9f1ebfc51e544c5025c147223b2cd2b53bc509d69ad2a780fa6f970228b332056d74cc6cf5ddb5
7
+ data.tar.gz: 00ce801ae2dc6f92b2267f9a02d37168b21c00e07ecc5670e444b1f58b12a87ac31d4e90042f8a03fd8bb0a5dfbf7bcfc3b27cf865359db03547e223f1110018
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
data/demo.rb CHANGED
@@ -6,16 +6,15 @@ $:.unshift File.join(File.dirname(__FILE__))
6
6
  require 'lib/recommendation'
7
7
 
8
8
  def demo
9
- supervisor = Recommendation::Supervisor.new(visitors)
10
- supervisor.train(new_comer)
11
- engine = Recommendation::Engine.new
9
+ recommender = Recommendation::Recommender.new(visitors)
10
+ recommender.train(new_comer)
12
11
 
13
- p engine.recommendation(supervisor.table, 'Toby') # => [["The Night Listener", 3.3477895267131017], ["Lady in the Water", 2.8325499182641614], ["Just My Luck", 2.530980703765565]]
12
+ p Recommendation::Recommender.recommendation(recommender.table, 'Toby') # => [["The Night Listener", 3.3477895267131017], ["Lady in the Water", 2.8325499182641614], ["Just My Luck", 2.530980703765565]]
14
13
 
15
- p engine.top_matches(supervisor.table, 'Toby') # => [["Lisa Rose", 0.9912407071619299], ["Mick LaSalle", 0.9244734516419049], ["Claudia Puig", 0.8934051474415647], ["Jack Matthews", 0.66284898035987], ["Gene Seymour", 0.38124642583151164]]
14
+ p Recommendation::Recommender.top_matches(recommender.table, 'Toby') # => [["Lisa Rose", 0.9912407071619299], ["Mick LaSalle", 0.9244734516419049], ["Claudia Puig", 0.8934051474415647], ["Jack Matthews", 0.66284898035987], ["Gene Seymour", 0.38124642583151164]]
16
15
 
17
- movies = supervisor.transform_table
18
- p engine.top_matches(movies, 'Superman Returns') # => [["You, Me and Dupree", 0.6579516949597695], ["Lady in the Water", 0.4879500364742689], ["Snake on the Plane", 0.11180339887498941], ["The Night Listener", -0.1798471947990544], ["Just My Luck", -0.42289003161103106]]
16
+ movies = recommender.transform_table
17
+ p Recommendation::Recommender.top_matches(movies, 'Superman Returns') # => [["You, Me and Dupree", 0.6579516949597695], ["Lady in the Water", 0.4879500364742689], ["Snake on the Plane", 0.11180339887498941], ["The Night Listener", -0.1798471947990544], ["Just My Luck", -0.42289003161103106]]
19
18
 
20
19
  end
21
20
 
data/doc/ChangeLog CHANGED
@@ -1,3 +1,10 @@
1
+ === 0.3.0 / 2013-12-13
2
+
3
+ * Improvement spec (for RSpec 2.14.x).
4
+
5
+ * Re-construction filenames.
6
+
7
+
1
8
  === 0.2.0 / 2013-10-19
2
9
 
3
10
  * Change output format to Array.
@@ -2,7 +2,6 @@
2
2
  # -*- coding: utf-8 -*-
3
3
 
4
4
  module Recommendation
5
- VERSION = "0.2.0"
6
- require File.dirname(__FILE__) + "/recommendation/supervisor"
7
- require File.dirname(__FILE__) + "/recommendation/engine"
5
+ require File.dirname(__FILE__) + "/recommendation/recommender"
6
+ require File.dirname(__FILE__) + "/recommendation/version"
8
7
  end
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ module Recommendation
5
+ class Recommender
6
+ attr_accessor :table
7
+
8
+ def initialize(params = {})
9
+ @table = params
10
+ end
11
+
12
+ def train(params = {})
13
+ @table.merge!(params)
14
+ end
15
+
16
+ def transform_table
17
+ new_table = {}
18
+ @table.each do |key, value|
19
+ value.each do |new_key, new_value|
20
+ new_table[new_key] ||= Hash.new
21
+ new_table[new_key][key] = new_value
22
+ end
23
+ end
24
+ new_table
25
+ end
26
+
27
+ class << self
28
+ def recommendation(table, user, similarity=:sim_pearson)
29
+ totals_h = Hash.new(0)
30
+ sim_sums_h = Hash.new(0)
31
+ table.each do |other, val|
32
+ next if other == user
33
+ sim = __send__(similarity, table, user, other)
34
+ next if sim <= 0
35
+ table[other].each do |item, val|
36
+ if !table[user].keys.include?(item) || table[user][item] == 0
37
+ totals_h[item] += table[other][item]*sim
38
+ sim_sums_h[item] += sim
39
+ end
40
+ end
41
+ end
42
+
43
+ rankings = Hash.new
44
+ totals_h.each do |item, total|
45
+ rankings[item] = total/sim_sums_h[item]
46
+ end
47
+
48
+ rankings.sort_by{|k, v| -v}
49
+ end
50
+
51
+ def top_matches(table, user, n=5, similarity=:sim_pearson)
52
+ scores = Array.new
53
+ table.each do |key, value|
54
+ if key != user
55
+ scores << [__send__(similarity, table, user,key), key]
56
+ end
57
+ end
58
+
59
+ result = Array.new
60
+ scores.sort.reverse[0,n].each do |k, v|
61
+ result << [v, k]
62
+ end
63
+ result
64
+ end
65
+
66
+ private
67
+
68
+ def sim_pearson(table, user1, user2)
69
+ shared_items_a = shared_items_a(table, user1, user2)
70
+
71
+ n = shared_items_a.size
72
+ return 0 if n == 0
73
+
74
+ sum1 = shared_items_a.inject(0) {|result, si|
75
+ result + table[user1][si]
76
+ }
77
+ sum2 = shared_items_a.inject(0) {|result, si|
78
+ result + table[user2][si]
79
+ }
80
+
81
+ sum1_sq = shared_items_a.inject(0) {|result, si|
82
+ result + table[user1][si]**2
83
+ }
84
+ sum2_sq = shared_items_a.inject(0) {|result, si|
85
+ result + table[user2][si]**2
86
+ }
87
+
88
+ sum_products = shared_items_a.inject(0) {|result, si|
89
+ result + table[user1][si]*table[user2][si]
90
+ }
91
+
92
+ num = sum_products - (sum1*sum2/n)
93
+ den = Math.sqrt((sum1_sq - sum1**2/n)*(sum2_sq - sum2**2/n))
94
+ return 0 if den == 0
95
+ return num/den
96
+ end
97
+
98
+ def shared_items(table, user1, user2)
99
+ shared_items_h = Hash.new
100
+ table[user1].each do |k, v|
101
+ shared_items_h[k] = 1 if table[user2].include?(k)
102
+ end
103
+ shared_items_h
104
+ end
105
+
106
+ def shared_items_a(table, user1, user2)
107
+ table[user1].nil? ? [] : table[user1].keys & table[user2].keys
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ module Recommendation
5
+ VERSION = "0.3.0"
6
+ end
@@ -2,15 +2,15 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: recommendation 0.2.0 ruby lib
5
+ # stub: recommendation 0.3.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "recommendation"
9
- s.version = "0.2.0"
9
+ s.version = "0.3.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ["id774"]
13
- s.date = "2013-10-19"
13
+ s.date = "2013-12-13"
14
14
  s.description = "Collaborative filtering for recommender system"
15
15
  s.email = "idnanashi@gmail.com"
16
16
  s.extra_rdoc_files = [
@@ -29,12 +29,11 @@ Gem::Specification.new do |s|
29
29
  "doc/LICENSE",
30
30
  "doc/README",
31
31
  "lib/recommendation.rb",
32
- "lib/recommendation/engine.rb",
33
- "lib/recommendation/supervisor.rb",
32
+ "lib/recommendation/recommender.rb",
33
+ "lib/recommendation/version.rb",
34
34
  "recommendation.gemspec",
35
35
  "script/build",
36
- "spec/lib/recommendation/engine_spec.rb",
37
- "spec/lib/recommendation/supervisor_spec.rb",
36
+ "spec/lib/recommendation/recommender_spec.rb",
38
37
  "spec/lib/recommendation_spec.rb",
39
38
  "spec/spec_helper.rb",
40
39
  "vendor/.gitkeep"
@@ -42,7 +41,7 @@ Gem::Specification.new do |s|
42
41
  s.homepage = "http://github.com/id774/recommendation"
43
42
  s.licenses = ["GPL"]
44
43
  s.require_paths = ["lib"]
45
- s.rubygems_version = "2.1.5"
44
+ s.rubygems_version = "2.1.11"
46
45
  s.summary = "recommendation"
47
46
 
48
47
  if s.respond_to? :specification_version then
@@ -0,0 +1,368 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+
4
+ require File.dirname(__FILE__) + '/../../spec_helper'
5
+
6
+ describe 'Recommendation::Recommender' do
7
+ context '#initialize and #table' do
8
+ subject {
9
+ recommender = Recommendation::Recommender.new
10
+ recommender.table
11
+ }
12
+
13
+ let(:expected) { {} }
14
+
15
+ it 'should have empty hash' do
16
+ expect(subject).to eq expected
17
+ end
18
+ end
19
+
20
+ context '#initialize with args and #table' do
21
+ subject {
22
+ recommender = Recommendation::Recommender.new(initial_data)
23
+ recommender.table
24
+ }
25
+
26
+ let(:expected) { initial_data }
27
+
28
+ it 'should have hash of args' do
29
+ expect(subject).to eq expected
30
+ end
31
+ end
32
+
33
+ context '#initialize with args and #train with append data' do
34
+ subject {
35
+ recommender = Recommendation::Recommender.new(initial_data)
36
+ recommender.train(append_data)
37
+ recommender.table
38
+ }
39
+
40
+ let(:expected) { merged_data }
41
+
42
+ it 'should have merged data' do
43
+ expect(subject).to eq expected
44
+ end
45
+ end
46
+
47
+ context '#initialize with merged data and #recommendation' do
48
+ subject {
49
+ recommender = Recommendation::Recommender.new(merged_data)
50
+ Recommendation::Recommender.recommendation(recommender.table, 'user_4')
51
+ }
52
+
53
+ let(:expected) { [["item_6", 220.0]] }
54
+
55
+ it 'should be suggested successful' do
56
+ expect(subject).to eq expected
57
+ end
58
+ end
59
+
60
+ context '#initialize with merged data and #top_matches' do
61
+ subject {
62
+ recommender = Recommendation::Recommender.new(merged_data)
63
+ Recommendation::Recommender.top_matches(recommender.table, 'user_4')
64
+ }
65
+
66
+ let(:expected) { [["user_2", 1.0], ["user_1", 1.0], ["user_3", 0]] }
67
+
68
+ it 'should be suggested successful' do
69
+ expect(subject).to eq expected
70
+ end
71
+ end
72
+
73
+ context '#recommendation' do
74
+
75
+ subject {
76
+ recommender = Recommendation::Recommender.new(visitors)
77
+ recommender.train(new_comer)
78
+ Recommendation::Recommender.recommendation(recommender.table, new_comer.keys[0])
79
+ }
80
+
81
+ let(:expected) {
82
+ [
83
+ ["The Night Listener", 3.3477895267131017],
84
+ ["Lady in the Water", 2.8325499182641614],
85
+ ["Just My Luck", 2.530980703765565]
86
+ ]
87
+ }
88
+
89
+ it 'should be suggesting interesting products' do
90
+ expect(subject).to eq expected
91
+ end
92
+ end
93
+
94
+ context '#top_matches' do
95
+
96
+ subject {
97
+ recommender = Recommendation::Recommender.new(visitors)
98
+ recommender.train(new_comer)
99
+ Recommendation::Recommender.top_matches(recommender.table, new_comer.keys[0])
100
+ }
101
+
102
+ let(:expected) {
103
+ [
104
+ ["Lisa Rose", 0.9912407071619299],
105
+ ["Mick LaSalle", 0.9244734516419049],
106
+ ["Claudia Puig", 0.8934051474415647],
107
+ ["Jack Matthews", 0.66284898035987],
108
+ ["Gene Seymour", 0.38124642583151164]
109
+ ]
110
+ }
111
+
112
+ it 'should be finding similar users' do
113
+ expect(subject).to eq expected
114
+ end
115
+ end
116
+
117
+ context 'reversed critics' do
118
+
119
+ subject {
120
+ recommender = Recommendation::Recommender.new(visitors)
121
+ recommender.train(new_comer)
122
+ movies = recommender.transform_table
123
+ Recommendation::Recommender.top_matches(movies, new_comer.values[0].keys[2])
124
+ }
125
+
126
+ let(:expected) {
127
+ [
128
+ ["You, Me and Dupree", 0.6579516949597695],
129
+ ["Lady in the Water", 0.4879500364742689],
130
+ ["Snake on the Plane", 0.11180339887498941],
131
+ ["The Night Listener", -0.1798471947990544],
132
+ ["Just My Luck", -0.42289003161103106]
133
+ ]
134
+ }
135
+
136
+ it 'should be found similar items' do
137
+ expect(subject).to eq expected
138
+ end
139
+ end
140
+
141
+ context 'recommendation for the unexisting user' do
142
+
143
+ subject {
144
+ recommender = Recommendation::Recommender.new(visitors)
145
+ recommender.train(new_comer)
146
+ Recommendation::Recommender.recommendation(recommender.table, 'hoge')
147
+ }
148
+
149
+ let(:expected) { [] }
150
+
151
+ it 'should return empty array' do
152
+ expect(subject).to eq expected
153
+ end
154
+ end
155
+
156
+ context 'top_matches for the unexisting item' do
157
+ subject {
158
+ recommender = Recommendation::Recommender.new(visitors)
159
+ recommender.train(new_comer)
160
+ Recommendation::Recommender.top_matches(recommender.table, 'fuga')
161
+ }
162
+
163
+ let(:expected) {
164
+ [
165
+ ["Toby", 0],
166
+ ["Mick LaSalle", 0],
167
+ ["Michael Phillips", 0],
168
+ ["Lisa Rose", 0],
169
+ ["Jack Matthews", 0]
170
+ ]
171
+ }
172
+
173
+ it 'should return all zero' do
174
+ expect(subject).to eq expected
175
+ end
176
+ end
177
+
178
+ context '#transform_table ' do
179
+ subject {
180
+ recommender = Recommendation::Recommender.new(visitors)
181
+ recommender.train(new_comer)
182
+ recommender.transform_table
183
+ }
184
+
185
+ it 'should return reversed critics' do
186
+ expect(subject).to eq reversed_critics
187
+ end
188
+ end
189
+ end
190
+
191
+ def initial_data
192
+ {
193
+ "user_1" => {
194
+ "item_1" => 100,
195
+ "item_2" => 140,
196
+ "item_3" => 160
197
+ },
198
+ "user_2" => {
199
+ "item_2" => 200,
200
+ "item_4" => 210,
201
+ "item_6" => 220
202
+ },
203
+ "user_3" => {
204
+ "item_3" => 300,
205
+ "item_6" => 330,
206
+ "item_9" => 360
207
+ }
208
+ }
209
+ end
210
+
211
+ def append_data
212
+ {
213
+ "user_1" => {
214
+ "item_2" => 400,
215
+ "item_7" => 410,
216
+ },
217
+ "user_4" => {
218
+ "item_2" => 150,
219
+ "item_4" => 230,
220
+ "item_7" => 580
221
+ }
222
+ }
223
+ end
224
+
225
+ def merged_data
226
+ {
227
+ "user_1" => {
228
+ "item_2" => 400,
229
+ "item_7" => 410
230
+ },
231
+ "user_2" => {
232
+ "item_2" => 200,
233
+ "item_4" => 210,
234
+ "item_6" => 220
235
+ },
236
+ "user_3" => {
237
+ "item_3" => 300,
238
+ "item_6" => 330,
239
+ "item_9" => 360
240
+ },
241
+ "user_4" => {
242
+ "item_2" => 150,
243
+ "item_4" => 230,
244
+ "item_7" => 580
245
+ }
246
+ }
247
+ end
248
+
249
+ def new_comer
250
+ {
251
+ 'Toby' => {
252
+ 'Snake on the Plane' => 4.5,
253
+ 'You, Me and Dupree' => 1.0,
254
+ 'Superman Returns' => 4.0
255
+ }
256
+ }
257
+ end
258
+
259
+ def visitors
260
+ {
261
+ 'Lisa Rose' => {
262
+ 'Lady in the Water' => 2.5,
263
+ 'Snake on the Plane' => 3.5,
264
+ 'Just My Luck' => 3.0,
265
+ 'Superman Returns' => 3.5,
266
+ 'You, Me and Dupree' => 2.5,
267
+ 'The Night Listener' => 3.0
268
+ },
269
+
270
+ 'Gene Seymour' => {
271
+ 'Lady in the Water' => 3.0,
272
+ 'Snake on the Plane' => 3.5,
273
+ 'Just My Luck' => 1.5,
274
+ 'Superman Returns' => 5.0,
275
+ 'The Night Listener' => 3.0,
276
+ 'You, Me and Dupree' => 3.5
277
+ },
278
+
279
+ 'Michael Phillips' => {
280
+ 'Lady in the Water' => 2.5,
281
+ 'Snake on the Plane' => 3.0,
282
+ 'Superman Returns' => 3.5,
283
+ 'The Night Listener' => 4.0
284
+ },
285
+
286
+ 'Claudia Puig' => {
287
+ 'Snake on the Plane' => 3.5,
288
+ 'Just My Luck' => 3.0,
289
+ 'The Night Listener' => 4.5,
290
+ 'Superman Returns' => 4.0,
291
+ 'You, Me and Dupree' => 2.5
292
+ },
293
+
294
+ 'Mick LaSalle' => {
295
+ 'Lady in the Water' => 3.0,
296
+ 'Snake on the Plane' => 4.0,
297
+ 'Just My Luck' => 2.0,
298
+ 'Superman Returns' => 3.0,
299
+ 'The Night Listener' => 3.0,
300
+ 'You, Me and Dupree' => 2.0
301
+ },
302
+
303
+ 'Jack Matthews' => {
304
+ 'Lady in the Water' => 3.0,
305
+ 'Snake on the Plane' => 4.0,
306
+ 'The Night Listener' => 3.0,
307
+ 'Superman Returns' => 5.0,
308
+ 'You, Me and Dupree' => 3.5
309
+ }
310
+ }
311
+ end
312
+
313
+ def reversed_critics
314
+ {
315
+ "Lady in the Water" => {
316
+ "Lisa Rose" => 2.5,
317
+ "Gene Seymour" => 3.0,
318
+ "Michael Phillips" => 2.5,
319
+ "Mick LaSalle" => 3.0,
320
+ "Jack Matthews" => 3.0
321
+ },
322
+
323
+ "Snake on the Plane" => {
324
+ "Lisa Rose" => 3.5,
325
+ "Gene Seymour" => 3.5,
326
+ "Michael Phillips" => 3.0,
327
+ "Claudia Puig" => 3.5,
328
+ "Mick LaSalle" => 4.0,
329
+ "Jack Matthews" => 4.0,
330
+ "Toby" => 4.5
331
+ },
332
+
333
+ "Just My Luck" => {
334
+ "Lisa Rose" => 3.0,
335
+ "Gene Seymour" => 1.5,
336
+ "Claudia Puig" => 3.0,
337
+ "Mick LaSalle" => 2.0
338
+ },
339
+
340
+ "Superman Returns" => {
341
+ "Lisa Rose" => 3.5,
342
+ "Gene Seymour" => 5.0,
343
+ "Michael Phillips" => 3.5,
344
+ "Claudia Puig" => 4.0,
345
+ "Mick LaSalle" => 3.0,
346
+ "Jack Matthews" => 5.0,
347
+ "Toby" => 4.0
348
+ },
349
+
350
+ "You, Me and Dupree" => {
351
+ "Lisa Rose" => 2.5,
352
+ "Gene Seymour" => 3.5,
353
+ "Claudia Puig" => 2.5,
354
+ "Mick LaSalle" => 2.0,
355
+ "Jack Matthews" => 3.5,
356
+ "Toby" => 1.0
357
+ },
358
+
359
+ "The Night Listener" => {
360
+ "Lisa Rose" => 3.0,
361
+ "Gene Seymour" => 3.0,
362
+ "Michael Phillips" => 4.0,
363
+ "Claudia Puig" => 4.5,
364
+ "Mick LaSalle" => 3.0,
365
+ "Jack Matthews" => 3.0
366
+ }
367
+ }
368
+ end
@@ -3,11 +3,9 @@
3
3
  require File.dirname(__FILE__) + '/../spec_helper'
4
4
 
5
5
  describe Recommendation do
6
- context 'const get :VERSION should' do
7
- it "return right version number" do
8
- expect = '0.2.0'
9
- Recommendation.const_get(:VERSION).should be_true
10
- Recommendation.const_get(:VERSION).should == expect
11
- end
6
+ context "VERSION" do
7
+ subject { Recommendation::VERSION }
8
+
9
+ it { expect(subject).to eq "0.3.0" }
12
10
  end
13
11
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: recommendation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - id774
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-19 00:00:00.000000000 Z
11
+ date: 2013-12-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cucumber
@@ -71,12 +71,11 @@ files:
71
71
  - doc/LICENSE
72
72
  - doc/README
73
73
  - lib/recommendation.rb
74
- - lib/recommendation/engine.rb
75
- - lib/recommendation/supervisor.rb
74
+ - lib/recommendation/recommender.rb
75
+ - lib/recommendation/version.rb
76
76
  - recommendation.gemspec
77
77
  - script/build
78
- - spec/lib/recommendation/engine_spec.rb
79
- - spec/lib/recommendation/supervisor_spec.rb
78
+ - spec/lib/recommendation/recommender_spec.rb
80
79
  - spec/lib/recommendation_spec.rb
81
80
  - spec/spec_helper.rb
82
81
  - vendor/.gitkeep
@@ -100,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
99
  version: '0'
101
100
  requirements: []
102
101
  rubyforge_project:
103
- rubygems_version: 2.1.5
102
+ rubygems_version: 2.1.11
104
103
  signing_key:
105
104
  specification_version: 4
106
105
  summary: recommendation
@@ -1,88 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # -*- coding: utf-8 -*-
3
-
4
- module Recommendation
5
- class Engine
6
- def recommendation(table, user, similarity=:sim_pearson)
7
- totals_h = Hash.new(0)
8
- sim_sums_h = Hash.new(0)
9
- table.each do |other, val|
10
- next if other == user
11
- sim = __send__(similarity, table, user, other)
12
- next if sim <= 0
13
- table[other].each do |item, val|
14
- if !table[user].keys.include?(item) || table[user][item] == 0
15
- totals_h[item] += table[other][item]*sim
16
- sim_sums_h[item] += sim
17
- end
18
- end
19
- end
20
-
21
- rankings = Hash.new
22
- totals_h.each do |item, total|
23
- rankings[item] = total/sim_sums_h[item]
24
- end
25
-
26
- rankings.sort_by{|k, v| -v}
27
- end
28
-
29
- def top_matches(table, user, n=5, similarity=:sim_pearson)
30
- scores = Array.new
31
- table.each do |key, value|
32
- if key != user
33
- scores << [__send__(similarity, table, user,key), key]
34
- end
35
- end
36
-
37
- result = Array.new
38
- scores.sort.reverse[0,n].each do |k, v|
39
- result << [v, k]
40
- end
41
- result
42
- end
43
-
44
- private
45
-
46
- def sim_pearson(table, user1, user2)
47
- shared_items_a = shared_items_a(table, user1, user2)
48
-
49
- n = shared_items_a.size
50
- return 0 if n == 0
51
-
52
- sum1 = shared_items_a.inject(0) {|result, si|
53
- result + table[user1][si]
54
- }
55
- sum2 = shared_items_a.inject(0) {|result, si|
56
- result + table[user2][si]
57
- }
58
-
59
- sum1_sq = shared_items_a.inject(0) {|result, si|
60
- result + table[user1][si]**2
61
- }
62
- sum2_sq = shared_items_a.inject(0) {|result, si|
63
- result + table[user2][si]**2
64
- }
65
-
66
- sum_products = shared_items_a.inject(0) {|result, si|
67
- result + table[user1][si]*table[user2][si]
68
- }
69
-
70
- num = sum_products - (sum1*sum2/n)
71
- den = Math.sqrt((sum1_sq - sum1**2/n)*(sum2_sq - sum2**2/n))
72
- return 0 if den == 0
73
- return num/den
74
- end
75
-
76
- def shared_items(table, user1, user2)
77
- shared_items_h = Hash.new
78
- table[user1].each do |k, v|
79
- shared_items_h[k] = 1 if table[user2].include?(k)
80
- end
81
- shared_items_h
82
- end
83
-
84
- def shared_items_a(table, user1, user2)
85
- table[user1].nil? ? [] : table[user1].keys & table[user2].keys
86
- end
87
- end
88
- end
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # -*- coding: utf-8 -*-
3
-
4
- module Recommendation
5
- class Supervisor
6
- def initialize(params = {})
7
- @table = params
8
- end
9
-
10
- def table
11
- @table
12
- end
13
-
14
- def train(params = {})
15
- @table.merge!(params)
16
- end
17
-
18
- def transform_table
19
- new_table = {}
20
- @table.each do |key, value|
21
- value.each do |new_key, new_value|
22
- new_table[new_key] ||= Hash.new
23
- new_table[new_key][key] = new_value
24
- end
25
- end
26
- new_table
27
- end
28
- end
29
- end
@@ -1,270 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # -*- coding: utf-8 -*-
3
-
4
- require File.dirname(__FILE__) + '/../../spec_helper'
5
-
6
- describe 'Recommendation::Engine' do
7
- describe 'recommendation' do
8
- it 'should be suggesting interesting products' do
9
- expected = [
10
- ["The Night Listener", 3.3477895267131017],
11
- ["Lady in the Water", 2.8325499182641614],
12
- ["Just My Luck", 2.530980703765565]
13
- ]
14
-
15
- supervisor = Recommendation::Supervisor.new(visitors)
16
- supervisor.train(new_comer)
17
- engine = Recommendation::Engine.new
18
-
19
- new_comer.keys[0].should be_eql 'Toby'
20
- result = engine.recommendation(supervisor.table, new_comer.keys[0])
21
-
22
- result.length.should be_eql 3
23
- result[0][0].should be_eql expected[0][0]
24
- result[0][1].should be_eql expected[0][1]
25
- result[1][0].should be_eql expected[1][0]
26
- result[1][1].should be_eql expected[1][1]
27
- result[2][0].should be_eql expected[2][0]
28
- result[2][1].should be_eql expected[2][1]
29
- end
30
- end
31
-
32
- describe 'top_matches' do
33
- it 'should be finding similar users' do
34
- expected = [
35
- ["Lisa Rose", 0.9912407071619299],
36
- ["Mick LaSalle", 0.9244734516419049],
37
- ["Claudia Puig", 0.8934051474415647],
38
- ["Jack Matthews", 0.66284898035987],
39
- ["Gene Seymour", 0.38124642583151164]
40
- ]
41
-
42
- supervisor = Recommendation::Supervisor.new(visitors)
43
- supervisor.train(new_comer)
44
- engine = Recommendation::Engine.new
45
-
46
- new_comer.keys[0].should be_eql 'Toby'
47
- result = engine.top_matches(supervisor.table, new_comer.keys[0])
48
-
49
- result.length.should be_eql 5
50
- result[0][0].should be_eql expected[0][0]
51
- result[0][1].should be_eql expected[0][1]
52
- result[1][0].should be_eql expected[1][0]
53
- result[1][1].should be_eql expected[1][1]
54
- result[2][0].should be_eql expected[2][0]
55
- result[2][1].should be_eql expected[2][1]
56
- result[3][0].should be_eql expected[3][0]
57
- result[3][1].should be_eql expected[3][1]
58
- result[4][0].should be_eql expected[4][0]
59
- result[4][1].should be_eql expected[4][1]
60
- end
61
- end
62
-
63
- describe 'transform_table ' do
64
- it 'should return reversed critics' do
65
- supervisor = Recommendation::Supervisor.new(visitors)
66
- supervisor.train(new_comer)
67
- engine = Recommendation::Engine.new
68
-
69
- movies = supervisor.transform_table
70
- movies.should be_eql reversed_critics
71
- end
72
- end
73
-
74
- describe 'reversed critics' do
75
- it 'should be found similar items' do
76
- expected = [
77
- ["You, Me and Dupree", 0.6579516949597695],
78
- ["Lady in the Water", 0.4879500364742689],
79
- ["Snake on the Plane", 0.11180339887498941],
80
- ["The Night Listener", -0.1798471947990544],
81
- ["Just My Luck", -0.42289003161103106]
82
- ]
83
-
84
- supervisor = Recommendation::Supervisor.new(visitors)
85
- supervisor.train(new_comer)
86
- engine = Recommendation::Engine.new
87
-
88
- movies = supervisor.transform_table
89
-
90
- new_comer.values[0].keys[2].should be_eql 'Superman Returns'
91
- result = engine.top_matches(movies, new_comer.values[0].keys[2])
92
-
93
- result.length.should be_eql 5
94
- result[0][0].should be_eql expected[0][0]
95
- result[0][1].should be_eql expected[0][1]
96
- result[1][0].should be_eql expected[1][0]
97
- result[1][1].should be_eql expected[1][1]
98
- result[2][0].should be_eql expected[2][0]
99
- result[2][1].should be_eql expected[2][1]
100
- result[3][0].should be_eql expected[3][0]
101
- result[3][1].should be_eql expected[3][1]
102
- result[4][0].should be_eql expected[4][0]
103
- result[4][1].should be_eql expected[4][1]
104
- end
105
- end
106
-
107
- describe 'recommendation for the unexisting user' do
108
- it 'should return empty array' do
109
- expected = []
110
-
111
- supervisor = Recommendation::Supervisor.new(visitors)
112
- supervisor.train(new_comer)
113
- engine = Recommendation::Engine.new
114
-
115
- result = engine.recommendation(supervisor.table, 'hoge')
116
- result.length.should be_eql 0
117
- end
118
- end
119
-
120
- describe 'top_matches for the unexisting item' do
121
- it 'should return all zero score' do
122
- expected = [
123
- ["Toby", 0],
124
- ["Mick LaSalle", 0],
125
- ["Michael Phillips", 0],
126
- ["Lisa Rose", 0],
127
- ["Jack Matthews", 0]
128
- ]
129
-
130
- supervisor = Recommendation::Supervisor.new(visitors)
131
- supervisor.train(new_comer)
132
- engine = Recommendation::Engine.new
133
-
134
- result = engine.top_matches(supervisor.table, 'fuga')
135
-
136
- result.length.should be_eql 5
137
- result[0][0].should be_eql expected[0][0]
138
- result[0][1].should be_eql expected[0][1]
139
- result[1][0].should be_eql expected[1][0]
140
- result[1][1].should be_eql expected[1][1]
141
- result[2][0].should be_eql expected[2][0]
142
- result[2][1].should be_eql expected[2][1]
143
- result[3][0].should be_eql expected[3][0]
144
- result[3][1].should be_eql expected[3][1]
145
- result[4][0].should be_eql expected[4][0]
146
- result[4][1].should be_eql expected[4][1]
147
- end
148
- end
149
- end
150
-
151
- def new_comer
152
- {
153
- 'Toby' => {
154
- 'Snake on the Plane' => 4.5,
155
- 'You, Me and Dupree' => 1.0,
156
- 'Superman Returns' => 4.0
157
- }
158
- }
159
- end
160
-
161
- def visitors
162
- {
163
- 'Lisa Rose' => {
164
- 'Lady in the Water' => 2.5,
165
- 'Snake on the Plane' => 3.5,
166
- 'Just My Luck' => 3.0,
167
- 'Superman Returns' => 3.5,
168
- 'You, Me and Dupree' => 2.5,
169
- 'The Night Listener' => 3.0
170
- },
171
-
172
- 'Gene Seymour' => {
173
- 'Lady in the Water' => 3.0,
174
- 'Snake on the Plane' => 3.5,
175
- 'Just My Luck' => 1.5,
176
- 'Superman Returns' => 5.0,
177
- 'The Night Listener' => 3.0,
178
- 'You, Me and Dupree' => 3.5
179
- },
180
-
181
- 'Michael Phillips' => {
182
- 'Lady in the Water' => 2.5,
183
- 'Snake on the Plane' => 3.0,
184
- 'Superman Returns' => 3.5,
185
- 'The Night Listener' => 4.0
186
- },
187
-
188
- 'Claudia Puig' => {
189
- 'Snake on the Plane' => 3.5,
190
- 'Just My Luck' => 3.0,
191
- 'The Night Listener' => 4.5,
192
- 'Superman Returns' => 4.0,
193
- 'You, Me and Dupree' => 2.5
194
- },
195
-
196
- 'Mick LaSalle' => {
197
- 'Lady in the Water' => 3.0,
198
- 'Snake on the Plane' => 4.0,
199
- 'Just My Luck' => 2.0,
200
- 'Superman Returns' => 3.0,
201
- 'The Night Listener' => 3.0,
202
- 'You, Me and Dupree' => 2.0
203
- },
204
-
205
- 'Jack Matthews' => {
206
- 'Lady in the Water' => 3.0,
207
- 'Snake on the Plane' => 4.0,
208
- 'The Night Listener' => 3.0,
209
- 'Superman Returns' => 5.0,
210
- 'You, Me and Dupree' => 3.5
211
- }
212
- }
213
- end
214
-
215
- def reversed_critics
216
- {
217
- "Lady in the Water" => {
218
- "Lisa Rose" => 2.5,
219
- "Gene Seymour" => 3.0,
220
- "Michael Phillips" => 2.5,
221
- "Mick LaSalle" => 3.0,
222
- "Jack Matthews" => 3.0
223
- },
224
-
225
- "Snake on the Plane" => {
226
- "Lisa Rose" => 3.5,
227
- "Gene Seymour" => 3.5,
228
- "Michael Phillips" => 3.0,
229
- "Claudia Puig" => 3.5,
230
- "Mick LaSalle" => 4.0,
231
- "Jack Matthews" => 4.0,
232
- "Toby" => 4.5
233
- },
234
-
235
- "Just My Luck" => {
236
- "Lisa Rose" => 3.0,
237
- "Gene Seymour" => 1.5,
238
- "Claudia Puig" => 3.0,
239
- "Mick LaSalle" => 2.0
240
- },
241
-
242
- "Superman Returns" => {
243
- "Lisa Rose" => 3.5,
244
- "Gene Seymour" => 5.0,
245
- "Michael Phillips" => 3.5,
246
- "Claudia Puig" => 4.0,
247
- "Mick LaSalle" => 3.0,
248
- "Jack Matthews" => 5.0,
249
- "Toby" => 4.0
250
- },
251
-
252
- "You, Me and Dupree" => {
253
- "Lisa Rose" => 2.5,
254
- "Gene Seymour" => 3.5,
255
- "Claudia Puig" => 2.5,
256
- "Mick LaSalle" => 2.0,
257
- "Jack Matthews" => 3.5,
258
- "Toby" => 1.0
259
- },
260
-
261
- "The Night Listener" => {
262
- "Lisa Rose" => 3.0,
263
- "Gene Seymour" => 3.0,
264
- "Michael Phillips" => 4.0,
265
- "Claudia Puig" => 4.5,
266
- "Mick LaSalle" => 3.0,
267
- "Jack Matthews" => 3.0
268
- }
269
- }
270
- end
@@ -1,111 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # -*- coding: utf-8 -*-
3
-
4
- require File.dirname(__FILE__) + '/../../spec_helper'
5
-
6
- describe 'Recommendation::Supervisor' do
7
- describe 'initialize and table' do
8
- it 'should have empty hash' do
9
- supervisor = Recommendation::Supervisor.new
10
- supervisor.table.length.should be_eql 0
11
- end
12
- end
13
-
14
- describe 'initialize and table with args' do
15
- it 'should have hash of args' do
16
- supervisor = Recommendation::Supervisor.new(initial_data)
17
- supervisor.table.should be_eql initial_data
18
- end
19
- end
20
-
21
- describe 'train' do
22
- it 'should merge additional data' do
23
- supervisor = Recommendation::Supervisor.new(initial_data)
24
- supervisor.train(append_data)
25
- supervisor.table.should be_eql merged_data
26
- end
27
- end
28
-
29
- describe 'integration with engine' do
30
- it 'should be suggesting successful' do
31
- supervisor = Recommendation::Supervisor.new(merged_data)
32
- engine = Recommendation::Engine.new
33
-
34
- expected = [["item_6", 220.0]]
35
- result = engine.recommendation(supervisor.table, 'user_4')
36
-
37
- result.length.should be_eql 1
38
- result[0][0].should be_eql expected[0][0]
39
- result[0][1].should be_eql expected[0][1]
40
-
41
- expected = [["user_2", 1.0], ["user_1", 1.0], ["user_3", 0]]
42
- result = engine.top_matches(supervisor.table, 'user_4')
43
-
44
- result.length.should be_eql 3
45
- result[0][0].should be_eql expected[0][0]
46
- result[0][1].should be_eql expected[0][1]
47
- result[1][0].should be_eql expected[1][0]
48
- result[1][1].should be_eql expected[1][1]
49
- result[2][0].should be_eql expected[2][0]
50
- result[2][1].should be_eql expected[2][1]
51
- end
52
- end
53
- end
54
-
55
- def initial_data
56
- {
57
- "user_1" => {
58
- "item_1" => 100,
59
- "item_2" => 140,
60
- "item_3" => 160
61
- },
62
- "user_2" => {
63
- "item_2" => 200,
64
- "item_4" => 210,
65
- "item_6" => 220
66
- },
67
- "user_3" => {
68
- "item_3" => 300,
69
- "item_6" => 330,
70
- "item_9" => 360
71
- }
72
- }
73
- end
74
-
75
- def append_data
76
- {
77
- "user_1" => {
78
- "item_2" => 400,
79
- "item_7" => 410,
80
- },
81
- "user_4" => {
82
- "item_2" => 150,
83
- "item_4" => 230,
84
- "item_7" => 580
85
- }
86
- }
87
- end
88
-
89
- def merged_data
90
- {
91
- "user_1" => {
92
- "item_2" => 400,
93
- "item_7" => 410
94
- },
95
- "user_2" => {
96
- "item_2" => 200,
97
- "item_4" => 210,
98
- "item_6" => 220
99
- },
100
- "user_3" => {
101
- "item_3" => 300,
102
- "item_6" => 330,
103
- "item_9" => 360
104
- },
105
- "user_4" => {
106
- "item_2" => 150,
107
- "item_4" => 230,
108
- "item_7" => 580
109
- }
110
- }
111
- end