Using reduce({}) to create nested hash to calculate averages, rails 6 - ruby-on-rails

I have a controller which looks like this:
def index
collection = Chart.all
render json: collection, meta: {averages: collection.averages }
end
So as you can see, what I'm trying to do is a regular GET to an Index method, which has a meta object, which returns "averages".
Chart belongs to an employee , and an employee has many areas. Chart also has many chart_axis_values. Each chart_axis_value has a .value (float) and a .name(string).
chart_axis_values are things like "motivation", "satisfaction", "positivity" and "collaboration". Each of them has a value(float), which we will use to calculate the average of each chart_axis_value in all Charts.
So, as you can see, an employee has a Chart, in which he has these 'axes', and each axis has a value.
Say I have 5 Charts, from 5 different employees, and each employee belongs to a different 'Area' in the company (HHRR, Marketing, Technology, etc...). When I make a GET request to get Index of Charts, I need to receive all 5 charts, each of them with their axes, and each axis has it's name and value. And in the "meta" object, I need to receive the average value for each chart_axis_value. So I will count how many times an axis appears(with it's name), and I will accumulate the values of each axis, and when I finish the GET request, I will calculate total_count/total_sum for each axis. All this will be returned in the JSON format, so I will be using a hash.
All this problem is solved, I already have the averages for each axis, but now I have a new problem.
Each employee has_many (zero or more) Areas, and when I do my get request, instead of getting all charts from all employees, I can filter by one or more Areas and only get the Charts of those employees that belong to a specific Area(or more, if I filter by more than one Area).
So what I want to do now is I want to receive, averages values for each Area. So I will be doing the same "counting" of times an axis appeared, and the same accumulation of the value of this axis, but I will be doing this for each area, so I can calculate the average for each Area.
Here's what I have so far.
## /models/chart.rb
class Chart < ApplicationRecord
belongs_to :employee
has_many :chart_axis_values
def self.averages ## Method for the meta in the controller
# first we generate a hash with the total sum and total objects
values = all.reduce({}) do |values, chart|
# we iterate over each axis
chart.chart_axis_values.each do |axis|
# ignore nils
unless axis.value.nil?
# first we define hash to keep count and total sum
values[axis.slug] ||= {count: 0, total: 0.0}
# We count and accumulate for each axis, so later on we calculate the average
values[axis.slug][:count] += 1
values[axis.slug][:total] += axis.value
end
end
values
end
# once we have the total we transform the hash calculating averages
values.each_with_object({}) do |(axis, values), averages|
averages[axis] = values[:total]/values[:count]
end
end
end
This way, I get the averages for each axis. Now I'm trying to do the same, but once for each Area, and I'm having lots of trouble on how to solve this. I was hoping someone here would look at my problem and maybe suggest a solution, or come up with something I can use and understand. I think my problem is very specific, so I understand if don't have the time to help me, but it's worth the try! As of now, I think I have to do something like this:
## /models/chart.rb
class Chart < ApplicationRecord
belongs_to :employee
has_many :chart_axis_values
def self.averages ## Method for the meta in the controller
# first we generate a hash with the total sum and total objects
values = all.reduce({}) do |values, chart|
# we iterate over each axis
chart.chart_axis_values.each do |axis|
# ignore nils
unless axis.value.nil?
chart.employee.areas.each do |area|
# first we define hash to keep count and total sum
values[area.name] ||= {axis.name=>{count: 0, total: 0.0}}
if values[area.name][axis.name].nil?
values[area.name].merge({axis.name=>{count: 0, total: 0.0}})
end
# We count and accumulate for each axis, so later on we calculate the average
values[area.name][axis.name][:count] += 1
values[area.name][axis.name][:total] += axis.value
end
end
end
values
end
# once we have the total we transform the hash calculating averages
values.each_with_object({}) do |(axis, values), averages|
averages[axis] = values[:total]/values[:count]
end
end
end
And that's as far as I got without turning the code into Frankenstein and making it stop making sense.
I've spent days on this problem, and I just can't seem to get it to work, I'm getting so frustrated, any help or suggestion will be appreciated, thank you.

Related

How to rank based on a selection?

For a reservation I would like to calculate the price for extra_guests (e.g. guests not included in the price).
Therefore, I would like to get all the guests for the reservation (reservation_guests). Done
Get their age_table Done
Rank the guests based on their age_tables' column :rank. Issue
The goal is to use the ranking to start calculating the extra_guest_price of the extra_guest with the lowest age_table ranking, in case there are guests not included in the standard reservation.
Issue
I just need a specific range of age_tables to rank based on :rank. My current set-up is wrong as I cannot use order on an array, but I'm not sure how to structure it to make it work.
def total_extra_guest_price(reservation, res_guest)
sum_guest = 0
sum_amount_guests = 0
age_table_list = []
reservation.reservation_extra_guests.each do |guest|
sum_amount_guests += guest.extra_guest_quantity
end
# persons included vs persons reservation
#if more persons
if reservation.room.room_category.persons_included < sum_amount_guests
#count number of too many persons
extra_persons = sum_amount_guests - reservation.room.room_category.persons_included
# iterate over extra_guests belonging to reservation to get age_tables
reservation.reservation_extra_guests.each do |extra_guest_item|
age_table_list << extra_guest_item.extra_guest.age_table
end
#rank age tables, so lowest rank can be used to calculate price. Issue!!!
age_table_list.order(:rank)
end
end
I'm not 100% clear about your question so my suggestion may not be the best solution. But you can try the sort_by()
age_table_list.sort_by{ |age_table| age_table.rank }
read more here

Act_As_Votable with Reddit style weighting algorithm in 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

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.

Filter order needing changes for invoice calculation?

Picture a normal invoice: On it, you have several items. Each item has a quantity and a price per unit, among other things (unit and description).
The total amount for each item is calculated like this: quantity * price per unit.
This is done for each item. Then, the overall invoice's net amount is the sum of all the totals. Add VAT, and you have the invoice's gross amount.
This is what I am trying to do with my Rails app. An invoice has many items and accepts nested attributes for them. Generally, this all works fine.
Following the logic, all I need to enter in manually is the price per unit and the quantity for each item as well as the invoice's VAT. The totals and the resulting net and gross amount should be calculated automatically. I want to achieve this using the before_save filter.
Here is my invoice model:
before_save :calculate_net_amount, :calculate_gross_amount
def calculate_net_amount
self.items do |item|
self.net_amount += item.total
end
end
def calculate_gross_amount
self.gross_amount = self.net_amount * (1 + self.vat_rate)
end
This is the item model:
before_save :calculate_total
def calculate_total
self.total = self.quantity * self.price_per_unit
end
And here is my spec that is failing:
it "calculates the net amount from all item totals" do
invoice = FactoryGirl.build(:invoice)
item = invoice.items.build(quantity: 2, unit: "Unit", description: "Desc", price_per_unit: 2)
invoice.save
invoice.net_amount.should == 4
end
It uses this invoice factory:
FactoryGirl.define do
factory :invoice do
association :client
currency "EUR"
vat_rate 0.19
net_amount 1
payment_on "2013-01-01"
status "paid"
end
end
The test basically does the following: An invoice with 2 things that both cost USD 2 should have a net amount of USD 4. Instead, the test returns 1, which seems to come from the factory and apparently isn't overwritten. If I remove it from the fixture, it says that it cannot calculate the gross amount anymore, since it can't use * on any nil object.
I am assuming I am doing something wrong with the filters and the order in which they are called - the total amounts are calculated correctly, so it has to be something about the calculate_net_amount method that's going wrong and as a result it can't calculate the gross amount anymore.
Can you see what I am doing wrong?
self.items do |item|
should be
self.items.each do |item|
Since items is an accessor, which is a method, it can technically take a block, but that block isn't getting called, so no summing is happening. It's an easy typo to make.
As an aside, it's better is to sum using inject:
self.net_amount = self.items.inject(0){|sum, item| sum + item.total}

How do I calculate the most popular combination of a order lines? (or any similar order/order lines db arrangement)

I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.

Resources