Ruby on Rails - Calculate Rank - ruby-on-rails

On Rails 4. I am building a contest application. I have three tables relevant to this use case:
Submissions - Attributes include:
:contest_year - The year the submission was created
:category_id - The contest category the submission is assigned to
:division_id - The contest division the submission is assigned to (out of five)
Scores - Attributes include:
:user_id - The ID of the judge who gives the score
:submission_id - The submission ID the score is tied to
:total_score - The integer number the judge provides as a score of the entry; usually out of ten
Judges - Attributes include:
:user_id - The user ID of the judge
:category_id - The category the judge is assigned to score
:division_id - The division the judge is assigned to score
One submission can have many scores (as there are multiple judges assigned to its category/division).
The first thing the app must do is find the submission's combined, final score out of all its child scores the various judges give it. I do this using a calculation in submission.rb:
def calculate_final_score
self.scores.average(:total_score)
end
So basically, it finds all the child scores of the submission and takes the average of those to find the final score. This final score is NOT an updated attribute of the submission table, it is a method calculation result.
Now, here is where my question is. I need to find a way to calculate the submission's ranking compared to other submissions with the SAME :contest_year, :category_id, and :division_id by comparing that above final score calculation. The ranking system MUST give the same rank when submissions have the same final score (a tie).
tl;dr, I need ranking behavior like this (sample submissions table):
----------------------------------------------------------------------
| ID | Contest Year | Division ID | Category ID | Final Score | Rank |
|--------------------------------------------------------------------|
| 1 | 2013 | 1 | 1 | 10 | 1 |
|--------------------------------------------------------------------|
| 2 | 2013 | 1 | 1 | 10 | 1 |
|--------------------------------------------------------------------|
| 3 | 2013 | 1 | 1 | 8 | 2 |
|--------------------------------------------------------------------|
| 4 | 2013 | 1 | 1 | 6 | 3 |
|--------------------------------------------------------------------|
| 4 | 2013 | 1 | 1 | 5 | 4 |
|--------------------------------------------------------------------|
| 5 | 2013 | 2 | 1 | 8 | 1 |
|--------------------------------------------------------------------|
| 6 | 2014 | 1 | 2 | 9 | 1 |
|--------------------------------------------------------------------|
| 7 | 2014 | 1 | 2 | 7 | 2 |
----------------------------------------------------------------------
This ranking information will be placed in my Active Admin submissions' index table page (and also as part of the CSV table output).

I am not much familiarly with Rails (or with Ruby). But in our Django application we had a similar situation where we needed to calculate ranks on some computed data. As we were using PostgreSQL as the DB backend, we used Postges's window functions (http://www.postgresql.org/docs/9.1/static/functions-window.html, http://www.postgresql.org/docs/9.1/static/functions-window.html, http://www.postgresql.org/docs/9.1/static/sql-expressions.html#SYNTAX-WINDOW-FUNCTIONS) for rank calculation. Following is a sample PostgresSQL query which can produce the result required by your question.
sql_query = %Q{SELECT
*, dense_rank() OVER (
PARTITION BY contest_year, category_id, division_id
ORDER BY final_score DESC
)
FROM (
SELECT
Submissions.id,
Submission.contest_year AS contest_year,
Submission.category_id AS category_id,
Submission.division_id AS division_id,
AVG(Scores.total_score) AS final_score
FROM Submissions
INNER JOIN Scores ON (Submissions.id = Scores.submission_id)
GROUP BY
Submissions.id,
Submission.contest_year,
Submission.category_id,
Submission.division_id
) AS FinalScores}
submissions_by_rank = Submission.find_by_sql(sql_query)
Note: You will need to add ORDER BY clause in the query if you want to order the result in a particular fashion.

In Ruby, assuming you have that #calculate_final_score method set up already:
class Submission < ActiveRecord::Base
def self.rank_all_submissions_by_group
keys = [ :contest_year, :category_id, :division_id ]
submission_groups = Submission.all.group_by do |sub|
values = keys.map { |k| sub.send(key) }
Hash[key.zip(values)]
end
return ranked_submissions_by_group = submission_groups.map do |group, submissions|
group[:ranked_submissions] = submissions.map do |s|
[s, s.calculate_final_score]
end.sort_by(&:last)
end
end
end
The data that'll be returned will look like:
[
{
:contest_year => 2013,
:division_id => 1,
:category_id => 1,
:ranked_submissions => [
[ <Submission>, 10 ],
[ <Submission>, 10 ],
[ <Submission>, 8 ],
[ <Submission>, 6 ],
[ <Submission>, 5 ],
],
},
{
:contest_year => 2013,
:division_id => 2,
:category_id => 1,
:ranked_submissions => [
[ <Submission>, 8 ],
],
},
{
:contest_year => 2014,
:division_id => 1,
:category_id => 2,
:ranked_submissions => [
[ <Submission>, 9 ],
[ <Submission>, 7 ],
],
},
]
It won't be very performant though, since it just goes through all submissions for all time, makes a lot of copies (I think?), and I'm not using the right sorting algorithm.
If you have any issues/concerns, let me know, and I can update my answer.
Additional Help
If you're looking for a way to run a raw query in Ruby, consider this trivial example a suggestion:
full_submissions_table_as_hash = ActiveRecord::Base.connection.select_all(<<-mysql)
SELECT *
FROM #{Submissions.table_name}
mysql
Another trivial example showing how the data can be rendered from a controller:
# assuming routes are properly set up to actions in HomeController
class HomeController < ActionController::Base
def ranked_submissions_text
render :text => Submission.rank_all_submissions_by_group
end
def ranked_submissions_json
render :json => Submission.rank_all_submissions_by_group
end
end

Related

How to Rank in Rails After Grouping

In my Rails 6 app where I'm using Postgres, I have a table called UserCategories.
| id | user_id | category_id | points| rank |
What I'm trying to do is:
Group the records by category_id
Sort the records for each category_id by points (desc)
Update the rank field based on the order the record is in for the category_id
Example (desired rank determined by the points per category_id):
| id | user_id | category_id | points| rank |
| 1 | 1 | 1 | 2 | | # I want rank to be 1
| 2 | 2 | 1 | 1 | | # I want rank to be 2
| 3 | 1 | 2 | 3 | | # I want rank to be 1
| 4 | 2 | 2 | 3 | | # I want rank to be 1
My model method:
def self.calculate_user_category_ranks
#user_categories = UserCategory.select(:id, :points, :user_id, :category_id, :rank).all.order(points: :desc).group_by(&:category_id)
# returns:
# {2=>[#<UserCategory:0x000000000de8be00 id: 2, user_id: 1, category_id: 2, points: 3, rank: 0>, #<UserLeague:0x000000000de8bce8 id: 4, user_id: 2, category_id: 2, points: 3, rank: 0>],
1=>[#<UserCategory:0x000000000de8bbf8 id: 1, user_id: 1, category_id: 1, points: 2, rank: 0>, <UserLeague:0x000000000de8bb30 id: 3, user_id: 2, category_id: 1, points: 1, rank: 0>]}
rank = 0
points_counter = 0
#user_categories.each do |id, points|
uc = UserCategory.find(id)
if points != point_counter
rank += 1
point_counter = points
end
uc.rank = rank
uc.save
end
end
After I execute this code:
| id | user_id | category_id | points| rank |
| 1 | 1 | 1 | 2 | 2 | # I want rank to be 1
| 2 | 2 | 1 | 1 | 0 | # I want rank to be 2
| 3 | 1 | 2 | 3 | 1 | # I want rank to be 1
| 4 | 2 | 2 | 3 | 0 | # I want rank to be 1
Can someone please help me determine what I'm doing wrong?
You may be choosing to put rank as a database column for reasons of efficiency, but the principles of database normalization would suggest that it is "bad practice" to have a column whose value is calculable from the other columns in the table. So, recognizing that you may not accept this solution for reasons of efficiency, let me suggest that, for any particular instance of UserCategory you can determine its rank in Ruby:
class UserCategory < ApplicationRecord
scope :in_the_same_category, ->(category_id) { where("category_id = ?", category_id }
def in_my_category
UserCategory.in_the_same_category(category_id)
end
def rank
in_my_category.
sort_by(&:points).
reverse.
map(&:points).
uniq.
index(points) + 1
end
end
Start with
UserCategory.group(:category_id)
Then build it up
If you really need to restrict the fields returned or add a where clause then
UserCategory.where(some_field: "some_value").select(:id, :points, :user_id, :category_id, :rank).group(:category_id).order(:points :desc)
https://apidock.com/rails/ActiveRecord/QueryMethods/group for more info

Count associations based on attribute value

Given a class, I would like to return all objects (index) sorted by the number of associations they have with a specific attribute.
For example, let's say User has_many :fish I want to show a list of all users and order them by how many :fish they have that are color: :red (Assuming that's an enum/integer value)
Users with 0 red fish, or 0 fish at all, should still be shown on the table, but sorted to the bottom.
-- User -- | - # of fish - | - # of red fish -
2 | 10 | 10
5 | 25 | 7
6 | 11 | 6
7 | 18 | 5
1 | 27 | 4
3 | 23 | 1
4 | 3 | 0
8 | 0 | 0
So far I've managed to sort the User by the number of fish they have using the left_join gem, but I'm having trouble sorting by an attribute on that model.
This is how I am sorting by the number of fish:
#users = User.left_join(:fish).group("user.id").order("count(user.id) desc")
Any help would be awesome! I'm assuming this is trivial, but I'm also displaying that data as well, so any way to store that data temporarily would be awesome too! (I'm assuming using a select and setting the counts as variables)
EDIT:
Solution, thanks to Anthony E.
#users = User.select("users.*")
.joins("LEFT JOIN fishes ON fishes.user_id = users.id")
.where(fishes: { color: Fish.colors[:red] } )
.group("users.id").order("COUNT(fishes.id) DESC")
.page(params[:page]) // To work with Kaminari/pagination
How about:
User.select("user.*, COUNT(fish.id) AS fish_count")
.joins(:fish)
.where(fish: { color: "red" })
.group("user.id")
.order("fish_count DESC")
The benefit is that you can call fish_count on each object in the returned returned collection because it's in your select
EDIT
To include users with 0 fish, use a LEFT JOIN and modify the where clause:
User.select("user.*, COUNT(fish.id) AS fish_count")
.joins("LEFT JOIN ON fish.user_id = user.id")
.where("fish.color = 'red' OR fish.color IS NULL")
.group("user.id")
.order("fish_count DESC")

Unlimited arbitrary properties (key/value pairs) for an ActiveRecord model

Using ruby on rails, I have a Customer table that I want to be able to add unlimited properties (key value pairs) to. I'm not sure what the key/value pairs will be yet so I'm not sure how to do this. For example, one customer could be:
Customer 1 properties:
color: 'yellow'
brand: 'nike'
sales: '33'
Customer 2 properties:
color: 'red'
phone_number: '1111111111'
purchases: '2'
Basically, customers can have any number of properties in a key/value pair.
How can I do this?
The "traditional" way to do this is with the Entity-Attribute-Value, or EAV pattern. As the name suggests, you'll create a new table with three columns: one for the "entity," which in this case is the Customer, one for the "attribute" name or key, and one for the value. So you'd have a table like this:
customer_properties
+----+-------------+--------------+------------+
| id | customer_id | key | value |
+----+-------------+--------------+------------+
| 1 | 1 | color | yellow |
| 2 | 1 | brand | nike |
| 3 | 1 | sales | 33 |
| 4 | 2 | color | red |
| 5 | 2 | phone_number | 1111111111 |
| 6 | 2 | purchases | 2 |
+----+-------------+--------------+------------+
You'll definitely want an INDEX on key and maybe on value (and customer_id, of course, but Rails will do that for you when you use relation or belongs_to in your migration).
Then in your models:
# customer.rb
class Customer < ActiveRecord::Base
has_many :customer_properties
end
# customer_property.rb
class CustomerProperty < ActiveRecord::Base
belongs_to :customer
end
This enables usage like this:
customer = Customer.joins(:customer_properties)
.includes(:customer_properties)
.where(customer_properties: { key: "brand", value: "nike" })
.first
customer.customer_properties.each_with_object({}) do |prop, hsh|
hsh[prop.key] = prop.val
end
# => { "color" => "yellow",
# "brand" => "nike",
# "sales" => "33" }
customer.customer_properties.create(key: "email", value: "foo#bar.com")
# => #<CustomerProperty id: 7, customer_id: 1, key: "email", ...>
As database design goes this is pretty solid, but as you can see it has some limitations: In particular, it's cumbersome. Also, you're restricted to a single value type (:string/VARCHAR is common). If you go this route you'll likely want to define some convenience methods on Customer to make accessing and updating properties less cumbersome. I'm guessing there are probably gems specifically for making the EAV pattern work nicely with ActiveRecord, but I don't know them off the top of my head and I hope you'll forgive me for not googling, since I'm mobile.
As Brad Werth points out, if you just need to store arbitrary properties and not query by them, serialize is a great alternative, and if you use PostgreSQL even the querying problem is surmountable thanks to its great hstore feature.
Good luck!
You may want to look into the hydra_attribute gem, which is an implementation of the Entity-Attribute-Value (EAV) pattern for ActiveRecord models.
You should be able to use serialize for this, and assign your properties hash to your properties attribute, and retrieve them in the same way.

ruby iteration based on table values

The project:
I'm creating a dynamic reporting system.
Metrics define the purpose of the reported data. For example:
"13 house fires."
The admin will define "house fires" as a metric through one form, the reporters will simply add the "13" through a different form.
However, there's another level: I want the reporting to be verbose across connected data points:
"13 house fires during January affecting 42 individuals (or 16 families) "
The verbage is stored in a table "metrics", the data is stored in a table "metrics_data"
Here's the metrics table from the sample above:
metric_id | parentID | childID | prefix | suffix | program_id
1 | 1 | 1 | | house fires | 1
2 | 1 | 2 | during | | 1
3 | 1 | 3 | affecting | individuals | 1
4 | 1 | 4 | (or | families | 1
The key for the sentence-based organization is the parentID - childID relationship.
Here's the metrics_data table:
metric_id | value | date
1 | 13 | 01/01/12
2 | nil | 01/01/12
3 | 42 | 01/01/12
4 | 16 | 01/01/12
The goal:
I want to organize the view (looping through the parentID) to show the metric verbosely:
Program #1
(parentID: 1): "# house fires during (date) affecting # individuals (or # families) "
(parentID: 2): "# shelters during (date) providing # overnight stays (for # individuals) "
Program #2
(parentID: 1): "# new volunteers recruited during (date) "
(parentID: 2): "# volunteers served # hours during (date) "
The code:
class Program < ActiveRecord::Base
has_many :programs_metrics
has_many :metrics, through: :programs_metrics
end
class Metric < ActiveRecord::Base
has_many :programs_metrics
has_many :metrics, through: :programs_metrics
end
Partial _program.html.erb (for programs/index.html.erb):
<% program.metrics.each do |metrics| %>
<div class="row offset1">
<% program.metrics.each do |pid| %> #how do I loop here based on parentID, sorted by childID?
<%= pid.prefix %>
#
<%= pid.suffix %>
<% end %>
</div>
<% end %>
I know I could split this out into a separate table and define the relationship between parentID and childID between two tables, but it seems overly complex to add another relationship layer.
You should probably treat the "parent" and "child" elements like attributes to Metrics, rather than trying to reuse the same model. An alternative would be to create another model called "Events" or whatever, and these Events are related to programs, and Metrics are related to Events. I would probably create a "Metric Type" as well instead of having a prefix or suffix associated directly with each record.
I think that trying to treat all the Metrics at the same level, and trying to loop through them with parentID and childID to create a sentence is infinitely more complicated than just adding another layer to the relationship.
Hopefully this is enough of a push...

Select minimum value from a group in activerecord

I need to fetch like the winner bids, and a bid can be for a different date (don't ask why), so I need to select the bid with minimum bid price for each day.
Here are my models
Leilao
has_many :bids
Bid
belongs_to :leilao
#winner_name
#price
#date
I tried a solution already and got close to what I need. The problem is that, in some cases, when I create a new bid with lower price, I don't know why the results do not change.
Leilao.find(1).bids.having('MIN("bids"."price")').group('date').all
This seems to work, but as I said, it does not work in some cases when I create a new bid. But it worked properly once. So, if you do know what might be happening, please tell me.
I then searched for some way for doing this and I got the following
Leilao.find(1).bids.minimum(:price, :group => :date)
which works properly, but with this, I just fetch the dates and prices and I need all the bid data.
I could get it by doing this, but it feels really bad to me
winner_bids = Leilao.find(1).bids.minimum(:price, :group => :date)
winners_data = []
winner_bids.each do |date, price|
winners_data << Leilao.find(1).bids.where(price: price, date: date).first
end
winners_data
Any idea a better way to do this? Or what's wrong with my first approach?
Performance is not an issue, since this is just for academic propose but it just feels nasty for me
Also those Leilao.find(1) is just for explaining it here, I'm not using it allover the place, no.
Thanks in advance
see this
mysql> select * from bids;
+----+-------------+-------+------------+-----------+
| id | winner_name | price | date | leilao_id |
+----+-------------+-------+------------+-----------+
| 1 | A | 1.1 | 2012-06-01 | 1 |
| 2 | A | 2.2 | 2012-06-01 | 1 |
| 3 | A | 3.3 | 2012-05-31 | 1 |
| 4 | A | 4.4 | 2012-05-31 | 1 |
+----+-------------+-------+------------+-----------+
4 rows in set (0.00 sec)
mysql> select * from bids where leilao_id = 1 group by date order by price asc;
+----+-------------+-------+------------+-----------+
| id | winner_name | price | date | leilao_id |
+----+-------------+-------+------------+-----------+
| 1 | A | 1.1 | 2012-06-01 | 1 |
| 3 | A | 3.3 | 2012-05-31 | 1 |
+----+-------------+-------+------------+-----------+
2 rows in set (0.00 sec)
in rails
1.9.2-p290 :013 > Leilao.find(1).bids.order(:price).group(:date).all
[
[0] #<Bid:0x00000003553838> {
:id => 1,
:winner_name => "A",
:price => 1.1,
:date => Fri, 01 Jun 2012,
:leilao_id => 1
},
[1] #<Bid:0x00000003553518> {
:id => 3,
:winner_name => "A",
:price => 3.3,
:date => Thu, 31 May 2012,
:leilao_id => 1
}
]
As Amol said its not going to work, the SQL looks like
SELECT "whatever".* FROM "whatever" GROUP BY date ORDER BY price
But the group_by will be applied before order_by
I had this trouble when i had market table where is stored different types of records, i solved it by writing a class method, maybe not the best approach but working
def self.zobraz_trh(user)
markets = []
area = self.group(:area).all.map &:area
area.each do |market|
markets << self.where(["area = ? and user_id != ?", market,user]).order(:price).first
end
markets
end
where self is Market class

Resources