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 +4 -4
- data/VERSION +1 -1
- data/demo.rb +6 -7
- data/doc/ChangeLog +7 -0
- data/lib/recommendation.rb +2 -3
- data/lib/recommendation/recommender.rb +111 -0
- data/lib/recommendation/version.rb +6 -0
- data/recommendation.gemspec +7 -8
- data/spec/lib/recommendation/recommender_spec.rb +368 -0
- data/spec/lib/recommendation_spec.rb +4 -6
- metadata +6 -7
- data/lib/recommendation/engine.rb +0 -88
- data/lib/recommendation/supervisor.rb +0 -29
- data/spec/lib/recommendation/engine_spec.rb +0 -270
- data/spec/lib/recommendation/supervisor_spec.rb +0 -111
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd59a543a20951969a1025319682cad97420a730
|
4
|
+
data.tar.gz: 79cf33ccaea0af435a7dccf2a3720895e1ad592d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 083fd844755e173b827dabf723fadd9d0065e0edc0d2a263ac9f1ebfc51e544c5025c147223b2cd2b53bc509d69ad2a780fa6f970228b332056d74cc6cf5ddb5
|
7
|
+
data.tar.gz: 00ce801ae2dc6f92b2267f9a02d37168b21c00e07ecc5670e444b1f58b12a87ac31d4e90042f8a03fd8bb0a5dfbf7bcfc3b27cf865359db03547e223f1110018
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
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
|
-
|
10
|
-
|
11
|
-
engine = Recommendation::Engine.new
|
9
|
+
recommender = Recommendation::Recommender.new(visitors)
|
10
|
+
recommender.train(new_comer)
|
12
11
|
|
13
|
-
p
|
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
|
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 =
|
18
|
-
p
|
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
data/lib/recommendation.rb
CHANGED
@@ -2,7 +2,6 @@
|
|
2
2
|
# -*- coding: utf-8 -*-
|
3
3
|
|
4
4
|
module Recommendation
|
5
|
-
|
6
|
-
require File.dirname(__FILE__) + "/recommendation/
|
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
|
data/recommendation.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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/
|
33
|
-
"lib/recommendation/
|
32
|
+
"lib/recommendation/recommender.rb",
|
33
|
+
"lib/recommendation/version.rb",
|
34
34
|
"recommendation.gemspec",
|
35
35
|
"script/build",
|
36
|
-
"spec/lib/recommendation/
|
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.
|
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
|
7
|
-
|
8
|
-
|
9
|
-
|
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.
|
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-
|
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/
|
75
|
-
- lib/recommendation/
|
74
|
+
- lib/recommendation/recommender.rb
|
75
|
+
- lib/recommendation/version.rb
|
76
76
|
- recommendation.gemspec
|
77
77
|
- script/build
|
78
|
-
- spec/lib/recommendation/
|
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.
|
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
|