Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Find element(s) closest to average of array

What would be a 'ruby' way to do the following; I'm still thinking in more imperative style programming and not really adapting to thinking in ruby. What I want to do is find the closest element in size to the average of an array, for example, consider the following array

[1,2,3] 

The average is 2.0. The method I want to write returns the element closest to the average from above and below it, in this case 1 and 3.

Another example will illustrate this better:

[10,20,50,33,22] avg is 27.0 method would return 22 and 33.
like image 259
macalaca Avatar asked Jan 12 '23 02:01

macalaca


2 Answers

This is not the most efficient, but it is (in my humble opinion) rather Ruby-esque.

class Array
  # Return the single element in the array closest to the average value
  def closest_to_average
    avg = inject(0.0,:+) / length
    min_by{ |v| (v-avg).abs }
  end
end

[1,2,3].closest_to_average
#=> 2 

[10,20,50,33,22].closest_to_average
#=> 22 

If you really want the n closest items, then:

class Array
  # Return a number of elements in the array closest to the average value
  def closest_to_average(results=1)
    avg = inject(0.0,:+) / length
    sort_by{ |v| (v-avg).abs }[0,results]
  end
end

[10,20,50,33,22].closest_to_average     #=> [22] 
[10,20,50,33,22].closest_to_average(2)  #=> [22, 33] 
[10,20,50,33,22].closest_to_average(3)  #=> [22, 33, 20] 

How this Works

avg = inject(0.0,:+) / length
is shorthand for:
avg = self.inject(0.0){ |sum,n| sum+n } / self.length
I start off with a value of 0.0 instead of 0 to ensure that the sum will be a floating point number, so that dividing by the length does not give me an integer-rounded value.

sort_by{ |v| (v-avg).abs }
sorts the array based on the difference between the number and average (lowest to highest), and then:
[0,results]
selects the first results number of entries from that array.

like image 191
Phrogz Avatar answered Jan 18 '23 21:01

Phrogz


I assume that what is desired is the largest element of the array that is smaller than the average and the smallest value of the array that is larger than the average. Such values exist if and only if the array has at least two elements and they are not all the same. Assuming that condition applies, we need only convert it from words to symbols:

avg = a.reduce(:+)/a.size.to_f
[ a.select { |e| e < avg }.max, a.select { |e| e > avg }.min ]

Another way, somewhat less efficient:

avg = a.reduce(:+)/a.size.to_f
b = (a + [avg]).uniq.sort
i = b.index(avg)
[ b[i-1], b[i+1] ]
like image 34
Cary Swoveland Avatar answered Jan 18 '23 21:01

Cary Swoveland