recommender 0.1.0 → 2.0.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
  SHA256:
3
- metadata.gz: 755889264ba0de9061b5c58e1eb8692f181338f08c2a770bed34ca74693e7142
4
- data.tar.gz: 8968a8eeca32885854a5fc082d739377874edc3e1b826486e2cbc21560253e3f
3
+ metadata.gz: 6024955f742097c38d66b229a1a39c2eac7665f9a6ccaa870d93d6b5327a8b83
4
+ data.tar.gz: df0f2fe577d47dcc775bc1fe884da3be87f5dbcdb8c23e2e69be470937a9f91f
5
5
  SHA512:
6
- metadata.gz: c9378374d88fc1b4e6d7ba84e3bd3a0fa6fe80a5f3c2e5e9d588f33c6254e04213d8880f9481c3e7dbed17bc265552a7b45c97a1d3fdb911e687f060f37b6d00
7
- data.tar.gz: 8f1596d387176345185dd693399b3817279343e30ad03e6b73b42e0a8b0105caf91b9330f767384aa3aa98fa8578f80dea871499b961f184185acd9fe59bef3c
6
+ metadata.gz: 133276891d8a885b903e8cf2512ff263b56980c7752649234281a7d38fa90dc46758da1a5b7d160919457c009854915bb0452fc7a2cd8660685c6d5ca1cec6b2
7
+ data.tar.gz: d035dc95292eec167e9acad8df02bca40f93084da8e5abad1aafd6d95f89e81366270cb09df843142cf8b0494ce57b5166326e62aff166880237513aaff06533
data/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
- ## [Unreleased]
1
+ \# Changelog
2
2
 
3
- ## [0.1.0] - 2024-06-18
3
+ \## \[Unreleased\]
4
4
 
5
- - Initial release
5
+ \## \[2.0.0\] - 2024-06-22
6
+ \### Added
7
+ \- Feature: Similarity based on multiple associations combined with weights.
8
+ \- Feature: User-item recommendations based on all their items.
9
+ \- Feature: Recommendations based on numerical ratings.
10
+ \- Feature: Recommendations based on a weighted mix of various associations.
11
+
12
+ \### Changed
13
+ \- Internal refactoring to support the new features.
14
+
15
+ \## \[0.1.0\] - 2024-06-18
16
+ \### Added
17
+ \- Initial release
data/README.md CHANGED
@@ -1,39 +1,70 @@
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.
8
11
 
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.
12
+ ### Installation
10
13
 
11
- Install the gem and add to the application's Gemfile by executing:
14
+ Add this line to your application's Gemfile:
12
15
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
16
+ ruby
14
17
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
18
+ Copy code
16
19
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_PRIOR_TO_RELEASE_TO_RUBYGEMS_ORG
20
+ `gem 'recommender'`
18
21
 
19
- ## Usage
22
+ And then execute:
20
23
 
21
- TODO: Write usage instructions here
24
+ sh
22
25
 
23
- ## Development
26
+ Copy code
24
27
 
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.
28
+ `bundle install`
26
29
 
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).
30
+ ### Usage
28
31
 
29
- ## Contributing
32
+ Include the `Recommender::Recommendation` module in your model and set the association:
30
33
 
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).
34
+ ```
35
+ class Album < ApplicationRecord
36
+ include Recommender::Recommendation
37
+ has_and_belongs_to_many :users
38
+ validates :name, presence: true, uniqueness: { case_sensitive: false }
32
39
 
33
- ## License
40
+ set_association :users
41
+ end
42
+ ```
34
43
 
35
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
44
+ Now you can get recommendations for an instance:
36
45
 
37
- ## Code of Conduct
46
+ ```
47
+ user = User.find(1)
48
+ recommendations = user.recommendations(results: 5)
49
+ recommendations.each do |recommended_album, score|
50
+ puts "#{recommended_album.name} - Score: #{score}"
51
+ end
52
+ ```
38
53
 
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).
54
+ ### How It Works
55
+
56
+ The gem computes recommendations by comparing the preferences of different users. It uses the following measures to calculate similarity:
57
+
58
+ - **Jaccard Index:** Measures the similarity between two sets by dividing the size of the intersection by the size of the union of the sets.
59
+ - **Dice-Sørensen Coefficient:** Calculates similarity as twice the size of the intersection divided by the sum of the sizes of the two sets.
60
+ - **Collaborative Weighting:** Further refines recommendations by considering the commonality and diversity of preferences.
61
+
62
+ 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.
63
+
64
+ ### Contributing
65
+
66
+ 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.
67
+
68
+ ### License
69
+
70
+ The gem is available as open source under the terms of the MIT License.
@@ -5,74 +5,85 @@ 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
-
13
- AssociationMetadata = Struct.new(:join_table, :foreign_key, :association_foreign_key, :reflection_name)
14
-
12
+
13
+ AssociationMetaData = Struct.new("AssociationMetaData", :join_table, :foreign_key, :association_foreign_key, :reflection_name, :weight)
14
+
15
15
  module ClassMethods
16
- attr_accessor :association_metadata
17
-
18
- def set_association(association_name)
19
- reflection = reflect_on_association(association_name.to_sym)
20
- raise ArgumentError, "Association '#{association_name}' not found" unless reflection
21
-
22
- self.association_metadata = build_association_metadata(reflection)
16
+ attr_accessor :association_meta_data
17
+
18
+ def set_associations(associations)
19
+ @association_meta_data = associations.map do |association_name, weight|
20
+ reflection = reflect_on_association(association_name.to_sym)
21
+ raise ArgumentError, "Association '#{association_name}' not found" unless reflection
22
+
23
+ build_association_meta_data(reflection, weight)
24
+ end
23
25
  end
24
-
26
+
25
27
  private
26
-
27
- def build_association_metadata(reflection)
28
+
29
+ def build_association_meta_data(reflection, weight)
28
30
  case reflection
29
31
  when ActiveRecord::Reflection::HasAndBelongsToManyReflection
30
- AssociationMetadata.new(
32
+ AssociationMetaData.new(
31
33
  reflection.join_table,
32
34
  reflection.foreign_key,
33
35
  reflection.association_foreign_key,
34
- reflection.name
36
+ reflection.name,
37
+ weight
35
38
  )
36
39
  when ActiveRecord::Reflection::ThroughReflection
37
- AssociationMetadata.new(
40
+ AssociationMetaData.new(
38
41
  reflection.through_reflection.table_name,
39
42
  reflection.through_reflection.foreign_key,
40
43
  reflection.association_foreign_key,
41
- reflection.name
44
+ reflection.name,
45
+ weight
42
46
  )
43
47
  when ActiveRecord::Reflection::HasManyReflection
44
- AssociationMetadata.new(
45
- reflection.klass.table_name, # reflection.name.to_s.pluralize,
48
+ AssociationMetaData.new(
49
+ reflection.klass.table_name,
46
50
  reflection.foreign_key,
47
51
  reflection.foreign_key,
48
- reflection.name
52
+ reflection.name,
53
+ weight
49
54
  )
50
55
  else
51
56
  raise ArgumentError, "Association '#{reflection.name}' is not a supported type"
52
57
  end
53
58
  end
54
59
  end
55
-
60
+
56
61
  def recommendations(results: 5)
57
62
  other_instances = self.class.where.not(id: id)
58
- self_items = send(self.class.association_metadata.reflection_name).pluck(:id).to_set
59
-
60
63
  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
67
- (instance_items - common_items).each do |item_id|
68
- # Exclude items that are already liked by the user
69
- acc[item_id] += weight unless self_items.include?(item_id)
64
+ self.class.association_meta_data.each do |meta_data|
65
+ self_items = send(meta_data.reflection_name).pluck(:id).to_set
66
+ instance_items = instance.send(meta_data.reflection_name).pluck(:id).to_set
67
+ common_items = self_items & instance_items
68
+
69
+ jaccard_index = common_items.size.to_f / (self_items | instance_items).size
70
+ dice_sorensen_coefficient = (2.0 * common_items.size) / (self_items.size + instance_items.size)
71
+ collaborative_weight = common_items.size.to_f / Math.sqrt(self_items.size * instance_items.size)
72
+
73
+ # Calculate weighted score for this association
74
+ association_weighted_score = meta_data.weight * (jaccard_index + dice_sorensen_coefficient + collaborative_weight) / 3.0
75
+
76
+ # Recommend items based on this association
77
+ (instance_items - common_items).each do |item_id|
78
+ acc[item_id] += association_weighted_score unless self_items.include?(item_id)
79
+ end
70
80
  end
81
+
71
82
  acc
72
83
  end
73
-
84
+
74
85
  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
86
+ association_table = self.class.reflect_on_association(self.class.association_meta_data.first.reflection_name).klass
76
87
  sorted_recommendation_ids.map { |id| [association_table.find(id), item_recommendations[id]] }
77
88
  end
78
89
  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.0'
5
5
  end
@@ -0,0 +1,42 @@
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_runtime_dependency 'activesupport', '~> 3.0', '>= 3.0.0'
35
+ spec.add_development_dependency "rspec", "~> 3.9.0"
36
+ spec.add_development_dependency "pg", "~> 1.1"
37
+ spec.add_development_dependency "factory_bot_rails", "~> 6.2"
38
+ spec.add_runtime_dependency 'rails', '~> 7.1.3'
39
+ spec.add_runtime_dependency 'database_cleaner-active_record', '~> 2.0'
40
+ spec.add_development_dependency 'pry-rails', '~> 0.3'
41
+ spec.add_runtime_dependency 'faker', '~> 2.21'
42
+ end
@@ -4,5 +4,5 @@ class Album < ApplicationRecord
4
4
  has_and_belongs_to_many :users
5
5
  validates :name, presence: true, uniqueness: { case_sensitive: false }
6
6
 
7
- set_association :users
7
+ set_associations users: 2.0
8
8
  end
@@ -6,5 +6,5 @@ class User < ApplicationRecord
6
6
  has_and_belongs_to_many :albums
7
7
 
8
8
  validates :name, presence: true, uniqueness: { case_sensitive: false }
9
- set_association :movies
9
+ set_associations movies: 3.0
10
10
  end