recommender 0.1.0 → 2.0.1

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
  SHA256:
3
- metadata.gz: 755889264ba0de9061b5c58e1eb8692f181338f08c2a770bed34ca74693e7142
4
- data.tar.gz: 8968a8eeca32885854a5fc082d739377874edc3e1b826486e2cbc21560253e3f
3
+ metadata.gz: f23eac1f93035fe6f01ceb18a5f62ac4062578eed8ea87f7e7ddb0781fb889af
4
+ data.tar.gz: 66b4a78cc848d0587f1451bff6694eba94653e91ee6a916b1a910c099636ed24
5
5
  SHA512:
6
- metadata.gz: c9378374d88fc1b4e6d7ba84e3bd3a0fa6fe80a5f3c2e5e9d588f33c6254e04213d8880f9481c3e7dbed17bc265552a7b45c97a1d3fdb911e687f060f37b6d00
7
- data.tar.gz: 8f1596d387176345185dd693399b3817279343e30ad03e6b73b42e0a8b0105caf91b9330f767384aa3aa98fa8578f80dea871499b961f184185acd9fe59bef3c
6
+ metadata.gz: 1291601deb871fb74aeee99f1936788daaf293757819f7440babe4881a470e500a57b366a7bd8a42d403845113d6ca707e824bd3fac0e97d13d22721635dee21
7
+ data.tar.gz: 5c2d3e693ef42fa4df000276213dd2e71edd72bce77c44606a6e7fa2f3be435d52f2758d3305ed648d238d7ff163f5f94eb6e13e425b525d304558caebfb5783
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
- ## [Unreleased]
1
+ # Changelog
2
2
 
3
- ## [0.1.0] - 2024-06-18
3
+ ## \[2.0.1\] - 2024-07-16
4
+
5
+ ### Added
6
+
7
+ - Feature: Similarity based on multiple associations combined with weights.
8
+ - Feature: User-item recommendations based on all their items.
9
+
10
+ ### Changed
11
+
12
+ - Internal refactoring to support the new features.
13
+
14
+ ## \[0.1.0\] - 2024-06-18
15
+
16
+ ### Added
4
17
 
5
18
  - Initial release
data/README.md CHANGED
@@ -1,39 +1,71 @@
1
- # Recommender
1
+ ## Recommender Gem
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ The `Recommender` gem is a versatile recommendation engine built for Ruby on Rails applications. It leverages collaborative filtering techniques to generate personalized recommendations based on user interactions and similarities. This gem supports various association types, including `has_and_belongs_to_many`, `has_many :through`, and `has_many`, making it flexible and easy to integrate into different relational database models.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/recommender`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ### Features
6
6
 
7
- ## Installation
7
+ - **Advanced Similarity Measures:** Utilizes the Jaccard Index, Dice-Sørensen Coefficient, and custom collaborative weighting to provide highly accurate recommendations. These measures calculate the similarity between users based on their shared preferences.
8
+ - **Multiple Association Support:** Compatible with `has_and_belongs_to_many`, `has_many :through`, and `has_many` associations, allowing seamless integration with different data models.
9
+ - **Customizable Recommendations:** Easily extendable and configurable to fit the specific needs of your application.
10
+ - **Lightweight and Efficient:** Designed to be efficient and minimalistic, ensuring fast recommendation calculations without heavy overhead.
11
+ - Feature: Similarity based on multiple associations combined with weights.
12
+ - Feature: User-item recommendations based on all their items.
8
13
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
14
+ ### Coming soon:
10
15
 
11
- Install the gem and add to the application's Gemfile by executing:
16
+ - Feature: Recommendations based on a weighted mix of various associations.
12
17
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
18
+ ### Installation
14
19
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
20
+ Add this line to your application's Gemfile:
16
21
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
22
+ `gem 'recommender'`
18
23
 
19
- ## Usage
24
+ And then execute:
20
25
 
21
- TODO: Write usage instructions here
26
+ `bundle install`
22
27
 
23
- ## Development
28
+ ### Usage
24
29
 
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+ Include the `Recommender::Recommendation` module in your model and set the association:
26
31
 
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+ ```ruby
33
+ class User < ApplicationRecord
34
+ include Recommender::Recommendation
28
35
 
29
- ## Contributing
36
+ has_many :movie_likes, dependent: :destroy
37
+ has_many :movies, through: :movie_likes
30
38
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/recommender. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/recommender/blob/main/CODE_OF_CONDUCT.md).
39
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
32
40
 
33
- ## License
41
+ set_association :movies
42
+ end
43
+ ```
34
44
 
35
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
45
+ Now you can get recommendations for an instance:
36
46
 
37
- ## Code of Conduct
47
+ ```ruby
48
+ user = User.find(1)
49
+ recommendations = user.recommendations(results: 5)
50
+ recommendations.each do |recommended_movie, score|
51
+ puts "#{recommended_movie.name} - Score: #{score}"
52
+ end
53
+ ```
38
54
 
39
- Everyone interacting in the Recommender project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/recommender/blob/main/CODE_OF_CONDUCT.md).
55
+ ### How It Works
56
+
57
+ The gem computes recommendations by comparing the preferences of different users. It uses the following measures to calculate similarity:
58
+
59
+ - **Jaccard Index:** Measures the similarity between two sets by dividing the size of the intersection by the size of the union of the sets.
60
+ - **Dice-Sørensen Coefficient:** Calculates similarity as twice the size of the intersection divided by the sum of the sizes of the two sets.
61
+ - **Collaborative Weighting:** Further refines recommendations by considering the commonality and diversity of preferences.
62
+
63
+ These measures are combined to generate a final similarity score, which is then used to recommend items that the user has not yet interacted with but are popular among similar users.
64
+
65
+ ### Contributing
66
+
67
+ Bug reports and pull requests are welcome on GitHub at [https://github.com/Mutuba/recommender](https://github.com/yourusername/recommender). This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
68
+
69
+ ### License
70
+
71
+ The gem is available as open source under the terms of the MIT License.
@@ -5,25 +5,25 @@ require "active_support/concern"
5
5
  module Recommender
6
6
  module Recommendation
7
7
  extend ActiveSupport::Concern
8
-
8
+
9
9
  included do
10
10
  extend ClassMethods
11
11
  end
12
-
12
+
13
13
  AssociationMetadata = Struct.new(:join_table, :foreign_key, :association_foreign_key, :reflection_name)
14
-
14
+
15
15
  module ClassMethods
16
16
  attr_accessor :association_metadata
17
-
17
+
18
18
  def set_association(association_name)
19
19
  reflection = reflect_on_association(association_name.to_sym)
20
20
  raise ArgumentError, "Association '#{association_name}' not found" unless reflection
21
-
22
- self.association_metadata = build_association_metadata(reflection)
21
+
22
+ @association_metadata ||= build_association_metadata(reflection)
23
23
  end
24
-
24
+
25
25
  private
26
-
26
+
27
27
  def build_association_metadata(reflection)
28
28
  case reflection
29
29
  when ActiveRecord::Reflection::HasAndBelongsToManyReflection
@@ -42,38 +42,68 @@ module Recommender
42
42
  )
43
43
  when ActiveRecord::Reflection::HasManyReflection
44
44
  AssociationMetadata.new(
45
- reflection.klass.table_name, # reflection.name.to_s.pluralize,
45
+ reflection.klass.table_name,
46
46
  reflection.foreign_key,
47
47
  reflection.foreign_key,
48
- reflection.name
48
+ reflection.name,
49
49
  )
50
50
  else
51
51
  raise ArgumentError, "Association '#{reflection.name}' is not a supported type"
52
52
  end
53
53
  end
54
54
  end
55
-
56
- def recommendations(results: 5)
55
+
56
+ def recommendations(results: 10)
57
57
  other_instances = self.class.where.not(id: id)
58
- self_items = send(self.class.association_metadata.reflection_name).pluck(:id).to_set
59
-
60
- item_recommendations = other_instances.reduce(Hash.new(0)) do |acc, instance|
61
- instance_items = instance.send(self.class.association_metadata.reflection_name).pluck(:id).to_set
62
- common_items = instance_items & self_items
63
- jaccard_index = common_items.size.to_f / (instance_items | self_items).size
64
- dice_sorensen_coefficient = (2.0 * common_items.size) / (instance_items.size + self_items.size)
65
- collaborative_weight = common_items.size.to_f / Math.sqrt(instance_items.size * self_items.size)
66
- weight = (jaccard_index + dice_sorensen_coefficient + collaborative_weight) / 3.0
58
+
59
+ self_items = associated_items.to_set
60
+
61
+ item_recommendations = calculate_recommendations(other_instances, self_items)
62
+
63
+ sorted_recommendation_ids = sort_recommendations(item_recommendations).take(results)
64
+
65
+ fetch_recommendation_objects(sorted_recommendation_ids, item_recommendations)
66
+ end
67
+
68
+ private
69
+
70
+ def associated_items
71
+ send(self.class.association_metadata.reflection_name).pluck(self.class.association_metadata.association_foreign_key)
72
+ end
73
+
74
+ def calculate_recommendations(other_instances, self_items)
75
+ other_instances.each_with_object(Hash.new(0)) do |instance, acc|
76
+ instance_items = instance.send(self.class.association_metadata.reflection_name).pluck(self.class.association_metadata.association_foreign_key).to_set
77
+ common_items = instance_items & self_items
78
+
79
+ weight = calculate_weight(common_items, instance_items, self_items)
80
+
67
81
  (instance_items - common_items).each do |item_id|
68
- # Exclude items that are already liked by the user
69
82
  acc[item_id] += weight unless self_items.include?(item_id)
70
83
  end
71
- acc
72
84
  end
73
-
74
- sorted_recommendation_ids = item_recommendations.keys.sort_by { |id| item_recommendations[id] }.reverse.take(results)
75
- association_table = self.class.reflect_on_association(self.class.association_metadata.reflection_name).klass
76
- sorted_recommendation_ids.map { |id| [association_table.find(id), item_recommendations[id]] }
85
+ end
86
+
87
+ def calculate_weight(common_items, instance_items, self_items)
88
+ jaccard_index = common_items.size.to_f / (instance_items | self_items).size
89
+ dice_sorensen_coefficient = (2.0 * common_items.size) / (instance_items.size + self_items.size)
90
+ collaborative_weight = common_items.size.to_f / Math.sqrt(instance_items.size * self_items.size)
91
+
92
+ (jaccard_index + dice_sorensen_coefficient + collaborative_weight) / 3.0
93
+ end
94
+
95
+ def sort_recommendations(item_recommendations)
96
+ item_recommendations.keys.sort_by { |id| item_recommendations[id] }.reverse
97
+ end
98
+
99
+ def fetch_recommendation_objects(sorted_recommendation_ids, item_recommendations)
100
+ association = self.class.reflect_on_association(self.class.association_metadata.reflection_name)
101
+ association_table = association.klass
102
+
103
+ sorted_recommendation_ids.map do |id|
104
+ value = item_recommendations[id]
105
+ [association_table.find(id), value.is_a?(Float) && value.nan? ? 0 : value]
106
+ end
77
107
  end
78
108
  end
79
109
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Recommender
4
- VERSION = "0.1.0"
4
+ VERSION = '2.0.1'
5
5
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/recommender/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "recommender"
7
+ spec.version = Recommender::VERSION
8
+ spec.authors = ["Mutuba"]
9
+ spec.email = ["danielmutubait@gmail.com"]
10
+ spec.summary = "A gem for providing recommendations using various similarity measures"
11
+ spec.description = "This gem provides recommendations by calculating similarity scores using the Jaccard Index, Dice-Sørensen Coefficient, and collaborative filtering."
12
+ spec.homepage = "https://github.com/Mutuba/recommender"
13
+ spec.license = "MIT"
14
+ spec.required_ruby_version = ">= 2.6.0"
15
+
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = "https://github.com/Mutuba/recommender"
19
+ spec.metadata["changelog_uri"] = "https://github.com/Mutuba/recommender/blob/main/CHANGELOG.md"
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (File.expand_path(f) == __FILE__) ||
24
+ f.start_with?('recommender-') || # Exclude built gem files
25
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .circleci appveyor Gemfile])
26
+ end
27
+ end
28
+
29
+ spec.test_files = Dir['spec/**/*']
30
+ spec.bindir = "exe"
31
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_development_dependency "rspec", "~> 3.9.0"
35
+ spec.add_development_dependency "pg", "~> 1.1"
36
+ spec.add_development_dependency "factory_bot_rails", "~> 6.2"
37
+ spec.add_runtime_dependency 'rails', '~> 7.1.3'
38
+ spec.add_runtime_dependency 'database_cleaner-active_record', '~> 2.0'
39
+ spec.add_development_dependency 'pry-rails', '~> 0.3'
40
+ spec.add_runtime_dependency 'faker', '~> 2.21'
41
+ end
@@ -1,11 +1,9 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- recommender (0.1.0)
5
- activesupport (>= 3.0.0)
6
- database_cleaner-active_record
4
+ recommender (2.0.0)
5
+ database_cleaner-active_record (~> 2.0)
7
6
  faker (~> 2.21)
8
- pry-rails
9
7
  rails (~> 7.1.3)
10
8
 
11
9
  GEM
@@ -89,7 +87,6 @@ GEM
89
87
  bigdecimal (3.1.8)
90
88
  bindex (0.8.1)
91
89
  builder (3.3.0)
92
- coderay (1.1.3)
93
90
  concurrent-ruby (1.3.3)
94
91
  connection_pool (2.4.1)
95
92
  crass (1.0.6)
@@ -122,7 +119,6 @@ GEM
122
119
  net-pop
123
120
  net-smtp
124
121
  marcel (1.0.4)
125
- method_source (1.1.0)
126
122
  mini_mime (1.1.5)
127
123
  minitest (5.24.0)
128
124
  mutex_m (0.2.0)
@@ -143,11 +139,6 @@ GEM
143
139
  nokogiri (1.16.6-x86_64-linux)
144
140
  racc (~> 1.4)
145
141
  pg (1.5.6)
146
- pry (0.14.2)
147
- coderay (~> 1.1)
148
- method_source (~> 1.0)
149
- pry-rails (0.3.9)
150
- pry (>= 0.10.4)
151
142
  psych (5.1.2)
152
143
  stringio
153
144
  puma (6.4.2)
@@ -6,5 +6,6 @@ class User < ApplicationRecord
6
6
  has_and_belongs_to_many :albums
7
7
 
8
8
  validates :name, presence: true, uniqueness: { case_sensitive: false }
9
+
9
10
  set_association :movies
10
11
  end