如何在Ruby中对数组进行分组和求和?

6

我有一个像这样的数组:

ar = [[5, "2014-01-27"],
[20, "2014-01-28"],
[5, "2014-01-28"],
[10, "2014-01-28"],
[15, "2014-01-29"],
[5, "2014-01-29"],
[5, "2014-01-30"],
[10, "2014-01-30"],
[5, "2014-01-30"]]

我需要做的是按日期分组数组项目,并汇总每个子数组中第一项中的数字。
所以输出结果类似于:
[[5, "2014-01-27"],
[35, "2014-01-28"],
[20, "2014-01-29"],
[20, "2014-01-30"]]
7个回答

10

ar.group_by(&:last).map{ |x, y| [y.inject(0){ |sum, i| sum + i.first }, x] }

编辑以添加解释:
我们按最后一个值(日期)进行分组,得到一个哈希表:

{"2014-01-27"=>[[5, "2014-01-27"]], "2014-01-28"=>[[20, "2014-01-28"], [5, "2014-01-28"], [10, "2014-01-28"]], "2014-01-29"=>[[15, "2014-01-29"], [5, "2014-01-29"]], "2014-01-30"=>[[5, "2014-01-30"], [10, "2014-01-30"], [5, "2014-01-30"]]}
然后以x为哈希键,将y作为[[number, date],[number,date]]对数组的存储。使用.inject(0)是指将sum初始化为0,然后我们将每个数组的第一个项(数字)加到该总和中,直到所有数组都被迭代并且所有数字都被添加。 然后我们对[y,x]进行操作,其中x是哈希键(日期),y是所有数字的总和。由于我们在映射时交换了它们的位置,因此此方法非常有效,因为我们使用了inject来避免对数组进行两次映射,并且不必在之后反转值。编辑:有趣的是,@bjhaid和我的答案之间的基准测试非常接近:
    user     system      total        real
5.117000   0.000000   5.117000 (  5.110292)
5.632000   0.000000   5.632000 (  5.644323)

1000000 次迭代 - 我的方法是最慢的


使用 chunk,我认为你需要先进行排序:[3,3,1,3,3].chunk {|e| e}.to_a # => [[3, [3, 3]], [1, [1]], [3, [3, 3]] - Cary Swoveland
请在您的代码回答中提供一些解释。 - helion3
您的示例对我更有效,在我的情况下,我需要将三个对象分组而不是两个。 - helenatxu

1
h = ar.group_by(&:last)
h.keys.each{|k| h[k] = h[k].map(&:first).inject(:+)}
h.map(&:reverse)

0
ar.group_by(&:last).map{ |d,g| [g.map(&:first).inject(:+), d] }

1
我认为如果您为原帖作者和其他访问者添加一些解释,会更有帮助。 - Reporter

0
result = {}
ar.map{|v,date| result[date] ||= 0; result[date] += v}

然后你会得到一个哈希表,其中键是日期,值是总和,你真的需要结果是一个数组吗?看起来你需要一个哈希表,但我不知道上下文是什么

也许你甚至不需要用 Ruby 来完成这个任务,如果这些数据都来自数据库,你可以通过查询进行分组和求和。


结果不一定要是一个数组,哈希也可以。但是你的例子似乎只输出总和...没有日期。 - Shpigford

0
ar.each_with_object(Hash.new(0)) { |x,hash| hash[x[1]] += x[0] }.map(&:reverse)
=> [[5, "2014-01-27"], [35, "2014-01-28"], [20, "2014-01-29"], [20, "2014-01-30"]]

解释

第一部分使用Hash.new作为提供给Enumerable#each_with_object的对象,生成一个Hash,将其键设置为日期(数组的第二个索引),将值设置为数组的第一个索引的总和。

[29] pry(main)> ar.each_with_object(Hash.new(0)) { |x,hash| hash[x[1]] += x[0] }
=> {"2014-01-27"=>5, "2014-01-28"=>35, "2014-01-29"=>20, "2014-01-30"=>20}

第二部分使用 Enumerable#map,它将哈希表中的每个 keyvalue 对作为数组处理,并将其传递给块/proc。Array#reverse 在每个传递的对上调用以反转并生成最终数组。
[30] pry(main)> {"2014-01-27"=>5, "2014-01-28"=>35, "2014-01-29"=>20, "2014-01-30"=>20}.map(&:reverse)
=> [[5, "2014-01-27"], [35, "2014-01-28"], [20, "2014-01-29"], [20, "2014-01-30"]]

1
请在您的代码答案中提供一些解释。 - helion3

0
我更喜欢@sawa的解决方案,它使用了“group_by”,但这里还有另一种方式,可以帮助说明这里可能的方法多样性。
首先将数组转换为哈希表,其中日期作为键。
h = ar.each_with_object(Hash.new {|h,k| h[k] = []}) { |(x,d),h| h[d] << x }
  # => {"2014-01-27"=>[5],
  #     "2014-01-28"=>[20, 5, 10],
  #     "2014-01-29"=>[15, 5],

接下来,将哈希表中每个值(数组)替换为其元素的总和:
h.keys.each { |k| h[k] = h[k].reduce(:+) }
  #   => ["2014-01-27", "2014-01-28", "2014-01-29", "2014-01-30"]
  # h => {"2014-01-27"=>5 , "2014-01-28"=>35,
  #       "2014-01-29"=>20, "2014-01-30"=>20}

请注意,这个表达式返回一个键的数组,但是散列h现在具有了所需的值。因此,我们无法链式地进行最后的语句:
h.map(&:reverse).sort_by(&:first)
  # => [[ 5, "2014-01-27"], [35, "2014-01-28"],
  #     [20, "2014-01-29"], [20, "2014-01-30"]]

我写成这样的原因之一是鼓励您考虑将哈希g作为最终结果,而不是另一个数组。(这也是@sawa解决方案中前两行后h的值)。请考虑在代码的后续操作中是否更有意义。

大部分都很简单,但第一个each with object需要一些解释。 object是一个哈希,在块中由局部变量h表示。 这个哈希是通过以下方式创建的:

Hash.new { |h,k| h[k] = [] }

这使得默认值为空数组。 第一次通过块时,d => "2014-01-27"。 由于哈希最初为空,因此没有键"2014-01-27"。 因此,h["2014-01-27"]被分配默认值[],之后 h ["2014-01-27"] << 5 ,导致 h => {"2014-01-27" => 5}


0

我觉得以下内容较少玄学

ar.group_by(&:last).map {|k, v| [v.map {|e| e[0]}.sum, k]}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接