acts_as_recommendable 0.0.2
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 +15 -0
- data/Gemfile +3 -0
- data/MIT-LICENSE +20 -0
- data/README +123 -0
- data/Rakefile +17 -0
- data/acts_as_recommendable.gemspec +29 -0
- data/init.rb +14 -0
- data/lib/acts_as_recommendable.rb +365 -0
- data/lib/cache_fix.rb +27 -0
- data/lib/optimizations.rb +73 -0
- data/lib/railtie.rb +12 -0
- data/lib/tasks/acts_as_recommendable_tasks.rake +56 -0
- data/test/.gitignore +3 -0
- data/test/acts_as_recommendable_test.rb +124 -0
- data/test/database.yml +18 -0
- data/test/fixtures/books.yml +81 -0
- data/test/fixtures/user_books.yml +189 -0
- data/test/fixtures/users.yml +41 -0
- data/test/schema.rb +19 -0
- data/test/test_helper.rb +43 -0
- metadata +186 -0
data/lib/cache_fix.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# Because Rails' FileStore doesn't Marshal data
|
2
|
+
rails_ver = Rails.respond_to?(:version) ? Rails.version : Rails::VERSION::STRING
|
3
|
+
if rails_ver == '2.1.0'
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
module Cache
|
7
|
+
class FileStore < Store
|
8
|
+
attr_reader :cache_path
|
9
|
+
|
10
|
+
def read(name, options = nil)
|
11
|
+
super
|
12
|
+
File.open(real_file_path(name), 'rb') { |f| Marshal.load(f) } rescue nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(name, value, options = nil)
|
16
|
+
super
|
17
|
+
ensure_cache_path(File.dirname(real_file_path(name)))
|
18
|
+
File.atomic_write(real_file_path(name), cache_path) { |f| Marshal.dump(value, f) }
|
19
|
+
rescue => e
|
20
|
+
logger.error "Couldn't create cache directory: #{name} (#{e.message})" if logger
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'inline'
|
2
|
+
module MadeByMany
|
3
|
+
module ActsAsRecommendable
|
4
|
+
class Optimizations
|
5
|
+
module InlineC
|
6
|
+
inline do |builder|
|
7
|
+
builder.include "<math.h>"
|
8
|
+
builder.include "<ruby.h>"
|
9
|
+
builder.c '
|
10
|
+
double c_sim_pearson(VALUE items, int n, VALUE prefs1, VALUE prefs2) {
|
11
|
+
double sum1 = 0.0;
|
12
|
+
double sum2 = 0.0;
|
13
|
+
double sum1Sq = 0.0;
|
14
|
+
double sum2Sq = 0.0;
|
15
|
+
double pSum = 0.0;
|
16
|
+
double num;
|
17
|
+
double den;
|
18
|
+
int i;
|
19
|
+
|
20
|
+
VALUE *items_a = RARRAY_PTR(items);
|
21
|
+
|
22
|
+
for(i=0; i<n; i++) {
|
23
|
+
double prefs1_item;
|
24
|
+
double prefs2_item;
|
25
|
+
|
26
|
+
VALUE prefs1_item_ob;
|
27
|
+
VALUE prefs2_item_ob;
|
28
|
+
|
29
|
+
if (!st_lookup(RHASH(prefs1)->ntbl, items_a[i], &prefs1_item_ob)) {
|
30
|
+
prefs1_item = 0.0;
|
31
|
+
} else {
|
32
|
+
prefs1_item = NUM2DBL(prefs1_item_ob);
|
33
|
+
}
|
34
|
+
|
35
|
+
if (!st_lookup(RHASH(prefs2)->ntbl, items_a[i], &prefs2_item_ob)) {
|
36
|
+
prefs2_item = 0.0;
|
37
|
+
} else {
|
38
|
+
prefs2_item = NUM2DBL(prefs2_item_ob);
|
39
|
+
}
|
40
|
+
|
41
|
+
sum1 += prefs1_item;
|
42
|
+
sum2 += prefs2_item;
|
43
|
+
sum1Sq += pow(prefs1_item, 2);
|
44
|
+
sum2Sq += pow(prefs2_item, 2);
|
45
|
+
pSum += prefs2_item * prefs1_item;
|
46
|
+
}
|
47
|
+
|
48
|
+
num = pSum - ( ( sum1 * sum2 ) / n );
|
49
|
+
den = sqrt( ( sum1Sq - ( pow(sum1, 2) ) / n ) * ( sum2Sq - ( pow(sum2, 2) ) / n ) );
|
50
|
+
if(den == 0){
|
51
|
+
return 0.0;
|
52
|
+
} else {
|
53
|
+
return num / den;
|
54
|
+
}
|
55
|
+
}'
|
56
|
+
end
|
57
|
+
end
|
58
|
+
class << self
|
59
|
+
include InlineC
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
module Logic
|
64
|
+
# Pearson score
|
65
|
+
def self.sim_pearson(prefs, items, person1, person2)
|
66
|
+
n = items.length
|
67
|
+
return 0 if n == 0
|
68
|
+
Optimizations.c_sim_pearson(items, n, prefs[person1], prefs[person2])
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/railtie.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rails'
|
2
|
+
|
3
|
+
ActiveRecord::Base.send(:include, MadeByMany::ActsAsRecommendable)
|
4
|
+
|
5
|
+
|
6
|
+
class MadeByMany::Railtie < ::Rails::Railtie
|
7
|
+
|
8
|
+
rake_tasks do
|
9
|
+
load File.join(File.dirname(__FILE__), 'tasks', 'acts_as_recommendable_tasks.rake')
|
10
|
+
# require File.join(File.dirname(__FILE__), 'tasks', 'acts_as_recommendable_tasks.rake')
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
namespace :recommendations do
|
2
|
+
|
3
|
+
desc 'builds the recommendations dataset'
|
4
|
+
task :build => [:environment] do
|
5
|
+
MadeByMany::ActsAsRecommendable::Logic.module_eval do
|
6
|
+
# This will need to change to your specific model:
|
7
|
+
options = User.aar_options
|
8
|
+
|
9
|
+
puts 'Finding items...'
|
10
|
+
|
11
|
+
# You may want to optimize this SQL, like this:
|
12
|
+
items = options[:on_class].pluck(:id)
|
13
|
+
|
14
|
+
prefs = {}
|
15
|
+
|
16
|
+
puts 'Finding users...'
|
17
|
+
|
18
|
+
# You may want to optimize this SQL
|
19
|
+
users = options[:class].includes(options[:on])
|
20
|
+
|
21
|
+
pbar = ProgressBar.create(:title => 'Gen matrix', :total => items.length)
|
22
|
+
items.each do |item_id|
|
23
|
+
prefs[item_id] ||= {}
|
24
|
+
users.each do |user|
|
25
|
+
if user.aar_items_with_scores[item_id]
|
26
|
+
score = user.aar_items_with_scores[item_id].aar_score
|
27
|
+
prefs[item_id][user.id] = score
|
28
|
+
end
|
29
|
+
end
|
30
|
+
pbar.increment
|
31
|
+
end
|
32
|
+
pbar.finish
|
33
|
+
matrix = [users.collect(&:id), prefs]
|
34
|
+
|
35
|
+
pbar = ProgressBar.create(:title => 'Gen dataset', :total => prefs.keys.length)
|
36
|
+
|
37
|
+
if options[:split_dataset]
|
38
|
+
generate_dataset(options, matrix) {|item, scores|
|
39
|
+
Rails.cache.write("aar_#{options[:on]}_#{item}", scores)
|
40
|
+
pbar.inc
|
41
|
+
}
|
42
|
+
else
|
43
|
+
result = {}
|
44
|
+
generate_dataset(options, matrix) {|item, scores|
|
45
|
+
result[item] = scores
|
46
|
+
pbar.increment
|
47
|
+
}
|
48
|
+
Rails.cache.write("aar_#{options[:on]}_dataset", result)
|
49
|
+
end
|
50
|
+
|
51
|
+
pbar.finish
|
52
|
+
|
53
|
+
puts 'Rebuild successful'
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/test/.gitignore
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Book < ActiveRecord::Base
|
4
|
+
has_many :user_books
|
5
|
+
has_many :users, :through => :user_books
|
6
|
+
end
|
7
|
+
|
8
|
+
class UserBook < ActiveRecord::Base
|
9
|
+
belongs_to :book
|
10
|
+
belongs_to :user
|
11
|
+
end
|
12
|
+
|
13
|
+
class User < ActiveRecord::Base
|
14
|
+
has_many :user_books
|
15
|
+
has_many :books, :through => :user_books
|
16
|
+
acts_as_recommendable :books, :through => :user_books
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
class ActsAsRecommendableTest < ActiveSupport::TestCase
|
21
|
+
|
22
|
+
def setup
|
23
|
+
@users = (1..10).collect { |n| User.create(:name => "Patient #{n}") }
|
24
|
+
@books = (1..20).collect { |n| Book.create(:name => "Book #{n}") }
|
25
|
+
|
26
|
+
(1..8).collect { |n| UserBook.create(:user_id => 1, :book_id => n) }
|
27
|
+
[2, 4, 5, 7, 8].collect { |n| UserBook.create(:user_id => 2, :book_id => n) }
|
28
|
+
[3, 4, 5, 6].collect { |n| UserBook.create(:user_id => 3, :book_id => n) }
|
29
|
+
[9, 10, 11].collect { |n| UserBook.create(:user_id => 4, :book_id => n) }
|
30
|
+
[9, 10].collect { |n| UserBook.create(:user_id => 5, :book_id => n) }
|
31
|
+
[1, 2, 19, 20].collect { |n| UserBook.create(:user_id => 6, :book_id => n) }
|
32
|
+
[1, 2, 20].collect { |n| UserBook.create(:user_id => 7, :book_id => n) }
|
33
|
+
[12, 13, 14, 15, 1, 10, 20].collect { |n| UserBook.create(:user_id => 8, :book_id => n) }
|
34
|
+
[12, 13, 1, 10, 20].collect { |n| UserBook.create(:user_id => 9, :book_id => n) }
|
35
|
+
[14, 1, 10, 20].collect { |n| UserBook.create(:user_id => 10, :book_id => n) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_available_methods
|
39
|
+
user = User.find(@users[0].id)
|
40
|
+
assert_not_nil user
|
41
|
+
assert_respond_to user, :similar_users
|
42
|
+
assert_respond_to user, :recommended_books
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_similar_users
|
46
|
+
sim_users = get_sim_users
|
47
|
+
assert_not_nil sim_users
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_similar_users_format
|
51
|
+
sim_users = get_sim_users
|
52
|
+
assert_kind_of Array, sim_users
|
53
|
+
assert_kind_of User, sim_users.first
|
54
|
+
assert_kind_of Numeric, sim_users.first.similar_score
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_similar_users_results
|
58
|
+
sim_users = get_sim_users
|
59
|
+
assert sim_users.include?(User.find(2))
|
60
|
+
assert_respond_to sim_users[0], :similar_score
|
61
|
+
assert !sim_users.include?(User.find(5))
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_similar_users_scores
|
65
|
+
sim_users = get_sim_users
|
66
|
+
assert_respond_to sim_users[0], :similar_score
|
67
|
+
assert sim_users[0].similar_score > 0
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_recommended_books
|
71
|
+
recommended_books = get_recommend_books
|
72
|
+
assert_not_nil recommended_books
|
73
|
+
end
|
74
|
+
|
75
|
+
def test_recommended_books_format
|
76
|
+
recommended_books = get_recommend_books
|
77
|
+
assert_kind_of Array, recommended_books
|
78
|
+
assert_kind_of Book, recommended_books.first
|
79
|
+
assert_kind_of Numeric, recommended_books.first.recommendation_score
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_recommended_books_results
|
83
|
+
recommended_books = get_recommend_books
|
84
|
+
assert_equal true, recommended_books.include?(Book.find(3))
|
85
|
+
assert recommended_books.find {|b| b == Book.find(3) }.recommendation_score > 0
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_recommended_books_scores
|
89
|
+
recommended_books = get_recommend_books
|
90
|
+
assert_respond_to recommended_books[0], :recommendation_score
|
91
|
+
assert recommended_books[0].recommendation_score > 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_dataset
|
95
|
+
use_dataset {
|
96
|
+
MadeByMany::ActsAsRecommendable::Logic.module_eval do
|
97
|
+
generate_dataset(User.aar_options) {|item, scores|
|
98
|
+
Rails.cache.write("aar_#{User.aar_options[:on]}_#{item}", scores)
|
99
|
+
}
|
100
|
+
end
|
101
|
+
recommended_books = get_recommend_books
|
102
|
+
assert_equal true, recommended_books.include?(Book.find(3))
|
103
|
+
assert recommended_books.find {|b| b == Book.find(3) }.recommendation_score > 0
|
104
|
+
}
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
def use_dataset
|
109
|
+
User.aar_options[:use_dataset] = true
|
110
|
+
yield
|
111
|
+
User.aar_options[:use_dataset] = false
|
112
|
+
end
|
113
|
+
|
114
|
+
def get_sim_users
|
115
|
+
user = User.find(1)
|
116
|
+
user.similar_users
|
117
|
+
end
|
118
|
+
|
119
|
+
def get_recommend_books
|
120
|
+
user = User.find(2)
|
121
|
+
user.recommended_books
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
sqlite:
|
2
|
+
:adapter: sqlite
|
3
|
+
:database: acts_as_recommendable.sqlite.db
|
4
|
+
sqlite3:
|
5
|
+
:adapter: sqlite3
|
6
|
+
:database: acts_as_recommendable.sqlite3.db
|
7
|
+
postgresql:
|
8
|
+
:adapter: postgresql
|
9
|
+
:username: postgres
|
10
|
+
:password: postgres
|
11
|
+
:database: acts_as_recommendable_test
|
12
|
+
:min_messages: ERROR
|
13
|
+
mysql:
|
14
|
+
:adapter: mysql
|
15
|
+
:host: localhost
|
16
|
+
:username: rails
|
17
|
+
:password:
|
18
|
+
:database: acts_as_recommendable_test
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
|
2
|
+
|
3
|
+
no_time_for_goodbye:
|
4
|
+
id: 1
|
5
|
+
name: No Time For Goodbye
|
6
|
+
|
7
|
+
the_outcast:
|
8
|
+
id: 2
|
9
|
+
name: The Outcast
|
10
|
+
|
11
|
+
the_forgotten_garden:
|
12
|
+
id: 3
|
13
|
+
name: The Forgotten Garden
|
14
|
+
|
15
|
+
the_road_home:
|
16
|
+
id: 4
|
17
|
+
name: The Road Home
|
18
|
+
|
19
|
+
east_of_the_sun:
|
20
|
+
id: 5
|
21
|
+
name: East of the Sun
|
22
|
+
|
23
|
+
the_kite_runner:
|
24
|
+
id: 6
|
25
|
+
name: The Kite Runner
|
26
|
+
|
27
|
+
the_book_thief:
|
28
|
+
id: 7
|
29
|
+
name: The Book Thief
|
30
|
+
|
31
|
+
watchmen:
|
32
|
+
id: 8
|
33
|
+
name: The Watchmen
|
34
|
+
|
35
|
+
down_river:
|
36
|
+
id: 9
|
37
|
+
name: Down River
|
38
|
+
|
39
|
+
the_ghost:
|
40
|
+
id: 10
|
41
|
+
name: The Ghost
|
42
|
+
|
43
|
+
the_secret:
|
44
|
+
id: 11
|
45
|
+
name: The Secret
|
46
|
+
|
47
|
+
eclipse:
|
48
|
+
id: 12
|
49
|
+
name: Eclipse
|
50
|
+
|
51
|
+
twilight:
|
52
|
+
id: 13
|
53
|
+
name: Twilight
|
54
|
+
|
55
|
+
the_memory_garden:
|
56
|
+
id: 14
|
57
|
+
name: The Memory Garden
|
58
|
+
|
59
|
+
the_discovery_of_france:
|
60
|
+
id: 15
|
61
|
+
name: The Discovery of France
|
62
|
+
|
63
|
+
devil_may_care:
|
64
|
+
id: 16
|
65
|
+
name: Devil May Care
|
66
|
+
|
67
|
+
new_moon:
|
68
|
+
id: 17
|
69
|
+
name: New Moon
|
70
|
+
|
71
|
+
the_return:
|
72
|
+
id: 18
|
73
|
+
name: The Return
|
74
|
+
|
75
|
+
wife_in_the_north:
|
76
|
+
id: 19
|
77
|
+
name: Wife in the North
|
78
|
+
|
79
|
+
fractured:
|
80
|
+
id: 20
|
81
|
+
name: Fractured
|
@@ -0,0 +1,189 @@
|
|
1
|
+
# Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html
|
2
|
+
|
3
|
+
one_one:
|
4
|
+
user_id: 1
|
5
|
+
book_id: 1
|
6
|
+
|
7
|
+
one_two:
|
8
|
+
user_id: 1
|
9
|
+
book_id: 2
|
10
|
+
|
11
|
+
one_three:
|
12
|
+
user_id: 1
|
13
|
+
book_id: 3
|
14
|
+
|
15
|
+
one_four:
|
16
|
+
user_id: 1
|
17
|
+
book_id: 4
|
18
|
+
|
19
|
+
one_five:
|
20
|
+
user_id: 1
|
21
|
+
book_id: 5
|
22
|
+
|
23
|
+
one_six:
|
24
|
+
user_id: 1
|
25
|
+
book_id: 6
|
26
|
+
|
27
|
+
one_seven:
|
28
|
+
user_id: 1
|
29
|
+
book_id: 7
|
30
|
+
|
31
|
+
one_eight:
|
32
|
+
user_id: 1
|
33
|
+
book_id: 8
|
34
|
+
|
35
|
+
two_one:
|
36
|
+
user_id: 2
|
37
|
+
book_id: 1
|
38
|
+
|
39
|
+
two_two:
|
40
|
+
user_id: 2
|
41
|
+
book_id: 2
|
42
|
+
|
43
|
+
two_four:
|
44
|
+
user_id: 2
|
45
|
+
book_id: 4
|
46
|
+
|
47
|
+
two_five:
|
48
|
+
user_id: 2
|
49
|
+
book_id: 5
|
50
|
+
|
51
|
+
|
52
|
+
two_seven:
|
53
|
+
user_id: 2
|
54
|
+
book_id: 7
|
55
|
+
|
56
|
+
two_eight:
|
57
|
+
user_id: 2
|
58
|
+
book_id: 8
|
59
|
+
|
60
|
+
three_three:
|
61
|
+
user_id: 3
|
62
|
+
book_id: 3
|
63
|
+
|
64
|
+
three_four:
|
65
|
+
user_id: 3
|
66
|
+
book_id: 4
|
67
|
+
|
68
|
+
three_five:
|
69
|
+
user_id: 3
|
70
|
+
book_id: 5
|
71
|
+
|
72
|
+
three_six:
|
73
|
+
user_id: 3
|
74
|
+
book_id: 6
|
75
|
+
|
76
|
+
four_one:
|
77
|
+
user_id: 4
|
78
|
+
book_id: 9
|
79
|
+
|
80
|
+
four_two:
|
81
|
+
user_id: 4
|
82
|
+
book_id: 10
|
83
|
+
|
84
|
+
four_three:
|
85
|
+
user_id: 4
|
86
|
+
book_id: 11
|
87
|
+
|
88
|
+
five_one:
|
89
|
+
user_id: 5
|
90
|
+
book_id: 9
|
91
|
+
|
92
|
+
five_two:
|
93
|
+
user_id: 5
|
94
|
+
book_id: 10
|
95
|
+
|
96
|
+
six_one:
|
97
|
+
user_id: 6
|
98
|
+
book_id: 1
|
99
|
+
|
100
|
+
six_two:
|
101
|
+
user_id: 6
|
102
|
+
book_id: 2
|
103
|
+
|
104
|
+
six_three:
|
105
|
+
user_id: 6
|
106
|
+
book_id: 19
|
107
|
+
|
108
|
+
six_four:
|
109
|
+
user_id: 6
|
110
|
+
book_id: 20
|
111
|
+
|
112
|
+
seven_one:
|
113
|
+
user_id: 7
|
114
|
+
book_id: 1
|
115
|
+
|
116
|
+
seven_two:
|
117
|
+
user_id: 7
|
118
|
+
book_id: 2
|
119
|
+
|
120
|
+
seven_four:
|
121
|
+
user_id: 7
|
122
|
+
book_id: 20
|
123
|
+
|
124
|
+
eight_one:
|
125
|
+
user_id: 8
|
126
|
+
book_id: 12
|
127
|
+
|
128
|
+
eight_two:
|
129
|
+
user_id: 8
|
130
|
+
book_id: 13
|
131
|
+
|
132
|
+
eight_three:
|
133
|
+
user_id: 8
|
134
|
+
book_id: 14
|
135
|
+
|
136
|
+
eight_four:
|
137
|
+
user_id: 8
|
138
|
+
book_id: 15
|
139
|
+
|
140
|
+
eight_five:
|
141
|
+
user_id: 8
|
142
|
+
book_id: 1
|
143
|
+
|
144
|
+
eight_six:
|
145
|
+
user_id: 8
|
146
|
+
book_id: 10
|
147
|
+
|
148
|
+
eight_seven:
|
149
|
+
user_id: 8
|
150
|
+
book_id: 20
|
151
|
+
|
152
|
+
nine_one:
|
153
|
+
user_id: 9
|
154
|
+
book_id: 12
|
155
|
+
|
156
|
+
nine_two:
|
157
|
+
user_id: 9
|
158
|
+
book_id: 13
|
159
|
+
|
160
|
+
|
161
|
+
nine_five:
|
162
|
+
user_id: 9
|
163
|
+
book_id: 1
|
164
|
+
|
165
|
+
nine_six:
|
166
|
+
user_id: 9
|
167
|
+
book_id: 10
|
168
|
+
|
169
|
+
nine_seven:
|
170
|
+
user_id: 9
|
171
|
+
book_id: 20
|
172
|
+
|
173
|
+
ten_three:
|
174
|
+
user_id: 10
|
175
|
+
book_id: 14
|
176
|
+
|
177
|
+
ten_five:
|
178
|
+
user_id: 10
|
179
|
+
book_id: 1
|
180
|
+
|
181
|
+
ten_six:
|
182
|
+
user_id: 10
|
183
|
+
book_id: 10
|
184
|
+
|
185
|
+
ten_seven:
|
186
|
+
user_id: 10
|
187
|
+
book_id: 20
|
188
|
+
|
189
|
+
|