Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 3 additions & 36 deletions lib/recommendable/helpers/calculations.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
require_relative 'calculations/similarity'

module Recommendable
module Helpers
module Calculations
Expand All @@ -12,42 +14,7 @@ class << self
# @return [Float] the numeric similarity between this user and the passed user
# @note Similarity values are asymmetrical. `Calculations.similarity_between(user_id, other_user_id)` will not necessarily equal `Calculations.similarity_between(other_user_id, user_id)`
def similarity_between(user_id, other_user_id)
user_id = user_id.to_s
other_user_id = other_user_id.to_s

similarity = liked_count = disliked_count = 0
in_common = Recommendable.config.ratable_classes.each do |klass|
liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, user_id)
other_liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, other_user_id)
disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, user_id)
other_disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, other_user_id)

results = Recommendable.redis.pipelined do
# Agreements
Recommendable.redis.sinter(liked_set, other_liked_set)
Recommendable.redis.sinter(disliked_set, other_disliked_set)

# Disagreements
Recommendable.redis.sinter(liked_set, other_disliked_set)
Recommendable.redis.sinter(disliked_set, other_liked_set)

Recommendable.redis.scard(liked_set)
Recommendable.redis.scard(disliked_set)
end

# Agreements
similarity += results[0].size
similarity += results[1].size

# Disagreements
similarity -= results[2].size
similarity -= results[3].size

liked_count += results[4]
disliked_count += results[5]
end

similarity / (liked_count + disliked_count).to_f
Similarity.new(user_id, other_user_id).calculate
end

# Used internally to update the similarity values between this user and all
Expand Down
72 changes: 72 additions & 0 deletions lib/recommendable/helpers/calculations/similarity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
module Recommendable
module Helpers
module Calculations
class Similarity
attr_reader :user_id, :other_user_id

def initialize(user_id, other_user_id)
@user_id = user_id.to_s
@other_user_id = other_user_id.to_s
@similarity = 0
@liked_count = 0
@disliked_count = 0
end

def calculate
Recommendable.config.ratable_classes.each do |klass|
sets = liked_and_disliked_sets(klass)
results = agreements_and_disagreements(*sets)
count_agreements_and_disagreements(results)
count_liked_and_disliked(results)
end

@similarity /= (@liked_count + @disliked_count).to_f
end

private

def agreements_and_disagreements(liked_set, other_liked_set, disliked_set, other_disliked_set)
Recommendable.redis.pipelined do
# Agreements
Recommendable.redis.sinter(liked_set, other_liked_set)
Recommendable.redis.sinter(disliked_set, other_disliked_set)

# Disagreements
Recommendable.redis.sinter(liked_set, other_disliked_set)
Recommendable.redis.sinter(disliked_set, other_liked_set)

Recommendable.redis.scard(liked_set)
Recommendable.redis.scard(disliked_set)
end
end

def count_agreements_and_disagreements(results)
add_agreements(results)
subtract_disagreements(results)
end

def add_agreements(results)
@similarity += results[0].size + results[1].size
end

def subtract_disagreements(results)
@similarity -= results[2].size + results[3].size
end

def count_liked_and_disliked(results)
@liked_count += results[4]
@disliked_count += results[5]
end

def liked_and_disliked_sets(klass)
liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, user_id)
other_liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, other_user_id)
disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, user_id)
other_disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, other_user_id)

return [liked_set, other_liked_set, disliked_set, other_disliked_set]
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
$LOAD_PATH.unshift File.expand_path('../../test', __FILE__)
require 'test_helper'

class SimilarityCalculationTest < MiniTest::Unit::TestCase
def setup
@user = Factory(:user)
5.times { |x| instance_variable_set(:"@user#{x+1}", Factory(:user)) }
5.times { |x| instance_variable_set(:"@movie#{x+1}", Factory(:movie)) }
5.upto(9) { |x| instance_variable_set(:"@movie#{x+1}", Factory(:documentary)) }
10.times { |x| instance_variable_set(:"@book#{x+1}", Factory(:book)) }

like(@user, [@movie1, @movie2, @movie3, @book4, @book5, @book6])
dislike(@user, [@book1, @book2, @book3, @movie4, @movie5, @movie6])

# @user.similarity_with(@user1) should == 1.0
like(@user1, [@movie1, @movie2, @movie3, @book4, @book5, @book6, @book7, @book8, @movie9, @movie10])
dislike(@user1, [@book1, @book2, @book3, @movie4, @movie5, @movie6, @movie7, @movie8, @book9, @book10])

# @user.similarity_with(@user2) should == 0.25
like(@user2, [@movie1, @movie2, @movie3, @book4, @book5, @book6])
like(@user2, [@book1, @book2, @book3])

# @user.similarity_with(@user3) should == 0.0
like(@user3, [@movie1, @movie2, @movie3])
like(@user3, [@book1, @book2, @book3])

# @user.similarity_with(@user4) should == -0.25
like(@user4, [@movie1, @movie2, @movie3])
like(@user4, [@book1, @book2, @book3, @movie4, @movie5, @movie6])

# @user.similarity_with(@user5) should == -1.0
dislike(@user5, [@movie1, @movie2, @movie3, @book4, @book5, @book6])
like(@user5, [@book1, @book2, @book3, @movie4, @movie5, @movie6])
end

def test_similarity_between_calculates_correctly
assert_equal similarity(@user.id, @user1.id), 1.0
assert_equal similarity(@user.id, @user2.id), 0.25
assert_equal similarity(@user.id, @user3.id), 0
assert_equal similarity(@user.id, @user4.id), -0.25
assert_equal similarity(@user.id, @user5.id), -1.0
end

def teardown
Recommendable.redis.flushdb
end

def similarity(user_id, other_user_id)
Recommendable::Helpers::Calculations::Similarity.new(user_id, other_user_id).calculate
end

def like(user, collection)
collection.each { |obj| user.like(obj) }
end

def dislike(user, collection)
collection.each { |obj| user.dislike(obj) }
end
end
24 changes: 0 additions & 24 deletions test/recommendable/helpers/calculations_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,6 @@ def setup
# @user.similarity_with(@user1) should == 1.0
[@movie1, @movie2, @movie3, @book4, @book5, @book6, @book7, @book8, @movie9, @movie10].each { |obj| @user1.like(obj) }
[@book1, @book2, @book3, @movie4, @movie5, @movie6, @movie7, @movie8, @book9, @book10].each { |obj| @user1.dislike(obj) }

# @user.similarity_with(@user2) should == 0.25
[@movie1, @movie2, @movie3, @book4, @book5, @book6].each { |obj| @user2.like(obj) }
[@book1, @book2, @book3].each { |obj| @user2.like(obj) }

# @user.similarity_with(@user3) should == 0.0
[@movie1, @movie2, @movie3].each { |obj| @user3.like(obj) }
[@book1, @book2, @book3].each { |obj| @user3.like(obj) }

# @user.similarity_with(@user4) should == -0.25
[@movie1, @movie2, @movie3].each { |obj| @user4.like(obj) }
[@book1, @book2, @book3, @movie4, @movie5, @movie6].each { |obj| @user4.like(obj) }

# @user.similarity_with(@user5) should == -1.0
[@movie1, @movie2, @movie3, @book4, @book5, @book6].each { |obj| @user5.dislike(obj) }
[@book1, @book2, @book3, @movie4, @movie5, @movie6].each { |obj| @user5.like(obj) }
end

def test_similarity_between_calculates_correctly
assert_equal Recommendable::Helpers::Calculations.similarity_between(@user.id, @user1.id), 1.0
assert_equal Recommendable::Helpers::Calculations.similarity_between(@user.id, @user2.id), 0.25
assert_equal Recommendable::Helpers::Calculations.similarity_between(@user.id, @user3.id), 0
assert_equal Recommendable::Helpers::Calculations.similarity_between(@user.id, @user4.id), -0.25
assert_equal Recommendable::Helpers::Calculations.similarity_between(@user.id, @user5.id), -1.0
end

def test_update_recommendations_ignores_rated_items
Expand Down