Act_As_Votable with Reddit style weighting algorithm in Rails - ruby-on-rails

I am creating a rails app that has a User and Post model that implements the Act_As_Votable gem.
I want users to be able to upvote and downvote posts, but also want to rank and sort posts by a weighted_score algorithm that takes into account the number of upvotes, downvotes, and time the post was created.
My weighted_score algorithm is taken from Reddit and better described here.
My Post Model:
class Post < ActiveRecord::Base
belongs_to :user
acts_as_votable
# Raw scores are = upvotes - downvotes
def raw_score
return self.upvotes.size - self.downvotes.size
end
def weighted_score
raw_score = self.raw_score
order = Math.log([raw_score.abs, 1].max, 10)
if raw_score > 0
sign = 1
elsif raw_score < 0
sign = -1
else
sign = 0
end
seconds = self.created_at.to_i - 1134028003
return ((order + sign * seconds / 45000)*7).ceil / 7.0
end
end
I want to use the Acts_As_Voteable gem because it supports caching which may decrease the number of hard disk writes and save time. Currently the weight_score of a post can be calculated on the fly but is not saved in the database, meaning I cannot do database sorts on posts with the highest weighted_score.
If I created a column in the post model I would have to update the posts table every time a user voted on a post, which defeats the purpose of using the Acts_As_Tagable gem (as I don't take advantage of its caching ability).
So I want to add a column to the votes table to store the weighted_score (which will then be calculated every time the post is voted on), as well as a method to the Votes model to calculate this score, however the gem does not provide a model when I run its generator. It only creates a votes table which I do not know how to access without a model.
Any help on how I can add such a weighted_score column and method to the votes model, or on how to achieve efficiently storing the weighted score of a post in a different manner is appreciated.

acts_as_voteable adds methods to your model to access the votes
http://juixe.com/techknow/index.php/2006/06/24/acts-as-voteable-rails-plugin/
positiveVoteCount = post.votes_for
negativeVoteCount = post.votes_against
totalVoteCount = post.votes_count
If you want to add a column, you can run a migration as normal on the table it creates. It also does appear to create a Vote model http://juixe.com/svn/acts_as_voteable/lib/vote.rb

I would add the weighted_score column to your Post model and handle updating via callback. For instance:
class Post < ActiveRecord::Base
#...
before_save :update_weighted_score
#...
def update_weighted_score
# check if some relevant variables have changed first, for example
if cached_votes_total.changed?
# do maths
weighted_score = blah
end
end

You can do this with MYSQL out of the box with decent results, used multi-line for easier readability.
Post.order("
LOG10( ABS( some_score ) + 1 ) * SIGN( some_score ) +
( UNIX_TIMESTAMP( created_at ) / 450000 ) DESC
")
450000 is the number to tweak that will given more weighting to the score vs. the created_at.
Closer to zero gives more weight to the new-ness.
45000 will roughly return scoring for the day
450000 will roughly return scoring for the week
4500000 will roughly return scoring for the month

Related

How to sum all columns in an association and divide by the count to get the average in postgesql / rails

class User < ApplicationRecord
has_many :visits
end
class Visit < ApplicationRecord
belongs_to :user
belongs_to :building
end
class Building < ApplicationRecord
has_many :visits
end
I have the above relationships. A visit has a start_at and end_at, which are timestamps. I am trying to find the average time a user spends at a building. I have the following query.
visits.select('(SUM(CAST(EXTRACT(EPOCH FROM end_at) AS integer)) - SUM(CAST(EXTRACT(EPOCH FROM start_at) AS integer))) / COUNT(visits) AS avg_time_spent')
This "works" but it only gives me the difference in seconds for each visit, not the average for them all. So let's say I have four visits, 2 that are 2 hours, 1 that is 4 hours, and 1 that is 1 hour; for a total of 9hours. The avg_time_spent should be 2.25 hours. Thanks for any help able to be offered.
I have tried some of the answers found on SO. I've tried a sub query with a UNION ALL, GROUPING SETS to no avail
Depending on from which side you're coming from you can do this in the following ways:
Visit
.group(:user_id, :building_id)
.pluck(:user_id, :building_id, 'AVG("visits"."end_at" - "visits"."start_at")')
.map { |*ids, visit_duration| [ids, visit_duration] }
.to_h
Produces a hash with a combination of the user and building id as key and the average visit time as value.
If you're coming from a single user:
user.visits.group(:building_id)
.pluck(:building_id, 'AVG("visits"."end_at" - "visits"."start_at")')
.to_h
Or if you're coming from a building:
building.visits.group(:user_id)
.pluck(:user_id, 'AVG("visits"."end_at" - "visits"."start_at")')
.to_h
I hope the above gives you some inspiration. This answer works only with the ids to keep the query simple. If you want the entire instance set as key you can look them up with a separate query.
An example of this could be:
average_time = user.visits # ...
buildings = average_time.keys.zip(Building.find(average_time.keys)).to_h
average_time.transform_keys! { |building_id| buildings[building_id] }
# The simpler approach
#
# average_time.transform_keys!(&:Building.method(:find))
#
# results in a 1+N query
You can try using postgres' average function AVG:
averaged = user.visits.select("AVG(end_at - start_at) as average_interval, building_id").group(:building_id).preload(:building)
averaged.map{|o| [o.building, o.average_interval]}

Storing values in array or additional table

I'm developing a booking system in rails 5.1 for hotels, whereas the price per night depends on the duration of a stay. Ok, so basically this is a question related to database design and I would like your opinion on which option to go with:
Option A: I just go ahead and save the prices in my Room table in an array, so that:
price_increments = [1_day,2_days,...,n_days] = [80,65,...,x]
I could then access this array by passing the duration of stay, so that:
def booking
days = (end_date - start_date).to_i
price = price_increments.at(days)
total = price * days
end
Option B: I create an additional table for prices, but then I wouldn't be quite sure how to access the respective price with regards to the duration, especially since the application is supposed to be a platform with multiple hotels?
What do you think? Is it save to go with Option A or shall I try to go with Option B? What would be considered to be best practice? Any advice appreciated:)
For a good reason you should develop your logic price_increments record: we need to know if it's a regular price or with major.
Any way i think it's a possible to do your opération before saving record in a singular table with before_save
def Booking < ActiveRecord::Base
before_save :init
PRICE_INCREMENT = [one, two, three, four]
def init
self.duration ||= (end_date - start_date).to_i
self.price ||= duration * PRICE_INCEREMENT[duration]
end
end

If same score then same rank in RAILS 3.2

Hi I have a ranking system wherein if they have same score or points then both users should have same rank.
I am getting it thru the index, but can't manage to make their indexes both equal if they have same score
user.rb
def get_rank
x = User.get_rank.index(self)
x ? (x + 1) : x
end
def self.get_rank
Response.joins(:answer).where("answers.correct is TRUE").map(&:user).uniq.sort_by(&:score).reject{|me| me.super_admin or me.questions.count < Question.count}.reverse
end
How can I make the users who have same scores to have just 1 similar rank.
E.g. if both users get 25 points, and 25 is the highest from the postings, then they must have the first rank.
Any workarounds will be appreciated
The question is rather confusing but I think you could make better use of the database functions. Maybe something like this works, since I don't know your full models, especially which object has the score of the user. I'm assuming its on the user object:
def get_rank
scores = User.select(:score).joins(:response, :answers).where(:answers => [:correct => true]).order('score DESC').group(:score).all
# example result: [24, 22, 21, 20 ...]
rank = scores.index(score) + 1
end
The result of that statement gives you a sorted array of all actually applied scores. Since you do know the current user's score, you can get the index of that score, which is also the rank number.

What's the most efficient way to keep track of an accumulated lifetime sales figure?

So I have a Vendor model, and a Sale model. An entry is made in my Sale model whenever an order is placed via a vendor.
On my vendor model, I have 3 cache columns. sales_today, sales_this_week, and sales_lifetime.
For the first two, I calculated it something like this:
def update_sales_today
today = Date.today.beginning_of_day
sales_today = Sale.where("created_at >= ?", today).find_all_by_vendor_id(self.id)
self.sales_today = 0
sales_today.each do |s|
self.sales_today = self.sales_today + s.amount
end
self.save
end
So that resets that value everytime it is accessed and re-calculates it based on the most current records.
The weekly one is similar but I use a range of dates instead of today.
But...I am not quite sure how to do Lifetime data.
I don't want to clear out my value and have to sum all the Sale.amount for all the sales records for my vendor, every single time I update this record. That's why I am even implementing a cache in the first place.
What's the best way to approach this, from a performance perspective?
I might use ActiveRecord's sum method in this case (docs). All in one:
today = Date.today
vendor_sales = Sale.where(:vendor_id => self.id)
self.sales_today = vendor_sales.
where("created_at >= ?", today.beginning_of_day).
sum("amount")
self.sales_this_week = vendor_sales.
where("created_at >= ?", today.beginning_of_week).
sum("amount")
self.sales_lifetime = vendor_sales.sum("amount")
This would mean you wouldn't have to load lots of sales objects in memory to add the amounts.
You can use callbacks on the create and destroy events for your Sales model:
class SalesController < ApplicationController
after_save :increment_vendor_lifetime_sales
before_destroy :decrement_vendor_lifetime_sales
def increment_vendor_lifetime_sales
vendor.update_attribute :sales_lifetime, vendor.sales_lifetime + amount
end
def decrement_vendor_lifetime_sales
vendor.update_attribute :sales_lifetime, vendor.sales_lifetime - amount
end
end

Rails how to randomly pick a row in a table?

How do I random pick a row in a table?
Example my table:
name age
Lars 24
Grete 56
Hans 56
I want to random pick a name.
Example:
#randomname = Model.where(:name 'Random')
And how do I only pick 1 random name each day. That I can use as a instant variable in view.
#randomname = Model.order('rand()').limit(1).first.name
A random column in Rails 3 is simply:
Model.columns.sample.name
If you want to get random object 1 per day, so you should store it somewhere. You can store it:
in separate file
in separate model
in the same model add rndm field
Lets implement the last one. It is quite easy. Imagine, that your Model called User. First, let's add new date field rndm:
rails g migration add_rndm_to_user rndm:date
rake db:migrate
Now we need to add some methods to your User model
class User < ActiveRecord::Base
def self.random
rndm = find_by_rndm Date.today
unless rndm
update_all :rndm => nil
rndm = self.order('rand()').first
rndm.update_attribute :rndm, Date.today
end
rndm
end
end
so now you can call User.random from your controller
I have some Rails 2 code that shows the idea i think, and it's very easy to convert to rails 3 :
random_monsters = self.find(:all, :conditions=>["level > 0 and level < ? and monster_type='monster'", user_level+2])
random_monster = random_monsters[rand(random_monsters.length)]
I've also seen somebody in SO propose an offset way like :
offset = rand(Model.count)
rand_record = Model.first(:offset => offset)

Resources