Grouping doubles to map to create histogram - java

Let`s assume, that we have List of samples (type Double) {1.5, 1.1, 2.2, 1.0, 2.2, 3.3}. How can I achieve a Map that holds Integer as key (groups) and number of occurrences as a Value of this Map?
For given example 1 -> 3, 2 -> 2, 3 -> 1. I know I can achieve this with if/else if or case logic but in my app it would be 30 different groups (30 cases or elseifs in code).
App is creating wind histogram from given wind speed pulled from DB (millions of samples).
My approach:
Map<Double, Long> map = windData.stream()
.collect(Collectors.groupingBy(WindData::getSpeed, Collectors.counting()));
Where windData is list holding wind speeds, and getSpeed is retrieving wind speed value. This approach generates groups of doubles, which means that it only count occurrences and I would like to get 0-1, 1-2, 2-3, 3-4 etc. groups.

Just round the double down to an int in the first parameter to groupingBy. Your question first says that List of samples contains Doubles but your code snippet suggests that it is actually a List<WindData>. Which is it?
import static java.util.stream.Collectors.counting;
import static java.util.stream.Collectors.groupingBy;
....
List<Double> speeds = ...;
Map<Integer, Long> histogram = speeds.stream()
.collect(groupingBy(Double::intValue, counting()));
On a separate note, unless you are pulling this data from the database anyway for some other reason, consider doing this on the database side with SQL group by

This is a pipeline producing "histogram" data from a stream of double values:
HashMap<Double, Long> res =
DoubleStream.of(1.5, 1.1, 2.2, 1.0, 2.2, 3.3)
.collect(HashMap<Double, Long>::new,
(map, dbl) -> map.merge(Math.floor(dbl), 1L, (k, v) -> k + v),
(map1, map2) -> map1.putAll(map2));
Printing res outputs {2.0=2, 1.0=3, 3.0=1}.
To apply this to your object stream, I'd just convert it to a double stream using mapToDouble:
windData.stream().mapToDouble(WindData::getSpeed)
.collect(HashMap<Double, Long>::new,
(map, dbl) -> map.merge(Math.floor(dbl), 1L, (k, v) -> k + v),
(map1, map2) -> map1.putAll(map2));
Note about the result: The keys of the map are the lower boundary of the interval (0 is 0-1 and 1 is 1-2)

Related

Optimal way to transfer values from a Map<K, V<List>> to a Map<otherKey, otherValue<List>>

Heres what i got to work with:
Map<Faction, List<Resource>>
Faction is the enum String value of a player faction (e.g "rr" for team red, "bb" for team blue)
Resources is String enum value of a Resource e.g "Wool", "Lumber"
So the list looks like this right now:
("rr", "wool")
("rr", "wool")
("rr", "lumber")
("bb", "wool")
So my goal is to have a Map<Resource, Integer>
where Resource is String name of a Resource from an enum
Integer represents the amount of Resource Cards
Example of target contained values: ("Wool", 4), ("Grain", 3), ("Lumber", 2)
So I'm trying to do this (in pseudocode):
extract all resources belonging to Faction "rr" and put them into a map <Resources, Integer> where each resource type should be represented once and Integer represents the sum of the amount of Resource cards
--> repeat this step for 3 more player Faction
I've played around with streams and foreach loops but have produced no valuable code yet because I struggle yet in the conception phase.
It seems that actual input data in Map<Faction, List<Resource>> look like:
{rr=[wool, wool, lumber], bb=[lumber, wool, grain]}
Assuming that appropriate enums are used for Resource and Faction, the map of resources to their amount can be retrieved using flatMap for the values in the input map:
Map<Faction, List<Resource>> input; // some input data
Map<Resource, Integer> result = input
.values() // Collection<List<Resource>>
.stream() // Stream<List<Resource>>
.flatMap(List::stream) // Stream<Resource>
.collect(Collectors.groupingBy(
resource -> resource,
LinkedHashMap::new, // optional map supplier to keep insertion order
Collectors.summingInt(resource -> 1)
));
or Collectors.toMap may be applied:
...
.collect(Collectors.toMap(
resource -> resource,
resource -> 1,
Integer::sum, // merge function to summarize amounts
LinkedHashMap::new // optional map supplier to keep insertion order
));

How to create a map with sum of all specific objects?

I have an object:
class Sample
{
String currency;
String amount;
}
I want an output map with sum of all objects of a specific currency.
Example-
INR 40
USD 80
EUR 20
Pointer. I have been working with the following code but its not working as I expected. You can use this code as reference:
Map<String, Double> finalResult = samples
.stream()
.collect(Collectors.toMap(
Sample::getCurrency,
e -> e.getAmount()
.stream()
.sum()));
You need to use groupingBy with summingDouble and not toMap:
Map<String, Double> finalResult = samples
.stream()
.collect(Collectors.groupingBy(
Sample::getCurrency,
Collectors.summingDouble(Sample::getAmount)
));
I consider amount is of type Double and your example is not correct (String amount;)
Just wanted to offer the following alternative for future consideration. I am also presuming you meant double for the amount as well as having the appropriate getters. You could have used Collectors.toMap as long as you included a merge function. It looks like you had the right idea but the wrong syntax.
List<Sample> samples =
List.of(new Sample("USD", 10), new Sample("USD", 30),
new Sample("INR", 40), new Sample("INR", 90),
new Sample("EUR", 20), new Sample("EUR", 50));
The third argument to toMap is a merge function to properly handle duplicates. In this cases it computes a sum of the values for each key as they are encountered.
Map<String, Double> result = samples.stream()
.collect(Collectors.toMap(
Sample::getCurrency, Sample::getAmount, Double::sum));
result.entrySet().forEach(System.out::println);
prints
EUR=70.0
USD=40.0
INR=130.0
If you really want your class to store the amount as a String, you can replace Sample::getAmount with sample->Double.valueOf(sample.getAmount()) to convert the String to a Double. But since you will probably be using these values repeatedly in computations, it is best, imo, to store the amount in its most frequently used form when the class instances are created.
Map<String, Double> finalResult = samples.stream().collect(
Collectors.groupingBy(
(Sample::getCurrency),
Collectors.summingDouble(
sample -> { return Double.parseDouble(sample.getAmount());
})));

Summing values of an object by first filtering other unique values of the same object

Let's say I have a list of an object lets call this object Order which has a quantity and price as fields.
for example as below:
Object Order (fields quantity and price) list contains the below values:
Quantity Price
5 200
6 100
3 200
1 300
Now I want to use Java-8 to fetch this list filtered in below way:
Quantity Price
8 200
6 100
1 300
Price being the unique value to filter from and summing any quantity that the price has, I want to form a new list based on this.
Please suggest how i can do this with java 8 lambda expression, thanks.
The following Stream does the trick:
List<Order> o = orders.stream().collect(
Collectors.collectingAndThen(
Collectors.groupingBy(Order::getPrice,Collectors.summingInt(Order::getQuantity)),
map -> map.entrySet().stream()
.map(e -> new Order(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
Let's break this down:
The following code returns a Map<Integer, Integer> which contains price as a key (the unique value you want to base the summing on) and its summed quantities. The key method is Collectors.groupingBy with classifier describing the key and the downstream defining a value, which would be a sum of quantities, hence Collectors.summingInt (depends on the quantity type):
Map<Integer, Integer> map = orders.stream().collect(
Collectors.groupingBy( // I want a Map<Integer, Integer>
Order::getPrice, // price is the key
Collectors.summingInt(Order::getQuantity)) // sum of quantities is the value
The desired structure is List<Order>, therefore you want to use the Collectors.collectingAndThen method with a Collector<T, A, R> downstream and Function<R, RR> finisher. The downstream is the grouping from the first point, the finisher will be a conversion of Map<Integer, Integer> back to List<Order>:
List<Order> o = orders.stream().collect(
Collectors.collectingAndThen(
grouping, // you know this one ;)
map -> map.entrySet()
.stream() // iterate entries
.map(e -> new Order(e.getKey(), e.getValue())) // new Order(qty, price)
.collect(Collectors.toList()))); // as a List<Order>

Lambdas and Streams (Array)

A bit weak with the concept of lambdas and streams so there might be stuff that really doesn't make any sense but I'll try to convey what I want to happen.
I have a class Invoice where there is an item name, price, and quantity.
I'm having to map the item name and to the total cost (price*quantity).
Though it doesn't work, hope it gives an idea what problem I am having:
invoiceList.stream()
.map(Invoice::getDesc)
.forEach(System.out.println(Invoice::getPrice*Invoice::getQty));
I can already tell the forEach would not work as it's mapping to a variable description(getDesc) and not the Invoice object where I can use it's methods to get other variables.
So, if item=pencil, price=1, qty=12, the output I would want is:
Pencil 12.00
This would be done on multiple Invoice objects.
Also, I am needing to sort them by their total and also filter those above a certain amount, eg. 100. How am I supposed to do that after having them placed in Map ?
if all you want to do is print to the console then it can be done as follows:
invoiceList.forEach(i -> System.out.println(i.getName() + " " + (i.getPrice() * i.getQty())));
If not then read on:
Using the toMap collector
Map<String, Double> result =
invoiceList.stream()
.collect(Collectors.toMap(Invoice::getName,
e -> e.getPrice() * e.getQuantity()));
This basically creates a map where the keys are the Invoice names and the values are the multiplication of the invoice price and quantity for that given Invoice.
Using the groupingBy collector
However, if there can be multiple invoices with the same name then you can use the groupingBy collector along with summingDouble as the downstream collector:
Map<String, Double> result =
invoiceList.stream()
.collect(groupingBy(Invoice::getName,
Collectors.summingDouble(e -> e.getPrice() * e.getQuantity())));
This groups the Invoice's by their names and then for each group sums the result of e.getPrice() * e.getQuantity().
Update:
if you want the toMap version and the result filtered then sorted by value ascending it can be done as follows:
Map<String, Double> result = invoiceList.stream()
.filter(e -> e.getPrice() * e.getQuantity() > 100)
.sorted(Comparator.comparingDouble(e -> e.getPrice() * e.getQuantity()))
.collect(Collectors.toMap(Invoice::getName,
e -> e.getPrice() * e.getQuantity(),
(left, right) -> left,
LinkedHashMap::new));
or with groupingBy approach :
Map<String, Double> result =
invoiceList.stream()
.collect(groupingBy(Invoice::getName,
Collectors.summingDouble(e -> e.getPrice() * e.getQuantity())))
.entrySet()
.stream()
.filter(e -> e.getValue() > 100)
.sorted(Map.Entry.comparingByValue())
.collect(Collectors.toMap(Map.Entry::getKey,
Map.Entry::getValue, (left, right) -> left,
LinkedHashMap::new));
So first, here's the code that I think you were trying to write:
invoiceList.stream()
.forEach(invoice -> System.out.println(invoice.getPrice() * invoice.getQty()));
Now let's look at what's going on here:
Calling .stream() on your list creates an object that has methods available for doing operations on the contents of the list. foreach is one such method that invokes a function on each element. map is another such method, but it returns another stream, where the contents are the contents of your original stream, but each element is replaced by the return value of the function that you pass to map.
If you look at the inside of the foreach call, you can see a lambda. This defines an anonymous function that will be called on each element of your invoiceList. The invoice variable to the left of the -> symbol is bound to each element of the stream, and the expression to the right executed.

Java 8 - Filter list inside map value

I am writing a method which takes an input Map of the form Map<Term, List<Integer>> where a Term is defined here.
Method:
Go over the keys of the Map and filter them using a Term attribute.
For each of the remaining keys, get the size of the corresponding list, cap it to 5 (min(List.size(), 5)) and add the output to a global var (say, totalSum)
Return totalSum
This is what I have written so far:
inputMap
.entrySet()
.stream()
.filter(entry -> entry.getKey().field().equals(fieldName)) // Keep only terms with fieldName
.forEach(entry -> entry.getValue()
.map(size -> Math.min(entry.getValue().size(), 5))) // These 2 lines do not work
.sum();
I am unable to take as input a stream of lists, output an integer for each of those lists and return the sum of all the outputs.
I can obviously write it using for loops, but I am trying to learn Java 8 and was curious if this problem is solvable using it.
You don't need the forEach method. You can map each entry of the Map to an int, and sum those integers :
int sum = inputMap
.entrySet()
.stream()
.filter(entry -> entry.getKey().field().equals(fieldName))
.mapToInt(entry -> Math.min(entry.getValue().size(), 5))
.sum();
With Eclipse Collections, the following will work using MutableMap and IntList.
MutableMap<Term, IntList> inputMap =
Maps.mutable.of(term1, IntLists.mutable.of(1, 2, 3),
term2, IntLists.mutable.of(4, 5, 6, 7));
long sum = inputMap
.select((term, intList) -> term.field().equals(fieldName))
.sumOfInt(intList -> Math.min(intList.size(), 5));
Note: I am a committer for Eclipse Collections.
The forEach invocation terminates the stream. You can use map directly without forEach.

Categories