Усреднение ключей Map с использованием lambda

Есть исходный Map

Map<Double, Integer> pairs = new HashMap<>();
Map<Double, Integer> avgPairs = new HashMap<>();
pairs.put(4.2,4);
pairs.put(4.205,6);
pairs.put(7.33,67);
pairs.put(3.16,2);
...

Каким образом из исходных пар получить усредненный map, если значения ключей отличаются друг от друга менее, чем на 0.5%? Процентное соотношение между ключами находится по формуле | (a — b) / [ (a + b) / 2 ] | * 100 %

Усредненный map - это среднее по ключам и сумма значений. Желательно функционально. Результат должен выглядеть так:

4.2025,10
7.33,67
3.16,2

Ответы (1 шт):

Автор решения: Alex Rudenko

Можно попытаться написать некую функцию для группировки значений ключей входной мапы, на первом проходе сгруппировать "близкие" ключи, тогда получится условная мапа Map<Integer, List<Map.Entry<Double, Integer>>>, а на втором вычислить средние значения ключей и суммы значений по группам:

Map<Double, Integer> avgPairs = pairs.entrySet()
    .stream()
    .sorted(Map.Entry.comparingByKey())
    .collect(Collectors.groupingBy(
        e -> (int)(e.getKey() * 10050/1000) // как пример, точность выбора не гарантируется
    )) // Map<Integer, List<Map.Entry<Double, Integer>>>
    .entrySet()
    .stream()
    .peek(System.out::println)
    .collect(Collectors.toMap(
        e -> 
            e.getValue().stream()
             .collect(Collectors.averagingDouble(Map.Entry::getKey)),
        e -> 
            e.getValue().stream()
             .collect(Collectors.summingInt(Map.Entry::getValue))
    ));
System.out.println(avgPairs);

Вывод:

73=[7.33=67]
42=[4.2=4, 4.205=6]
31=[3.16=2]
{3.16=2, 4.202500000000001=10, 7.33=67}

Несколько более точное решение, но использующее побочные эффекты -- генерацию предельного значения диапазона и идентификатор последнего подходящего диапазона.

Поскольку входной поток пар "ключ-значение" отсортирован, то при поступлении ключа, можно проверить, существует ли диапазон, если да, меньше ли входной ключ предельного значения и соответственно вернуть текущий индекс, или запомнить новое предельное значение и инкрементировать индекс.

Однако, такое решение не будет гарантированно работать при использовании параллельных потоков по причине использования побочных эффектов.

AtomicInteger index = new AtomicInteger(-1);
Double[] max = new Double[1];
        
Map<Double, Integer> avgPairs = pairs.entrySet()
    .stream()
    .sorted(Map.Entry.comparingByKey())
    .collect(Collectors.groupingBy(
        e -> max[0] == null || e.getKey() > max[0] ? updateMax(e.getKey(), max, index).incrementAndGet() : index.get()
    )) // Map<Integer, List<Map.Entry<Double, Integer>>>
    .entrySet()
    .stream()
    .peek(System.out::println)
    .collect(Collectors.toMap(
        e -> 
            e.getValue().stream()
             .collect(Collectors.averagingDouble(Map.Entry::getKey)),
        e -> 
            e.getValue().stream()
             .collect(Collectors.summingInt(Map.Entry::getValue))
    ));
System.out.println(Arrays.toString(max));  
System.out.println(avgPairs);
static AtomicInteger updateMax(double d, Double[] max, AtomicInteger index) {
    max[0] = d * 1.005;
    return index;
}

Вывод (в тестовые данные добавлено pairs.put(4.23, 9);):

0=[3.16=2]
1=[4.2=4, 4.205=6]
2=[4.23=9]
3=[7.33=67]
[7.366649999999999] // 7,33 * 1,005
{4.23=9, 3.16=2, 4.202500000000001=10, 7.33=67}
→ Ссылка