madhub
madhub

Reputation: 161

How to compute average of multiple numbers in sequence using Java 8 lambda

If I have collections Point , how do I compute average of x,y using Java 8 stream on a single iteration.

Following example creates two stream & iterates twice on the input collection to compute the average of x & y. Is their any way to computer average x,y on single iteration using java 8 lambda :

List<Point2D.Float> points = 
Arrays.asList(new Point2D.Float(10.0f,11.0f), new Point2D.Float(1.0f,2.9f));
// java 8, iterates twice
double xAvg = points.stream().mapToDouble( p -> p.x).average().getAsDouble();
double yAvg = points.stream().mapToDouble( p -> p.y).average().getAsDouble();

Upvotes: 12

Views: 9690

Answers (7)

James Mudd
James Mudd

Reputation: 2373

Just an update since Java 12 there is quite a nice solution to this using teeing collector. The code would look like this

import java.awt.geom.Point2D;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

class Scratch {
    public static void main(String[] args) {
        List<Point2D.Double> points = Arrays.asList(
                new Point2D.Double(10.0,11.0),
                new Point2D.Double(1.0,2.9)
        );

        Point2D.Double averagePoint = points.stream()
                .collect(Collectors.teeing(
                        Collectors.averagingDouble(point -> point.getX()),
                        Collectors.averagingDouble(point -> point.getY()),
                        (avgX, avgY) -> new Point2D.Double(avgX, avgY)
                        ));

        System.out.println(averagePoint);
    }
}

and the output will be Point2D.Double[5.5, 6.95]

Upvotes: 2

Brian Goetz
Brian Goetz

Reputation: 95346

Write a trivial collector. Look at the implementation of the averagingInt collector (from Collectors.java):

public static <T> Collector<T, ?, Double>
averagingInt(ToIntFunction<? super T> mapper) {
    return new CollectorImpl<>(
            () -> new long[2],
            (a, t) -> { a[0] += mapper.applyAsInt(t); a[1]++; },
            (a, b) -> { a[0] += b[0]; a[1] += b[1]; return a; },
            a -> (a[1] == 0) ? 0.0d : (double) a[0] / a[1], CH_NOID);
}

This can be easily adapted to sum along two axes instead of one (in a single pass), and return the result in some simple holder:

AverageHolder h = streamOfPoints.collect(averagingPoints());

Upvotes: 8

Hamed Moghaddam
Hamed Moghaddam

Reputation: 559

Here is the simplest solution. You add up all values of x and y using "add" method of Point2D and then use "multiply" method to get average. Code should be like this

    int size = points.size();
    if (size != 0){
        Point2D center = points.parallelStream()
                        .map(Body::getLocation)
                        .reduce( new Point2D(0, 0), (a, b) -> a.add(b) )
                        .multiply( (double) 1/size );
        return center;    
    }

Upvotes: 1

Lukas Eder
Lukas Eder

Reputation: 220832

If you don't mind using an additional library, we've added support for tuple collectors to jOOλ, recently.

Tuple2<Double, Double> avg = points.stream().collect(
    Tuple.collectors(
        Collectors.averagingDouble(p -> p.x),
        Collectors.averagingDouble(p -> p.y)
    )
);

In the above code, Tuple.collectors() combines several java.util.stream.Collector instances into a single Collector that collects individual values into a Tuple.

This is much more concise and reusable than any other solution. The price you'll pay is that this currently operates on wrapper types, instead of primitive double. I guess we'll have to wait until Java 10 and project valhalla for primitive type specialisation in generics.

In case you want to roll your own, instead of creating a dependency, the relevant method looks like this:

static <T, A1, A2, D1, D2> Collector<T, Tuple2<A1, A2>, Tuple2<D1, D2>> collectors(
    Collector<T, A1, D1> collector1
  , Collector<T, A2, D2> collector2
) {
    return Collector.of(
        () -> tuple(
            collector1.supplier().get()
          , collector2.supplier().get()
        ),
        (a, t) -> {
            collector1.accumulator().accept(a.v1, t);
            collector2.accumulator().accept(a.v2, t);
        },
        (a1, a2) -> tuple(
            collector1.combiner().apply(a1.v1, a2.v1)
          , collector2.combiner().apply(a1.v2, a2.v2)
        ),
        a -> tuple(
            collector1.finisher().apply(a.v1)
          , collector2.finisher().apply(a.v2)
        )
    );
}

Where Tuple2 is just a simple wrapper for two values. You might as well use AbstractMap.SimpleImmutableEntry or something similar.

I've also detailed this technique in an answer to another Stack Overflow question.

Upvotes: 8

Daniel Dietrich
Daniel Dietrich

Reputation: 2272

With the current 1.2.0 snapshot of Javaslang you may write

import javaslang.collection.List;

List.of(points)
        .unzip(p -> Tuple.of(p.x, p.y))
        .map((l1, l2) -> Tuple.of(l1.average(), l2.average())));

Unfortunately Java 1.8.0_31 has a compiler bug which does not compile it :'(

You get a Tuple2 avgs which contains the computed values:

double xAvg = avgs._1;
double yAvg = avgs._2;

Here is the general behavior of average():

// = 2
List.of(1, 2, 3, 4).average();

// = 2.5
List.of(1.0, 2.0, 3.0, 4.0).average();

// = BigDecimal("0.5")
List.of(BigDecimal.ZERO, BigDecimal.ONE).average();

// = UnsupportedOpertationException("average of nothing")
List.nil().average();

// = UnsupportedOpertationException("not numeric")
List.of("1", "2", "3").average();

// works well with java.util collections
final java.util.Set<Integer> set = new java.util.HashSet<>();
set.add(1);
set.add(2);
set.add(3);
set.add(4);
List.of(set).average(); // = 2

Upvotes: 2

a better oliver
a better oliver

Reputation: 26828

avarage() is a reduction operation, so on generic streams you would use reduce(). The problem is that it doesn't offer a finishing operation. If you want to calculate the average by first summing up all values and then dividing them by their count it gets a bit trickier.

List<Point2D.Float> points = 
        Arrays.asList(new Point2D.Float(10.0f,11.0f), new Point2D.Float(1.0f,2.9f));
int counter[] = {1};

Point2D.Float average = points.stream().reduce((avg, point) -> {
                         avg.x += point.x;
                         avg.y += point.y;

                         ++counter[0];

                        if (counter[0] == points.size()) {
                          avg.x /= points.size();
                          avg.y /= points.size();
                        }

                       return avg;
                     }).get();

Some notes: counter[]has to be an array because variables used by lambdas have to be effectively final, so we can't use a simple int.

This version of reduce() returns an Optional, so we have to use get() to get the value. If the stream can be empty then get() would throw an exception obviously, but we can use the Optional to our advantage then.

I'm not entirely sure if this works with parallel streams.

You could also do the following. It's probably less accurate but may be better suited if you have a lot of really, really large numbers:

double factor = 1.0 / points.size();
Point2D.Float average = points.stream().reduce(new Point2D.Float(0.0f,0.0f),
                         (avg, point) -> {
                             avg.x += point.x * factor;
                             avg.y += point.y * factor;
                             return avg;
                         });

On the other hand, if accuracy was a big concern you wouldn't use float anyway ;)

Upvotes: 0

Alexis C.
Alexis C.

Reputation: 93842

One way would be to define a class that aggregates the points' x and y values.

public class AggregatePoints {

    private long count = 0L;
    private double sumX = 0;
    private double sumY = 0;

    public double averageX() { 
        return sumX / count; 
    }

    public double averageY() { 
        return sumY / count; 
    }

    public void merge(AggregatePoints other) {
      count += other.count;
      sumX += other.sumX;
      sumY += other.sumY;
    }

    public void add(Point2D.Float point) {
      count += 1;
      sumX += point.getX();
      sumY += point.getY();
    }
}

Then you just collect the Stream into a new instance:

 AggregatePoints agg = points.stream().collect(AggregatePoints::new,
                                               AggregatePoints::add,
                                               AggregatePoints::merge);
 double xAvg = agg.averageX();
 double yAvg = agg.averageY();

Alhough iterating two times on the list is a simple solution. I would do it unless I really have a performance problem.

Upvotes: 4

Related Questions