计算平均值在深层嵌套的哈希中,然后由另一个字段

时间:2015-11-14 23:30:16

标签: ruby-on-rails ruby hash

我正在尝试找出循环浏览一些深度嵌套数据的最有效方法,找到值的平均值并返回一个新的哈希值,并按日期分组数据。

原始数据如下所示:

[
    client_id: 2,
    date: "2015-11-14",
    txbps: {
        "22"=>{
            "43"=>17870.153846153848,
            "44"=>15117.866666666667
        }
    },
    client_id: 1,
    date: "2015-11-14",
    txbps: {
        "22"=>{
            "43"=>38113.846153846156,
            "44"=>33032.0
        }
    },
    client_id: 4,
    date: "2015-11-14",
    txbps: {
        "22"=>{
            "43"=>299960.0,
            "44"=>334182.4
        }
    },
]

我有大约10,000,000个这样的循环,所以我有点担心性能。

最终结果,需要看起来像这样。 val需要是txbps的平均值

[
    {
        date: "2015-11-14",
        avg: 178730.153846153848
    },
    {
        date: "2015-11-15",
        avg: 123987.192873978987
    },
    {
        date: "2015-11-16",
        avg: 126335.982123876283
    }
]

我试过这个开始:

results.map { |val| val["txbps"].values.map { |a| a.values.sum } }

但那给了我这个:

[[5211174.189281798, 25998.222222222223], [435932.442835184, 56051.555555555555], [5718452.806735582, 321299.55555555556]]

我无法弄清楚如何完成它。我也无法在网上找到任何好的参考资料。

我还尝试先按日期分组:

res.map { |date, values| values.map { |client| client["txbps"].map { |tx,a| { date: date, client_id: client[':'], tx: (a.values.inject(:+) / a.size).to_i } } } }.flatten

[
    {
        : date=>"2015-11-14",
        : client_id=>"2",
        : tx=>306539
    },
    {
        : date=>"2015-11-14",
        : client_id=>"2",
        : tx=>25998
    },
    {
        : date=>"2015-11-14",
        : client_id=>"2",
        : tx=>25643
    },
    {
        : date=>"2015-11-14",
        : client_id=>"2",
        : tx=>56051
    },
    {
        : date=>"2015-11-14",
        : client_id=>"1",
        : tx=>336379
    },
    {
        : date=>"2015-11-14",
        : client_id=>"1",
        : tx=>321299
    }
]

如果可能的话,我怎样才能一次完成。

----编辑----

进一步了解:

res.map { |a,b|
  {
    date: a[:date], val: a["txbps"].values.map { |k,v|
      k.values.sum / k.size
    }.first
  }
}.
group_by { |el| el[:date] }.map { |date,list|
  {
    key: date, val: list.map { |elem| elem[:val] }.reduce(:+) / list.size
  }
}

但那是史诗 - 有更快,更简单的方法吗?

2 个答案:

答案 0 :(得分:1)

尝试#inject

.map类似,它是一种将可枚举(列表,哈希,几乎可以在Ruby中循环的任何东西)转换为不同对象的方法。与.map相比,很多更灵活,这非常有用。可悲的是,这种方法的成本非常难以包裹。我认为Drew Olson在他的answer中解释得最好。

  

您可以将第一个块参数视为累加器:每次运行块的结果都存储在累加器中,然后传递给块的下一次执行。在上面显示的代码的情况下,您将累加器,结果默认为0.每次运行该块将给定的数字添加到当前总数,然后将结果存储回累加器。下一个块调用具有这个新值,添加它,再次存储它,并重复。

示例:

要汇总数组中的所有数字(使用#inject),您可以执行以下操作:

array = [5,10,7,8]
#            |- Initial Value   
array.inject(0) { |sum, n| sum + n } #=> 30
#                   |- You return the new value for the accumulator in this block.

要查找数字数组的平均值,您可以找到一个总和,然后除以。如果在注入函数(num)内划分{|sum, num| sum + (num / array.size)}变量,则需要乘以您必须进行的计算量。

array = [5,10,7,8]
array.inject(0.0) { |sum, num| sum + num } / array.size #=> 7.5

方法

如果在类上创建方法是你的风格,你可以在Array类上定义一个方法(来自John Feminella的answer)。在需要查找数组的总和或平均值之前,将此代码放在某处:

class Array
  def sum
    inject(0.0) { |result, el| result + el }
  end

  def mean 
    sum / size
  end
end

然后

array = [5,10,7,8].sum #=> 30
array = [5,10,7,8].mean #=> 7.5

宝石

如果您喜欢将代码放入黑盒子或真正珍贵的矿物质中,那么您可以使用fegoa89的average gemgem install average。它还支持#mode#median

[5,10,7,8].mean #=> 7.5

解决方案:

假设您的对象如下所示:

data = [
    {
        date: "2015-11-14",
        ...
        txbps: {...},
    },
    {
        date: "2015-11-14",
        ...
        txbps: {...},
    },
    ...
]

此代码可以满足您的需求,但它有点复杂。

class Array
  def sum
    inject(0.0) { |result, el| result + el }
  end

  def mean 
    sum / size
  end
end

data = (data.inject({}) do |hash, item|
    this = (item[:txbps].values.map {|i| i.values}).flatten # Get values of values of `txbps`
    hash[item[:date]] = (hash[item[:date]] || []) + this # If a list already exists for this date, use it, otherwise create a new list, and add the info we created above.
    hash # Return the hash for future use
end).map do |day, value| 
    {date: day, avg: value.mean} # Clean data
end

会将您的对象合并为按日期分组的数组:

{:date=>"2015-11-14", :avg=>123046.04444444446}

答案 1 :(得分:1)

数据结构

我假设你的输入数据是一个哈希数组。例如:

arr = [
  {
    client_id: 2,
    date: "2015-11-14",
    txbps: {
      "22"=>{
        "43"=>17870.15,
        "44"=>15117.86
      }
    }
  },
  {
    client_id: 1,
    date: "2015-11-15",
    txbps: {
      "22"=>{
        "43"=>38113.84,
        "44"=>33032.03,
      }
    }
  },

  {
    client_id: 4,
    date: "2015-11-14",
    txbps: {
      "22"=>{
        "43"=>299960.0,
        "44"=>334182.4
      }
    }
  },
  {
    client_id: 3,
    date: "2015-11-15",
    txbps: {
      "22"=>{
        "43"=>17870.15,
        "44"=>15117.86
      }
    }
  }
]

<强>代码

根据我对问题的理解,您可以按如下方式计算平均值:

def averages(arr)
  h = arr.each_with_object(Hash.new { |h,k| h[k] = [] }) { |g,h|
    g[:txbps].values.each { |f| h[g[:date]].concat(f.values) } }
  h.merge(h) { |_,v| (v.reduce(:+)/(v.size.to_f)).round(2) }
end

示例

对于上面的arr

avgs = averages(arr)
  #=> {"2015-11-14"=>166782.6, "2015-11-15"=>26033.47} 

方法第一行中散列h的值为:

{"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
 "2015-11-15"=>[38113.84, 33032.03, 17870.15, 15117.86]} 

averages返回的哈希值转换为所需的哈希值数组

avgs不是所需输出的形式。转换是一件简单的事情,但您可以考虑将哈希输出保留为此格式。转换只是:

avgs.map { |d,avg| { date: d, avg: avg } }
 #=> [{:date=>"2015-11-14", :avg=>166782.6},
 #    {:date=>"2015-11-15", :avg=>26033.47}]

<强>解释

我不会详细解释该方法的工作原理,而是提供一种替代形式的方法完全相同的东西,但是更简洁,略微不像Ruby那样。我还在最后将哈希转换为哈希数组:

def averages(arr)
  h = {}
  arr.each do |g|
    vals = g[:txbps].values      
    vals.each do |f|
      date = g[:date]
      h[date] = [] unless h.key?(date)
      h[date].concat(f.values)
    end
  end

  keys = h.keys
  keys.each do |k|
    val = h[k]
    h[k] = (val.reduce(:+)/(val.size.to_f)).round(2)
  end

  h.map { |d,avg| { date: d, avg: avg } }
end

现在让我插入一些puts语句,打印出计算中的各种中间值,以帮助解释发生了什么:

def averages(arr)
  h = {}
  arr.each do |g|
    puts "g=#{g}"
    vals = g[:txbps].values      
    puts "vals=#{vals}"
    vals.each do |f|
      puts "  f=#{f}"
      date = g[:date]
      puts "  date=#{date}"
      h[date] = [] unless h.key?(date)
      puts "  before concat, h=#{h}"
      h[date].concat(f.values)
      puts "  after concat, h=#{h}"
    end
    puts
  end

  puts "h=#{h}"
  keys = h.keys
  puts "keys=#{keys}"

  keys.each do |k|
    val = h[k]
    puts "  k=#{k}, val=#{val}"
    puts "  val.reduce(:+)=#{val.reduce(:+)}"
    puts "  val.size.to_f=#{val.size.to_f}"
    h[k] = (val.reduce(:+)/(val.size.to_f)).round(2)
    puts "  h[#{k}]=#{h[k]}"
    puts
  end

  h.map { |d,avg| { date: d, avg: avg } }
end

再次执行averages

averages(arr)

g={:client_id=>2, :date=>"2015-11-14", :txbps=>{"22"=>{"43"=>17870.15, "44"=>15117.86}}}
vals=[{"43"=>17870.15, "44"=>15117.86}]
  f={"43"=>17870.15, "44"=>15117.86}
  date=2015-11-14
  before concat, h={"2015-11-14"=>[]}
  after concat, h={"2015-11-14"=>[17870.15, 15117.86]}

g={:client_id=>1, :date=>"2015-11-15", :txbps=>{"22"=>{"43"=>38113.84, "44"=>33032.03}}}
vals=[{"43"=>38113.84, "44"=>33032.03}]
  f={"43"=>38113.84, "44"=>33032.03}
  date=2015-11-15
  before concat, h={"2015-11-14"=>[17870.15, 15117.86], "2015-11-15"=>[]}
  after concat, h={"2015-11-14"=>[17870.15, 15117.86], "2015-11-15"=>[38113.84, 33032.03]}

g={:client_id=>4, :date=>"2015-11-14", :txbps=>{"22"=>{"43"=>299960.0, "44"=>334182.4}}}
vals=[{"43"=>299960.0, "44"=>334182.4}]
  f={"43"=>299960.0, "44"=>334182.4}
  date=2015-11-14
  before concat, h={"2015-11-14"=>[17870.15, 15117.86],
                    "2015-11-15"=>[38113.84, 33032.03]}
  after concat, h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
                   "2015-11-15"=>[38113.84, 33032.03]}

g={:client_id=>3, :date=>"2015-11-15", :txbps=>{"22"=>{"43"=>17870.15, "44"=>15117.86}}}
vals=[{"43"=>17870.15, "44"=>15117.86}]
  f={"43"=>17870.15, "44"=>15117.86}
  date=2015-11-15
  before concat, h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
                    "2015-11-15"=>[38113.84, 33032.03]}
  after concat, h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
                   "2015-11-15"=>[38113.84, 33032.03, 17870.15, 15117.86]}

h={"2015-11-14"=>[17870.15, 15117.86, 299960.0, 334182.4],
   "2015-11-15"=>[38113.84, 33032.03, 17870.15, 15117.86]}
keys=["2015-11-14", "2015-11-15"]
  k=2015-11-14, val=[17870.15, 15117.86, 299960.0, 334182.4]
  val.reduce(:+)=667130.41
  val.size.to_f=4.0
  h[2015-11-14]=166782.6

  k=2015-11-15, val=[38113.84, 33032.03, 17870.15, 15117.86]
  val.reduce(:+)=104133.87999999999
  val.size.to_f=4.0
  h[2015-11-15]=26033.47

  #=> [{:date=>"2015-11-14", :avg=>166782.6},
  #    {:date=>"2015-11-15", :avg=>26033.47}]