Beräkna distribution från samling i Java

Att förvandla en samling siffror (eller objekt vars fält du vill inspektera) till en fördelning av dessa siffror är en vanlig statistisk teknik och används i olika sammanhang i rapportering och datadrivna applikationer.

Givet en samling:

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

Du kan inspektera deras fördelning som ett antal (frekvens för varje element) och lagra resultaten i en karta:

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

Eller kan du normalisera värdena baserade på det totala antalet värden – alltså uttrycka dem i procent:

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

Eller till och med uttrycka dessa procentsatser i en 0..100 format istället för a 0..1 format.

I den här guiden tar vi en titt på hur du kan beräkna en fördelning från en samling – både med hjälp av primitiva typer och objekt vars fält du kanske vill rapportera i din ansökan.

Med tillägget av funktionellt programmeringsstöd i Java är det enklare än någonsin att beräkna distributioner. Vi kommer att arbeta med en samling siffror och en samling av Books:

public class Book {

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

   
}

Beräkna distribution av samling i Java

Låt oss först ta en titt på hur du kan beräkna en fördelning för primitiva typer. Genom att arbeta med objekt kan du helt enkelt anropa anpassade metoder från dina domänklasser för att ge mer flexibilitet i beräkningarna.

Som standard kommer vi att representera procentsatserna som en dubbel från 0.00 till 100.00.

Primitiva typer

Låt oss skapa en lista över heltal och skriva ut deras distribution:

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

Fördelningen beräknas med:

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()))))));
}

Denna metod accepterar en lista och streamar den. Under streaming är värdena grupperade efter deras heltalsvärde – och deras värden är det räknade med hjälp av Collectors.counting(), innan de samlas in i en Map där nycklarna representerar ingångsvärdena och dubbla representerar deras procentsatser i fördelningen.

De viktigaste metoderna här är collect() som accepterar två samlare. Nyckelsamlaren samlar in genom att helt enkelt gruppera efter nyckelvärdena (indataelement). Värdesamlaren samlar in via collectingAndThen() metod som gör att vi kan räkna värdena och formatera dem sedan i ett annat format, som t.ex count * 100.00 / list.size() som låter oss uttrycka de räknade elementen i procent:

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

Sortera distribution efter värde eller nyckel

När du skapar distributioner – du vill vanligtvis sortera värdena. Oftare än inte kommer detta att vara över nyckel. Java HashMaps garanterar inte att insättningsordningen bibehålls, så vi måste använda en LinkedHashMap vilket gör det. Dessutom är det enklast att återströmma kartan och samla in den igen nu när den är mycket mindre och mycket mer hanterbar.

Den tidigare operationen kan snabbt kollapsa flera tusen poster till små kartor, beroende på antalet nycklar du har att göra med, så återströmning är inte dyrt:

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));
}

Objekt

Hur kan detta göras för objekt? Samma logik gäller! Istället för en identifieringsfunktion (Integer::intValue), kommer vi att använda det önskade fältet istället – till exempel publiceringsåret för våra böcker. Låt oss skapa några böcker, lagra dem i en lista och sedan beräkna fördelningarna för utgivningsåren:

Kolla in vår praktiska, praktiska guide för att lära dig Git, med bästa praxis, branschaccepterade standarder och medföljande fuskblad. Sluta googla Git-kommandon och faktiskt lära Det!

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);

Låt oss beräkna fördelningen av publishedYear fält:

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));
}

Justera "%.2f" för att ställa in flyttalsprecisionen. Detta resulterar i:

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

50% av de givna böckerna (2/4) publicerades 2011, 25% (1/4) publicerades 2014 och 25% (1/4) 2017. Tänk om du vill formatera detta resultat annorlunda, och normalisera räckvidden in 0..1?

Beräkna normaliserad (procentuell) fördelning av samling i Java

För att normalisera procenttalen från a 0.0...100.0 räckvidd till a 0..1 utbud – vi anpassar helt enkelt collectingAndThen() ring till inte multiplicera antalet med 100.0 innan du dividerar med samlingens storlek.

Tidigare Long antal returneras av Collectors.counting() konverterades implicit till en dubbel (multiplikation med ett dubbelt värde) – så den här gången vill vi uttryckligen få doubleValue() av 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));
}

Justera "%.4f" för att ställa in flyttalsprecisionen. Detta resulterar i:

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

Beräkna elementantal (frekvens) för samling

Slutligen – vi kan få elementantalet (frekvensen av alla element) i samlingen genom att helt enkelt inte dividera antalet med samlingens storlek! Detta är ett helt icke-normaliserat antal:

   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));
}

Detta resulterar i:

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

Det finns faktiskt två böcker från 2011 och en från 2014 och 2017 vardera.

Slutsats

Att beräkna distributioner av data är en vanlig uppgift i datarika applikationer och kräver inte användning av externa bibliotek eller komplex kod. Med funktionellt programmeringsstöd gjorde Java det enkelt att arbeta med samlingar!

I detta korta utkast har vi tagit en titt på hur du kan beräkna frekvensräkningar för alla element i en samling, samt hur du beräknar distributionskartor normaliserade till procenttal mellan 0 och 1 såväl som 0 och 100 i Java.

Tidsstämpel:

Mer från Stackabuse