Pengelompokan adalah salah satu operasi paling berguna dalam analisis data. Dari daftar transaksi, kita mungkin ingin tahu total pengeluaran per kategori, jumlah transaksi per hari, atau nilai tertinggi per produk. Pada chapter ini kita akan mempelajari groupBy, groupingBy, eachCount, fold, reduce, dan aggregate — operasi yang memetakan koleksi menjadi ringkasan per kunci.
groupBy: Klasik #
groupBy { key } menghasilkan Map<K, List<T>>. Tiap kunci memetakan ke list elemen-elemen yang berbagi kunci tersebut.
val buah = listOf("Apel", "Anggur", "Mangga", "Alpukat")
val perHuruf = buah.groupBy { it.first() }
// {A=[Apel, Anggur, Alpukat], M=[Mangga]}groupBy mengiterasi seluruh collection sekaligus dan menyimpan list elemen per kunci. Bila kita hanya ingin agregat ringkasan (jumlah, total, rata-rata), groupingBy lebih efisien karena tidak menyimpan list per grup.
groupingBy: Lazy Group #
groupingBy { key } mengembalikan objek Grouping<K, T> yang belum mengiterasi apa pun. Agregasi baru dijalankan saat kita memanggil salah satu operasi terminal seperti eachCount, fold, reduce, atau aggregate.
| Operasi | Mengembalikan | Kegunaan |
|---|---|---|
eachCount() | Map<K, Int> | Hitung elemen per grup |
fold(initial) { acc, x -> ... } | Map<K, R> | Akumulasi dengan nilai awal |
reduce { key, acc, x -> ... } | Map<K, R> | Akumulasi tanpa nilai awal |
aggregate { key, acc, x, first -> ... } | Map<K, R> | Paling fleksibel: lambda tahu apakah elemen pertama |
fold vs reduce #
fold dan reduce adalah dua operasi akumulasi paling mendasar:
| Operasi | Menerima | Mengembalikan | Tipe hasil |
|---|---|---|---|
reduce { acc, x -> ... } | (acc, x) -> acc | Sama dengan tipe elemen | T |
fold(initial) { acc, x -> ... } | (acc, x) -> acc | Sama dengan tipe initial | R (bebas) |
reduceRight / foldRight | Sama, dari kanan | Sama | T atau R |
foldIndexed / reduceIndexed | Menerima indeks | Sama | T atau R |
Perbedaan utama: reduce mengambil elemen pertama sebagai nilai awal dan tipe hasil harus sama dengan tipe elemen. fold menerima nilai awal eksplisit dan tipe hasil bebas — bahkan bisa berbeda dari tipe elemen.
Contoh Lengkap #
data class Transaksi(val kategori: String, val jumlah: Int)
fun main() {
val transaksi = listOf(
Transaksi("Makanan", 25000),
Transaksi("Transport", 15000),
Transaksi("Makanan", 30000),
Transaksi("Hiburan", 50000),
Transaksi("Transport", 20000),
Transaksi("Makanan", 10000)
)
// groupBy: kelompokkan per kategori
val perKategori = transaksi.groupBy { it.kategori }
println("GroupBy kategori:")
perKategori.forEach { (k, v) -> println(" $k: $v") }
// groupingBy + eachCount: hitung jumlah per kategori
val jumlahPerKategori = transaksi.groupingBy { it.kategori }.eachCount()
println("Jumlah transaksi per kategori: $jumlahPerKategori")
// groupingBy + fold: jumlahkan total per kategori
val totalPerKategori = transaksi
.groupingBy { it.kategori }
.fold(0) { acc, t -> acc + t.jumlah }
println("Total per kategori: $totalPerKategori")
// groupingBy + reduce: max per kategori
val maxPerKategori = transaksi
.groupingBy { it.kategori }
.reduce { _, acc, t -> if (acc.jumlah > t.jumlah) acc else t }
println("Transaksi terbesar per kategori:")
maxPerKategori.forEach { (k, v) -> println(" $k: $v") }
// fold vs reduce
val deret = listOf(1, 2, 3, 4, 5)
val jumlahReduce = deret.reduce { acc, x -> acc + x } // mulai dari elemen pertama
val jumlahFold = deret.fold(0) { acc, x -> acc + x } // mulai dari 0
println("reduce: $jumlahReduce, fold: $jumlahFold")
// fold dengan tipe berbeda
val ringkasan = deret.fold("Deret: ") { acc, x -> "$acc$x " }
println("fold dengan String: $ringkasan")
// foldRight dan reduceRight: dari kanan
val dariKanan = deret.foldRight(0) { x, acc -> acc + x }
println("foldRight: $dariKanan")
// foldIndexed
val denganIdx = deret.foldIndexed("") { i, acc, x -> "$acc[$i:$x] " }
println("foldIndexed: $denganIdx")
// aggregate: kombinasi banyak nilai sekaligus
val statistik = transaksi
.groupingBy { it.kategori }
.aggregate { _, acc: Transaksi?, t, first ->
if (first) t else if (acc!!.jumlah > t.jumlah) acc else t
}
println("Aggregate (max per kategori):")
statistik.forEach { (k, v) -> println(" $k: $v") }
}Output yang Dihasilkan:
GroupBy kategori:
Makanan: [Transaksi(kategori=Makanan, jumlah=25000), Transaksi(kategori=Makanan, jumlah=30000), Transaksi(kategori=Makanan, jumlah=10000)]
Transport: [Transaksi(kategori=Transport, jumlah=15000), Transaksi(kategori=Transport, jumlah=20000)]
Hiburan: [Transaksi(kategori=Hiburan, jumlah=50000)]
Jumlah transaksi per kategori: {Makanan=3, Transport=2, Hiburan=1}
Total per kategori: {Makanan=65000, Transport=35000, Hiburan=50000}
Transaksi terbesar per kategori:
Makanan: Transaksi(kategori=Makanan, jumlah=30000)
Transport: Transaksi(kategori=Transport, jumlah=20000)
Hiburan: Transaksi(kategori=Hiburan, jumlah=50000)
reduce: 15, fold: 15
fold dengan String: Deret: 1 2 3 4 5
foldRight: 15
foldIndexed: [0:1] [1:2] [2:3] [3:4] [4:5]
Aggregate (max per kategori):
Makanan: Transaksi(kategori=Makanan, jumlah=30000)
Transport: Transaksi(kategori=Transport, jumlah=20000)
Hiburan: Transaksi(kategori=Hiburan, jumlah=50000)Mari kita bedah baris-baris penting di atas:
transaksi.groupBy { it.kategori }: mengiterasi seluruh list dan menyimpan tiap elemen ke dalam list per kategori. HasilnyaMap<String, List<Transaksi>>lengkap dengan semua elemen asli.transaksi.groupingBy { it.kategori }.eachCount():groupingByhanya membentuk group source;eachCountadalah operasi terminal yang mengiterasi dan menghitung jumlah elemen per kategori. Tidak menyimpan list elemen.groupingBy { ... }.fold(0) { acc, t -> acc + t.jumlah }:foldpada grouping menerima nilai awal0per grup dan akumulator lambda. HasilnyaMap<String, Int>berisi totaljumlahper kategori.groupingBy { ... }.reduce { _, acc, t -> ... }:reducepada grouping tidak memiliki nilai awal — akumulator dimulai dari elemen pertama tiap grup. Lambda menerimakey,acc, dan elemen saat ini.deret.reduce { acc, x -> acc + x }:reducemulai dari elemen pertama (1) dan menambahkan elemen berikutnya. Hasilnya15bertipeIntsama seperti elemen.deret.fold(0) { acc, x -> acc + x }:foldmulai dari nilai awal0. Hasilnya juga15, tetapi nilai awal eksplisit memungkinkan tipe hasil berbeda.deret.fold("Deret: ") { acc, x -> "$acc$x " }: tipe hasilString— berbeda dari tipe elemenInt.reducetidak bisa melakukan ini karena tipe hasil harus sama dengan tipe elemen.deret.foldRight(0) { x, acc -> acc + x }: urutan lambda terbalik —(elemen, akumulator)bukan(akumulator, elemen). Iterasi dari elemen terakhir ke pertama.deret.foldIndexed("") { i, acc, x -> ... }: lambda menerima(indeks, akumulator, elemen). Berguna ketika posisi elemen memengaruhi logika.groupingBy { ... }.aggregate { _, acc, t, first -> ... }: lambda menerimakey,acc(akumulator ataunull), elemen saat ini, danBooleanapakah elemen ini adalah yang pertama untuk grup tersebut. Paling fleksibel — bisa menggabungkan beberapa statistik sekaligus.
Referensi Lanjutan #
- Collection grouping
— panduan resmi
groupBy,groupingBy,eachCount - Grouping API reference
— API reference
kotlin.collections.Grouping
Setelah memahami agregasi per grup, kita akan beralih ke operasi penyortiran yang mengatur urutan elemen sesuai kriteria yang kita tentukan.