Skip to content

Instantly share code, notes, and snippets.

@benjiwheeler
Created December 9, 2014 22:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save benjiwheeler/c1e9918c770510869f55 to your computer and use it in GitHub Desktop.
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
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