Java 8 Streams множественная группировка по

У меня есть запись температуры примерно такая

dt        |AverageTemperature |AverageTemperatureUncertainty|City   |Country |Latitude|Longitude
----------+-------------------+-----------------------------+-------+--------+--------+---------
1963-01-01|-5.417000000000002 |0.5                          |Karachi|Pakistan|57.05N  |10.33E  
1963-02-01|-4.7650000000000015|0.328                        |Karachi|Pakistan|57.05N  |10.33E  
1964-01-01|-5.417000000000002 |0.5                          |Karachi|Pakistan|57.05N  |10.33E  
1964-02-01|-4.7650000000000015|0.328                        |Karachi|Pakistan|57.05N  |10.33E  
1965-01-01|11.417000000000002 |0.5                          |Karachi|Pakistan|57.05N  |10.33E 
1965-02-01|12.7650000000000015|0.328                        |Karachi|Pakistan|57.05N  |10.33E

Мне нужно проанализировать это в POJO и вычислить среднюю дельту в соответствии со следующей постановкой задачи:

Use the Streams API to calculate the average annual temperature delta for each country. To calculate delta the average temperature in 1900 would be subtracted from the average temperature in 1901 to obtain the delta from 1900 to 1901 for a particular city. The average of all these deltas is the average annual temperature delta for a city. The average of all cities in a country is the average of a country.

Мой умеренный POJO выглядит следующим образом с геттерами и сеттерами

public class Temperature {
    private java.util.Date date;
    private double averageTemperature;
    private double averageTemperatureUncertainty;
    private String city;
    private String country;
    private String latitude;
    private String longitude;
}

Я вел список температур, так как эта проблема решается с помощью потоков.

Чтобы вычислить дельту, я пытаюсь использовать следующие потоки, но я все еще не могу рассчитать фактическую дельту, так как мне нужно вычислить среднюю дельту страны, я выполнил группировку по стране, городу и дате.

Map<String, Map<String, Map<Integer, Double>>> countriesMap = this.getTemperatures().stream()
                .sorted(Comparator.comparing(Temperature::getDate))
                .collect(Collectors.groupingBy(Temperature::getCountry,
                        Collectors.groupingBy(Temperature::getCity,
                        Collectors.groupingBy
                                (t -> {
                                            Calendar calendar = Calendar.getInstance();
                                            calendar.setTime(t.getDate());
                                            return calendar.get(Calendar.YEAR);
                                        }, 
                        Collectors.averagingDouble(Temperature::getAverageTemperature)))));

Чтобы вычислить дельту, нам нужно будет вычислить разницы. для Map<Integer, Double>.

Для вычисления разницы я придумал следующий код, но не смог связать следующий код с приведенным выше.

Stream.of(10d, 20d, 10d) //this is sample data that I that I get in `Map<Integer, Double>` of countriesMap
        .map(new Function<Double, Optional<Double>>() {
            Optional<Double> previousValue = Optional.empty();
            @Override
            public Optional<Double> apply(Double current) {
                Optional<Double> value = previousValue.map(previous -> current - previous);
                previousValue = Optional.of(current);
                return value;
            }
        })
        .filter(Optional::isPresent)
        .map(Optional::get)
        .forEach(System.out::println);

Как я могу рассчитать дельту, используя потоки за один раз, или как выполнить потоковые операции через countriesMap, чтобы вычислить дельту и достичь упомянутого состояния проблемы?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
8
0
364
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Чтобы сократить формулировку проблемы до меньшего блока, можно воспользоваться другим подходом, который вы можете изучить - это проанализировать температуру yearly и вычислить для них дельту, а затем ее обработать average. Однако это должно быть сделано для всех значений типа Map<Integer, Double> внутри внутреннего Map в вашем вопросе. Это выглядело бы примерно так:

Map<Integer, Double> unitOfWork = new HashMap<>(); // innermost map you've attained ('yearToAverageTemperature' map)
unitOfWork = unitOfWork.entrySet()
        .stream()
        .sorted(Map.Entry.comparingByKey())
        .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
// the values sorted based on the year from a sorted map
List<Double> srtedValPerYear = new ArrayList<>(unitOfWork.values());
// average of deltas from the complete list 
double avg = IntStream.range(0, srtedVal.size() - 1)
        .mapToDouble(i -> (srtedVal.get(i + 1) - srtedVal.get(i)))
        .average().orElse(Double.NaN);

Отметим далее, что это всего лишь среднее значение одной записи City для <Year, AverageTemperature>, вам придется перебирать весь ваш набор ключей City, а также весь ваш набор ключей Country, чтобы исчерпывающе определить такие средние значения.

Дальнейшее преобразование этой единицы работы в метод, перебор всей карты карт, это может быть выполнено следующим образом:

// The average of all cities in a country is the average of a country.
AtomicReference<Double> countryValAvg = new AtomicReference<>(0.0);
countriesMap.forEach((country, cityMap) -> {
    // The average of all these deltas is the average annual temperature delta for a city.
    AtomicReference<Double> cityAvgTemp = new AtomicReference<>((double) 0);
    cityMap.forEach((city, yearMap) -> cityAvgTemp.set(cityAvgTemp.get() + averagePerCity(yearMap)));
    double avgAnnualTempDeltaPerCity = cityAvgTemp.get() / cityMap.size();

    countryValAvg.set(countryValAvg.get() + avgAnnualTempDeltaPerCity);
});
System.out.println(countryValAvg.get() / countriesMap.size());

где averagePerCity - метод, выполняющий следующие

double averagePerCity(Map<Integer, Double> unitOfWork) {
    unitOfWork = unitOfWork.entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
    List<Double> srtedVal = new ArrayList<>(unitOfWork.values());
    return IntStream.range(0, srtedVal.size() - 1)
            .mapToDouble(i -> (srtedVal.get(i + 1) - srtedVal.get(i)))
            .average().orElse(Double.NaN);
}

Примечание: В приведенном выше коде могут отсутствовать проверки, это просто для того, чтобы дать представление о том, как всю проблему можно разбить на более мелкие части, а затем решить.

Редактировать1: Какой можно было бы улучшить дальше, как:

// The average of all cities in a country is the average of a country.
AtomicReference<Double> countryValAvg = new AtomicReference<>(0.0);
countriesMap.forEach((country, cityMap) -> {
    // The average of all these deltas is the average annual temperature delta for a city.
    double avgAnnualTempDeltaPerCity = cityMap.values()
            .stream()
            .mapToDouble(Quick::averagePerCity) // Quick is my class name
            .average()
            .orElse(Double.NaN);
    countryValAvg.set(countryValAvg.get() + avgAnnualTempDeltaPerCity);
});
System.out.println(countryValAvg.get() / countriesMap.size());

Редактировать2: И далее в

double avgAnnualTempDeltaPerCity = countriesMap.values().stream()
        .mapToDouble(cityMap -> cityMap.values()
                .stream()
                .mapToDouble(Quick::averagePerCity) // Quick is my class name
                .average()
                .orElse(Double.NaN))
        .average().orElse(Double.NaN);

Вместо AtomicReference<Double> cityAvgTemp = new AtomicReference<>((double) 0); cityMap.forEach((city, yearMap) -> cityAvgTemp.set(cityAvgTemp.get() + averagePerCity(yearMap))); double avgAnnualTempDeltaPerCity = cityAvgTemp.get() / cityMap.size(); вы можете просто использовать double avgAnnualTempDeltaPerCity = cityMap.values().stream().mapToDouble(this::averagePerCity).‌​average();

Holger 11.01.2019 09:16

@Holger double avgAnnualTempDeltaPerCity = cityAvgTemp.get() / cityMap.size(); Разве это не avgAnnualTempDeltaPerCountry.?

user2578909 11.01.2019 11:21

@HammadNaeem нет, дельта температуры для каждой страны оценивается вне этой итерации.

Naman 11.01.2019 11:23

Вы можете повторить упрощение и для внешней карты; вместо AtomicReference<Double> countryValAvg = new AtomicReference<>(0.0); countriesMap.forEach((country, cityMap) -> { /* code not using country */ countryValAvg.set(countryValAvg.get() + avgAnnualTempDeltaPerCity); }) double result = countryValAvg.get() / countriesMap.size(); можно просто использовать double result = countriesMap.values().stream().mapToDouble(cityMap -> /* expression used to initialize avgAnnualTempDeltaPerCity */).average();

Holger 11.01.2019 11:28

Другие вопросы по теме