In Multimap how to return value which has highest occurrence

Viewed 147

I have a MultiMap from guava library

Multimap<Integer,String> maps = ArrayListMultimap.create();

      maps.put(1, "foo");
      maps.put(1, "bar");
      maps.put(1, "foo");
      maps.put(2, "Hello");
      maps.put(2, "foo");
      maps.put(2, "World");
      maps.put(2, "World");

In this for the key 1, I need to return value which has the highest occurrence. In the above case, it has to return map as

Expected Result:

[1,foo]
[2,World]

I tried

Stream result1 = maps.keySet().stream() 
                  .map(i -> 
                              maps.get(i).stream() 
                                  .collect(
                                          Collectors.groupingBy(v -> v, Collectors.counting())
                                          )
                                  );

Result:

{{bar=1, foo=2}=1, {Hello=1, foo=1, World=2}=1}
5 Answers

This is not going to be very strait-forward. You first need to group by key, obviously. Then based on that Key you need to find the max occurrences of the corresponding Value (for 1 == foo, for example). The only way to find that max is to traverse the Collection<String> that is mapped to a certain key. This complicates things even more since you use a Multimap and you could easily have things like:

    maps.put(1, "foo");
    maps.put(1, "bar");
    maps.put(1, "bar");
    maps.put(1, "foo");

As such, IMO, this could be written as:

Map<Integer, List<String>> result =
        maps.keySet()
            .stream()
            .collect(Collectors.toMap(
                Function.identity(),
                x -> {
                    Map<String, Long> freqMap = maps.get(x)
                                                    .stream()
                                                    .collect(Collectors.groupingBy(
                                                        Function.identity(),
                                                        Collectors.counting())
                                                    );
                    long max = Collections.max(freqMap.values());
                    return freqMap.entrySet()
                                  .stream()
                                  .filter(y -> y.getValue() == max)
                                  .map(Entry::getKey)
                                  .collect(Collectors.toList());
                }
            ));
  • I first group by Key (1 and 2)
  • Then get the Collection<String> that is mapped to that Key
  • Then compute a Map<String, Long> that represents the frequency of values. For example : ["foo" = 2]; ["bar" = 1]
  • I then look at the max number of occurrences. Since you are using a multimap, you could have a case when ["foo" = 2]; ["bar" = 2], for the same Key, so we need to take both foo and bar as the result.
  • Based on that max, I find out the corresponding values.

What you seem to be looking for could be achieved while iterating over the entries as :

Map<Integer, String> integerStringMap = maps.asMap()
        .entrySet()
        .stream()
        .collect(Collectors.toMap(Map.Entry::getKey,
                e -> mostFrequentWord(e.getValue())));

The implementation of mostFrequentWord should return a String such as:

static String mostFrequentWord(Collection<String> values) {
    return values.stream()
            .collect(Collectors.groupingBy(v -> v, Collectors.counting()))
            .entrySet().stream()
            .max(Map.Entry.comparingByValue())
            .map(Map.Entry::getKey)
            .orElseThrow(() -> new UnsupportedOperationException(
                    "Empty collection as values in multimap"));
}
maps.keySet().stream().distinct().collect(Collectors.toMap(key -> key, key -> {
    Map<String, Long> valueByCount = maps.get(key).stream().collect(Collectors.groupingBy(s -> s, Collectors.counting()));
    return valueByCount.entrySet()
        .stream()
        .max(Comparator.comparing(Map.Entry::getValue))
        .map(Map.Entry::getKey)
        .orElse(null);
}));

You could do like below:

var valueFrequency = maps.entries().stream()
          .collect(groupingBy(Function.identity(), counting()));

var result = valueFrequency.entrySet()
                  .stream()
                  .max(Map.Entry.comparingByValue())
                  .stream()
                  .flatMap(maxFreq -> valueFrequency.entrySet().stream()
                                          .filter(val -> val.getValue().equals(maxFreq.getValue()))
                                          .map(Map.Entry::getKey))
                  .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

Without streams, but using Guava stuff like Multiset and Maps.transformValues:

@Test
public void shouldFindHightestOccurrencesInMultimapValues() {
    //given
    Multimap<Integer, String> maps = ArrayListMultimap.create();
    maps.put(1, "foo");
    maps.put(1, "bar");
    maps.put(1, "foo");
    maps.put(2, "Hello");
    maps.put(2, "foo");
    maps.put(2, "World");
    maps.put(2, "World");
    //when
    Map<Integer, String> result = ImmutableMap.copyOf(Maps.transformValues(maps.asMap(), this::findHighestOccurrence));
    //then
    assertThat(result).containsOnly(
            entry(1, "foo"),
            entry(2, "World"));
}

private String findHighestOccurrence(Collection<String> values) {
    return Multisets.copyHighestCountFirst(ImmutableMultiset.copyOf(values)).iterator().next();
}

If there was a MultisetMultimap subtype (i.e. Map<K, Multiset<V>>-like specialized Multimap, it would probably be the best structure to store your data in this case.

Related