Como crio uma média a partir de uma matriz Ruby?

209

Como obter uma média de uma matriz?

Se eu tiver a matriz:

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

A média me daria 3.375.

dotty
fonte
11
Se você está recebendo 21.75 como a média desses números, algo está muito errado ...
ceejayoz
2
dotty, não sei como você conseguiu 21,75, mas a média / média desse conjunto de dados é 3,375 e a soma é 27. Não sei ao certo que tipo de função de agregação renderia 21,75. Verifique novamente e verifique se a média é realmente o que você procura!
Paul Sasik
2
Eu não tenho idéia de onde eu tenho 21,75. Deve ter pressionado algo como 0 + 48 + 2 + 5 + 0 + 2 + 6 na calculadora!
Dotty
16
Como isso também é marcado como ruby-on-rails, vale a pena analisar os cálculos de registros ativos se você estiver calculando a média de uma matriz ActiveRecord. Person.average (: age,: country => 'Brazil') retorna a idade média das pessoas no Brasil. Muito legal!
Kyle Heironimus

Respostas:

259

Tente o seguinte:

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

Observe o .to_fque você deseja evitar problemas da divisão inteira. Você também pode fazer:

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

Você pode defini-lo como parte de Arrayoutro usuário sugerido, mas precisa evitar a divisão de números inteiros ou seus resultados estarão errados. Além disso, isso geralmente não é aplicável a todos os tipos de elementos possíveis (obviamente, uma média só faz sentido para coisas que podem ser calculadas em média). Mas se você quiser seguir esse caminho, use o seguinte:

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

  def mean 
    sum / size
  end
end

Se você nunca viu injectantes, não é tão mágico quanto parece. Ele itera sobre cada elemento e, em seguida, aplica um valor acumulador a ele. O acumulador é então entregue ao próximo elemento. Nesse caso, nosso acumulador é simplesmente um número inteiro que reflete a soma de todos os elementos anteriores.

Editar: O comentarista Dave Ray propôs uma boa melhoria.

Edit: A proposta do comentarista Glenn Jackman, usando arr.inject(:+).to_f, também é legal, mas talvez um pouco inteligente demais, se você não sabe o que está acontecendo. O :+é um símbolo; quando passado para injetar, aplica o método nomeado pelo símbolo (neste caso, a operação de adição) a cada elemento em relação ao valor do acumulador.

John Feminella
fonte
6
Você pode eliminar to_f e? operador por meio de um valor inicial para injectar: arr.inject(0.0) { |sum,el| sum + el } / arr.size.
27630 Dave Ray
103
. Ou: arr.inject (: +) to_f / arr.size # => 3,375
glenn jackman
5
Eu não acho que isso justifique a adição à classe Array, pois não é generalizável para todos os tipos que Arrays podem conter.
247 Sarah Sarah Mei
8
@ John: Isso não é exatamente a conversão de Symbol # to_proc - faz parte da injectinterface, mencionada na documentação. O to_procoperador é &.
213 Chuck
21
Se você estiver usando o Rails, Array#injecté um exagero aqui. Apenas use #sum. Exemploarr.sum.to_f / arr.size
nickh 31/07
113
a = [0,4,8,2,5,0,2,6]
a.instance_eval { reduce(:+) / size.to_f } #=> 3.375

Uma versão disso que não usa instance_evalseria:

a = [0,4,8,2,5,0,2,6]
a.reduce(:+) / a.size.to_f #=> 3.375
Corban Brook
fonte
4
Eu não acho muito inteligente. Eu acho que resolve o problema linguisticamente. Ou seja, ele usa reduzir, o que é exatamente correto. Os programadores devem ser incentivados a entender o que está correto, por que está correto e depois se propagar. Para uma operação trivial como a média, é verdade, não é preciso ser "inteligente". Mas, ao entender o que "reduzir" é um caso trivial, pode-se começar a aplicá-lo a problemas muito mais complexos. voto positivo.
22612 pduey
3
por que a necessidade de instance_eval aqui?
Tybro0103
10
instance_evalpermite executar o código enquanto especifica apenas auma vez, para que ele possa ser encadeado com outros comandos. Ou seja, em random_average = Array.new(10) { rand(10) }.instance_eval { reduce(:+) / size.to_f } vez derandom = Array.new(10) { rand(10) }; random_average = random.reduce(:+) / random.size
Benjamin Manns
2
Eu não sei, usar instance_eval dessa maneira parece estranho, e há muitas dicas associadas que tornam essa abordagem uma má ideia, IMO. (Por exemplo, se você tentasse acessar uma variável de instância ou um método selfdentro desse bloco, teria problemas.) instance_evalÉ mais para metaprogramação ou DSL.
precisa saber é o seguinte
1
@ Ajedi32 Concordo, não use isso no código do seu aplicativo. No entanto, foi muito bom poder colar no meu repl (:
animatedgif
94

Eu acredito que a resposta mais simples é

list.reduce(:+).to_f / list.size
Shu Wu
fonte
1
Levei um momento para encontrá-lo - reduceé um método do Enumerablemixin usado por Array. E, apesar do nome, concordo com o @ShuWu ... a menos que você esteja usando o Rails que implementa sum.
Tom Harrison
Eu vejo soluções aqui, que eu sei que parecem extremamente legais, mas eu tenho medo que, se eu ler meu código no futuro, elas ficarão sem sentido. Obrigado pela solução limpa!
atmosx
No meu sistema, isso é 3x mais rápido que a resposta aceita.
sergio
48

Eu estava esperando Math.average (values), mas não tive essa sorte.

values = [0,4,8,2,5,0,2,6]
average = values.sum / values.size.to_f
Denny Abraham
fonte
3
Eu não sabia que #sum foi adicionado pelo Rails! Obrigado por apontar isso.
Denny Abraham
11
Após o Natal de 2016 (Ruby 2.4), o Array terá um summétodo, portanto essa parece ser uma resposta correta após 6 anos, digna do prêmio Nostradamus.
steenslag
38

As versões do Ruby> = 2.4 têm um método # sum enumerável.

E para obter uma média de ponto flutuante, você pode usar o número inteiro # fdiv

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

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

Para versões mais antigas:

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

Alguns testes comparativos das principais soluções (na ordem das mais eficientes):

Matriz grande:

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)

Matrizes pequenas:

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)
Stevenspiel
fonte
Seu benchmark está um pouco errado. benchmark / ips é realmente melhor para esse tipo de comparação. Também sugiro usar uma matriz preenchida aleatoriamente com números negativos e positivos, bem como flutuadores, para obter um resultado mais realista. Você verá que instance_eval é mais lento que array.sum.fdiv. Em cerca de 8x para carros alegóricos. e cerca de x1,12 para números inteiros. Além disso, diferentes sistemas operacionais fornecerão resultados diferentes. no meu mac alguns desses métodos são 2 vezes mais lenta do que no meu Linux Gota
Konung
O método sum também usa a fórmula de Gauss, em intervalos, em vez de calcular a soma.
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
astropânico
fonte
2
Isso retorna valores incorretos devido à divisão inteira. Experimente com, por exemplo, [2,3] .mean, que retorna 2 em vez de 2,5.
John Feminella
1
Por que uma matriz vazia deve ter uma soma em nilvez de 0?
Andrew Grimm
1
Porque você pode obter a diferença entre [] e [0]. E acho que todo mundo que quer um meio real pode usar to_i ou substituir o zero acima por um 0
astropânico
4

Deixe-me colocar algo em competição que resolva a divisão pelo problema zero:

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

Devo admitir, no entanto, que "try" é um ajudante do Rails. Mas você pode facilmente resolver isso:

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

BTW: Eu acho correto que a média de uma lista vazia seja nula. A média de nada é nada, não 0. Portanto, esse é o comportamento esperado. No entanto, se você mudar para:

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

o resultado para Arrays vazios não será uma exceção, como eu esperava, mas retorna NaN ... Eu nunca vi isso antes em Ruby. ;-) Parece ser um comportamento especial da classe Float ...

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

o que eu não gosto na solução aceita

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

é que realmente não funciona de maneira puramente funcional. precisamos de uma variável arr para calcular arr.size no final.

para resolver isso puramente funcionalmente, precisamos acompanhar dois valores: a soma de todos os elementos e o 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 aprimorou esta solução: em vez de o argumento r ser uma matriz, poderíamos usar a desestruturação para separá-la imediatamente em duas variáveis

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

se você quiser ver como funciona, adicione alguns putos:

[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

Também poderíamos usar uma estrutura em vez de uma matriz para conter a soma e a contagem, mas depois temos que declarar a estrutura primeiro:

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(:/)
bjelli
fonte
Esta é a primeira vez que vejo o end.methodruby, obrigado por isso!
Epigene
A matriz passada para o método de injeção pode ser dispersa. arr.inject([0.0,0]) { |(sum, size), el| [ sum + el, size + 1 ] }.inject(:/)
Santhosh
@Santhosh: sim, isso é muito mais legível! Eu não chamaria isso de "dispersão", porém, eu chamaria isso de "desestruturação" tony.pitluga.com/2011/08/08/destructuring-with-ruby.html
bjelli
3

Para diversão do público, mais uma solução:

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
Boris Stitnicky
fonte
1
Se isso fosse mais alto na votação, eu não teria entendido! Muito bom.
Matt Stevens
Limpar é melhor que inteligente , esse trecho de código não é claro.
Sebastian Palma
2

Não tem ruby ​​neste PC, mas algo nessa medida deve funcionar:

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

average = total/values.size
saret
fonte
2

Adicionar Array#average.

Eu fazia a mesma coisa com frequência, então achei prudente estender a Arrayclasse apenas com um averagemétodo simples . Ele não funciona para nada além de uma matriz de números como números inteiros ou flutuantes ou decimais, mas é útil quando você o usa corretamente.

Estou usando Ruby on Rails, então coloquei isso, config/initializers/array.rbmas você pode colocá-lo em qualquer lugar que esteja incluído na inicialização, 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
Joshua Pinter
fonte
1
a = [0,4,8,2,5,0,2,6]
sum = 0
a.each { |b| sum += b }
average = sum / a.length
erik
fonte
4
Isso retornará valores incorretos devido à divisão inteira. Por exemplo, se um é [2, 3], o resultado esperado é de 2,5, mas você vai voltar 2.
John Feminella
1
a = [0,4,8,2,5,0,2,6]
a.empty? ? nil : a.reduce(:+)/a.size.to_f
=> 3.375

Resolve dividir por zero, divisão inteira e é fácil de ler. Pode ser facilmente modificado se você escolher que uma matriz vazia retorne 0.

Também gosto dessa variante, mas é um pouco mais prolixo.

a = [0,4,8,2,5,0,2,6]
a.empty? ? nil : [a.reduce(:+), a.size.to_f].reduce(:/)
=> 3.375
Matt Stevens
fonte
1
arr = [0,4,8,2,5,0,2,6]
average = arr.inject(&:+).to_f / arr.size
# => 3.375
Rahul Patel
fonte
1

Este método pode 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])
Kishor Budhathoki
fonte
1
Bem-vindo ao estouro de pilha aqui poster original da pergunta quer a resposta de 3.375 e sua solução dá 3. i, e 27/8 = 3.
Ajay Barot
Obrigado por seus comentários. Eu sei que o Pôster original da pergunta quer resposta como 3.375 e é isso que esse método faz, pois forneci à variável 'var' um valor flutuante (ou seja, 0,0). Munim Munna Eu tenho que concordar com você.
Kishor Budhathoki
0

Sem ter que repetir a matriz (por exemplo, perfeita para one-liners):

[1, 2, 3, 4].then { |a| a.sum.to_f / a.size }
Dorian
fonte
-1
[1,2].tap { |a| @asize = a.size }.inject(:+).to_f/@asize

Variável de instância curta, mas usando

Alex Leschenko
fonte
2
Eu faria, em a_size = nil; [1,2].tap { |a| a_size = a.size }.inject(:+).to_f/a_sizevez de criar uma variável de instância.
Andrew Grimm
-1

Você pode tentar algo como o seguinte:

a = [1,2,3,4,5]
# => [1, 2, 3, 4, 5]
(a.sum/a.length).to_f
# => 3.0
Paul Marclay
fonte