我该如何从 Ruby 数组中创建平均值?

233

如何从数组中获取平均值?

如果我有以下数组:

[0,4,8,2,5,0,2,6]

取平均值的话会得到 3.375。


12
如果你计算那些数字的平均值得到了21.75,那么肯定出了什么严重的错误... - ceejayoz
2
Dotty,我不确定你是怎么得到21.75的,但是这组数据的平均值是3.375,总和为27。我不确定什么样的聚合函数会产生21.75。请再次确认并确保平均值确实是你想要的! - Paul Sasik
2
我完全不知道21.75是从哪里来的。可能是在计算器上按了0+48+2+5+0+2+6之类的东西! - dotty
17
因为这个也被标记为ruby-on-rails,如果你正在对一个ActiveRecord数组求平均值,那么值得研究的是Active Record计算。Person.average(:age, :country => 'Brazil')返回来自巴西的人们年龄的平均值。非常酷! - Kyle Heironimus
24个回答

272

试试这个:

arr = [5, 6, 7, 8]
arr.inject{ |sum, el| sum + el }.to_f / arr.size
=> 6.5

请注意.to_f,当避免整数除法问题时需要使用它。您也可以执行以下操作:


arr = [5, 6, 7, 8]
arr.inject(0.0) { |sum, el| sum + el } / arr.size
=> 6.5

您可以按照另一位评论者的建议将其定义为Array的一部分,但是您需要避免使用整数除法,否则结果将会出错。此外,这并不适用于每种可能的元素类型(显然,平均值只对可以平均的事物有意义)。但如果您想要这样做,请使用以下方法:

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

  def mean 
    sum / size
  end
end

如果您之前没有见过inject,它并没有看起来那么神奇。它遍历每个元素,然后对其应用累加器值。然后将累加器传递给下一个元素。在这种情况下,我们的累加器只是反映所有先前元素的总和的整数。

编辑:评论者Dave Ray提出了一个不错的改进。

编辑:评论者Glenn Jackman的建议,使用arr.inject(:+).to_f,也很好,但如果您不知道发生了什么,则可能有点太聪明了。 :+ 是一个符号;当传递给inject时,它将该符号命名的方法(在本例中为加法运算)应用于每个元素与累加器值。


6
你可以通过在inject方法中传递一个初始值来消除to_f?操作符:arr.inject(0.0) { |sum,el| sum + el } / arr.size - Dave Ray
108
或者:arr.inject(:+).to_f / arr.size # => 3.375 - glenn jackman
5
我不认为这需要添加到数组类中,因为它不能推广到数组可能包含的所有类型。 - Sarah Mei
8
@John:这并不是完全的Symbol#to_proc转换,它是inject接口的一部分,在文档中有提到。to_proc运算符是& - Chuck
26
如果你在使用Rails,那么Array#inject在这里就有点过头了。只需要使用#sum即可。例如:arr.sum.to_f / arr.size - nickh
显示剩余5条评论

122
a = [0,4,8,2,5,0,2,6]
a.instance_eval { reduce(:+) / size.to_f } #=> 3.375

不使用 instance_eval 的版本如下:

a = [0,4,8,2,5,0,2,6]
a.reduce(:+) / a.size.to_f #=> 3.375

6
我认为这并不是过于聪明的方法,我认为它用惯用语解决了问题。也就是说,它使用了 reduce,这是完全正确的。应该鼓励程序员理解什么是正确的,为什么是正确的,然后传播。对于像平均值这样的微不足道的操作,确实不需要太“聪明”。但是通过理解在一个微不足道的情况下“reduce”的作用,人们可以开始将其应用于更加复杂的问题。点赞。 - pduey
3
为什么这里需要使用instance_eval? - tybro0103
11
instance_eval 允许您只指定一次 a 并运行代码,因此可以与其他命令链接在一起。例如,使用 instance_eval 可以这样写:random_average = Array.new(10) { rand(10) }.instance_eval { reduce(:+) / size.to_f },而不是这样写:random = Array.new(10) { rand(10) }; random_average = random.reduce(:+) / random.size - Benjamin Manns
3
我觉得用instance_eval这种方式很奇怪,而且它有很多容易出问题的陷阱,所以我认为这种方法不好。例如,如果你试图在块内部访问self的实例变量或方法,就会遇到问题。instance_eval更适合元编程或DSL。 - Ajedi32
1
instance_eval类似于使用tapyield_self等方法来处理一系列的方法调用:只要适当且有帮助,就请使用它!在这种情况下,我绝对相信它是有用的。 - Tyler Rick
显示剩余5条评论

96

我认为最简单的答案是

list.reduce(:+).to_f / list.size

1
我花了一点时间才找到它——reduce是由Array使用的Enumerable mixin的一个方法。尽管它的名字如此,但我同意@ShuWu的看法...除非你正在使用实现了sum的Rails。 - Tom Harrison
我在这里看到一些解决方案,它们看起来非常整洁,但我担心如果将来阅读我的代码,它们会像胡言乱语一样。感谢提供干净的解决方案! - patm
在我的系统上,这比被接受的答案快3倍。 - builder-7000

58

我希望使用 Math.average(values) 进行计算,但没有这样的运气。

values = [0,4,8,2,5,0,2,6]
average = values.sum / values.size.to_f

3
我不知道Rails已经添加了#sum方法!感谢你指出这一点。 - Denny Abraham
15
在2016年圣诞节之后(Ruby 2.4),数组将拥有一个名为sum的方法,因此在6年之后这似乎是一个正确的答案,值得获得诺斯特拉达姆斯奖。请注意,此处使用的动词是“will”,表示未来进行时。 - steenslag

55

Ruby版本>=2.4具有Enumerable#sum方法。

要获取浮点平均值,可以使用Integer#fdiv

arr = [0,4,8,2,5,0,2,6]

arr.sum.fdiv(arr.size)
# => 3.375

针对旧版本:

arr.reduce(:+).fdiv(arr.size)
# => 3.375

13

无需重复数组(例如,非常适合一行代码):

[1, 2, 3, 4].then { |a| a.sum.to_f / a.size }

2
我喜欢.then - Victor

12

以下是一些排名最高的解决方案的基准测试结果(按效率排序):

大型数组:

array = (1..10_000_000).to_a

Benchmark.bm do |bm|
  bm.report { array.instance_eval { reduce(:+) / size.to_f } }
  bm.report { array.sum.fdiv(array.size) }
  bm.report { array.sum / array.size.to_f }
  bm.report { array.reduce(:+).to_f / array.size }
  bm.report { array.reduce(:+).try(:to_f).try(:/, array.size) }
  bm.report { array.inject(0.0) { |sum, el| sum + el }.to_f / array.size }
  bm.report { array.reduce([ 0.0, 0 ]) { |(s, c), e| [ s + e, c + 1 ] }.reduce(:/) }
end


    user     system      total        real
0.480000   0.000000   0.480000   (0.473920)
0.500000   0.000000   0.500000   (0.502158)
0.500000   0.000000   0.500000   (0.508075)
0.510000   0.000000   0.510000   (0.512600)
0.520000   0.000000   0.520000   (0.516096)
0.760000   0.000000   0.760000   (0.767743)
1.530000   0.000000   1.530000   (1.534404)

小数组:

array = Array.new(10) { rand(0.5..2.0) }

Benchmark.bm do |bm|
  bm.report { 1_000_000.times { array.reduce(:+).to_f / array.size } }
  bm.report { 1_000_000.times { array.sum / array.size.to_f } }
  bm.report { 1_000_000.times { array.sum.fdiv(array.size) } }
  bm.report { 1_000_000.times { array.inject(0.0) { |sum, el| sum + el }.to_f / array.size } }
  bm.report { 1_000_000.times { array.instance_eval { reduce(:+) / size.to_f } } }
  bm.report { 1_000_000.times { array.reduce(:+).try(:to_f).try(:/, array.size) } }
  bm.report { 1_000_000.times { array.reduce([ 0.0, 0 ]) { |(s, c), e| [ s + e, c + 1 ] }.reduce(:/) } }
end


    user     system      total        real
0.760000   0.000000   0.760000   (0.760353)
0.870000   0.000000   0.870000   (0.876087)
0.900000   0.000000   0.900000   (0.901102)
0.920000   0.000000   0.920000   (0.920888)
0.950000   0.000000   0.950000   (0.952842)
1.690000   0.000000   1.690000   (1.694117)
1.840000   0.010000   1.850000   (1.845623)

你的基准测试有点问题。对于这种比较,benchmark/ips 更好。我建议使用一个由负数、正数和浮点数随机填充的数组,以获得更真实的结果。你会发现 instance_eval 比 array.sum.fdiv 慢。对于浮点数慢了约 8 倍,对于整数慢了约 x1.12。此外,不同的操作系统将给出不同的结果。在我的 Mac 上,其中一些方法比在我的 Linux Droplet 上慢两倍。 - konung
同时,sum方法使用的是高斯公式,在范围内计算总和,而非逐个相加。 - Santhosh

4
class Array
  def sum 
    inject( nil ) { |sum,x| sum ? sum+x : x }
  end

  def mean 
    sum.to_f / size.to_f
  end
end

[0,4,8,2,5,0,2,6].mean

2
这会返回错误的值,因为它使用整数除法。尝试使用例如[2,3].mean进行计算,它将返回2而不是2.5。 - John Feminella
1
一个空数组为什么应该有一个 nil 的总和而不是0? - Andrew Grimm
1
因为你可以得到 [] 和 [0] 之间的差异。我认为每个真正需要意义的人都可以使用 to_i 或者将上面的 nil 替换为 0。 - astropanic

4

让我介绍一种解决除零问题的竞争方案:

a = [1,2,3,4,5,6,7,8]
a.reduce(:+).try(:to_f).try(:/,a.size) #==> 4.5

a = []
a.reduce(:+).try(:to_f).try(:/,a.size) #==> nil

不过我必须承认,“try”是Rails中的帮助程序。但你可以轻松解决这个问题:

class Object;def try(*options);self&&send(*options);end;end
class Array;def avg;reduce(:+).try(:to_f).try(:/,size);end;end

顺便说一句:我认为空列表的平均值是nil是正确的。没有任何数据的平均值应该是无,而不是0。所以这是预期的行为。然而,如果你将其更改为:

class Array;def avg;reduce(0.0,:+).try(:/,size);end;end

对于空数组,其结果不会像我预期的那样抛出异常,而是返回NaN... 在Ruby中我从未见过这种情况。;-) 似乎是Float类的特殊行为...

0.0/0 #==> NaN
0.1/0 #==> Infinity
0.0.class #==> Float

3

我不喜欢被接受的解决方案的原因

arr = [5, 6, 7, 8]
arr.inject{ |sum, el| sum + el }.to_f / arr.size
=> 6.5

问题在于它并不是以纯函数的方式运作。

我们需要一个变量 arr 来计算最终的 arr.size。

要完全以函数式的方式解决这个问题,我们需要跟踪两个值:所有元素的总和和元素数量。

[5, 6, 7, 8].inject([0.0,0]) do |r,ele|
    [ r[0]+ele, r[1]+1 ]
end.inject(:/)
=> 6.5   

Santhosh改进了这个解决方案: 不用将参数r定义为数组,我们可以使用解构语法立即将其拆分成两个变量

[5, 6, 7, 8].inject([0.0,0]) do |(sum, size), ele| 
   [ sum + ele, size + 1 ]
end.inject(:/)

如果您想看看它是如何工作的,请添加一些puts:

[5, 6, 7, 8].inject([0.0,0]) do |(sum, size), ele| 
   r2 = [ sum + ele, size + 1 ]
   puts "adding #{ele} gives #{r2}"
   r2
end.inject(:/)

adding 5 gives [5.0, 1]
adding 6 gives [11.0, 2]
adding 7 gives [18.0, 3]
adding 8 gives [26.0, 4]
=> 6.5

我们也可以使用结构体而不是数组来包含总和和计数,但这样我们必须先声明结构体:

我们也可以使用结构体而不是数组来包含总和和计数,但这样我们必须先声明结构体:

R=Struct.new(:sum, :count)
[5, 6, 7, 8].inject( R.new(0.0, 0) ) do |r,ele|
    r.sum += ele
    r.count += 1
    r
end.inject(:/)

这是我第一次在Ruby中看到end.method的使用,谢谢您! - Epigene
传递给inject方法的数组可以被分散。arr.inject([0.0,0]) { |(sum, size), el| [ sum + el, size + 1 ] }.inject(:/) - Santhosh
@Santhosh:是的,这样更易读了!但我不会称其为“分散”,我会称其为“解构”http://tony.pitluga.com/2011/08/08/destructuring-with-ruby.html - bjelli

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