Come faccio a creare una media da un array Ruby?


209

Come ottenere una media da un array?

Se ho l'array:

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

La media mi darebbe 3.375.


11
Se
ottieni

2
dotty, non sono sicuro di come hai ottenuto 21,75 ma la media / media per quel set di dati è 3.375 e la somma è 27. Non sono sicuro che tipo di funzione di aggregazione produrrebbe 21,75. Ricontrolla e assicurati che la media sia davvero ciò che cerchi!
Paul Sasik,

2
Non ho idea da dove ho ottenuto 21,75. Deve aver premuto qualcosa come 0 + 48 + 2 + 5 + 0 + 2 + 6 sulla calcolatrice!
dotty

16
Poiché questo è anche etichettato ruby-on-rails, vale la pena esaminare i calcoli dei record attivi se si sta facendo la media di un array ActiveRecord. Person.average (: age,: country => 'Brazil') restituisce l'età media delle persone dal Brasile. Abbastanza bello!
Kyle Heironimus,

Risposte:


260

Prova questo:

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

Nota .to_f, che ti consigliamo di evitare problemi dalla divisione di numeri interi. Puoi anche fare:

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

Puoi definirlo come Arraysuggerito da un altro commentatore, ma devi evitare la divisione di interi o i tuoi risultati saranno errati. Inoltre, questo non è generalmente applicabile a ogni possibile tipo di elemento (ovviamente, una media ha senso solo per le cose che possono essere mediate). Ma se vuoi seguire questa strada, usa questo:

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

  def mean 
    sum / size
  end
end

Se non l'hai mai visto injectprima, non è magico come potrebbe apparire. Esegue l'iterazione su ciascun elemento e quindi applica un valore di accumulatore ad esso. L'accumulatore viene quindi passato all'elemento successivo. In questo caso, il nostro accumulatore è semplicemente un numero intero che riflette la somma di tutti gli elementi precedenti.

Modifica: commentatore Dave Ray ha proposto un bel miglioramento.

Modifica: la proposta del commentatore Glenn Jackman, usando arr.inject(:+).to_f, è anche carina ma forse un po 'troppo intelligente se non sai cosa sta succedendo. Il :+è un simbolo; quando viene passato per l'iniezione, applica il metodo indicato dal simbolo (in questo caso, l'operazione di aggiunta) a ciascun elemento rispetto al valore dell'accumulatore.


6
Puoi eliminare to_f e? operatore passando un valore iniziale per iniettare: arr.inject(0.0) { |sum,el| sum + el } / arr.size.
Dave Ray,

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

5
Non credo che ciò meriti l'aggiunta alla classe Array, poiché non è generalizzabile a tutti i tipi che possono contenere Array.
Sarah Mei,

8
@John: non è esattamente la conversione da Symbol # a_proc - fa parte injectdell'interfaccia, menzionata nella documentazione. L' to_procoperatore è &.
Chuck,

21
Se stai usando Rails, Array#injectè eccessivo qui. Basta usare #sum. Ad esempioarr.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 versione di questo che non utilizza instance_evalsarebbe:

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

4
Non penso che sia troppo intelligente. Penso che risolva il problema in modo idiomatico. Cioè, utilizza ridurre, che è esattamente corretto. I programmatori dovrebbero essere incoraggiati a capire cosa è corretto, perché è corretto e quindi propagarsi. Per un'operazione banale come media, vera, non è necessario essere "intelligenti". Ma capendo cosa significa "ridurre" per un caso banale, si può quindi iniziare ad applicarlo a problemi molto più complessi. upvote.
martedì

3
perché la necessità di instance_eval qui?
tybro0103,

10
instance_evalti consente di eseguire il codice specificando solo auna volta, quindi può essere concatenato con altri comandi. Vale a dire random_average = Array.new(10) { rand(10) }.instance_eval { reduce(:+) / size.to_f } invecerandom = Array.new(10) { rand(10) }; random_average = random.reduce(:+) / random.size
Benjamin Manns il

2
Non lo so, l'uso di instance_eval in questo modo sembra strano, e ha molti gotcha associati ad esso che rendono questo approccio una cattiva idea, IMO. (Ad esempio, se si provasse ad accedere a una variabile di istanza oa un metodo selfall'interno di quel blocco, si verificherebbero problemi.) instance_evalÈ più per metaprogrammazione o DSL.
Ajedi32,

1
@ Ajedi32 Sono d'accordo, non utilizzarlo nel codice dell'applicazione. È stato comunque molto bello poter incollare nel mio sostituto (:
animatedgif il

94

Credo che la risposta più semplice sia

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

1
Mi ci è voluto un momento per trovarlo - reduceè un metodo del Enumerablemixin usato da Array. E nonostante il suo nome, sono d'accordo con @ShuWu ... a meno che tu non stia usando Rails che implementa sum.
Tom Harrison,

Vedo delle soluzioni qui, che so che sembrano estremamente pulite, ma temo che se leggerò il mio codice in futuro, saranno incomprensibili. Grazie per la soluzione pulita!
atmosx,

Sul mio sistema questo è 3 volte più veloce della risposta accettata.
sergio

48

Speravo in Math.average (valori), ma nessuna tale fortuna.

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

3
Non mi ero reso conto che #sum fosse stato aggiunto da Rails! Grazie per la segnalazione.
Denny Abraham,

11
Dopo Natale 2016 (Ruby 2.4), Array avrà un summetodo, quindi questa sembra essere una risposta corretta dopo 6 anni, degna del premio Nostradamus.
steenslag,

38

Le versioni di Ruby> = 2.4 hanno una somma enumerabile # metodo .

E per ottenere una media in virgola mobile, puoi usare Integer # fdiv

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

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

Per le versioni precedenti:

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

9

Alcuni benchmarking delle migliori soluzioni (in ordine di efficienza):

Matrice 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)

Matrici piccole:

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)

Il tuo benchmark è un po 'sbagliato. benchmark / ips è in realtà migliore per questo tipo di confronti. Vorrei anche suggerire di utilizzare un array popolato in modo casuale con numeri negativi e positivi e float, per ottenere un risultato più realistico. Scoprirai che instance_eval è più lento di array.sum.fdiv. Di circa 8 volte per i galleggianti. e circa x1.12 per gli interi. Inoltre, diversi sistemi operativi daranno risultati diversi. sul mio mac alcuni di questi metodi sono 2 volte più lenti rispetto al mio Linux Droplet
konung

Anche il metodo sum usa la formula di Gauss, sugli intervalli invece di calcolare la somma.
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
Ciò restituisce valori errati a causa della divisione di numeri interi. Provalo con, ad esempio, [2,3] .mean, che restituisce 2 invece di 2.5.
John Feminella,

1
Perché un array vuoto dovrebbe avere una somma nilanziché 0?
Andrew Grimm,

1
Perché puoi ottenere la differenza tra [] e [0]. E penso che tutti coloro che vogliono un vero mezzo possono usare to_i o sostituire il precedente con uno 0
astropanico

4

Consentitemi di mettere in competizione qualcosa che risolva la divisione per zero problemi:

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 ammettere, tuttavia, che "provare" è un aiuto di Rails. Ma puoi risolverlo facilmente:

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

A proposito: penso che sia corretto che la media di una lista vuota sia nulla. La media di nulla è nulla, non 0. Quindi questo è il comportamento previsto. Tuttavia, se si passa a:

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

il risultato per gli array vuoti non sarà un'eccezione come mi aspettavo, ma invece restituisce NaN ... Non l'avevo mai visto prima in Ruby. ;-) Sembra essere un comportamento speciale della classe Float ...

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

4

cosa non mi piace della soluzione accettata

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

è che non funziona davvero in modo puramente funzionale. abbiamo bisogno di una variabile arr per calcolare la dimensione arr alla fine.

per risolverlo in modo puramente funzionale dobbiamo tenere traccia di due valori: la somma di tutti gli elementi e il numero di elementi.

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

Santhosh è migliorato su questa soluzione: invece che l'argomento r è un array, possiamo usare la destrutturazione per separarlo immediatamente in due variabili

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

se vuoi vedere come funziona, aggiungi alcuni put:

[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

Potremmo anche usare una struttura anziché una matrice per contenere la somma e il conteggio, ma poi dobbiamo prima dichiarare la struttura:

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

Questa è la prima volta che vedo end.methodusato nel rubino, grazie per questo!
Epigene,

L'array passato al metodo di iniezione può essere disperso. arr.inject([0.0,0]) { |(sum, size), el| [ sum + el, size + 1 ] }.inject(:/)
Santhosh,

@Santhosh: sì, è molto più leggibile! Non definirei questa "dispersione", la definirei "distruttiva" tony.pitluga.com/2011/08/08/destructuring-with-ruby.html
bjelli,

3

Per divertimento pubblico, l'ennesima soluzione:

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
Se questo fosse più alto nelle votazioni non lo avrei capito! Molto bene.
Matt Stevens,

Cancella è meglio che intelligente , questo pezzo di codice non è chiaro.
Sebastian Palma,

2

Non hai ruby ​​su questo pc, ma qualcosa in questa misura dovrebbe funzionare:

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

average = total/values.size

2

Inserisci Array#average .

Stavo facendo la stessa cosa abbastanza spesso, quindi ho pensato che fosse prudente estendere la Arraylezione semplicemente con un sempliceaverage metodo . Non funziona per nulla oltre a una matrice di numeri come numeri interi o float o decimali, ma è utile quando lo usi nel modo giusto.

Sto usando Ruby on Rails, quindi l'ho inserito config/initializers/array.rbma puoi posizionarlo ovunque incluso all'avvio, ecc.

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
Ciò restituirà valori errati a causa della divisione di numeri interi. Ad esempio, se a è [2, 3], il risultato atteso è 2,5, ma restituirai 2.
John Feminella,

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

Risolve la divisione per zero, la divisione intera ed è facile da leggere. Può essere facilmente modificato se si sceglie di avere un array vuoto restituito 0.

Mi piace anche questa variante, ma è un po 'più prolissa.

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

Questo metodo può essere utile.

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
Benvenuto nello stack di overflow qui Il poster originale della domanda vuole la risposta come 3.375 e la tua soluzione dà 3. i, e 27/8 = 3.
Ajay Barot

Grazie per i vostri commenti. So che il Poster originale della domanda vuole una risposta come 3.375 e questo è ciò che fa questo metodo dato che ho dato alla variabile 'var' un valore float (cioè 0,0). Munim Munna Devo essere d'accordo con te, c'è davvero una risposta simile.
Kishor Budhathoki il

0

Senza dover ripetere l'array (ad es. Perfetto per una linea):

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

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

Breve ma con variabile d'istanza


2
Farei a_size = nil; [1,2].tap { |a| a_size = a.size }.inject(:+).to_f/a_sizepiuttosto che creare una variabile di istanza.
Andrew Grimm,

-1

Puoi provare qualcosa di simile al seguente:

a = [1,2,3,4,5]
# => [1, 2, 3, 4, 5]
(a.sum/a.length).to_f
# => 3.0
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.