Created
December 9, 2014 22:14
-
-
Save benjiwheeler/c1e9918c770510869f55 to your computer and use it in GitHub Desktop.
recursive quality scoring class that allows for arbitrary scoring methods and cycles of dependency
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Quality < ActiveRecord::Base | |
belongs_to :measureable, :polymorphic => true, :inverse_of => :qualities | |
# translates a raw value to a log scale, using expected mean and halflife. | |
# eg, suppose we're scoring the relative reliability of something based on its age. | |
# if 10 days is halflife, and expected mean is 2 days, the raw values and log values will be: | |
# raw value | log value | |
# -------------------------- | |
# 0 days | -.17 | |
# 1 day | -.09 | |
# 2 days | 0 | |
# 5 days | .23 | |
# 12 days | .5 | |
# 22 days | .67 | |
# 100 days | .91 | |
# | |
# in practice, often the mean is simply 0. | |
def self.unit_scale_log_val(raw_val, expected_mean, halflife) | |
diff_from_mean = raw_val - expected_mean | |
sign = diff_from_mean < 0 ? -1.0 : 1.0 | |
return sign * (1.0 - halflife/(diff_from_mean.abs + halflife)) | |
end | |
# from a scorimg method name, get the "aspect" of the object's quality that we are considering. | |
# eg, for "post_overall_quality" or "user_overall_quality", would return "overall". | |
def self.aspect_from_method(method_name) | |
if method_name =~ /^(user_|post_|group_)?(.+)_quality$/ | |
return $2 | |
end | |
return nil | |
end | |
# prevents endless recursion of scoring, by returning a cached value if levels of | |
# scoring are too deep at this point | |
def self.use_cached_if_too_deep(measureable_obj, aspect, cur_call_depth, max_call_depth) | |
if cur_call_depth > max_call_depth | |
existing_quality = 0.0 | |
db_existing_quality = measureable_obj.qualities.where(aspect: aspect) | |
if db_existing_quality.present? | |
existing_quality = db_existing_quality.first.score | |
end | |
return existing_quality | |
end | |
return nil # cached value not appropriate because we're not too deep! | |
end | |
# takes some object and a list of fields whose value to weigh, and ascribes the | |
# resulting score to that object's "aspect" (like "overall") | |
# example of items array: [{method: :user_likes_received_quality, weight: 1.0}] | |
def self.create_quality(measureable_obj, aspect, cur_call_depth, max_call_depth, items) | |
total_score = 0.0 | |
total_weight = 1.0 # prevent division by 0 | |
items.each do |item| | |
if item.has_key? :weight | |
item_weight = item[:weight].to_f.abs | |
# using the :method attribute of this item, calculate the quality of that part of the "aspect" | |
item_score = Quality.send(item[:method].to_s, measureable_obj, cur_call_depth, max_call_depth) | |
total_score += item_score * item_weight | |
total_weight += item_weight | |
end | |
end | |
# ensure no division by 0; return values should be zero-centered | |
return 0 if total_weight - 1.0 == 0 | |
new_quality_val = total_score / (total_weight - 1.0) | |
quality_obj = measureable_obj.qualities.find_or_create_by(aspect: aspect.to_s).update_attributes(score: new_quality_val) | |
return new_quality_val | |
end | |
##################### post quality | |
# calculates score for a post based on the likes it has received | |
def self.post_likes_received_quality(measureable_obj, cur_call_depth=1, max_call_depth=2) | |
# these lines should ideally be static for all quality functions | |
aspect = Quality.aspect_from_method(__method__) | |
cached_val = Quality.use_cached_if_too_deep(measureable_obj, aspect, cur_call_depth, max_call_depth) | |
return cached_val unless cached_val.nil? | |
# determine this value | |
total_likes_score = 0.0 | |
measureable_obj.favorites.each do |favorite| | |
total_likes_score += favorite.value | |
end | |
new_quality_val = Quality.unit_scale_log_val(total_likes_score, 0.0, 2.0) | |
quality_obj = measureable_obj.qualities.find_or_create_by(aspect: aspect.to_s).update_attributes(score: new_quality_val) | |
return new_quality_val | |
end | |
# calculates overall score for a post | |
def self.post_overall_quality(measureable_obj, cur_call_depth=1, max_call_depth=2) | |
# these lines should be static for all quality functions | |
aspect = Quality.aspect_from_method(__method__) | |
cached_val = Quality.use_cached_if_too_deep(measureable_obj, aspect, cur_call_depth, max_call_depth) | |
return cached_val unless cached_val.nil? | |
# determine this value | |
weighted_list = [{method: :post_likes_received_quality, weight: 1.0}] | |
new_quality_val = Quality.create_quality(measureable_obj, aspect, cur_call_depth+1, max_call_depth, weighted_list) | |
return new_quality_val | |
end | |
##################### user quality | |
def self.user_likes_received_quality(measureable_obj, cur_call_depth=1, max_call_depth=2) | |
# these lines should be static for all quality functions | |
aspect = Quality.aspect_from_method(__method__) | |
cached_val = Quality.use_cached_if_too_deep(measureable_obj, aspect, cur_call_depth, max_call_depth) | |
return cached_val unless cached_val.nil? | |
# determine this value; recursively considers the quality of each post by user | |
total_likes_score = 0.0 | |
measureable_obj.posts.each do |post_by_user| | |
post_score = Quality.post_likes_received_quality(post_by_user, cur_call_depth+1, max_call_depth) | |
total_likes_score += post_score | |
end | |
new_quality_val = Quality.unit_scale_log_val(total_likes_score, 0.0, 20.0) | |
quality_obj = measureable_obj.qualities.find_or_create_by(aspect: aspect.to_s).update_attributes(score: new_quality_val) | |
return new_quality_val | |
end | |
def self.user_overall_quality(measureable_obj, cur_call_depth=1, max_call_depth=2) | |
# these lines should be static for all quality functions | |
aspect = Quality.aspect_from_method(__method__) | |
cached_val = Quality.use_cached_if_too_deep(measureable_obj, aspect, cur_call_depth, max_call_depth) | |
return cached_val unless cached_val.nil? | |
# determine this value | |
weighted_list = [{method: :user_likes_received_quality, weight: 1.0}] | |
new_quality_val = Quality.create_quality(measureable_obj, aspect, cur_call_depth+1, max_call_depth, weighted_list) | |
return new_quality_val | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment