Beregn distribusjon fra samling i Java

Å gjøre en samling av tall (eller objekter hvis felt du ønsker å inspisere) til en fordeling av disse tallene er en vanlig statistisk teknikk, og brukes i ulike sammenhenger i rapportering og datadrevne applikasjoner.

Gitt en samling:

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

Du kan inspisere distribusjonen deres som en telling (frekvensen av hvert element), og lagre resultatene i et kart:

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

Eller det kan du normalisere verdiene basert på det totale antallet verdier – og dermed uttrykke dem i prosenter:

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

Eller til og med uttrykk disse prosentene i en 0..100 format i stedet for a 0..1 format.

I denne veiledningen skal vi se på hvordan du kan beregne en fordeling fra en samling – både ved å bruke primitive typer og objekter hvis felt du kanskje vil rapportere i søknaden din.

Med tillegg av funksjonell programmeringsstøtte i Java – er det enklere enn noensinne å beregne distribusjoner. Vi skal jobbe med en samling av tall og en samling av Books:

public class Book {

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

   
}

Beregn distribusjon av samling i Java

La oss først ta en titt på hvordan du kan beregne en fordeling for primitive typer. Arbeid med objekter lar deg ganske enkelt kalle tilpassede metoder fra domeneklassene dine for å gi mer fleksibilitet i beregningene.

Som standard vil vi representere prosentene som en dobbel fra 0.00 til 100.00.

Primitive typer

La oss lage en liste over heltall og skrive ut distribusjonen deres:

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

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

Denne metoden godtar en liste og streamer den. Mens strømmet er verdiene gruppert etter deres heltallsverdi – og deres verdier er det telles ved hjelp av Collectors.counting(), før den samles inn i en Map der tastene representerer inngangsverdiene og doblene representerer deres prosentandeler i fordelingen.

Nøkkelmetodene her er collect() som godtar to samlere. Nøkkelsamleren samler inn ved ganske enkelt å gruppere etter nøkkelverdiene (inndataelementer). Verdisamleren samler inn via collectingAndThen() metode, som lar oss telle verdiene og formater dem deretter i et annet format, for eksempel count * 100.00 / list.size() som lar oss uttrykke de talte elementene i prosenter:

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

Sorter distribusjon etter verdi eller nøkkel

Når du oppretter distribusjoner - vil du vanligvis sortere verdiene. Oftere enn ikke vil dette være over nøkkel. Java HashMaps garanterer ikke å beholde rekkefølgen for innsetting, så vi må bruke en LinkedHashMap som gjør det. I tillegg er det enklest å strømme kartet på nytt og samle det på nytt nå som det er mye mindre og mye mer håndterbart.

Den forrige operasjonen kan raskt kollapse flere tusen poster til små kart, avhengig av antall nøkler du har å gjøre med, så re-streaming er ikke 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));
}

Objekter

Hvordan kan dette gjøres for objekter? Samme logikk gjelder! I stedet for en identifiseringsfunksjon (Integer::intValue), bruker vi ønsket felt i stedet – for eksempel publiseringsåret for bøkene våre. La oss lage noen bøker, lagre dem i en liste og deretter beregne fordelingen av utgivelsesårene:

Sjekk ut vår praktiske, praktiske guide for å lære Git, med beste praksis, bransjeaksepterte standarder og inkludert jukseark. Slutt å google Git-kommandoer og faktisk lære den!

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

La oss beregne fordelingen av publishedYear felt:

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

Juster "%.2f" for å angi flytepunktpresisjonen. Dette resulterer i:

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

50 % av de gitte bøkene (2/4) ble utgitt i 2011, 25 % (1/4) ble utgitt i 2014 og 25 % (1/4) i 2017. Hva om du ønsker å formatere dette resultatet annerledes, og normalisere rekkevidden i 0..1?

Beregn normalisert (prosentvis) fordeling av samling i Java

For å normalisere prosentene fra a 0.0...100.0 rekkevidde til a 0..1 rekkevidde – vi tilpasser ganske enkelt collectingAndThen() ring til ikke gang antallet med 100.0 før du deler på størrelsen på samlingen.

Tidligere Long antall returnert av Collectors.counting() ble implisitt konvertert til en dobbel (multiplikasjon med en dobbel verdi) – så denne gangen vil vi eksplisitt 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));
}

Juster "%.4f" for å angi flytepunktpresisjonen. Dette resulterer i:

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

Beregn elementantall (frekvens) for samling

Til slutt – vi kan få elementantallet (frekvensen av alle elementene) i samlingen ved ganske enkelt ikke å dele antallet med størrelsen på samlingen! Dette er et fullstendig ikke-normalisert antall:

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

Dette resulterer i:

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

Faktisk er det to bøker fra 2011, og en fra 2014 og 2017 hver.

konklusjonen

Å beregne distribusjoner av data er en vanlig oppgave i datarike applikasjoner, og krever ikke bruk av eksterne biblioteker eller kompleks kode. Med funksjonell programmeringsstøtte gjorde Java arbeidet med samlinger til en lek!

I dette korte utkastet har vi tatt en titt på hvordan du kan beregne frekvenstellinger for alle elementer i en samling, samt hvordan du beregner distribusjonskart normalisert til prosenter mellom 0 og 1 i tillegg til 0 og 100 i Java.

Tidstempel:

Mer fra Stackabuse