Module: Recommendable::Helpers::Calculations
- Defined in:
- lib/recommendable/helpers/calculations.rb
Class Method Summary collapse
-
.predict_for(user_id, klass, item_id) ⇒ Float
Predict how likely it is that a user will like an item.
-
.similarity_between(user_id, other_user_id) ⇒ Float
Calculate a numeric similarity value that can fall between -1.0 and 1.0.
-
.update_recommendations_for(user_id) ⇒ Object
Used internally to update this user’s prediction values across all recommendable types.
- .update_score_for(klass, id) ⇒ Object
-
.update_similarities_for(user_id) ⇒ Object
Used internally to update the similarity values between this user and all other users.
Class Method Details
.predict_for(user_id, klass, item_id) ⇒ Float
Predict how likely it is that a user will like an item. This probability is not based on percentage. 0.0 indicates that the user will neither like nor dislike the item. Values that approach Infinity indicate a rising likelihood of liking the item while values approaching -Infinity indicate a rising probability of disliking the item.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/recommendable/helpers/calculations.rb', line 145 def predict_for(user_id, klass, item_id) user_id = user_id.to_s item_id = item_id.to_s similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id) liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, item_id) disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, item_id) similarity_sum = 0.0 similarity_sum += Recommendable.redis.smembers(liked_by_set).inject(0) do |memo, id| memo += Recommendable.redis.zscore(similarity_set, id).to_f end similarity_sum += Recommendable.redis.smembers(disliked_by_set).inject(0) do |memo, id| memo -= Recommendable.redis.zscore(similarity_set, id).to_f end liked_by_count = Recommendable.redis.scard(liked_by_set) disliked_by_count = Recommendable.redis.scard(disliked_by_set) prediction = similarity_sum / (liked_by_count + disliked_by_count).to_f prediction.finite? ? prediction : 0.0 end |
.similarity_between(user_id, other_user_id) ⇒ Float
Similarity values are asymmetrical. ‘Calculations.similarity_between(user_id, other_user_id)` will not necessarily equal `Calculations.similarity_between(other_user_id, user_id)`
Calculate a numeric similarity value that can fall between -1.0 and 1.0. A value of 1.0 indicates that both users have rated the same items in the same ways. A value of -1.0 indicates that both users have rated the same items in opposite ways.
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# File 'lib/recommendable/helpers/calculations.rb', line 14 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) # Agreements similarity += Recommendable.redis.sinter(liked_set, other_liked_set).size similarity += Recommendable.redis.sinter(disliked_set, other_disliked_set).size # Disagreements similarity -= Recommendable.redis.sinter(liked_set, other_disliked_set).size similarity -= Recommendable.redis.sinter(disliked_set, other_liked_set).size liked_count += Recommendable.redis.scard(liked_set) disliked_count += Recommendable.redis.scard(disliked_set) end similarity / (liked_count + disliked_count).to_f end |
.update_recommendations_for(user_id) ⇒ Object
Used internally to update this user’s prediction values across all recommendable types. This is called by the background worker.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/recommendable/helpers/calculations.rb', line 87 def update_recommendations_for(user_id) user_id = user_id.to_s nearest_neighbors = Recommendable.config.nearest_neighbors || Recommendable.config.user_class.count Recommendable.config.ratable_classes.each do |klass| rated_sets = [ Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, user_id), Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, user_id), Recommendable::Helpers::RedisKeyMapper.hidden_set_for(klass, user_id), Recommendable::Helpers::RedisKeyMapper.bookmarked_set_for(klass, user_id) ] temp_set = Recommendable::Helpers::RedisKeyMapper.temp_set_for(Recommendable.config.user_class, user_id) similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id) recommended_set = Recommendable::Helpers::RedisKeyMapper.recommended_set_for(klass, user_id) most_similar_user_ids = Recommendable.redis.zrevrange(similarity_set, 0, nearest_neighbors - 1) least_similar_user_ids = Recommendable.redis.zrange(similarity_set, 0, nearest_neighbors - 1) # Get likes from the most similar users sets_to_union = most_similar_user_ids.inject([]) do |sets, id| sets << Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, id) end # Get dislikes from the least similar users least_similar_user_ids.inject(sets_to_union) do |sets, id| sets << Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, id) end return if sets_to_union.empty? # SDIFF rated items so they aren't recommended Recommendable.redis.sunionstore(temp_set, *sets_to_union) item_ids = Recommendable.redis.sdiff(temp_set, *rated_sets) scores = item_ids.map { |id| [predict_for(user_id, klass, id), id] } scores.each do |s| Recommendable.redis.zadd(recommended_set, s[0], s[1]) end Recommendable.redis.del(temp_set) if number_recommendations = Recommendable.config.recommendations_to_store length = Recommendable.redis.zcard(recommended_set) Recommendable.redis.zremrangebyrank(recommended_set, 0, length - number_recommendations - 1) end end true end |
.update_score_for(klass, id) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 |
# File 'lib/recommendable/helpers/calculations.rb', line 168 def update_score_for(klass, id) score_set = Recommendable::Helpers::RedisKeyMapper.score_set_for(klass) liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, id) disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, id) liked_by_count = Recommendable.redis.scard(liked_by_set) disliked_by_count = Recommendable.redis.scard(disliked_by_set) return 0.0 unless liked_by_count + disliked_by_count > 0 z = 1.96 n = liked_by_count + disliked_by_count phat = liked_by_count / n.to_f begin score = (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) rescue Math::DomainError score = 0 end Recommendable.redis.zadd(score_set, score, id) true end |
.update_similarities_for(user_id) ⇒ Object
Used internally to update the similarity values between this user and all other users. This is called by the background worker.
42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/recommendable/helpers/calculations.rb', line 42 def update_similarities_for(user_id) user_id = user_id.to_s # For comparison. Redis returns all set members as strings. similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id) # Only calculate similarities for users who have rated the items that # this user has rated relevant_user_ids = Recommendable.config.ratable_classes.inject([]) do |memo, klass| liked_set = Recommendable::Helpers::RedisKeyMapper.liked_set_for(klass, user_id) disliked_set = Recommendable::Helpers::RedisKeyMapper.disliked_set_for(klass, user_id) item_ids = Recommendable.redis.sunion(liked_set, disliked_set) unless item_ids.empty? sets = item_ids.map do |id| liked_by_set = Recommendable::Helpers::RedisKeyMapper.liked_by_set_for(klass, id) disliked_by_set = Recommendable::Helpers::RedisKeyMapper.disliked_by_set_for(klass, id) [liked_by_set, disliked_by_set] end memo | Recommendable.redis.sunion(*sets.flatten) else memo end end relevant_user_ids.each do |id| next if id == user_id # Skip comparing with self. Recommendable.redis.zadd(similarity_set, similarity_between(user_id, id), id) end if knn = Recommendable.config.nearest_neighbors length = Recommendable.redis.zcard(similarity_set) kfn = Recommendable.config.furthest_neighbors || 0 Recommendable.redis.zremrangebyrank(similarity_set, kfn, length - knn - 1) end true end |