Calcola la distribuzione dalla raccolta in Java

Trasformare una raccolta di numeri (o oggetti i cui campi si desidera ispezionare) in una distribuzione di tali numeri è una tecnica statistica comune e viene utilizzata in vari contesti nelle applicazioni di creazione di report e basate sui dati.

Data una collezione:

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

Puoi ispezionare la loro distribuzione come conteggio (frequenza di ciascun elemento) e memorizzare i risultati in una mappa:

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

O puoi normalizzare i valori in base al numero totale dei valori – esprimendoli così in percentuale:

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

O anche esprimere queste percentuali in a 0..100 formato invece di a 0..1 formato.

In questa guida, daremo un'occhiata a come calcolare una distribuzione da una raccolta, sia utilizzando tipi primitivi che oggetti i cui campi potresti voler segnalare nella tua applicazione.

Con l'aggiunta del supporto alla programmazione funzionale in Java, il calcolo delle distribuzioni è più facile che mai. Lavoreremo con una raccolta di numeri e una raccolta di Books:

public class Book {

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

   
}

Calcola la distribuzione della raccolta in Java

Diamo prima un'occhiata a come calcolare una distribuzione per i tipi primitivi. Lavorare con gli oggetti ti consente semplicemente di chiamare metodi personalizzati dalle tue classi di dominio per fornire maggiore flessibilità nei calcoli.

Per impostazione predefinita, rappresenteremo le percentuali come un doppio da 0.00 a 100.00.

Tipi primitivi

Creiamo un elenco di numeri interi e stampiamo la loro distribuzione:

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

La distribuzione si calcola con:

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

Questo metodo accetta un elenco e lo trasmette in streaming. Durante lo streaming, i valori sono raggruppati per il loro valore intero - e i loro valori sono contati utilizzando Collectors.counting(), prima di essere raccolti in a Map dove le chiavi rappresentano i valori di input e i doppi rappresentano le loro percentuali nella distribuzione.

I metodi chiave qui sono collect() che accetta due collezionisti. Il key-collector raccoglie semplicemente raggruppando per valori chiave (elementi di input). Il valore-collezionista raccoglie tramite il collectingAndThen() metodo, che ci consente di farlo contare i valori e quindi formattarli in un altro formato, ad esempio count * 100.00 / list.size() che ci permette di esprimere in percentuale gli elementi contati:

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

Ordina la distribuzione per valore o chiave

Quando crei le distribuzioni, in genere vorrai ordinare i valori. Il più delle volte, questo passerà chiave. Giava HashMaps non garantisce di preservare l'ordine di inserimento, quindi dovremo usare a LinkedHashMap che fa. Inoltre, è più semplice riprodurre in streaming la mappa e raccoglierla di nuovo ora che è di dimensioni molto più ridotte e molto più gestibile.

L'operazione precedente può comprimere rapidamente più migliaia di record in piccole mappe, a seconda del numero di chiavi con cui hai a che fare, quindi il ri-streaming non è costoso:

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

Oggetti

Come è possibile farlo per gli oggetti? Vale la stessa logica! Invece di una funzione di identificazione (Integer::intValue), utilizzeremo invece il campo desiderato, ad esempio l'anno di pubblicazione per i nostri libri. Creiamo alcuni libri, li memorizziamo in un elenco e poi calcoliamo le distribuzioni degli anni di pubblicazione:

Dai un'occhiata alla nostra guida pratica e pratica per l'apprendimento di Git, con le migliori pratiche, gli standard accettati dal settore e il cheat sheet incluso. Smetti di cercare su Google i comandi Git e in realtà imparare esso!

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

Calcoliamo la distribuzione del publishedYear campo:

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

Aggiusta il "%.2f" per impostare la precisione in virgola mobile. Questo risulta in:

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

Il 50% dei libri indicati (2/4) è stato pubblicato nel 2011, il 25% (1/4) è stato pubblicato nel 2014 e il 25% (1/4) nel 2017. E se si desidera formattare questo risultato in modo diverso e normalizzare la gamma in 0..1?

Calcola la distribuzione normalizzata (percentuale) della raccolta in Java

Per normalizzare le percentuali da a 0.0...100.0 gamma ad a 0..1 gamma – adatteremo semplicemente il collectingAndThen() chiama a non moltiplica il conteggio per 100.0 prima di dividere per la dimensione della collezione.

In precedenza, il Long conteggio restituito da Collectors.counting() è stato implicitamente convertito in un double (moltiplicazione con un valore doppio), quindi questa volta vorremo ottenere esplicitamente il doubleValue() della 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));
}

Aggiusta il "%.4f" per impostare la precisione in virgola mobile. Questo risulta in:

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

Calcola il conteggio degli elementi (frequenza) della raccolta

Infine, possiamo ottenere il conteggio degli elementi (frequenza di tutti gli elementi) nella raccolta semplicemente non dividendo il conteggio per la dimensione della raccolta! Questo è un conteggio completamente non normalizzato:

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

Questo risulta in:

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

In effetti, ci sono due libri del 2011 e uno del 2014 e del 2017 ciascuno.

Conclusione

Il calcolo delle distribuzioni di dati è un'attività comune nelle applicazioni ricche di dati e non richiede l'uso di librerie esterne o codice complesso. Con il supporto della programmazione funzionale, Java ha reso il lavoro con le raccolte un gioco da ragazzi!

In questa breve bozza, abbiamo dato un'occhiata a come calcolare i conteggi di frequenza di tutti gli elementi in una raccolta, nonché a come calcolare le mappe di distribuzione normalizzate a percentuali tra 0 ed 1 così come 0 ed 100 in Giava.

Timestamp:

Di più da Impilamento