Calculate a weighted average

If I have an enumerable:

[%{rate: 0.10, amount: 100}, %{rate: 0.12, amount: 50}, %{rate: 0.15, amount: 200}]

How would I go about calculating a weighted average using Enum strictly?

I can use Enum.map_reduce/3 to calculate the numerator, but since that returns a tuple I can’t pass it to another Enum function.

1 Like

This is the most idiomatic I could come up with

{enum, sumprod} = 
  Enum.map_reduce(enum, 0, fn %{rate: r, amount: a} = e, acc -> 
    {e, (r * a) + acc} 
  end)

sum = Enum.reduce(enum, 0, fn %{amount: a}, acc -> a + acc end)

sumprod / sum

As I understand the documentation of Enum.map_reduce/3, you are not changing the input list, so you could use reduce/3 directly:

sumprod = Enum.reduce(enum, 0, fn %{rate: r, amount: a}, acc -> (r * a) + acc end)

We then can do the remainder of the calculation as you did already. But that does require a second pass over the input list. We can do it in a single one:

{sumprod, sum} = Enum.reduce(enum, {0, 0}, fn %{rate: r, amount: a}, {sumprod, sum} -> {(r * a) + sumprod, a + sum} end)
avg = sumprod / sum

There is currently (and probably never will be) no reduce variant that does a finalizing operation when the end of the Enumerable is reached.

Such finalizings have to happen outside of the Enum, and in my opinion somehat destroy pipeability, but to be honest, its darn easy to read and understand.

3 Likes

It’s not to bad to make the computation run as part of the pipeline.

enum
|> Enum.reduce({0, 0}, fn %{rate: r, amount: a}, {sumprod, sum} -> {(r * a) + sumprod, a + sum} end)
|> (fn {sumprod, sum} -> sumprod / sum end).()

or even

defmodule Helper do
  def pipe(value, fun) do
    apply fun, [value]
  end
end

enum
|> Enum.reduce({0, 0}, fn %{rate: r, amount: a}, {sumprod, sum} -> {(r * a) + sumprod, a + sum} end)
|> Helper.pipe(fn {sumprod, sum} -> sumprod / sum end)
1 Like

I do use dedicated helpers only in the pipe, if it were at the end I tend to bind the result of the pipe to a variable and then use the variable in the returning expression. But I think this is a matter of taste.

Especially after piping into anonymous functions or captures is a “bit” ugly :wink:

This single pass is about the best you can do. Thanks!

Some other options to consider:

enum
|> Enum.reduce({0,0}, fn %{rate: r, amount: a}, {avg, sum} -> {(r * a + avg * sum) / (a + sum), a + sum} end)
|> elem(0)
(Enum.reduce(enum, fn %{rate: r, amount: a}, %{rate: avg, amount: sum} -> %{ rate: (r * a + avg * sum) / (a + sum), amount: a + sum} end)).rate

The first maintains a tuple that includes the current weighted average after every iteration, so no final calculation is needed.

The second does the same, but using a map. This is a bit more verbose, but cleaner access to the final result. This version does not require an initial accumulator value, as it can just start with the value of the first element.

You should haver mentioned, that your implementations will probably be much slower because of the additional computations made for each element in the list.