Vivek Sethi Vivek Sethi - 1 month ago 12
Java Question

Calculate weighted average with Java 8 streams

How do I go about calculating weighted mean of a

Map<Double, Integer>
where the Integer value is the weight for the Double value to be averaged.
eg: Map has following elements:


  1. (0.7, 100) // value is 0.7 and weight is 100

  2. (0.5, 200)

  3. (0.3, 300)

  4. (0.0, 400)



I am looking to apply the following formula using Java 8 streams, but unsure how to calculate the numerator and denominator together and preserve it at the same time. How to use reduction here?

enter image description here

Answer

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