Average of an array of nested hashes with same structure - ruby

Let's assume I have an array of nested hash like this one :
array = [
{
"id": 8444,
"version": "2.1.0",
"data": {
"data1": {
"data1-1": {
"a": 132.6,
"b": 128.36,
"c": 153.59,
"d": 136.48
}
},
"data2": {
"data2-1": {
"a": 1283.0,
"b": 1254.0,
"c": 1288.5,
"d": 1329.0
}
}
}
},
{
"id": 8443,
"version": "2.1.0",
"data": {
"data1": {
"data1-1": {
"a": 32.6,
"b": 28.36,
"c": 53.59,
"d": 36.48
}
},
"data2": {
"data2-1": {
"a": 283.0,
"b": 254.0,
"c": 288.5,
"d": 329.0
}
}
}
},
{
"id": 8442,
"version": "2.1.0",
"data": {
"data1": {
"data1-1": {
"a": 32.6,
"b": 28.36,
"c": 53.59,
"d": 36.48
}
},
"data2": {
"data2-1": {
"a": 283.0,
"b": 254.0,
"c": 288.5,
"d": 329.0
}
}
}
}
]
Each hash of the array has the same map structure.
I would like to create a new hash with the same hash map structure than data and for each values of a, b, c, d to have the average.
What is the best approach for this ? Because I cannot group_by key since I have the same key in different subkey (data1-1 and data2-1)
The result would then be :
{
"data1": {
"data1-1": {
"a": 65.9,
"b": 61.7,
"c": 86.9,
"d": 69.8
}
},
"data2": {
"data2-1": {
"a": 616.3,
"b": 587.3,
"c": 621.8,
"d": 662.3
}
}
}
I have tried this:
array.reduce({}) do |acc, hash|
hash[:data].each do |k,v|
acc[k] = v
end
end
# => {:data1=>{:"data1-1"=>{:a=>32.6, :b=>28.36, :c=>53.59, :d=>36.48}},
# :data2=>{:"data2-1"=>{:a=>283.0, :b=>254.0, :c=>288.5, :d=>329.0}}}

Let's group the hashes using .
grouped = array.each_with_object({}) do |h, acc|
h[:data].each do |k, v|
acc[k] ||= []
acc[k] << v
end
end
Result:
{:data1 => [{:"data1-1" => {:a=>132.6, :b=>128.36, :c=>153.59, :d=>136.48}},
{:"data1-1" => {:a=>32.6, :b=>28.36, :c=>53.59, :d=>36.48}},
{:"data1-1" => {:a=>32.6, :b=>28.36, :c=>53.59, :d=>36.48}}],
:data2 => [{:"data2-1" => {:a=>1283.0, :b=>1254.0, :c=>1288.5, :d=>1329.0}},
{:"data2-1" => {:a=>283.0, :b=>254.0, :c=>288.5, :d=>329.0}},
{:"data2-1" => {:a=>283.0, :b=>254.0, :c=>288.5, :d=>329.0}}]}
Now, let's transform those values.
grouped = grouped.transform_values do |arr|
k = arr.first.keys.first
arr.collect { |a| {k => a[k]} }.each_with_object({}) do |h, acc|
h.each do |k, v|
acc[k] ||= []
acc[k] << v
end
end
end
# => {:data1=>{:"data1-1"=>[{:a=>132.6, :b=>128.36, :c=>153.59, :d=>136.48},
# {:a=>32.6, :b=>28.36, :c=>53.59, :d=>36.48},
# {:a=>32.6, :b=>28.36, :c=>53.59, :d=>36.48}]},
# :data2=>{:"data2-1"=>[{:a=>1283.0, :b=>1254.0, :c=>1288.5, :d=>1329.0},
# {:a=>283.0, :b=>254.0, :c=>288.5, :d=>329.0},
# {:a=>283.0, :b=>254.0, :c=>288.5, :d=>329.0}]}}
This is much closer to your stated goal.
Let's transform some values again.
grouped = grouped.transform_values do |v|
k = v.keys.first
values = v.values.first.each_with_object({}) do |h, acc|
h.each do |hk, hv|
acc[hk] ||= []
acc[hk] << hv
end
end
{ k => values }
end
# => {:data1=>{:"data1-1"=>{:a=>[132.6, 32.6, 32.6],
# :b=>[128.36, 28.36, 28.36],
# :c=>[153.59, 53.59, 53.59],
# :d=>[136.48, 36.48, 36.48]}},
# :data2=>{:"data2-1"=>{:a=>[1283.0, 283.0, 283.0],
# :b=>[1254.0, 254.0, 254.0],
# :c=>[1288.5, 288.5, 288.5],
# :d=>[1329.0, 329.0, 329.0]}}}
Even closer. Averaging an array of numbers is easy. We just need to transform values.
Replacing the previous bit of code with:
grouped = grouped.transform_values do |v|
k = v.keys.first
values = v.values.first.each_with_object({}) do |h, acc|
h.each do |hk, hv|
acc[hk] ||= []
acc[hk] << hv
end
end
{ k => values.transform_values { |v| v.sum / v.size } }
end
# => {:data1=>{:"data1-1"=>{:a=>65.93333333333334,
# :b=>61.693333333333335,
# :c=>86.92333333333333,
# :d=>69.81333333333333}},
# :data2=>{:"data2-1"=>{:a=>616.3333333333334,
# :b=>587.3333333333334,
# :c=>621.8333333333334,
# :d=>662.3333333333334}}}

You could write that as follows.
fac = 1.fdiv(array.size)
#=> 0.3333333333333333
array.each_with_object({}) do |g,h|
g[:data].each do |k,v|
h[k] ||= {}
v.each do |kk,vv|
h[k][kk] ||= Hash.new(0)
vv.each { |kkk,vvv| h[k][kk][kkk] += fac * vvv }
end
end
end
#=> { :data1=> {
# :"data1-1"=> {
# :a=>65.93333333333334,
# :b=>61.693333333333335,
# :c=>86.92333333333333,
# :d=>69.81333333333332
# }
# },
# :data2=> {
# :"data2-1"=> {
# :a=>616.3333333333334,
# :b=>587.3333333333333,
# :c=>621.8333333333333,
# :d=>662.3333333333333
# }
# }
# }
Note that if arr is an array of numbers the average can be computed as
(arr.sum).fdiv(arr.size)
or as
fac = 1.fdiv(arr.size)
arr.sum { |m| fac * m }
I have employed the second method to avoid the need to construct intermediate arrays.
One may alternatively write the following.
array.each_with_object(Hash.new { |h,k| h[k] = {} }) do |g,h|
g[:data].each do |k,v|
v.each do |kk,vv|
h[k][kk] ||= Hash.new(0)
vv.each { |kkk,vvv| h[k][kk][kkk] += fac * vvv }
end
end
end
See Hash::new for explanations of Hash.new(0) and Hash.new { |h,k| h[k] = {} }.

Related

Convert array of array into array of hash - Ruby

I want to be able to turn this array of array:
[["Cargo Freight", 37],
["Semiconductor", 35]]
Into this array of hash:
[
{
name: "Cargo Freight",
id: 37
},
{
name: "Semiconductor",
id: 35
}]
Any help would be much appreciated.
Here is a simple solution:
array = [["Cargo Freight", 37],["Semiconductor", 35]]
array.map { |a| { name: a[0], id: a[1] } }
It can be done with the ruby reduce method
a = [["Cargo Freight", 37], ["Semiconductor", 35]]
a.reduce([]) { |array, el| array << {name: el.first, id: el.last} }
OR
a.reduce([]) { |array, el| array << [[:name, :id], el].to_h}
I would do this:
KEYS = %i[name id]
array = [["Cargo Freight", 37],["Semiconductor", 35]]
array.map { |e| KEYS.zip(e).to_h }
#=> [{:name=>"Cargo Freight", :id=>37}, {:name=>"Semiconductor", :id=>35}]

Group array of hashes by value and retain structure (hash) in Ruby

I have a hash like this:
hash = {
'en-us': {
where: 'USA'
},
'en-us-zone2': {
where: 'USA'
},
'en-nl': {
where: 'Netherlands'
},
'en-pt': {
where: 'Portugal'
},
}
And I tried to group them using group_by:
result = hash.group_by { |k,v| v[:where] }
However, It returns full of array not by array of hashes. So here is expected and actual results:
Actual Result:
{ "USA"=>
[[:"en-us", {:where=>"USA"}], [:"en-us-zone2", {:where=>"USA"}]],
"Netherlands"=>
[[:"en-nl", {:where=>"Netherlands"}]],
"Portugal"=>
[[:"en-pt", {:where=>"Portugal"}]]
}
Expected Result:
{ "USA"=>
[{:"en-us" => {:where=>"USA"}}, {:"en-us-zone2" => {:where=>"USA"}}]
"Netherlands"=>
[{:"en-nl" => {:where=>"Netherlands"}}]
"Portugal"=>
[{:"en-pt" => {:where=>"Portugal"}}]
}
See, Actual is Array of arrays, instead of array of hashes. Hash keys becomes first array element.
How can I group my hash based on :where ?
This should work:
hash.group_by { |k,v| v[:where] }.each { |_, v| v.map! { |array| { array[0] => array[1] } } }
Or with transform_values
hash.group_by { |k,v| v[:where] }.transform_values { |v| v.map { |array| {array[0] => array[1] } } }
https://ruby-doc.org/core-2.4.0/Hash.html#method-i-transform_values
It's ugly but works:
hash = {
'en-us': {
where: 'USA'
},
'en-us-zone2': {
where: 'USA'
},
'en-nl': {
where: 'Netherlands'
},
'en-pt': {
where: 'Portugal'
},
}
hash.
group_by { |_, v| v[:where] }.
map do |k, v|
[
k,
v.map { |a, b| {a => b} }
]
end.to_h
# => {"USA"=>[{:"en-us"=>{:where=>"USA"}}, {:"en-us-zone2"=>{:where=>"USA"}}], "Netherlands"=>[{:"en-nl"=>{:where=>"Netherlands"}}], "Portugal"=>[{:"en-pt"=>{:where=>"Portugal"}}]}

Populating a hash from an array

I have this array:
params[:types] = [type1, type2, type3...]
I would like to populate my hash the following way using the above array:
params[:hash] = {
"type1" => {
something: something
},
"type2" => {
something: something
},
}
Using a for loop like for index in i ...params[:types] just populates the hash with the last value in the array.
You can use the each_with_object method to do this:
params = {}
params[:types] = ["type1", "type2", "type3"]
params[:types].each_with_object({}) { |k, h| h[k] = { "something" => "something" } }
That last line will return:
=> {"type1"=>{"something"=>"something"}, "type2"=>{"something"=>"something"}, "type3"=>{"something"=>"something"}}
Here is a code snippet example that does what you need.
hash = {}
array.each do |a|
hash[a.to_s] = { "something" => "something" }
end
output:
hash
=> {
"type1" => {
"something" => "something"
},
"type2" => {
"something" => "something"
},
"type3" => {
"something" => "something"
}
}
You could do this:
params = { types: ["type1", "type2", "type3"] }
Hash[params[:types].product([{"something" => "something"}])]
#=> {"type1"=>{"something"=>"something"},
# "type2"=>{"something"=>"something"},
# "type3"=>{"something"=>"something"}}
or with Ruby 2.1,
params[:types].product([{"something" => "something"}]).to_h
If you want a different hash for each element of params[:types]:
hashes = [{ "something1"=>"something1" }, { "something2"=>"something2" },
{ "something3"=>"something3" }]
then
Hash[params[:types].zip(hashes)]
#=> {"type1"=>{"something1"=>"something1"},
# "type2"=>{"something2"=>"something2"},
# "type3"=>{"something3"=>"something3"}}

Split an array of hashes in two based on the value of a key

How can split this array of hashes in two based on the value of the ate key?
array = [
{ name: "Gad", ate: true },
{ name: "Lad", ate: false },
{ name: "Bad", ate: true },
{ name: "Sad", ate: false }
]
Example output
array_1 = [
{ name: "Gad", ate: true },
{ name: "Bad", ate: true }
]
array_2 = [
{ name: "Lad", ate: false },
{ name: "Sad", ate: false }
]
Use the Enumerable#partition method:
array.partition { |x| x[:ate] }
# => [[{:name=>"Gad", :ate=>true}, {:name=>"Bad", :ate=>true}],
# [{:name=>"Lad", :ate=>false}, {:name=>"Sad", :ate=>false}]]
Or:
array_1, array_2 = array.partition { |x| x[:ate] }
array_1
# => [{:name=>"Gad", :ate=>true}, {:name=>"Bad", :ate=>true}]
array_2
# => [{:name=>"Lad", :ate=>false}, {:name=>"Sad", :ate=>false}]
array_one, array_two = *array.group_by { |x| x[:ate] }.map(&:last)
=> array_one
=> # [{:name=>"Gad", :ate=>true}, {:name=>"Bad", :ate=>true}]
=> array_two
=> # [{:name=>"Lad", :ate=>false}, {:name=>"Sad", :ate=>false}]
thx #CarySwoveland
I can't compete with partition, but here's another way:
trues = array.select { |h| h[:ate] }
falses = array - trues

hash deep_merge with arrays

I've searched through all the answers regarding the use of deep_merge; however, I am still having trouble with my particular issue. I'm trying to merge 2 hashes and add a specific key for each match. For example:
UPDATED THE FORMAT
Hash 1:
{
"actions"=> [
{
"causes"=> [
{
"shortDescription"=>"short description for run 1",
"userId"=>"user.a"
}
]
}
],
"artifacts"=> [],
"fullDisplayName"=>"Run #1",
"result"=>"FAILURE",
"changeSet"=> {
"items"=>[],
"kind"=>nil
},
"culprits"=> []
}
Hash 2:
{
"actions"=> [
{
"causes"=> [
{
"shortDescription"=>"short description for run 2",
"userId"=>"user.b"
}
]
}
],
"artifacts"=> [],
"fullDisplayName"=>"Run #2",
"result"=>"FAILURE",
"changeSet"=> {
"items"=>[],
"kind"=>nil
},
"culprits"=> []
}
Key list:
["key-one","key-two"]
I would like the resulting hash to be:
{
"actions"=> [
{
"causes"=> [
{
"shortDescription"=> {
"key-one" => "short description for run 1",
"key-two" => "short description for run 2"
},
"userId"=> {
"key-one" => "user.a",
"key-two" => "user.b"
}
}
]
}
],
"artifacts"=> {
"key-one" => [],
"key-two" => []
},
"fullDisplayName"=> {
"key-one" => "Run #1",
"key-two" => "Run #2"
},
"result"=> {
"key-one" => "FAILURE",
"key-two" => "FAILURE"
},
"changeSet"=> {
"items"=> {
"key-one" => [], "key-two" => []
},
"kind"=> {
"key-one" => nil,
"key-two" => nil
}
},
"culprits"=> {
"key-one" => [],
"key-two" => []
}
}
If h1 and h2 are the two hashes you wish to merge, and h3 is the desired result, I think the following should work:
#merge_key1, #merge_key2 = "key-one", "key-two"
def merge_em(g1, g2)
case g1
when Array
g1.size.times {|i| merge_em(g1[i], g2[i])}
when Hash
g1.keys.each do |k|
v = g1[k]
if (Hash === v || (Array === v && !v.empty?))
merge_em(v, g2[k])
else
g1[k] = {#merge_key1 => v, #merge_key2 => g2[k]}
end
end
end
end
h = Marshal.load(Marshal.dump(h1))
merge_em(h, h2)
p (h == h3) # => true
A few notes:
This solution depends on the hash structures; it may not work if the structures are changed.
Marshal.load(Marshal.dump(h1)) is used to make a "deep copy" of h1, so that h1 is not modified. If h1 can be modified, merge_em(h1, h2) is sufficient, with h1 being the merged hash.
The gem awesome_print gives you a nicely-formatted display of complex structures like these. All you'd need to do here is require 'ap' followed by ap h. Try it!

Resources