Calculați distribuția din colecție în Java

Nodul sursă: 1734738

Transformarea unei colecții de numere (sau obiecte ale căror câmpuri ați dori să le inspectați) într-o distribuție a acestor numere este o tehnică statistică comună și este folosită în diferite contexte în aplicații de raportare și bazate pe date.

Dată o colecție:

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

Puteți inspecta distribuția lor ca număr (frecvența fiecărui element) și puteți stoca rezultatele într-o hartă:

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

Sau puteți normaliza valorile bazate pe numărul total de valori – exprimându-le astfel în procente:

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

Sau chiar exprima aceste procente în a 0..100 format în loc de a 0..1 format.

În acest ghid, vom arunca o privire asupra modului în care puteți calcula o distribuție dintr-o colecție – atât folosind tipuri primitive, cât și obiecte ale căror câmpuri ați putea dori să le raportați în aplicația dvs.

Cu adăugarea suportului de programare funcțională în Java – calcularea distribuțiilor este mai ușor ca niciodată. Vom lucra cu o colecție de numere și o colecție de Books:

public class Book {

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

   
}

Calculați distribuția colecției în Java

Să aruncăm mai întâi o privire la modul în care puteți calcula o distribuție pentru tipurile primitive. Lucrul cu obiecte vă permite pur și simplu să apelați metode personalizate din clasele de domeniu pentru a oferi mai multă flexibilitate în calcule.

În mod implicit, vom reprezenta procentele ca o dublă de la 0.00 la 100.00.

Tipuri primitive

Să creăm o listă de numere întregi și să le imprimăm distribuția:

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

Distribuția se calculează cu:

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

Această metodă acceptă o listă și o transmite în flux. În timp ce sunt transmise în flux, valorile sunt grupate după valoarea lor întreagă – iar valorile lor sunt numărat folosind Collectors.counting(), înainte de a fi colectat într-un Map unde cheile reprezintă valorile de intrare iar dublele reprezintă procentele acestora în distribuție.

Metodele cheie aici sunt collect() care acceptă doi colectori. Colectorul de chei colectează prin simpla grupare după valorile cheii (elementele de intrare). Colectorul de valoare colectează prin intermediul collectingAndThen() metoda, care ne permite numără valorile și apoi formatați-le într-un alt format, cum ar fi count * 100.00 / list.size() care ne permite să exprimăm elementele numărate în procente:

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

Sortați distribuția după valoare sau cheie

Când creați distribuții – de obicei veți dori să sortați valorile. De cele mai multe ori, asta va trece cheie. Java HashMaps nu garantează păstrarea ordinii de inserare, deci va trebui să folosim a LinkedHashMap care face. În plus, este mai ușor să retransmiteți harta și să o colectați din nou acum, deoarece are o dimensiune mult mai mică și mult mai ușor de gestionat.

Operația anterioară poate restrânge rapid mai multe mii de înregistrări în hărți mici, în funcție de numărul de chei cu care aveți de-a face, astfel încât re-streamarea nu este costisitoare:

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

Obiecte

Cum se poate face acest lucru pentru obiecte? Se aplica aceeasi logica! În loc de o funcție de identificare (Integer::intValue), vom folosi în schimb câmpul dorit – cum ar fi anul publicat pentru cărțile noastre. Să creăm câteva cărți, să le stocăm într-o listă și apoi să calculăm distribuțiile anilor de publicare:

Consultați ghidul nostru practic și practic pentru a învăța Git, cu cele mai bune practici, standarde acceptate de industrie și fisa de cheat incluse. Opriți căutarea pe Google a comenzilor Git și de fapt învăţa aceasta!

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

Să calculăm distribuția publishedYear camp:

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

Ajustează "%.2f" pentru a seta precizia în virgulă mobilă. Aceasta are ca rezultat:

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

50% din cărțile date (2/4) au fost publicate în 2011, 25% (1/4) au fost publicate în 2014 și 25% (1/4) în 2017. Ce se întâmplă dacă doriți să formatați acest rezultat diferit și să normalizați intervalul în 0..1?

Calculați distribuția normalizată (procentuală) a colecției în Java

Pentru a normaliza procentele de la a 0.0...100.0 interval la a 0..1 gama – pur și simplu vom adapta collectingAndThen() Sună la nu înmulțiți numărul cu 100.0 înainte de a împărți la dimensiunea colecției.

Anterior, Long count returnat de Collectors.counting() a fost convertită implicit într-o dublă (înmulțire cu o valoare dublă) – așa că de data aceasta, vom dori să obținem în mod explicit doubleValue() a 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));
}

Ajustează "%.4f" pentru a seta precizia în virgulă mobilă. Aceasta are ca rezultat:

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

Calculați numărul de elemente (frecvența) de colecție

În cele din urmă – putem obține numărul de elemente (frecvența tuturor elementelor) din colecție pur și simplu nu împărțind numărul la dimensiunea colecției! Acesta este un număr complet nenormalizat:

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

Rezultă:

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

Într-adevăr, există două cărți din 2011 și una din 2014 și 2017 fiecare.

Concluzie

Calcularea distribuțiilor de date este o sarcină comună în aplicațiile bogate în date și nu necesită utilizarea de biblioteci externe sau cod complex. Cu suport de programare funcțional, Java a făcut ca lucrul cu colecții să fie o briză!

În această scurtă schiță, am analizat cum puteți calcula frecvența tuturor elementelor dintr-o colecție, precum și cum să calculați hărțile de distribuție normalizate la procente între 0 și 1 precum și 0 și 100 în Java.

Timestamp-ul:

Mai mult de la Stackabuse