recommendation 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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