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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ *.sqlite3.db
2
+ *.sqlite3
3
+ *.log
@@ -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
@@ -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
+