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.
- .similarity_total_for(user_id, set) ⇒ Object
-
.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.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/recommendable/helpers/calculations.rb', line 165 def predict_for(user_id, klass, item_id) user_id = user_id.to_s item_id = item_id.to_s 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 += similarity_total_for(user_id, liked_by_set) similarity_sum -= similarity_total_for(user_id, disliked_by_set) liked_by_count, disliked_by_count = Recommendable.redis.pipelined do Recommendable.redis.scard(liked_by_set) Recommendable.redis.scard(disliked_by_set) end 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 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# 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) 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 end |
.similarity_total_for(user_id, set) ⇒ Object
184 185 186 187 188 189 190 191 192 193 |
# File 'lib/recommendable/helpers/calculations.rb', line 184 def similarity_total_for(user_id, set) similarity_set = Recommendable::Helpers::RedisKeyMapper.similarity_set_for(user_id) ids = Recommendable.redis.smembers(set) similarity_values = Recommendable.redis.pipelined do ids.each do |id| Recommendable.redis.zscore(similarity_set, id) end end similarity_values.map(&:to_f).reduce(&:+).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.
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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/recommendable/helpers/calculations.rb', line 103 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, least_similar_user_ids = Recommendable.redis.pipelined do Recommendable.redis.zrevrange(similarity_set, 0, nearest_neighbors - 1) Recommendable.redis.zrange(similarity_set, 0, nearest_neighbors - 1) end # 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 sets_to_union = 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] } Recommendable.redis.pipelined do scores.each do |s| Recommendable.redis.zadd(recommended_set, s[0], s[1]) end 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
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/recommendable/helpers/calculations.rb', line 195 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, disliked_by_count = Recommendable.redis.pipelined do Recommendable.redis.scard(liked_by_set) Recommendable.redis.scard(disliked_by_set) end 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.
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
# File 'lib/recommendable/helpers/calculations.rb', line 55 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 similarity_values = relevant_user_ids.map { |id| similarity_between(user_id, id) } Recommendable.redis.pipelined do relevant_user_ids.zip(similarity_values).each do |id, similarity_value| next if id == user_id # Skip comparing with self. Recommendable.redis.zadd(similarity_set, similarity_value, id) end 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 |