mkafkas
mkafkas

Reputation: 99

How to calculate average in a multidimensional array conditionally if their first values are the same

I have an array containing values and arrays like below:

arr = [ 
        [ 0, [ [22,3],[23,5] ] ],
        [ 0, [ [22,1],[23,2] ] ],
        [ 1, [ [22,4],[23,4] ] ],
        [ 1, [ [22,2],[23,4] ] ]
      ]

I want to calculate the average based on first two elements and want to have a result set either in hash or array as below:

result = {
          22 => [(3+1)/2, (4+2)/2], 
          23 => [(5+2)/2, (4+4)/2] 
         }

where for example:

key is 22 and value is an array containing average of third elements in the input array grouped by the first element 3 and 1, 4 and 2 and sorted by the first element 0 and 1


How the array is created

Maybe it might be helpful to mention about my logic.

The array is obtained by the following code out of my ActiveRecord objects:

arr = u.feedbacks.map{|f| [f.week, 
                           f.answers.map{|a|  [a.question.id, a.name.to_i]}]}

where models are associated as below:

feedback belongs_to :user
feedback has_and_belongs_to_many :answers
answer belongs_to :question

For each question I wanted to create an array containing average of answers grouped by the feedback week.

Upvotes: 0

Views: 646

Answers (2)

SHS
SHS

Reputation: 7744

With a bit of debugging, the following should help get much faster results:

Answer.
joins(:question, :feedbacks). # assuming that answer has_many feedbacks
group(["questions.id", "feedbacks.week"]). # assuming week is integer column
average("CAST(answers.name AS INT)"). # assuming that name is string-y column
each_with_object({}) do |(keys, average), hash|
  question_id, week = keys
  hash[question_id] ||= []
  hash[question_id][week] = average
end

If you want to keep things the way they are (not advised), then one working (albeit hard-to-follow) solution is this:

arr = [ 
  [0, [[22, 3], [23, 5]]],
  [0, [[22, 1], [23, 2]]],
  [1, [[22, 4], [23, 4]]],
  [1, [[22, 2], [23, 4]]]
]

arr.each_with_object({}) do |(a, b), hash|
  c, d, e, f = b.flatten
  # for first row this will be c, d, e, f = 22, 3, 23, 5

  hash[c] ||= []
  hash[c][a] ||= []
  hash[c][a] << d

  hash[e] ||= []
  hash[e][a] ||= []
  hash[e][a] << f


end.each_with_object({}) do |(k, v), hash|
  # k are your 'keys' like 22, 23
  # v is an array of arrays that you want to find out the averages of

  hash[k] = \
    v.map do |array|
      array.reduce(:+).fdiv(array.size)
    end
end

Upvotes: 2

mmhan
mmhan

Reputation: 731

If it were me and I could have my way, I would refactor the way arr was created from the first place, since

  1. The dimensionality of the array is counter-intuitive
  2. The transformation again take its toll, affecting readibility, in turn, maintainability.

But I have no more insights than what I could see from the code you have shown. So, I played around with it a little bit, perhaps the code below is what you wanted?

totals = {}

arr.each do |row|
    index, answers = row

    answers.each do |answer|
        question, count = answer
        totals[question] ||= []
        totals[question][index] ||= []
        totals[question][index] << count
    end
end

Below is the output of totals and by then it's trivial to get your average.

{
    22 =>[[3, 1], [4, 2]], 
    23=>[[5, 2], [4, 4]]
}

EDIT Below is the solution that I have worked out by using each_with_object I learned from @Humza

arr = [ 
    [ 0, [ [22,3],[23,5] ] ],
    [ 0, [ [22,1],[23,2] ] ],
    [ 1, [ [22,4],[23,4] ] ],
    [ 1, [ [22,2],[23,4] ] ]
]

result = arr.each_with_object({}) do |(index, feedbacks), totals|
    feedbacks.each do |(question, count)|
        totals[question] ||= {}
        totals[question][index] ||= []
        totals[question][index] << count
    end
    totals
end.each_with_object({}) do |(question, totals), result|
    result[question] = totals.map do |(index, total)|
        total.reduce(:+).fdiv(total.length)
    end
end

puts result.inspect
## Output 
# {22=>[2.0, 3.0], 23=>[3.5, 4.0]}

Upvotes: 1

Related Questions