Weighted Average Grade Calculation - hash iteration with nil - ruby-on-rails

I'm having trouble calculating a weighted ratio in Ruby and Rails. As an illustrative scenario, say we have a a weighted average grade calculation for a class in school. I have tests "a", "b" and "c" which have class-level weighting of 0.25, 0.50, and 0.25, respectively. Jenny's scores are 0.95, 0.85 and nil, respectively where she was exempt for the last test. When a student is exempt from the test, the weighting should not count against them. Consider that jenny_grade_scores below is an ActiveRecord Object and TEST_WEIGHTS is a constant in the model.
jenny_grade_scores = { "test_a" => 0.95, "test_b" => 0.85, "test_c" => nil }
TEST_WEIGHTS = { "test_a_weight" => 0.25, "test_b_weight" => 0.50, "test_c_weight" => 0.25 }
What's a more efficient way to calculate the weighted score for this than using:
jenny_test_weights = TEST_WEIGHTS #initialize a student test weight variable and adjust if test score is nil
jenny_test_weights["test_a_weight"] = 0.0 if jenny_grade_scores["test_a"].nil?
jenny_test_weights["test_b_weight"] = 0.0 if jenny_grade_scores["test_b"].nil?
jenny_test_weights["test_c_weight"] = 0.0 if jenny_grade_scores["test_c"].nil?
numerator = (jenny_grade_scores["test_a"] * jenny_test_weights["test_a_weight"] +jenny_grade_scores["test_b"] * jenny_test_weights["test_b_weight"] + jenny_grade_scores["test_c"] * jenny_test_weights["test_c_weight"])
denominator = (jenny_test_weights["test_a_weight"] + jenny_test_weights["test_b_weight"] + jenny_test_weights["test_c_weight"])
final_score = numerator / denominator
In this example, the result should be 0.88333333

What about converting all test scores to float?
>> jenny_grade_scores.inject({}) { |hash, array| hash[array[0]] = array[1].to_f; hash }
=> {"test_a"=>0.95, "test_b"=>0.85, "test_c"=>0.0}
If you don't mind refactoring your code, consider the following:
WEIGHTS = { a: 0.25, b: 0.5, c: 0.25 }
grades = { a: 0.95, b: 0.85 , c: nil }
SUM_OF_WEIGHTS = WEIGHTS.inject(0) do |sum, (test, weight)|
sum += grades[test].nil? ? 0 : weight
end # => 0.75
weighted_score = grades.inject(0) do |w, (test, score)|
w += (score.to_f * WEIGHTS[test])
end # => 0.6625
final_score = weighted_score / SUM_OF_WEIGHTS # => 0.8833333333333333
Footnotes:
Renaming the test keys to same key in all related hashes can simplify the problem.
nil when converted to float becomes 0.0.
inject & reduce can do summation or similar collection related operation elegantly. More over its more idiomatic Ruby, try & learn them if you are not familiar with it.
Updated for non-penalized weight calculation.

Related

How to fix "String to Integer" error in Ruby

I need the shipping cost to be determined by the various rates.
I've tried a hodgepodge of things for the last 5 hours.
If I'm supposed to use .to_s .to_f, I've tried and done so incorrectly.
if (weight < 2)
rate = 0.10
elsif ((weight >= 2) or (weight < 10))
rate = 0.20
elsif ((weight >= 10) or (weight < 40))
rate = 0.30
elsif ((weight >= 40) or (weight < 70))
rate = 0.50
elsif ((weight >= 70) or (weight < 100))
rate = 0.75
else (weight >= 100)
rate = 0.90
end
rate = rate.to_i
ship_cost = weight * price * rate
ship_cost = ship_cost.to_i
The result is supposed to show a shipping cost after the rate is applied. I keep getting to the String to Integer error.
The problem is somehow one or more variables within the multiplication is a string, which results in the TypeError error you're getting, as in:
'a' * 'b' # '*': no implicit conversion of String into Integer (TypeError)
If you want to suppress the error, you can manually convert them into integers or floats. Meaning if the string doesn't have a numeric representation, it'll return 0:
'asd'.to_i # 0
'1'.to_i. # 1
'-9.9'.to_i # -9
'-9.9'.to_f # -9.9
Alternatively, you can handle the rate assignation by using a "dictionary" which holds the min and max value weight can be to return X. Creating a range from min to max and asking if it includes the value of weight you can get assign its value:
dict = {
[-Float::INFINITY, 2] => 0.10,
[2, 10] => 0.20,
[10, 40] => 0.30,
[40, 70] => 0.50,
[70, 100] => 0.75,
[100, Float::INFINITY] => 0.90
}
p dict.find { |(start, finish), _| (start...finish).include?(-42.12) }.last # 0.1
p dict.find { |(start, finish), _| (start...finish).include?(0) }.last # 0.1
p dict.find { |(start, finish), _| (start...finish).include?(1) }.last # 0.1
p dict.find { |(start, finish), _| (start...finish).include?(23) }.last # 0.3
p dict.find { |(start, finish), _| (start...finish).include?(101) }.last # 0.9
A less verbose and more idiomatically correct solution is to use a case statement with ranges:
def shipping_rate(weight)
case weight
when 0...2
0.10
when 2...10
0.20
when 10...40
0.30
when 40...70
0.50
when 70...100
0.75
when 100...Float::INFINITY
0.90
end
end
Declaring a range with ... excludes the end value. So (40...70).cover?(70) == false. That lets us avoid overlap issues.
require "minitest/autorun"
class TestShippingRate < Minitest::Test
def test_correct_rate
assert_equal 0.10, shipping_rate(1)
assert_equal 0.20, shipping_rate(3)
assert_equal 0.30, shipping_rate(39)
assert_equal 0.50, shipping_rate(40)
assert_equal 0.75, shipping_rate(70)
assert_equal 0.90, shipping_rate(101)
end
end
# Finished in 0.002255s, 443.3896 runs/s, 2660.3374 assertions/s.
# 1 runs, 6 assertions, 0 failures, 0 errors, 0 skips
If you want to use a dict like Sebastian Palma suggested you can do it with hash with ranges for keys instead:
def shipping_rate(weight)
{
0...2 => 0.10,
2...10 => 0.20,
10...40 => 0.30,
40...70 => 0.50,
70...100 => 0.75,
100...Float::INFINITY => 0.90
}.find { |k, v| break v if k.cover? weight }
end
Using case is a bit more flexible though as you can add a else condition or handle string arguments:
def shipping_rate(weight)
case weight
when 0...2
0.10
when 2...10
0.20
when 10...40
0.30
when 40...70
0.50
when 70...100
0.75
when 100...Float::INFINITY
0.90
# I'm not saying this is a good idea as the conversion should happen
# upstream. Its just an example of what you can do
when String
shipping_rate(weight.to_f) # recursion
else
raise "Oh noes. This should not happen."
end
end

How to round Decimals to the First Significant Figure in Ruby

I am attempting to solve an edge case to a task related to a personal project.
It is to determine the unit price of a service and is made up of the total_amount and cost.
Examples include:
# 1
unit_price = 300 / 1000 # = 0.3
# 2
unit_price = 600 / 800 # = 0.75
# 3
unit_price = 500 / 1600 # = 0.3125
For 1 and 2, the unit_prices can stay as they are. For 3, rounding to 2 decimal places will be sufficient, e.g. (500 / 1600).round(2)
The issue arises when the float becomes long:
# 4
unit_price = 400 / 56000 # = 0.007142857142857143
What's apparent is that the float is rather long. Rounding to the first significant figure is the aim in such instances.
I've thought about using a regular expression to match the first non-zero decimal, or to find the length of the second part and apply some logic:
unit_price.match ~= /[^.0]/
unit_price.to_s.split('.').last.size
Any assistance would be most welcome
One should use BigDecimal for this kind of computation.
require 'bigdecimal'
bd = BigDecimal((400.0 / 56000).to_s)
#⇒ 0.7142857142857143e-2
bd.exponent
#⇒ -2
Example:
[10_000.0 / 1_000, 300.0 / 1_000, 600.0 / 800,
500.0 / 1_600, 400.0 / 56_000].
map { |bd| BigDecimal(bd.to_s) }.
map do |bd|
additional = bd.exponent >= 0 ? 0 : bd.exponent + 1
bd.round(2 - additional) # THIS
end.
map(&:to_f)
#⇒ [10.0, 0.3, 0.75, 0.31, 0.007]
You can detect the length of the zeros string with regex. It's a bit ugly, but it works:
def significant_round(number, places)
match = number.to_s.match(/\.(0+)/)
return number unless match
zeros = number.to_s.match(/\.(0+)/)[1].size
number.round(zeros+places)
end
pry(main)> significant_round(3.14, 1)
=> 3.14
pry(main)> significant_round(3.00014, 1)
=> 3.0001
def my_round(f)
int = f.to_i
f -= int
coeff, exp = ("%e" % f).split('e')
"#{coeff.to_f.round}e#{exp}".to_f + int
end
my_round(0.3125)
#=> 0.3
my_round(-0.3125)
#=> -0.3
my_round(0.0003625)
#=> 0.0004
my_round(-0.0003625)
#=> -0.0004
my_round(42.0031)
#=> 42.003
my_round(-42.0031)
#=> -42.003
The steps are as follows.
f = -42.0031
int = f.to_i
#=> -42
f -= int
#=> -0.0031000000000034333
s = "%e" % f
#=> "-3.100000e-03"
coeff, exp = s.split('e')
#=> ["-3.100000", "-03"]
c = coeff.to_f.round
#=> -3
d = "#{c}e#{exp}"
#=> "-3e-03"
e = d.to_f
#=> -0.003
e + int
#=> -42.003
To instead keep only the most significant digit after rounding, change the method to the following.
def my_round(f)
coeff, exp = ("%e" % f).split('e')
"#{coeff.to_f.round}e#{exp}".to_f
end
If f <= 0 this returns the same as the earlier method. Here is an example when f > 0:
my_round(-42.0031)
#=> -40.0

Rails rounding decimal to the nearest power of ten

I'm looking for a rails function that could return the number to the nearest power of ten(10,100,1000), and also need to support number between 0 and 1 (0.1, 0.01, 0.001):
round(9) = 10
round(19) = 10
round(79) = 100
round(812.12) = 1000
round(0.0321) = 0.01
round(0.0921) = 0.1
I've looking on : Round number down to nearest power of ten
the accepted answer using the length of the string, that can't applied to number between 0 and 1.
updated
Round up to nearest power of 10 this one seems great. But I still can't make it work in rails.
I'm not sure about any function which automatically rounds the number to the nearest power of ten. You can achieve it by running the following code:
def rounded_to_nearest_power_of_ten(value)
abs_value = value.abs
power_of_ten = Math.log10(abs_value)
upper_limit = power_of_ten.ceil
lower_limit = power_of_ten.floor
nearest_value = (10**upper_limit - abs_value).abs > (10**lower_limit - abs_value).abs ? 10**lower_limit : 10**upper_limit
value > 0 ? nearest_value : -1*nearest_value
end
Hope this helps.
Let's simplify your problem to the following form - let the input numbers be in the range [0.1, 1), how would rounding of such numbers look like then?
The answer would be simple - for numbers smaller than 0.5 we would return the number 0.1, for larger numbers it would be 1.0.
All we have to do is to make sure that our number will be in that range. We will "move" decimal separator and remember how many moves we made in second variable. This operation is called normalization.
def normalize(fraction)
exponent = 0
while fraction < (1.0/10.0)
fraction *= 10.0
exponent -= 1
end
while fraction >= 1.0
fraction /= 10.0
exponent += 1
end
[fraction, exponent]
end
Using above code you can represent any floating number as a pair of normalized fraction and exponent in base 10. To recreate original number we will move decimal point in opposite direction using formula
original = normalized * base^{exponent}
With data property normalized we can use it in our simple rounding method like that:
def round(number)
fraction, exponent = normalize(number)
if fraction < 0.5
0.1 * 10 ** exponent
else
1.0 * 10 ** exponent
end
end
if the number is >= 1.0, this should work.
10 ** (num.floor.to_s.size - ( num.floor.to_s[0].to_i > 4 ? 0 : 1))
Try this:
def round_tenth(a)
if a.to_f >= 1
return 10 ** (a.floor.to_s.size - ( a.floor.to_s[0].to_i > 4 ? 0 : 1))
end
#a = 0.0392
c = a.to_s[2..a.to_s.length]
b = 0
c.split('').each_with_index do |s, i|
if s.to_i != 0
b = i + 1
break
end
end
arr = Array.new(100, 0)
if c[b-1].to_i > 4
b -= 1
if b == 0
return 1
end
end
arr[b-1] = 1
return ("0." + arr.join()).to_f
end
class Numeric
def name
def helper x, y, z
num = self.abs
r = 1
while true
result = nil
if num.between?(x, y)
if num >= y/2.0
result = y.round(r+1)
else
result = x.round(r)
end
return self.negative? ? -result : result
end
x *= z; y *= z; r += 1
end
end
if self.abs < 1
helper 0.1, 1, 0.1
else
helper 1, 10, 10
end
end
end
Example
-0.049.name # => -0.01
12.name # => 10
and so on, you are welcome!

Generate random numbers with a given distribution

Check out this question:
Swift probability of random number being selected?
The top answer suggests to use a switch statement, which does the job. However, if I have a very large number of cases to consider, the code looks very inelegant; I have a giant switch statement with very similar code in each case repeated over and over again.
Is there a nicer, cleaner way to pick a random number with a certain probability when you have a large number of probabilities to consider? (like ~30)
This is a Swift implementation strongly influenced by the various
answers to Generate random numbers with a given (numerical) distribution.
For Swift 4.2/Xcode 10 and later (explanations inline):
func randomNumber(probabilities: [Double]) -> Int {
// Sum of all probabilities (so that we don't have to require that the sum is 1.0):
let sum = probabilities.reduce(0, +)
// Random number in the range 0.0 <= rnd < sum :
let rnd = Double.random(in: 0.0 ..< sum)
// Find the first interval of accumulated probabilities into which `rnd` falls:
var accum = 0.0
for (i, p) in probabilities.enumerated() {
accum += p
if rnd < accum {
return i
}
}
// This point might be reached due to floating point inaccuracies:
return (probabilities.count - 1)
}
Examples:
let x = randomNumber(probabilities: [0.2, 0.3, 0.5])
returns 0 with probability 0.2, 1 with probability 0.3,
and 2 with probability 0.5.
let x = randomNumber(probabilities: [1.0, 2.0])
return 0 with probability 1/3 and 1 with probability 2/3.
For Swift 3/Xcode 8:
func randomNumber(probabilities: [Double]) -> Int {
// Sum of all probabilities (so that we don't have to require that the sum is 1.0):
let sum = probabilities.reduce(0, +)
// Random number in the range 0.0 <= rnd < sum :
let rnd = sum * Double(arc4random_uniform(UInt32.max)) / Double(UInt32.max)
// Find the first interval of accumulated probabilities into which `rnd` falls:
var accum = 0.0
for (i, p) in probabilities.enumerated() {
accum += p
if rnd < accum {
return i
}
}
// This point might be reached due to floating point inaccuracies:
return (probabilities.count - 1)
}
For Swift 2/Xcode 7:
func randomNumber(probabilities probabilities: [Double]) -> Int {
// Sum of all probabilities (so that we don't have to require that the sum is 1.0):
let sum = probabilities.reduce(0, combine: +)
// Random number in the range 0.0 <= rnd < sum :
let rnd = sum * Double(arc4random_uniform(UInt32.max)) / Double(UInt32.max)
// Find the first interval of accumulated probabilities into which `rnd` falls:
var accum = 0.0
for (i, p) in probabilities.enumerate() {
accum += p
if rnd < accum {
return i
}
}
// This point might be reached due to floating point inaccuracies:
return (probabilities.count - 1)
}
Is there a nicer, cleaner way to pick a random number with a certain probability when you have a large number of probabilities to consider?
Sure. Write a function that generates a number based on a table of probabilities. That's essentially what the switch statement you've pointed to is: a table defined in code. You could do the same thing with data using a table that's defined as a list of probabilities and outcomes:
probability outcome
----------- -------
0.4 1
0.2 2
0.1 3
0.15 4
0.15 5
Now you can pick a number between 0 and 1 at random. Starting from the top of the list, add up probabilities until you've exceeded the number you picked, and use the corresponding outcome. For example, let's say the number you pick is 0.6527637. Start at the top: 0.4 is smaller, so keep going. 0.6 (0.4 + 0.2) is smaller, so keep going. 0.7 (0.6 + 0.1) is larger, so stop. The outcome is 3.
I've kept the table short here for the sake of clarity, but you can make it as long as you like, and you can define it in a data file so that you don't have to recompile when the list changes.
Note that there's nothing particularly specific to Swift about this method -- you could do the same thing in C or Swift or Lisp.
This seems like a good opportunity for a shameless plug to my small library, swiftstats:
https://github.com/r0fls/swiftstats
For example, this would generate 3 random variables from a normal distribution with mean 0 and variance 1:
import SwiftStats
let n = SwiftStats.Distributions.Normal(0, 1.0)
print(n.random())
Supported distributions include: normal, exponential, binomial, etc...
It also supports fitting sample data to a given distribution, using the Maximum Likelihood Estimator for the distribution.
See the project readme for more info.
You could do it with exponential or quadratic functions - have x be your random number, take y as the new random number. Then, you just have to jiggle the equation until it fits your use case. Say I had (x^2)/10 + (x/300). Put your random number in, (as some floating-point form), and then get the floor with Int() when it comes out. So, if my random number generator goes from 0 to 9, I have a 40% chance of getting 0, and a 30% chance of getting 1 - 3, a 20% chance of getting 4 - 6, and a 10% chance of an 8. You're basically trying to fake some kind of normal distribution.
Here's an idea of what it would look like in Swift:
func giveY (x: UInt32) -> Int {
let xD = Double(x)
return Int(xD * xD / 10 + xD / 300)
}
let ans = giveY (arc4random_uniform(10))
EDIT:
I wasn't very clear above - what I meant was you could replace the switch statement with some function that would return a set of numbers with a probability distribution that you could figure out with regression using wolfram or something. So, for the question you linked to, you could do something like this:
import Foundation
func returnLevelChange() -> Double {
return 0.06 * exp(0.4 * Double(arc4random_uniform(10))) - 0.1
}
newItemLevel = oldItemLevel * returnLevelChange()
So that function returns a double somewhere between -0.05 and 2.1. That would be your "x% worse/better than current item level" figure. But, since it's an exponential function, it won't return an even spread of numbers. The arc4random_uniform(10) returns an int from 0 - 9, and each of those would result in a double like this:
0: -0.04
1: -0.01
2: 0.03
3: 0.1
4: 0.2
5: 0.34
6: 0.56
7: 0.89
8: 1.37
9: 2.1
Since each of those ints from the arc4random_uniform has an equal chance of showing up, you get probabilities like this:
40% chance of -0.04 to 0.1 (~ -5% - 10%)
30% chance of 0.2 to 0.56 (~ 20% - 55%)
20% chance of 0.89 to 1.37 (~ 90% - 140%)
10% chance of 2.1 (~ 200%)
Which is something similar to the probabilities that other person had. Now, for your function, it's much more difficult, and the other answers are almost definitely more applicable and elegant. BUT you could still do it.
Arrange each of the letters in order of their probability - from largest to smallest. Then, get their cumulative sums, starting with 0, without the last. (so probabilities of 50%, 30%, 20% becomes 0, 0.5, 0.8). Then you multiply them up until they're integers with reasonable accuracy (0, 5, 8). Then, plot them - your cumulative probabilities are your x's, the things you want to select with a given probability (your letters) are your y's. (you obviously can't plot actual letters on the y axis, so you'd just plot their indices in some array). Then, you'd try find some regression there, and have that be your function. For instance, trying those numbers, I got
e^0.14x - 1
and this:
let letters: [Character] = ["a", "b", "c"]
func randLetter() -> Character {
return letters[Int(exp(0.14 * Double(arc4random_uniform(10))) - 1)]
}
returns "a" 50% of the time, "b" 30% of the time, and "c" 20% of the time. Obviously pretty cumbersome for more letters, and it would take a while to figure out the right regression, and if you wanted to change the weightings you're have to do it manually. BUT if you did find a nice equation that did fit your values, the actual function would only be a couple lines long, and fast.

Classifying new instance with bayesian net

Say I have the following bayesian network:
And I want to classify a new instance on wether H=true or H=false,
the new instance looks e.g. like this: Fl=true, A=false, S=true, and Ti=false.
How can I classify the instance with respect to H?
I can compute the probability by multiplying the probabilities from the tables:
0.4 * 0.7 * 0.5 * 0.2 = 0.028
What does this say about whether the new instance is a positive instance H or not?
EDIT
I will try the compute the probability according to Bernhard Kausler's suggestion:
So this is Bayes' rule:
P(H|S,Ti,Fi,A) = P(H,S,Ti,Fi,A) / P(S,Ti,Fi,A)
to compute de denominator:
P(S,Ti,Fi,A) = P(H=T,S,Ti,Fi,A)+P(H=F,S,Ti,Fi,A) = (0.7 * 0.5 * 0.8 * 0.4 * 0.3) + (0.3 * 0.5 * 0.8 * 0.4 * 0.3) =0.048
P(H,S,Ti,Fi,A) = 0.336
so P(H|S,Ti,Fi,A) = 0.0336 / 0.048 = 0.7
now i compute P(H=false|S,Ti,Fi,A) = P(H=false,S,Ti,Fi,A) / P(S,Ti,Fi,A)
we already have the value for P(S,Ti,Fi,A´. I's ´0.048.
P(H=false,S,Ti,Fi,A) =0.0144
so P(H=false|S,Ti,Fi,A) = 0.0144 / 0.048 = 0.3
the Probability for P(H=true,S,Ti,Fi,A) is the highest. so the new instance will be classified as H=True
Is this correct?
Addition: We do not need to calculate P(H=false|S,Ti,Fi,A) because it is 1 - P(H=true|S,Ti,Fi,A).
So, you want to compute the conditional probability P(H|S,Ti,Fi,A). To do that, you have to use Bayes' rule:
P(H|S,Ti,Fi,A) = P(H,S,Ti,Fi,A) / P(S,Ti,Fi,A)
where
P(S,Ti,Fi,A) = P(H=T,S,Ti,Fi,A)+P(H=F,S,Ti,Fi,A)
You then calculate both conditional probabilities P(H=T|S,Ti,Fi,A) and P(H=F|S,Ti,Fi,A) and make a prediction according to which probability is higher.
Just multiplying up the numbers like you did won't help and doesn't even give you a proper probability since the product is not normalized.

Resources