Comment créer une moyenne à partir d'un tableau Ruby?

209

Comment trouver une moyenne à partir d'un tableau?

Si j'ai le tableau:

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

La moyenne me donnerait 3,375.

dingue
la source
11
Si vous obtenez 21,75 comme moyenne de ces chiffres, quelque chose ne va pas ...
ceejayoz
2
dotty, je ne sais pas comment vous avez obtenu 21,75 mais la moyenne / moyenne pour cet ensemble de données est 3,375 et la somme est 27. je ne sais pas quel type de fonction d'agrégation donnerait 21,75. Veuillez vérifier et assurez-vous que la moyenne est vraiment ce que vous recherchez!
Paul Sasik
2
Je n'ai aucune idée d'où j'ai obtenu 21,75. Doit avoir pressé quelque chose comme 0 + 48 + 2 + 5 + 0 + 2 + 6 sur la calculatrice!
dotty
16
Comme il s'agit également de ruby-on-rails, les calculs d'enregistrements actifs méritent d'être examinés si vous faites la moyenne d'un tableau ActiveRecord. Person.average (: age,: country => 'Brazil') renvoie l'âge moyen des Brésiliens. Plutôt cool!
Kyle Heironimus

Réponses:

260

Essaye ça:

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

Notez le .to_f, que vous voudrez pour éviter tout problème de division entière. Vous pouvez également faire:

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

Vous pouvez le définir comme faisant partie d' Arrayun autre commentateur l'a suggéré, mais vous devez éviter la division entière ou vos résultats seront erronés. En outre, cela n'est généralement pas applicable à tous les types d'éléments possibles (évidemment, une moyenne n'a de sens que pour les choses qui peuvent être moyennées). Mais si vous voulez suivre cette voie, utilisez ceci:

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

  def mean 
    sum / size
  end
end

Si vous ne l'avez pas vu injectauparavant, ce n'est pas aussi magique qu'il y paraît. Il itère sur chaque élément et lui applique ensuite une valeur d'accumulateur. L'accumulateur est ensuite remis à l'élément suivant. Dans ce cas, notre accumulateur est simplement un entier qui reflète la somme de tous les éléments précédents.

Edit: le commentateur Dave Ray a proposé une belle amélioration.

Edit: La proposition du commentateur Glenn Jackman, en utilisant arr.inject(:+).to_f, est bien aussi mais peut-être un peu trop intelligente si vous ne savez pas ce qui se passe. Le :+est un symbole; lorsqu'il est passé à l'injection, il applique la méthode nommée par le symbole (dans ce cas, l'opération d'addition) à chaque élément par rapport à la valeur de l'accumulateur.

John Feminella
la source
6
Vous pouvez éliminer to_f et? opérateur en faisant passer une valeur initiale à injecter: arr.inject(0.0) { |sum,el| sum + el } / arr.size.
Dave Ray
103
Ou: arr.inject (: +). To_f / arr.size # => 3.375
glenn jackman
5
Je ne pense pas que cela justifie d'ajouter à la classe Array, car il n'est pas généralisable à tous les types que les tableaux peuvent contenir.
Sarah Mei
8
@John: Ce n'est pas exactement la conversion Symbol # to_proc - cela fait partie de l' injectinterface, mentionnée dans la documentation. L' to_procopérateur est &.
Chuck
21
Si vous utilisez Rails, Array#injectc'est exagéré ici. Utilisez simplement #sum. Par exemplearr.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

Une version de ceci qui n'utilise pas instance_evalserait:

a = [0,4,8,2,5,0,2,6]
a.reduce(:+) / a.size.to_f #=> 3.375
Ruisseau Corban
la source
4
Je ne pense pas que ce soit trop intelligent. Je pense que cela résout idiomatiquement le problème. C'est à dire, il utilise réduire, ce qui est exactement correct. Les programmeurs doivent être encouragés à comprendre ce qui est correct, pourquoi c'est correct, puis à se propager. Pour une opération banale comme la moyenne, c'est vrai, il n'est pas nécessaire d'être "intelligent". Mais en comprenant ce que "réduire" est pour un cas trivial, on peut alors commencer à l'appliquer à des problèmes beaucoup plus complexes. upvote.
pduey
3
pourquoi le besoin d'instance_eval ici?
tybro0103
10
instance_evalvous permet d'exécuter le code en ne spécifiant aqu'une seule fois, afin qu'il puisse être enchaîné avec d'autres commandes. C'est-à-dire random_average = Array.new(10) { rand(10) }.instance_eval { reduce(:+) / size.to_f } au lieu derandom = Array.new(10) { rand(10) }; random_average = random.reduce(:+) / random.size
Benjamin Manns
2
Je ne sais pas, utiliser instance_eval de cette façon semble juste bizarre, et il y a beaucoup de pièges associés qui font de cette approche une mauvaise idée, OMI. (Par exemple, si vous avez essayé d'accéder à une variable d'instance ou à une méthode à l' selfintérieur de ce bloc, vous rencontrez des problèmes.) instance_evalEst plus pour la métaprogrammation ou DSL.
Ajedi32
1
@ Ajedi32 Je suis d'accord, ne l'utilisez pas dans votre code d'application. C'était cependant très agréable de pouvoir coller dans ma réplique (:
animatedgif
94

Je crois que la réponse la plus simple est

list.reduce(:+).to_f / list.size
Shu Wu
la source
1
Il m'a fallu un moment pour le trouver - reduceest une méthode de Enumerablemixage utilisée par Array. Et malgré son nom, je suis d'accord avec le @ShuWu ... sauf si vous utilisez Rails qui implémente sum.
Tom Harrison
Je vois des solutions ici, que je connais, qui semblent extrêmement soignées, mais je crains que si je lis mon code à l'avenir, elles aimeront le charabia. Merci pour la solution propre!
atmosx
Sur mon système, c'est 3 fois plus rapide que la réponse acceptée.
sergio
48

J'espérais pour Math.average (valeurs), mais pas de chance.

values = [0,4,8,2,5,0,2,6]
average = values.sum / values.size.to_f
Denny Abraham
la source
3
Je ne savais pas que #sum avait été ajouté par Rails! Merci d'avoir fait remarquer cela.
Denny Abraham
11
Après Noël 2016 (Ruby 2.4), Array aura une summéthode, donc cela semble être une réponse correcte après 6 ans, digne du prix Nostradamus.
steenslag
38

Les versions Ruby> = 2.4 ont une méthode de somme Enumerable # .

Et pour obtenir une moyenne à virgule flottante, vous pouvez utiliser Integer # fdiv

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

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

Pour les anciennes versions:

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

Quelques analyses comparatives des meilleures solutions (dans l'ordre des plus efficaces):

Grand tableau:

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)

Petits tableaux:

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
la source
Votre indice de référence est un peu faux. benchmark / ips est en fait meilleur pour ce genre de comparaisons. Je suggère également d'utiliser un tableau rempli aléatoirement avec des nombres négatifs et positifs ainsi que des flottants, pour obtenir un résultat plus réaliste. Vous constaterez que instance_eval est plus lent que array.sum.fdiv. Par environ 8x pour les flotteurs. et environ x1.12 pour les entiers. En outre, différents systèmes d'exploitation donneront des résultats différents. sur mon mac, certaines de ces méthodes sont 2 fois plus lentes que sur mon Linux Droplet
konung
La méthode de somme utilise également la formule de Gauss, sur des plages au lieu de calculer la somme.
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
astropanic
la source
2
Cela renvoie des valeurs incorrectes en raison de la division entière. Essayez-le avec, par exemple, [2,3] .mean, qui renvoie 2 au lieu de 2,5.
John Feminella
1
Pourquoi un tableau vide devrait-il avoir une somme de nilplutôt que 0?
Andrew Grimm
1
Parce que vous pouvez obtenir la différence entre [] et [0]. Et je pense que tous ceux qui veulent une vraie moyenne peuvent utiliser to_i ou remplacer le zéro ci-dessus par un 0
astropanic
4

Permettez-moi de mettre quelque chose en compétition qui résout le problème de la division par zéro:

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

Je dois admettre, cependant, que «essayer» est un assistant Rails. Mais vous pouvez facilement résoudre ce problème:

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

BTW: Je pense qu'il est exact que la moyenne d'une liste vide est nulle. La moyenne de rien n'est rien, pas 0. C'est donc le comportement attendu. Cependant, si vous passez à:

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

le résultat pour les tableaux vides ne sera pas une exception comme je m'y attendais, mais à la place, il renvoie NaN ... Je n'ai jamais vu cela auparavant dans Ruby. ;-) Semble être un comportement spécial de la classe Float ...

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

ce que je n'aime pas dans la solution acceptée

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

c'est qu'il ne fonctionne pas vraiment de manière purement fonctionnelle. nous avons besoin d'une variable arr pour calculer arr.size à la fin.

pour résoudre ce problème de manière purement fonctionnelle, nous devons garder une trace de deux valeurs: la somme de tous les éléments et le nombre d'éléments.

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

Santhosh a amélioré cette solution: au lieu que l'argument r soit un tableau, nous pourrions utiliser la déstructuration pour le séparer immédiatement en deux variables

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

si vous voulez voir comment ça marche, ajoutez quelques 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

Nous pourrions également utiliser une structure au lieu d'un tableau pour contenir la somme et le nombre, mais nous devons ensuite déclarer la structure en premier:

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
la source
C'est la première fois que je la vois end.methodutilisée en rubis, merci pour ça!
Epigene
La matrice passée pour injecter la méthode peut être dispersée. arr.inject([0.0,0]) { |(sum, size), el| [ sum + el, size + 1 ] }.inject(:/)
Santhosh
@Santhosh: oui, c'est beaucoup plus lisible! Je n'appellerais pas cela "dispersant" cependant, je l'appellerais "déstructurant" tony.pitluga.com/2011/08/08/destructuring-with-ruby.html
bjelli
3

Pour le plaisir du public, encore une autre solution:

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
la source
1
Si c'était plus haut dans le vote, je ne l'aurais pas compris! Très bien.
Matt Stevens
Clair est mieux qu'intelligent , ce morceau de code n'est pas clair.
Sebastian Palma
2

Ne pas avoir de rubis sur ce PC, mais quelque chose dans cette mesure devrait fonctionner:

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

average = total/values.size
saret
la source
2

Ajouter Array#average.

Je faisais la même chose assez souvent, donc j'ai pensé qu'il était prudent de simplement étendre la Arrayclasse avec une averageméthode simple . Cela ne fonctionne pas pour autre chose qu'un tableau de nombres comme des entiers ou des flottants ou des décimales, mais c'est pratique lorsque vous l'utilisez correctement.

J'utilise Ruby on Rails, donc je l'ai placé config/initializers/array.rbmais vous pouvez le placer n'importe où qui est inclus au démarrage, 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
la source
1
a = [0,4,8,2,5,0,2,6]
sum = 0
a.each { |b| sum += b }
average = sum / a.length
erik
la source
4
Cela renverra des valeurs incorrectes en raison de la division entière. Par exemple, si a est [2, 3], le résultat attendu est 2,5, mais vous en retournerez 2.
John Feminella
1
a = [0,4,8,2,5,0,2,6]
a.empty? ? nil : a.reduce(:+)/a.size.to_f
=> 3.375

Résout la division par zéro, division entière et est facile à lire. Peut être facilement modifié si vous choisissez qu'un tableau vide renvoie 0.

J'aime aussi cette variante, mais elle est un peu plus verbeuse.

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

Cette méthode peut être 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])
Kishor Budhathoki
la source
1
Bienvenue sur stack overflow here L'affiche originale de la question veut que la réponse soit 3,375 et votre solution donne 3. i, e 27/8 = 3.
Ajay Barot
Merci pour vos commentaires. Je sais que l'affiche originale de la question veut une réponse de 3,375 et c'est ce que fait cette méthode car j'ai donné à la variable 'var' une valeur flottante (c'est-à-dire 0,0). Munim Munna Je dois être d'accord avec vous, il y a en effet un ans similaire.
Kishor Budhathoki
0

Sans avoir à répéter le tableau (par exemple parfait pour les lignes simples):

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

Courte mais utilisant une variable d'instance

Alex Leschenko
la source
2
Je ferais a_size = nil; [1,2].tap { |a| a_size = a.size }.inject(:+).to_f/a_sizeplutôt que de créer une variable d'instance.
Andrew Grimm
-1

Vous pouvez essayer quelque chose comme ceci:

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