概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: Calculate a mean average from a Ruby array - Andy Croll
- 原文公開日: 2020/02/02
- 著者: Andy Croll
Ruby: 配列要素の平均値を取るときのコツ(翻訳)
Rubyは、要素がintegerの配列の平均値を生成するネイティブのメソッドを提供していません。Math
ライブラリは、より複雑な計算メソッドに注力しており、Array
組み込みの#average
メソッドや#mean
メソッドはありません。
つまり、これらのメソッドを独自に作成する余地がある分、自分の足を撃ち抜く可能性も生じます。
以下のようにすること
要素がintegerの配列の平均値を算出するには、以下のようにArray#sum
を用いる。
a = [1, 2, 3, 4, 5, 6, 7, 8]
a.sum(0.0) / a.size
#=> 4.5
そうすべき理由
Array#sum
は、inject
よりもずっとずっと高速です。
Array#sum
が導入されたのはRuby 2.4からでした。Array#sum
以外のやり方(訳注: inject
を使う方法)がネット上に山ほど落ちている理由は、これです。
benchmark-ips
gemで実装ごとのパフォーマンスの違いを比較できます。
require "benchmark/ips"
# 要素が1,000個の配列を生成する
a = Array.new(1000) { |_| rand(1000) }
Benchmark.ips do |x|
x.report("sum(0.0) / size") do
a.sum(0.0) / a.size
end
x.report('inject(0.0) / size') do
a.inject(0.0) { |result, i| result + i } / a.size
end
x.compare!
end
私のノートPCでは、ネイティブの#sum
を用いると50倍も速くなりました。このメソッドは、まさにこうしたパフォーマンス向上のためにRubyに組み込まれたのです。
Calculating -------------------------------------
sum(0.0) / size 680.425k (± 6.5%) i/s - 3.432M in 5.06s
inject(0.0) / size 13.513k (± 5.0%) i/s - 67.586k in 5.01s
Comparison:
sum(0.0) / size: 680425.2 i/s
inject(0.0) / size: 13512.7 i/s - 50.35x slower
訳注: 訳者のMacbook Pro (2019)では69倍の違いが生じました。
#sum
してから#size
するのと同様の手法は他にもいろいろありますが、パフォーマンス上の寄与が最も大きいのは、最初にネイティブの#sum
メソッドを用いることです。
さらに多くの実装でベンチマークしてみましょう。
require "benchmark/ips"
# 要素が1,000個の配列を生成する
a = Array.new(1000) { |_| rand(1000) }
Benchmark.ips do |x|
x.report("sum(0.0) / size") do
a.sum(0.0) / a.size
end
x.report("sum.to_f / size") do
a.sum.to_f / a.size
end
x.report("sum / size.to_f") do
a.sum / a.size.to_f
end
x.report("sum.fdiv(size)") do
a.sum.fdiv(a.size)
end
x.report('inject(0.0, :+) / size') do
a.inject(0.0, :+) / a.size
end
x.report('inject(0.0) / size') do
a.inject(0.0) { |result, i| result + i } / a.size
end
x.report('inject(0).to_f / size') do
a.inject(0) { |result, i| result + i }.to_f / a.size
end
x.report('inject(0) / size.to_f') do
a.inject(0) { |result, i| result + i } / a.size.to_f
end
x.report('inject(0).fdiv(size)') do
a.inject(0) { |result, i| result + i }.fdiv(a.size)
end
x.compare!
end
結果は以下のとおりです。
Comparison:
sum(0.0) / size: 668222.4 i/s
sum / size.to_f: 660291.4 i/s - same-ish
sum.to_f / size: 655929.1 i/s - same-ish
sum.fdiv(size): 621960.0 i/s - same-ish
inject(0.0, :+) / size: 30823.6 i/s - 21.68x slower
inject(0) / size.to_f: 18740.7 i/s - 35.66x slower
inject(0).to_f / size: 18320.1 i/s - 36.47x slower
inject(0).fdiv(size): 18082.6 i/s - 36.95x slower
inject(0.0) / size: 15264.0 i/s - 43.78x slower
他の方法はあるか
RailsのActive Recordには#average
メソッドがあります。このメソッドは、データベース上の数値カラムを直接SQLで計算します。
求めているユースケースがそれであれば、ぜひこの#average
を使うべきです。Active Recordモデルをインスタンス化してからRubyの配列をイテレートすると、ほぼ確実に遅くなります。