Reputation: 2183
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
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
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
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
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
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