msanteler
msanteler

Reputation: 2183

Averaging values across multiple hashes

EDIT I am accepting @CarySwoveland's answer because he got the closest on the first try, accounting for the most scenarios, and outputting the data into a hash so that you don't need to rely on order. Many honerable mentions though! Be sure to check out @ArupRakshit's answer as well if you want your output in an array!

I have an array of hashes like:

@my_hashes = [{"key1" => "10", "key2" => "5"...},{"key1" => "", "key2" => "9"...},{"key1" => "6", "key2" => "4"...}]

and I want an average for each key across the array. ie. 8.0,6.0...

Note that the hashes all have the exact same keys, in order, even if the value for the key is blank. Right now this works:

<%= @my_hashes[0].keys.each do |key| %>
    <% sum = 0 %>
    <% count = 0 %>
    <% @my_hashes.each do |hash| %>
        <% sum += hash[key].to_f %>
        <% count += if hash[key].blank? then 0 else 1 end  %>       
    <% end %>
    <%= (sum/count) %>
<% end %>

but I feel like there may be a better way... any thoughts?

Upvotes: 1

Views: 614

Answers (5)

Arup Rakshit
Arup Rakshit

Reputation: 118299

Do as below

@my_hashes = [{"key1" => "10", "key2" => "5"},{"key1" => "", "key2" => "9"},{"key1" => "6", "key2" => "4"}]
ar = @my_hashes[0].keys.map do |k|
   a = @my_hashes.map { |h| h[k].to_f unless h[k].blank? }.compact
   a.inject(:+)/a.size unless a.empty? #Accounting for "key1" => nil or "key1" => ""
end
ar # => [8, 6]

Upvotes: 2

Cary Swoveland
Cary Swoveland

Reputation: 110755

Another way:

@my_hashes = [ {"key1"=>"10", "key2"=>"5"},
               {"key1"=>  "", "key2"=>"9"},
               {"key1"=> "6", "key2"=>"4"} ]

def avg(arr) arr.any? ? arr.reduce(:+)/arr.size.to_f : 0.0 end

(@my_hashes.each_with_object ( Hash.new { |h,k| h[k]=[] } ) {
  |mh,h| mh.keys.each { |k| h[k] << mh[k].to_f unless mh[k].empty? } })
           .each_with_object({}) { |(k,v),h| h[k] = avg(v) }
  # => {"key1"=>8.0, "key2"=>6.0}

The object created by the first each_with_object is a hash whose default value is an empty array. That hash is represented by the block variable h. This means that if h[k] << mh[k].to_f is to be executed when h.key?(k) => false, h[k] = [] is executed first.

One could alternatively drop the avg method and create a temporary variable before computing the averages:

h = @my_hashes.each_with_object ( Hash.new { |h,k| h[k]=[] } ) { |mh,h|
      mh.keys.each { |k| h[k] << mh[k].to_f unless mh[k].empty? } }

h.each_with_object({}) { |(k,v),h|
  h[k] = ( avg(v) arr.any? ? arr.reduce(:+)/arr.size.to_f : 0.0 }

Upvotes: 1

hirolau
hirolau

Reputation: 13921

No super clean solution, but I would write:

a = [
  {:a => 2, :b => 10},
  {:a => 4, :b => 20},
  {:a => 2, :b => 10},
  {:a => 8, :b => 40},
]

grouped = a.flat_map(&:to_a).group_by{|x,|x}

grouped.keys.each do |key|
  len = grouped[key].size
  grouped[key] = 1.0 * grouped[key].map(&:last).inject(:+) / len
end

Upvotes: 0

lbalceda
lbalceda

Reputation: 35

Try this

@my_hashes = [{"key1" => "10", "key2" => "5"},{"key1" => "", "key2" => "9"},{"key1" => "6", "key2" => "4"}]

average_values = @my_hashes.map(&:values).transpose.map { |arr| 
    arr.map(&:to_f).inject(:+) / arr.size 
}
with_keys = Hash[@my_hashes.first.keys.zip(average_values)]


average_values # =>  [5.333333333333333, 6.0]
with_keys # => {"key1"=>5.333333333333333, "key2"=>6.0}

if you want to exclude empty values from the average, could change average_values to reject empty values

average_values = @my_hashes.map(&:values).transpose.map { |arr| 
    arr.reject!(&:empty?)
    arr.map(&:to_f).inject(:+) / arr.size 
}

average_values # => [8.0, 6.0]

Upvotes: 1

Arthur Corenzan
Arthur Corenzan

Reputation: 911

I think I found a quite elegant solution.

Here is a sample array:

a = [
  {:a => 2, :b => 10},
  {:a => 4, :b => 20},
  {:a => 2, :b => 10},
  {:a => 8, :b => 40},
]

And the solution:

class Array
  def average
    self.reduce(&:+) / self.size
  end
end

r = a[0].keys.map do |key|
  [key, a.map { |hash| hash[key] }.average]
end

puts Hash[*r.flatten]

Upvotes: 1

Related Questions