Как мне создать среднее из массива 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
Поскольку это также помечено как ruby-on-rails, вычисления активных записей заслуживают внимания, если вы усредняете массив ActiveRecord. Person.average (: age,: country => 'Brazil') возвращает средний возраст людей из Бразилии. Довольно круто!
Кайл Хейронимус

Ответы:


260

Попробуй это:

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
Гленн Джекман

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
Я не думаю, что это слишком умно. Я думаю, что это решает проблему идиоматически. Т.е. используется уменьшение, что совершенно правильно. Программистов следует поощрять понимать, что правильно, почему это правильно, а затем распространять. Для такой простой операции, как средняя, ​​правда, не нужно быть «умным». Но, понимая, что такое «сокращение» для тривиального случая, можно начать применять его к гораздо более сложным задачам. upvote.
pduey

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 имеют метод 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

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)

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

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

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

Однако я должен признать, что «try» является помощником Rails. Но вы можете легко решить это:

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

Кстати: я думаю, что это правильно, что среднее значение пустого списка равно нулю. Среднее ничего - ничто, а не 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   

Santhosh улучшил это решение: вместо аргумента 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в ruby, спасибо за это!
Эпиген

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

@ Сантош: да, это намного более читабельно! Я бы не назвал это «рассеиванием», я бы назвал это «деструктурированием» 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 методом. Он не работает ни для чего, кроме массива чисел, таких как целые числа или числа с плавающей запятой или десятичные числа, но это удобно, когда вы используете его правильно.

Я использую 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
Это вернет неправильные значения из-за целочисленного деления. Например, если a равно [2, 3], ожидаемый результат равен 2,5, но вы
вернете

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' значение с плавающей запятой (то есть; 0,0). Munim Munna Я должен согласиться с тобой, что действительно есть похожий ответ.
Кишор

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а не создать переменную экземпляра.
Эндрю Гримм

-1

Вы можете попробовать что-то вроде следующего:

a = [1,2,3,4,5]
# => [1, 2, 3, 4, 5]
(a.sum/a.length).to_f
# => 3.0
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.