rails return list of users with average rating above 5 - sql

I have 2 models User and Rating as follows:
class User < ActiveRecord::Base
has_many :ratings
end
class Rating < ActiveRecord::Base
belongs_to :user
end
Each user receives multiple ratings from 1 - 10. I want to return all users with an average rating of > 5. I've got this so far...
User.joins(:ratings).where('rating > ?', 5)
But that code returns all Users with any rating above 5. I want Users with an Average rating above 5.
I've seen other posts like this and that are asking similar questions, but I'm having a brainfart today, and can't simulate their question into an appropriate answer.

If you're looking at all users, why join first?
#avg = Ratings.group(:user_id).average("rating") #returns hash which contains arrays
#avg.each do |avg| #0 is user_id, 1 is value
puts avg[0] if avg[1] > 5
end

You need to defined method average for user rating.
Check link below this is good example of moving float to average.
How do I create an average from a Ruby array?

Hope this helps someone in the future. This will find the average rating of each user through the ratings table, and return all users with an average rating above 5.
User.joins(:ratings).merge(Rating.group(:user_id).having('AVG(rating) > 5'))
.having was my missing link. More examples of .having here and here

Related

Select users whose last associated object was created less than x days before

I am building a Rails 4.2.7.1 which uses Postgres database and I need to write a feature for certain group of users.
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
I need to select users from certain location who have exactly one payment and I also need to be able to pick users whose payment created_at attribute is exactly x
I tried
location.users
.without_deleted
.where(num_payments: 1)
.joins(:payments)
.where('payments.user_id = users.id').order('created_at
DESC').where("payments.created_at < ?", Date.today).group('users.id')
but it did not give me expected results.
Thanks!
You should start from User since this is what you want at end, and take joins with payments since you want to query it along.
User.joins(:payments)
.where(location_id: location.id, num_payments: 1)
.where(payments: { created_at: Date.today })

Find records with the condition from other model rails

I have Bank and Rating models. Bank has many ratings. Also, bank can be active and inactive (when it's license is suspended).
Inactive bank has date field (license_suspended) in DB with the date, when license was suspended. Active banks has nil in this field.
I need to find ratings only for active banks. I can find all banks with license_suspended: nil and then find associated rating with current date and add it one-by-one to array, but I think there is a better way to do it. I need something like this:
#ratings = Rating.where(date: Date.today.beginning_of_month, bank: bank.license_suspended.blank?)
Thanks!
class Bank < ActiveRecord::Base
has_many :ratings
scope :active, -> { where(license_suspended: nil) }
end
class Rating < ActiveRecord::Base
belongs_to :bank
end
I think this will do what you want:
Rating.joins(:bank).where(date: Date.today).where(bank: {license_suspended: nil})
Or this:
Rating.joins(:bank).where(date: Date.today).merge(Bank.active) //this way you reuse active scope from Bank model
This will result in the following query:
SELECT "ratings".* FROM "ratings" INNER JOIN "banks" ON "banks"."id" = "ratings"."bank_id" WHERE "ratings"."date" = 'today_date' AND banks.license_suspended IS NULL
Assuming Ratings belong to a Bank, this looks like it'll do what you want:
Rating.joins(:bank).where(date: Date.today.beginning_of_month, bank: {license_suspended: nil}

Parent entity sorted by votes difference

I have three models that look like this (I just left the stuff important for the question):
class Symbol < ActiveRecord::Base
has_many :mnemonic
end
class Mnemonic < ActiveRecord::Base
belongs_to :symbol
has_many :mnemonic_votes
end
class MnemonicVote < ActiveRecord::Base
belongs_to :mnemonic
attr_accessible :vote_up
end
:vote_up is of boolean type which if true means someone upvoted the mnemonic, and if false means someone downvoted it.
I would like to get top three mnemonics by vote difference. Let's say there are 5 mnemonic records in the database with the following number of up/down votes (MnemonicVote records with true/false as :vote_up field):
mnemonic up down total
mnemonic1 3 2 1
mnemonic2 17 3 14
mnemonic3 2 5 -3
mnemonic4 11 7 4
mnemonic5 5 5 0
I would like to get the following three mnemonics (with counts) by descending order:
mnemonic2 14
mnemonic4 4
mnemonic1 1
I wrote this actual code which gives me the result I want, but I am aware it sucks and I don't like how I did it because the data gets grouped and sorted after all the MnemonicVote records associated with a certaing Mnemonic record are fetched from the DB:
#mnemonics = Mnemonic.where(symbol_id: self.id) # here I fetch all Mnemonics associated with Symbol table
#mnemonics.sort_by { |mnemonic| mnemonic.votes_total }.reverse!
return #mnemonics.take(3)
where mnemonic.votes_total is a calculated attribute on Mnemonic object. I would like to get the same result by using a single AR (or even SQL) query. How can this be accomplished? Thanks.
I believe this is what you want:
Mnemonic.
joins(:mnemonic_votes).
select("mnemonics.*, SUM(IF(mnemonic_votes.upvote, 1, -1)) AS vote").
group("mnemonics.id").
order("vote DESC").
map { |m| [m.symbol, m.vote.to_i] }
Both answers were on the right track, except the IF clause that did not work with PostgreSQL (I would get function if(boolean, integer, integer) does not exist). Here is my final solution in case someone needs it:
Mnemonic.
select("mnemonics.*, SUM(CASE WHEN mnemonic_votes.vote_up THEN 1 ELSE -1 END) AS votes_total").
joins(:mnemonic_votes).
where(symbol_id: self.id).
group("mnemonics.id").
order("votes_total DESC").
limit(3)

Combining Active Record group, join, maximum & minimum

I'm trying to get to grips with the Active Record query interface. I have two models:
class Movie < ActiveRecord::Base
has_many :datapoints
attr_accessible :genre
end
class Datapoint < ActiveRecord::Base
belongs_to :movie
attr_accessible :cumulative_downloads, :timestamp
end
I want to find the incremental downloads per genre for a given time period.
So far I've managed to get the maximum and minimum downloads per movie within a time period, like so:
maximums = Datapoint.joins(:movie)
.where(["datapoints.timestamp > ?", Date.today - #timespan])
.group('datatpoints.movie_id')
.maximum(:cumulative_downloads)
This then allows me to calculate the incremental per movie, before aggregating this into the incremental per genre.
Clearly this is a bit ham-fisted, and I'm sure it would be possible to do this in one step (and using hash conditions). I just can't get my head around how. Can you help?
Much appreciated!
Derek.
I think this will allow you to calculate maximum per genre:
Movie.joins(:datapoints).where(datapoints: {timestamp: (Time.now)..(Time.now+1.year)}).group(:genre).maximum(:cumulative_downloads)
Edit 1
You can get the diffs in a couple of steps:
rel = Movie.joins(:datapoints).where(datapoints: {timestamp: (Time.now)..(Time.now+1.year)}).group(:genre)
mins = rel.minimum(:cumulative_downloads)
maxs = rel.maximum(:cumulative_downloads)
res = {}
maxs.each{|k,v| res[k] = v-mins[k]}
Edit 2
Your initial direction was almost there. All you have to do is calculate the diff per movie in the SQL and stage the data so you can collect it with one pass. I'm sure there's a way to do it all in SQL, but I'm not sure it will be as simple.
# get the genre and diff per movie
result = Movie.select('movies.genre, MAX(datapoints.cumulative_downloads)-MIN(datapoints.cumulative_downloads) as diff').joins(:datapoints).group(:movie_id)
# sum the diffs per genre
per_genre = Hash.new(0)
result.each{|m| per_genre[m.genre] += m.diff}
Edit 3
Including the movie_id in the select and the genre in the group:
# get the genre and diff per movie
result = Movie
.select('movies.movie_id, movies.genre, MAX(datapoints.cumulative_downloads)-MIN(datapoints.cumulative_downloads) as diff')
.joins(:datapoints)
.group('movies.movie_id, movies.genre')
# sum the diffs per genre
per_genre = Hash.new(0)
result.each{|m| per_genre[m.genre] += m.diff}

SQL: Get a selected row index from a query

I have an applications that stores players ratings for each tournament. So I have many-to-many association:
Tournament
has_many :participations, :order => 'rating desc'
has_many :players, :through => :participations
Participation
belongs_to :tournament
belongs_to :player
Player
has_many :participations
has_many :tournaments, :through => :participations
The Participation model has a rating field (float) that stores rating value (it's like score points) for each player at each tournament.
The thing I want - get last 10 ranks of the player (rank is a position of the player at particular tournament based on his rating: the more rating - the higher rank). For now to get a player's rank on a tournament I'm loading all participations for this tournament, sort them by rating field and get the player's participation index with ruby code:
class Participation < ActiveRecord::Base
belongs_to :player
belongs_to :tournament
def rank
tournament.participations.index(self)
end
end
Method rank of the participation gets its parent tournament, loads all tournamentr's participations (ordered by rating desc) and get own index inside this collection
and then something like:
player.participations.last.rank
The one thing I don't like - it need to load all participations for the tournament, and in case I need player ranks for last 10 tournaments it loads over 5.000 items (and its amount will grow when new players added).
I believe that there should be way to use SQL for it. Actually I tried to use SQL variables:
find_by_sql("select #row:=#row+1 `rank`, p.* from participations p, (SELECT #row:=0) r where(p.tournament_id = #{tournament_id}) order by rating desc limit 10;")
This query selects top-10 ratings from the given tournament. I've been trying to modify it to select last 10 participations for a given user and his rank.
Will appreciate any kind of help. (I think solution will be a SQL request, since it's pretty complex for ActiveRecord).
P.S. I'm using rails3.0.0.beta4
UPD:
Here is final sql request that gets last 10 ranks of the player (in addition it loads the participated tournaments as well)
SELECT *, (
SELECT COUNT(*) FROM participations AS p2
WHERE p2.tour_id = p1.tour_id AND p2.rating > p1.rating
) AS rank
FROM participations AS p1 LEFT JOIN tours ON tours.id = p1.tour_id WHERE p1.player_id = 68 ORDER BY tours.date desc LIMIT 10;
First of all, should this:
Participation
belongs_to :tournament
belongs_to :players
be this?
Participation
belongs_to :tournament
belongs_to :player
Ie, singular player after the belongs_to?
I'm struggling to get my head around what this is doing:
class Participation
def rank_at_tour(tour)
tour.participations.index(self)
end
end
You don't really explain enough about your schema to make it easy to reverse engineer. Is it doing the following...?
"Get all the participations for the given tour and return the position of this current participation in that list"? Is that how you calculate rank? If so i agree it seems like a very convoluted way of doing it.
Do you do the above for the ten participation objects you get back for the player and then take the average? What is rating? Does that have anything to do with rank? Basically, can you explain your schema a bit more and then restate what you want to do?
EDIT
I think you just need a more efficient way of finding the position. There's one way i could think of off the top of my head - get the record you want and then count how many are above it. Add 1 to that and you get the position. eg
class Participation
def rank_at_tour(tour)
tour.participations.count("rating > ?", self.rating) + 1
end
end
You should see in your log file (eg while experimenting in the console) that this just makes a count query. If you have an index on the rating field (which you should have if you don't) then this will be a very fast query to execute.
Also - if tour and tournament are the same thing (as i said you seem to use them interchangeably) then you don't need to pass tour to participation since it belongs to a tour anyway. Just change the method to rank:
class Participation
def rank
self.tour.participations.count("rating > ?", self.rating) + 1
end
end
SELECT *, (
SELECT COUNT(*) FROM participations AS p2
WHERE p2.tour_id = p1.tour_id AND p2.rating > p1.rating
) AS rank
FROM participations AS p1 LEFT JOIN tours ON tours.id = p1.tour_id WHERE p1.player_id = 68 ORDER BY tours.date desc LIMIT 10;