Calculate weighted average with Java 8 streams
You can use this procedure to calculate the weighted average of a map. Note that the key of the map entry should contain the value and the value of the map entry should contain the weight.
/**
* Calculates the weighted average of a map.
*
* @throws ArithmeticException If divide by zero happens
* @param map A map of values and weights
* @return The weighted average of the map
*/
static Double calculateWeightedAverage(Map<Double, Integer> map) throws ArithmeticException {
double num = 0;
double denom = 0;
for (Map.Entry<Double, Integer> entry : map.entrySet()) {
num += entry.getKey() * entry.getValue();
denom += entry.getValue();
}
return num / denom;
}
You can look at its unit test to see a usecase.
/**
* Tests our method to calculate the weighted average.
*/
@Test
public void testAveragingWeighted() {
Map<Double, Integer> map = new HashMap<>();
map.put(0.7, 100);
map.put(0.5, 200);
Double weightedAverage = calculateWeightedAverage(map);
Assert.assertTrue(weightedAverage.equals(0.5666666666666667));
}
You need these imports for the unit tests:
import org.junit.Assert;
import org.junit.Test;
You need these imports for the code:
import java.util.HashMap;
import java.util.Map;
I hope it helps.
You can create your own collector for this task:
static <T> Collector<T,?,Double> averagingWeighted(ToDoubleFunction<T> valueFunction, ToIntFunction<T> weightFunction) {
class Box {
double num = 0;
long denom = 0;
}
return Collector.of(
Box::new,
(b, e) -> {
b.num += valueFunction.applyAsDouble(e) * weightFunction.applyAsInt(e);
b.denom += weightFunction.applyAsInt(e);
},
(b1, b2) -> { b1.num += b2.num; b1.denom += b2.denom; return b1; },
b -> b.num / b.denom
);
}
This custom collector takes two functions as parameter: one is a function returning the value to use for a given stream element (as a ToDoubleFunction
), and the other returns the weight (as a ToIntFunction
). It uses a helper local class storing the numerator and denominator during the collecting process. Each time an entry is accepted, the numerator is increased with the result of multiplying the value with its weight, and the denominator is increased with the weight. The finisher then returns the division of the two as a Double
.
A sample usage would be:
Map<Double,Integer> map = new HashMap<>();
map.put(0.7, 100);
map.put(0.5, 200);
double weightedAverage =
map.entrySet().stream().collect(averagingWeighted(Map.Entry::getKey, Map.Entry::getValue));