Oblicz dystrybucję z kolekcji w Javie

Przekształcenie zbioru liczb (lub obiektów, których pola chcesz sprawdzić) w rozkład tych liczb jest powszechną techniką statystyczną i jest wykorzystywana w różnych kontekstach w raportowaniu i aplikacjach opartych na danych.

Biorąc pod uwagę kolekcję:

1, 1, 2, 1, 2, 3, 1, 4, 5, 1, 3

Możesz sprawdzić ich rozkład jako liczebność (częstotliwość każdego elementu) i zapisać wyniki na mapie:

{
"1": 5,
"2": 2,
"3": 2,
"4": 1,
"5": 1
}

Lub możesz normalizować wartości na podstawie łącznej liczby wartości – wyrażając je w ten sposób w procentach:

{
"1": 0.45,
"2": 0.18,
"3": 0.18,
"4": 0.09,
"5": 0.09
}

Lub nawet wyrazić te wartości procentowe w 0..100 format zamiast a 0..1 Format.

W tym przewodniku przyjrzymy się, jak można obliczyć rozkład z kolekcji — zarówno przy użyciu typów pierwotnych, jak i obiektów, których pola możesz chcieć raportować w swojej aplikacji.

Dzięki dodaniu obsługi programowania funkcyjnego w Javie – obliczanie dystrybucji jest łatwiejsze niż kiedykolwiek. Będziemy pracować ze zbiorem liczb i zbiorem Books:

public class Book {

    private String id;
    private String name;
    private String author;
    private long pageNumber;
    private long publishedYear;

   
}

Oblicz dystrybucję kolekcji w Javie

Przyjrzyjmy się najpierw, jak obliczyć rozkład dla typów pierwotnych. Praca z obiektami pozwala po prostu wywoływać niestandardowe metody z klas domeny, aby zapewnić większą elastyczność obliczeń.

Domyślnie będziemy reprezentować wartości procentowe jako podwójne z 0.00 do 100.00.

Typy prymitywne

Stwórzmy listę liczb całkowitych i wypiszmy ich rozkład:

List integerList = List.of(1, 1, 2, 1, 2, 3, 1, 4, 5, 1, 3);
System.out.println(calculateIntegerDistribution(integerList));

Rozkład obliczany jest za pomocą:

public static Map calculateIntegerDistribution(List list) {
    return list.stream()
            .collect(Collectors.groupingBy(Integer::intValue,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.2f", count * 100.00 / list.size()))))));
}

Ta metoda akceptuje listę i przesyła ją strumieniowo. Podczas przesyłania strumieniowego wartości są pogrupowane według ich wartość całkowita – a ich wartości to liczone za pomocą Collectors.counting(), przed zebraniem w Map gdzie klucze reprezentują wartości wejściowe, a dublety reprezentują ich udziały procentowe w rozkładzie.

Kluczowe metody tutaj to collect() który akceptuje dwa kolektory. Kolektor kluczy zbiera, po prostu grupując według wartości kluczy (elementów wejściowych). Kolekcjoner wartości zbiera za pośrednictwem collectingAndThen() metoda, która pozwala nam policz wartości a następnie sformatuj je w innym formacie, na przykład count * 100.00 / list.size() co pozwala wyrazić zliczane elementy w procentach:

{1=45.45, 2=18.18, 3=18.18, 4=9.09, 5=9.09}

Sortuj dystrybucję według wartości lub klucza

Tworząc rozkłady — zazwyczaj będziesz chciał posortować wartości. Częściej niż nie, będzie to klucz. Jawa HashMaps nie gwarantuje zachowania kolejności wstawiania, więc będziemy musieli użyć LinkedHashMap co robi. Ponadto najłatwiej jest ponownie przesłać mapę i ponownie ją zebrać, ponieważ jest znacznie mniejszy i łatwiejszy w zarządzaniu.

Poprzednia operacja może szybko zwinąć wiele tysięcy rekordów na małe mapy, w zależności od liczby kluczy, z którymi masz do czynienia, więc ponowne przesyłanie strumieniowe nie jest drogie:

public static Map calculateIntegerDistribution(List list) {
    return list.stream()
            .collect(Collectors.groupingBy(Integer::intValue,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.2f", count.doubleValue() / list.size()))))))
            
            
            .entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

Obiekty

Jak można to zrobić dla przedmiotów? Obowiązuje ta sama logika! Zamiast funkcji identyfikacji (Integer::intValue), zamiast tego użyjemy odpowiedniego pola, takiego jak rok wydania naszych książek. Stwórzmy kilka książek, przechowujmy je na liście, a następnie obliczmy rozkłady lat wydawniczych:

Zapoznaj się z naszym praktycznym, praktycznym przewodnikiem dotyczącym nauki Git, zawierającym najlepsze praktyki, standardy przyjęte w branży i dołączoną ściągawkę. Zatrzymaj polecenia Google Git, a właściwie uczyć się to!

Book book1 = new Book("001", "Our Mathematical Universe", "Max Tegmark", 432, 2014);
Book book2 = new Book("002", "Life 3.0", "Max Tegmark", 280, 2017);
Book book3 = new Book("003", "Sapiens", "Yuval Noah Harari", 443, 2011);
Book book4 = new Book("004", "Steve Jobs", "Water Isaacson", 656, 2011);

List books = Arrays.asList(book1, book2, book3, book4);

Obliczmy rozkład publishedYear pole:

public static Map calculateDistribution(List books) {
    return books.stream()
            .collect(Collectors.groupingBy(Book::getPublishedYear,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.2f", count * 100.00 / books.size()))))))
            
            .entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

Poprawić "%.2f" aby ustawić dokładność zmiennoprzecinkową. To skutkuje:

{2011=50.0, 2014=25.0, 2017=25.0}

50% podanych książek (2/4) ukazało się w 2011 roku, 25% (1/4) ukazało się w 2014 roku, a 25% (1/4) w 2017 roku. zakres w 0..1?

Oblicz znormalizowany (procentowy) rozkład kolekcji w Javie

Aby znormalizować wartości procentowe z a 0.0...100.0 zakres do 0..1 zakres – po prostu dostosujemy collectingAndThen() Dzwonić do nie pomnóż liczbę przez 100.0 przed podzieleniem przez wielkość kolekcji.

Poprzednio Long liczba zwrócona przez Collectors.counting() został domyślnie przekonwertowany na double (mnożenie z wartością double) – więc tym razem będziemy chcieli jawnie uzyskać doubleValue() ukończenia count:

    public static Map calculateDistributionNormalized(List books) {
        return books.stream()
            .collect(Collectors.groupingBy(Book::getPublishedYear,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Double.parseDouble(String.format("%.4f", count.doubleValue() / books.size()))))))
            
            .entrySet()
            .stream()
            .sorted(comparing(e -> e.getKey()))
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

Poprawić "%.4f" aby ustawić dokładność zmiennoprzecinkową. To skutkuje:

{2011=0.5, 2014=0.25, 2017=0.25}

Oblicz liczbę elementów (częstotliwość) kolekcji

Wreszcie – możemy uzyskać liczbę elementów (częstotliwość wszystkich elementów) w kolekcji, po prostu nie dzieląc liczby przez rozmiar kolekcji! Jest to w pełni nieznormalizowana liczba:

   public static Map calculateDistributionCount(List books) {
        return books
            .stream()
            .collect(Collectors.groupingBy(Book::getPublishedYear,
                    Collectors.collectingAndThen(Collectors.counting(),
                            count -> (Integer.parseInt(String.format("%s", count.intValue()))))))
            
            .entrySet()
            .stream()
            .sorted(Map.Entry.comparingByKey())
            .collect(Collectors.toMap(e -> Integer.parseInt(e.getKey().toString()),
                    Map.Entry::getValue,
                    (a, b) -> {
                        throw new AssertionError();
                    },
                    LinkedHashMap::new));
}

To skutkuje:

{2011=2, 2014=1, 2017=1}

Rzeczywiście, istnieją dwie książki z 2011 roku i po jednej z 2014 i 2017 roku.

Wnioski

Obliczanie dystrybucji danych jest typowym zadaniem w aplikacjach bogatych w dane i nie wymaga użycia bibliotek zewnętrznych ani złożonego kodu. Dzięki funkcjonalnemu wsparciu programowania Java praca z kolekcjami stała się dziecinnie prosta!

W tym krótkim szkicu przyjrzeliśmy się, w jaki sposób można obliczyć liczbę częstotliwości wszystkich elementów w kolekcji, a także jak obliczać mapy rozkładu znormalizowane do wartości procentowych między 0 i 1 jak również 0 i 100 w Javie.

Znak czasu:

Więcej z Nadużycie stosu