Calculate average conditionally based on an attribute - ruby-on-rails

I have a requirement where I need to calculate the average of units sold for a product based on the company they were sold at. From there I will calculate the percentage difference to unit sold. There is one model with products in it. Each product has the attributes of:
product_name
unit_sold
company
There are many companies.
This code works for calculating the average on all records, however I'd like to calculate the average conditionally based on the attribute 'company'.
def average_UnitSold
self.class.average(:unit_sold)
end
def averagechange_UnitSold
(self.unit_sold - average_UnitSold) / average_UnitSold * 100
end
I came up with this, but it is not working:
def average_UnitSold
self.class.sum(:unit_sold), :conditions => "company = self.company")) / :unit_sold
end
Any ideas?
On another note, is a more viable approach storing all these averages somewhere and only updating them on a daily basis more efficient?
Based on the answer, I have now implemented this code, and it seems to work:
def self.average_unit_sold(company)
where(company: company).average(:unit_sold)
end
def average_unit_sold
self.class.average_unit_sold(self.company)
end
def averagechange_UnitSold
(self.unit_sold - average_unit_sold) / average_unit_sold * 100
end

It's very strange that you're doing this in an instance method, since the result doesn't actually have anything to do with a particular instance. Instead, define a class method:
class Product < ActiveRecord::Base
# `self.average_unit_sold` is a class method that takes `company` as an
# argument and executes an SQL query like this (where 'some_company' is the
# company given in the argument):
#
# SELECT AVG(products.unit_sold) FROM products
# WHERE products.company = 'some_company'
#
def self.average_unit_sold(company)
where(company: company).average(:unit_sold)
end
end
# ...then...
Product.average_unit_sold(some_company)
If you really want to have an instance method, you can add one (but keep the logic in a class method):
# `average_unit_sold` is an instance method that takes the value of the
# instance's own `company` attribute and calls `Product.average_unit_sold`:
def average_unit_sold
self.class.average_unit_sold(self.company)
end
(This could also be a scope, but for aesthetic reasons I prefer to use scopes only when the result is a model instance or collection of instances, which isn't the case here.)

Assuming you have your associations set up correctly this is pretty easy to accomplish. So assuming that a Company has many products:
class Company < ActiveRecord::Base
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :company
end
Average units sold for all companies:
Product.average(:unit_sold)
Average units sold for one company:
company = Company.find(1)
company.products.average(:unit_sold)

Related

Is it possible to use aggregate functions on an ActiveRecord Virtual Attribute of a Model?

I have two Models similar to these (please don't mind the context, I'm just trying to illustrate an example and a client-purchase relation was the first thing that came to mind):
# client.rb
# name:string
class Client < ActiveRecord::Base
has_many :purchases, dependent: :destroy
def profit
self.purchases.sum('price - cost')
end
end
# purchase.rb
# client_id:integer, product_id:integer, cost:decimal, price:decimal
class Purchase < ActiveRecord::Base
belongs_to :client
belongs_to :product
end
This works for single records, like this:
> a=Client.first # OK
> a.profit # 40.23
However, if I want to make a SUM of the value of "profit" (which is a SUM on itself), I get an error, which is perfectly understandable.
> a.sum(:profit) # no such column: profit
Is there a way to get a SUM out of these without running a loop through the entire table? Currently I do something like this:
sum=0
Client.each do |a|
sum+=a.profit
end
This works, however I'm wondering if there's an easier way to do this via ActiveRecord, or perhaps even in SQL?
Thanks in advance.
You can't use your :profit method directly, but you can get the calculated value for each Client without iterating through each. Use group and sum together:
Purchase.group(:client_id).sum('price - cost')
This will return a Hash where the keys are the client_id and the value is the sum of all purchases price - cost for that client.

Having trouble chaining ActiveRecord::Associations record

I'm trying to retrieve an associated column named "contribution_amount" for each user but I'm getting undefined method error and I can't figure out why.
Controller has:
#payments = Payment.where(:contribution_date => Date.today).pluck(:user_id)
#users = User.where(:id => #payments).find_each do |user|
user.payments.contribution_amount
end
models have:
class User < ActiveRecord::Base
has_many :payments
end
class Payment < ActiveRecord::Base
belongs_to :user
end
Exact error in console is
`undefined method `contribution_amount' for #<ActiveRecord::Associations::CollectionProxy::ActiveRecord_Associations_CollectionProxy_Payment:0x007fb89b6b2c08>`
user.payments is a scope; that is, it represents a collection of Payment records. The contribution_amount method is only available on individual Payment records. You could say user.payments.first.contribution_amount, but I'm not sure that's your goal.
Are you trying to sum the contribution amounts? In that case, you'd want to use a method which aggregates collections of records: user.payments.sum(:contribution_amount).
Veering off-topic for a moment, it is generally better to push scoping methods down into your models. For example:
class User < ActiveRecord::Base
def self.with_payment_contribution_after(date)
joins(:payments).merge(Payment.with_contribution_after(date))
end
def self.with_contribution_amount
joins(:payments).group("users.id")
.select("users.*, sum(payments.contribution_amount) as contribution_amount")
end
end
class Payment < ActiveRecord::Base
def self.with_contribution_after(date)
where(:contribution_date => date)
end
end
# In your controller
#users = User.with_payment_contribution_after(Date.today)
.with_contribution_amount
# In a view somewhere
#users.first.contribution_amount
The advantages to structuring your code this way are:
Your scopes are not longer locked away in a controller method, so you can easily reuse them other places.
Your controller method can become simpler and more declarative. That is, it can express what information it wants, not how that information is acquired.
Breaking scopes down into smaller pieces implies that our code is better decomposed, and that which has been decomposed can be recomposed.
It's easier to test scopes via model unit tests then via controller testing.

Calculating karma from upvotes/downvotes on a user's posts

I have a User and Post model and am using the acts_as_votable gem (which gives me a 'votes' table) to upvote/downvote a post. I want to assign karma to each user, where karma is the number of upvotes a user has gained from all his/her posts minus the total number of downvotes.
Currently I have this instance method on the User's model to calculate karma:
def karma
count = 0
self.posts.each do |post|
count += post.upvotes.size - post.downvotes.size
end
return count
end
I don't think the above is very efficient with a run time of O(2n), because for every post two additional database queries are required, one for the upvotes, and one for the downvotes.
Any ideas on how to combined the above into a single query, or otherwise make it more efficient?
Not only about efficiency, such design is not good in terms of OOP. The User model considers too much beyond its scope. Karma is something belongs to user and should not be tied in anything else.
A better approach is to separate "Karma" from Post.
# Add a "karma" column
$ rails g migration AddKarmaToUser
# Or use a dedicated table
class User < ActiveRecord::Base
has_one :karma
Then, use Controller or service object to change karma. I'll cover Controller here for simplicity. Do not use Model callbacks as that is cross models.
class PostsController < ApplicationController
def upvote
post = Post.find(params[:id])
post.upvote # pseudo API of acts_as_votable
current_user.increase_karma
end
def downvote
# ...
current_user.decrease_karma
end
end
# Model
class User < ActiveRecord::Base
attr_accessible :karma
def increase_karma(count=1)
update_attribute(:karma, karma + count)
end
def descrease_karma(count=1)
# ...
end
end
# View
user.karma

How to display this instance method from rails model?

I have two rails models, Usage & Price. So far they look like this:
class Usage < ActiveRecord::Base
has_one :price
def spend
usage.amount * price.amount
end
end
and
class Price < ActiveRecord::Base
belongs_to :usage
end
I'm trying to call the "spend" by doing this in the console:
Usage.create(amount:100)
Price.create(amount:0.1)
usage=Usage.find(1)
puts usage.price
Where am I going wrong??
You need to create the price through the usage model to have the association work.
usage = Usage.create(amount:100)
usage.create_price(amount:0.1)
As the price belongs to usage, you should first create the usage object, and then using that you can create the price object.
usage = Usage.create(amount:100)
price = usage.price.create(amount:0.1)
Then you will get that price related to the usage model.
Then in the usage model you can write,
class Usage < ActiveRecord::Base
has_one :price
def spend
self.amount * (self.price.amount)
end
end
You can call the association like above, self.price.amount where "self" is the Usage object.
The problem lay in two things I was doing. Thanks to blamattina for pointing out this is how you create new associated objects after they've been defined in the model:
u = Usage.create(amount:100)
u.create_price(amount:0.1)
Also, in the model itself, when referring to the parent class within a model's instance method, the class should be referred to as self:
def spend
self.amount * price.amount
end
That last bit was where I was going wrong, and spend can be easily called with u.spend!

performance/ruby/rails/db question

I'm creating an online bookmaker odds comparison site for soccer and I'm wondering how to calculate the best odds in Ruby/Rails.
I have two models: fixture and odds
Fixture has home and away teams, and odds model has bookmaker ID, home odds, draw odds and away odds.
I have selections which just stores the selected fixtures/teams in the DB.
I'm thinking of doing it this way where I create an multi-dimensional array of the different bookmakers and then add the fixture_id and 1,2 or 3 for home/draw/away and then use that as the key to add the odds
Something like odds[bookmaker][fixture][1/2/3] = price then add up the odds = count(odds[bookmaker][fixture][1/2/3])?
Is there an easier way? Maybe do it in the DB?
Without taking performance into account - it's probably not an issue and anyway, we shouldn't optimise for performance until we know we have a problem - I'd say you might introduce a Bookmaker model (if only to store the name) and start making use of ActiveRecord associations. I'd also consider splitting Odds into the three individual result types, which could be more flexible, especially if you want to add more bets later. You might get something like:
class Bookmaker < ActiveRecord::Base
has_many :odds
end
class Odd < ActiveRecord::Base # good name? Price is almost as common and less likely to be misinterpreted
belongs_to :fixture
belongs_to :bookmaker
# let's assume we use result type = 1/2/3 or maybe :home/:draw/:away
end
class Fixture < ActiveRecord::Base
has_many :odds
end
What you look to be trying to do is calculate the best price for each result across all bookies making a price on that fixture, or the "overround". If it's less than 100% then a potential arbitrage exists.
class Odd
named_scope :for_result, lambda { |res_tp| {:conditions => ['type = ?', res_tp]}}
end
class Fixture
def best_price(res_type)
# assumes you have odds stored as a percentage
odds.for_result(res_type).minimum(:pctage)
end
def overround
[:home, :away, :draw].inject(0.0){|sum, res_tp| sum + best_price(res_tp)}
end
end
I'm sure the above doesn't exactly fit your data, but it might give an idea of how you might go about it.

Resources