¿Cómo creo un promedio a partir de una matriz Ruby?


209

¿Cómo conseguiría encontrar un promedio de una matriz?

Si tengo la matriz:

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

El promedio me daría 3.375.


11
Si usted está recibiendo 21.75 como el promedio de esos números, algo anda muy mal ...
ceejayoz

2
dotty, no estoy seguro de cómo obtuviste 21.75, pero el promedio / media para ese conjunto de datos es 3.375 y la suma es 27. No estoy seguro de qué tipo de función de agregación produciría 21.75. ¡Verifique dos veces y asegúrese de que el promedio sea realmente lo que busca!
Paul Sasik

2
No tengo idea de dónde obtuve 21.75. ¡Debe haber presionado algo como 0 + 48 + 2 + 5 + 0 + 2 + 6 en la calculadora!
dotty

16
Dado que esto también está etiquetado como ruby-on-rails, vale la pena considerar los cálculos de registros activos si está promediando una matriz ActiveRecord. Person.average (: age,: country => 'Brazil') devuelve la edad promedio de las personas de Brasil. ¡Muy genial!
Kyle Heironimus

Respuestas:


260

Prueba esto:

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

Tenga en cuenta .to_flo que querrá para evitar cualquier problema de la división de enteros. También puedes hacer:

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

Puede definirlo como parte de lo Arrayque sugirió otro comentarista, pero debe evitar la división de enteros o sus resultados serán incorrectos. Además, esto generalmente no es aplicable a todos los tipos de elementos posibles (obviamente, un promedio solo tiene sentido para cosas que se pueden promediar). Pero si quieres ir por esa ruta, usa esto:

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

  def mean 
    sum / size
  end
end

Si no lo has visto injectantes, no es tan mágico como podría parecer. Se itera sobre cada elemento y luego le aplica un valor acumulador. El acumulador se entrega al siguiente elemento. En este caso, nuestro acumulador es simplemente un número entero que refleja la suma de todos los elementos anteriores.

Editar: El comentarista Dave Ray propuso una buena mejora.

Editar: La propuesta del comentarista Glenn Jackman, usando arr.inject(:+).to_f, también es buena, pero tal vez un poco demasiado inteligente si no sabes lo que está sucediendo. El :+es un símbolo; cuando se pasa a inyectar, aplica el método nombrado por el símbolo (en este caso, la operación de suma) a cada elemento contra el valor del acumulador.


66
Puedes eliminar to_f y? operador haciendo pasar un valor inicial para inyectar: arr.inject(0.0) { |sum,el| sum + el } / arr.size.
Dave Ray

103
O bien: arr.inject (: +). To_f / arr.size # => 3.375
glenn jackman

55
No creo que esto justifique agregar a la clase Array, ya que no es generalizable a todos los tipos que pueden contener Arrays.
Sarah Mei

8
@John: Eso no es exactamente la conversión de Symbol # to_proc: es parte de la injectinterfaz, mencionada en la documentación. El to_procoperador es &.
Chuck

21
Si está utilizando Rails, Array#injectes excesivo aquí. Solo úsalo #sum. Por ejemploarr.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

Una versión de esto que no use instance_evalsería:

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

44
No creo que sea demasiado inteligente. Creo que resuelve el problema idiomáticamente. Es decir, utiliza reduce, que es exactamente correcto. Se debe alentar a los programadores a comprender lo que es correcto, por qué es correcto y luego propagarlo. Para una operación trivial como promedio, cierto, uno no necesita ser "inteligente". Pero al comprender qué es "reducir" para un caso trivial, uno puede comenzar a aplicarlo a problemas mucho más complejos. voto a favor
pduey

3
¿Por qué la necesidad de instancia_eval aquí?
tybro0103

10
instance_evalle permite ejecutar el código mientras solo especifica auna vez, para que pueda encadenarse con otros comandos. Es decir, en random_average = Array.new(10) { rand(10) }.instance_eval { reduce(:+) / size.to_f } lugar derandom = Array.new(10) { rand(10) }; random_average = random.reduce(:+) / random.size
Benjamin Manns

2
No lo sé, usar instance_eval de esta manera parece extraño, y tiene muchas trampas asociadas que hacen que este enfoque sea una mala idea, OMI. (Por ejemplo, si intentas acceder a una variable de instancia o un método selfdentro de ese bloque, te encontrarás con problemas). instance_evalEs más para metaprogramación o DSL.
Ajedi32

1
@ Ajedi32 Estoy de acuerdo, no use esto en su código de aplicación. Sin embargo, fue muy agradable poder pegar en mi respuesta (:
animatedgif

94

Creo que la respuesta más simple es

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

1
Me tomó un momento encontrarlo, reducees un método de Enumerablemezcla utilizado por Array. Y a pesar de su nombre, estoy de acuerdo con @ShuWu ... a menos que esté usando Rails que implementa sum.
Tom Harrison el

Veo soluciones aquí, sé que se ven extremadamente ordenadas, pero me temo que si leo mi código en el futuro, les gustarán las tonterías. ¡Gracias por la solución limpia!
atmosx

En mi sistema, esto es 3 veces más rápido que la respuesta aceptada.
sergio el

48

Esperaba Math.average (valores), pero no tuve tanta suerte.

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

3
¡No me di cuenta de que Rails agregó #sum! Gracias por señalar eso.
Denny Abraham

11
Después de la Navidad de 2016 (Ruby 2.4), Array tendrá un summétodo, por lo que parece ser una respuesta correcta después de 6 años, digna del premio Nostradamus.
steenslag

38

Las versiones de Ruby> = 2.4 tienen un método de suma # Enumerable .

Y para obtener el promedio de coma flotante, puede usar Integer # fdiv

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

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

Para versiones anteriores:

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

9

Algunos puntos de referencia de las mejores soluciones (en orden de la más eficiente):

Gran matriz:

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)

Pequeñas matrices:

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)

Su punto de referencia está un poco equivocado. benchmark / ips es realmente mejor para este tipo de comparaciones. También sugeriría usar una matriz poblada aleatoriamente con números negativos y positivos, así como flotantes, para obtener un resultado más realista. Encontrará que instancia_eval es más lento que array.sum.fdiv. Aproximadamente 8x para carrozas. y aproximadamente x1.12 para enteros. Además, diferentes sistemas operativos darán resultados diferentes. En mi Mac, algunos de estos métodos son 2 veces más lentos que en mi Droplet de Linux
inició el

También el método de suma utiliza la fórmula de Gauss, en rangos en lugar de calcular la suma.
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
Esto devuelve valores incorrectos debido a la división de enteros. Pruébelo con, por ejemplo, [2,3] .mean, que devuelve 2 en lugar de 2.5.
John Feminella

1
¿Por qué una matriz vacía debe tener una suma de en nillugar de 0?
Andrew Grimm

1
Porque puedes obtener la diferencia entre [] y [0]. Y creo que todos los que quieran un medio real pueden usar to_i o reemplazar el cero anterior con un 0
astropánico

4

Permítanme traer algo a la competencia que resuelve el problema de división por cero:

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

Sin embargo, debo admitir que "intentar" es un ayudante de Rails. Pero puedes resolver esto fácilmente:

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

Por cierto: creo que es correcto que el promedio de una lista vacía sea nulo. El promedio de nada es nada, no 0. Entonces, ese es el comportamiento esperado. Sin embargo, si cambia a:

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

el resultado para las matrices vacías no será una excepción como esperaba, sino que devuelve NaN ... Nunca lo había visto antes en Ruby. ;-) Parece ser un comportamiento especial de la clase Float ...

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

4

lo que no me gusta de la solución aceptada

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

es que realmente no funciona de una manera puramente funcional. necesitamos una variable arr para calcular arr.size al final.

Para resolver esto de manera puramente funcional, necesitamos hacer un seguimiento de dos valores: la suma de todos los elementos y el número de elementos.

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

Santhosh mejoró esta solución: en lugar de que el argumento r sea una matriz, podríamos usar la desestructuración para separarlo inmediatamente en dos variables

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

si quieres ver cómo funciona, agrega algunas opciones:

[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

También podríamos usar una estructura en lugar de una matriz para contener la suma y el recuento, pero luego tenemos que declarar la estructura primero:

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

Esta es la primera vez que la veo end.methoden rubí, ¡gracias por esto!
Epigene

La matriz pasada al método de inyección se puede dispersar. arr.inject([0.0,0]) { |(sum, size), el| [ sum + el, size + 1 ] }.inject(:/)
Santhosh

@Santhosh: sí, ¡eso es mucho más legible! Sin embargo, no llamaría a esto "dispersión", lo llamaría "desestructuración" tony.pitluga.com/2011/08/08/destructuring-with-ruby.html
bjelli

3

Para diversión pública, otra solución más:

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
Si esto fuera más alto en la votación, ¡no lo habría entendido! Muy bien.
Matt Stevens

Claro es mejor que inteligente , este fragmento de código no es claro.
Sebastian Palma

2

No tenga Ruby en esta PC, pero algo en esta medida debería funcionar:

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

average = total/values.size

2

Añadir Array#average.

Estaba haciendo lo mismo con bastante frecuencia, así que pensé que era prudente extender la Arrayclase con un averagemétodo simple . No funciona para nada más que una matriz de números como enteros o flotantes o decimales, pero es útil cuando lo usa correctamente.

Estoy usando Ruby on Rails, así que coloqué esto config/initializers/array.rbpero puedes colocarlo en cualquier lugar que esté incluido en el arranque, etc.

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

44
Esto devolverá valores incorrectos debido a la división de enteros. Por ejemplo, si a es [2, 3], el resultado esperado es 2.5, pero devolverá 2.
John Feminella

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

Resuelve dividir por cero, división entera y es fácil de leer. Se puede modificar fácilmente si elige que una matriz vacía devuelva 0.

También me gusta esta variante, pero es un poco más prolija.

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

1
arr = [0,4,8,2,5,0,2,6]
average = arr.inject(&:+).to_f / arr.size
# => 3.375

1

Este método puede ser útil.

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
Bienvenido al desbordamiento de pila aquí El póster original de la pregunta quiere la respuesta como 3.375 y su solución da 3. i, e 27/8 = 3.
Ajay Barot

Gracias por tus comentarios. Sé que el Cartel original de la pregunta quiere una respuesta como 3.375 y eso es lo que hace este método, ya que le he dado a la variable 'var' un valor flotante (es decir, 0.0). Munim Munna Tengo que estar de acuerdo con usted, de hecho hay una respuesta similar.
Kishor Budhathoki

0

Sin tener que repetir la matriz (p. Ej., Perfecta para líneas simples):

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

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

Corto pero usando variable de instancia


2
Lo haría en a_size = nil; [1,2].tap { |a| a_size = a.size }.inject(:+).to_f/a_sizelugar de crear una variable de instancia.
Andrew Grimm

-1

Podría intentar algo como lo siguiente:

a = [1,2,3,4,5]
# => [1, 2, 3, 4, 5]
(a.sum/a.length).to_f
# => 3.0
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.