Як створити середнє значення з масиву Ruby?


209

Як би знайти середнє значення з масиву?

Якщо у мене є масив:

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

Усереднення дасть мені 3.375.


11
Якщо ви отримуєте 21,75 як середнє число цих номерів, щось дуже не так ...
ceejayoz

2
точка, не знаю, як у вас 21,75, але середнє значення / середнє значення для цього набору даних становить 3,375, а сума - 27. Я не впевнений, яка функція агрегації дала б 21,75. Будь ласка, перевірте та переконайтеся, що середній показник - це те, що ви хочете!
Пол Сасік

2
Я поняття не маю, звідки я отримав 21,75. Потрібно було натиснути щось на зразок 0 + 48 + 2 + 5 + 0 + 2 + 6 на калькуляторі!
крапка

16
Оскільки це також позначено рубіном на рейки, варто розглянути активні обчислення записів, якщо ви усереднюєте масив ActiveRecord. Person.average (: вік,: країна => 'Бразилія') повертає середній вік людей з Бразилії. Дуже здорово!
Кайл Хайронімус

Відповіді:


259

Спробуйте це:

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, це не так магічно, як може здатися. Він повторюється над кожним елементом, а потім застосовує до нього значення акумулятора. Потім акумулятор передається наступному елементу. У цьому випадку наш акумулятор - це ціле число, яке відображає суму всіх попередніх елементів.

Редагувати: Коментатор Дейв Рей запропонував приємне поліпшення.

Редагувати: Пропозиція коментатора Глена Джекмана arr.inject(:+).to_f, як правило, теж приємна, але, можливо, трохи розумна, якщо ви не знаєте, що відбувається. :+Є символом; при передачі ін'єкції він застосовує метод, названий символом (у цьому випадку операцією додавання), до кожного елемента проти значення акумулятора.


6
Ви можете усунути to_f і? оператор, передаючи початкове значення впорснути: arr.inject(0.0) { |sum,el| sum + el } / arr.size.
Дейв Рей

103
Або: arr.inject (: +). To_f / arr.size # => 3.375
glenn jackman

5
Я не думаю, що це вимагає додавання до класу Array, оскільки це не може бути узагальненим для всіх типів, які можуть містити масиви.
Сара Мей

8
@John: Це не зовсім перетворення Symbol # to_proc - це частина injectінтерфейсу, згаданого в документації. to_procОператор &.
Чак

21
Якщо ви використовуєте Rails, Array#injectтут надмірна кількість. Просто використовуйте #sum. Напр.arr.sum.to_f / arr.size
nickh

113
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

4
Я не думаю, що це занадто розумно. Я думаю, що це вирішує проблему ідіоматично. Тобто, він використовує скорочення, що абсолютно правильно. Програмістів слід заохочувати розуміти, що правильно, чому це правильно, а потім пропагувати. Для такої тривіальної операції, як середня, справжня, не потрібно бути "розумним". Але розуміючи, що таке «зменшення» для тривіального випадку, можна потім почати застосовувати його до набагато складніших проблем. піднести
пдуї

3
чому тут потрібна instance_eval?
tybro0103

10
instance_evalдозволяє запускати код, вказуючи лише aодин раз, тому він може бути пов'язаний з іншими командами. Тобто 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
Бенджамін Маннс

2
Я не знаю, якщо використовувати instance_eval таким чином, просто здається дивним, і у нього є багато пов'язаних з цим проблем, які роблять такий підхід поганою ідеєю, IMO. (Наприклад, якщо ви намагалися отримати доступ до змінної екземпляра або методу selfвсередині цього блоку, у вас виникли проблеми.) instance_evalБільше для метапрограмування або DSL.
Ajedi32

1
@ Ajedi32 Я згоден, не використовуйте це у коді програми. Однак було дуже приємно мати змогу вставити в мою відповідь (:
animatedgif

94

Я вважаю, що найпростіша відповідь

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

1
Знадобилося мені хвилинку, щоб знайти його - reduceце метод Enumerableміксину, який використовує Array. І незважаючи на його назву, я погоджуюся з @ShuWu ... якщо ви не використовуєте Rails, який реалізує sum.
Том Гаррісон

Я бачу тут рішення, що я знаю, що вони виглядають надзвичайно акуратно, але боюся, якщо в майбутньому я прочитаю свій код, вони будуть сподобатися хитрістю. Дякуємо за чисте рішення!
atmosx

У моїй системі це в 3 рази швидше, ніж прийнята відповідь.
Серхіо

48

Я сподівався на Math.average (значення), але такої удачі немає.

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

3
Я не розумів, що #sum був доданий Rails! Дякуємо, що вказали на це.
Денні Авраам

11
Після Різдва 2016 роки (Ruby 2,4), масив буде мати sumметод, так це , здається, правильна відповідь після 6 років, гідні нагороди Nostradamus.
steenslag

38

У версіях Ruby> = 2.4 використовується метод " Сума" # .

А щоб отримати середнє значення з плаваючою комою, ви можете використовувати 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

9

Деякі порівняльні показники найкращих рішень (в порядку найбільш ефективного):

Великий масив:

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)

Ваш орієнтир трохи неправильний. бенчмарк / ips насправді кращий для таких видів порівнянь. Також я б запропонував використовувати масив, заповнений випадковим чином з від'ємними та додатними числами, а також поплавцями, щоб отримати більш реалістичний результат. Ви побачите, що instance_eval повільніше, ніж array.sum.fdiv. Приблизно в 8 разів для поплавців. і приблизно x1.12 для цілих чисел. Також різні ОС дадуть різні результати. на моєму комп'ютері деякі з цих методів у 2 рази повільніше, ніж на моїй Linux Droplet
konung

Також метод sum використовує формулу Гаусса на діапазонах замість обчислення суми.
Сантош

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.
Джон Фемінелла

1
Чому порожній масив повинен мати суму, nilа не 0?
Ендрю Грімм

1
Тому що ви можете отримати різницю між [] та [0]. І я думаю, що кожен, хто хоче справжнього значення, може скористатися to_i або замінити вищезгаданий нуль на 0
астропанічний

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

Але я мушу визнати, що "спробувати" - це помічник Рейла. Але ви можете легко вирішити це:

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

BTW: Я думаю, що правильно, що середнє значення порожнього списку дорівнює нулю. В середньому ніщо - це нічого, не 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

4

що мені не подобається у прийнятому рішенні

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   

Сантош покращив це рішення: замість того, щоб аргумент r був масивом, ми могли б використовувати деструктування, щоб негайно виділити його на дві змінні

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

якщо ви хочете побачити, як це працює, додайте кілька ставок:

[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(:/)

Це вперше я бачу end.methodвикористаного в рубіні, дякую за це!
Епіген

Масив, переданий методу ін'єкції, може бути розсіяний. arr.inject([0.0,0]) { |(sum, size), el| [ sum + el, size + 1 ] }.inject(:/)
Сантош

@Santhosh: так, це набагато читабельніше! Я б не назвав це "розсіюванням", я б назвав це "руйнуванням" tony.pitluga.com/2011/08/08/destructuring-with-ruby.html
bjelli

3

Для громадського розваги ще одне рішення:

a = 0, 4, 8, 2, 5, 0, 2, 6
a.reduce [ 0.0, 0 ] do |(s, c), e| [ s + e, c + 1 ] end.reduce :/
#=> 3.375

1
Якби це було вище під час голосування, я б цього не зрозумів! Дуже добре.
Метт Стівенс

Очистити краще, ніж розумно , цей фрагмент коду не зрозумілий.
Себастьян Пальма

2

Немає рубіну на цьому ПК, але щось у цій мірі має працювати:

values = [0,4,8,2,5,0,2,6]
total = 0.0
values.each do |val|
 total += val
end

average = total/values.size

2

Додайте Array#average .

Я робив те саме, що часто, тому я вважав за доцільне просто розширити Arrayклас простимaverage методом. Він не працює ні для чого, крім масиву чисел, таких як Integers або Floats або Decimals, але це зручно при правильному використанні.

Я використовую Ruby on Rails, тому я розмістив це, config/initializers/array.rbале ви можете розмістити його в будь-якому місці, що включено до завантаження тощо.

config/initializers/array.rb

class Array

  # Will only work for an Array of numbers like Integers, Floats or Decimals.
  #
  # Throws various errors when trying to call it on an Array of other types, like Strings.
  # Returns nil for an empty Array.
  #
  def average
    return nil if self.empty?

    self.sum / self.size
  end

end

1
a = [0,4,8,2,5,0,2,6]
sum = 0
a.each { |b| sum += b }
average = sum / a.length

4
Це поверне неправильні значення через ціле ділення. Наприклад, якщо а дорівнює [2, 3], очікуваний результат 2,5, але ви повернетесь 2.
Джон Фемінелла,

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

Розв’язує ділення на нуль, ціле ділення і легко читається. Можна легко змінити, якщо ви вирішите мати порожній масив 0.

Мені також подобається цей варіант, але він трохи більш багатослівний.

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


1

Цей метод може бути корисним.

def avg(arr)
  val = 0.0

  arr.each do |n|
    val += n
  end

  len = arr.length

  val / len 
end

p avg([0,4,8,2,5,0,2,6])

1
Ласкаво просимо, щоб сюди переповнювались Оригінальний афіша питання хоче відповісти як 3.375, а ваше рішення дає 3. i, e
Ajay

Дякую за ваші коментарі. Я знаю, що в оригінальному плакаті запитання хочеться відповісти як 3.375, і це те, що робить цей метод, коли я дав змінній 'var' значення float (тобто; 0,0). Мунім Мунна. Я маю згоду з тим, що дійсно є аналогічний анс.
Кішор Будхатокі

0

Без необхідності повторювати масив (наприклад, ідеально підходить для одноклассників):

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

-1
[1,2].tap { |a| @asize = a.size }.inject(:+).to_f/@asize

Коротка, але з використанням змінної екземпляра


2
Я б зробив, a_size = nil; [1,2].tap { |a| a_size = a.size }.inject(:+).to_f/a_sizeа не створив змінну екземпляра.
Ендрю Грімм

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.