Pengelompokan dan Penggabungan

GroupBy, Join, dan agregasi data.

Daftar isi

Pengelompokan menyatukan elemen yang kuncinya sama menjadi satu grup, sedangkan penggabungan menyandingkan elemen dari dua sumber berdasarkan kunci yang cocok, dan keduanya merupakan tulang punggung pelaporan data di LINQ. Topik ini membahas GroupBy beserta agregasi per grup seperti rata-rata dan jumlah, kemudian Join dalam bentuk metode maupun bentuk query. Semua contoh dijalankan dengan data catatan sensor suhu dan antrean tiket loket, dan keluaran aslinya ditempel setelah setiap potongan kode. Kedua operator bekerja pada IEnumerable<T> yang sama dengan operator saring dan proyeksi, sehingga hasilnya dapat dirangkai lebih jauh.

GroupBy untuk membentuk grup#

Metode GroupBy menerima lambda pemilih kunci, yaitu fungsi ringkas tanpa nama, dan menghasilkan barisan grup, yaitu objek bertipe IGrouping<TKey, TElement> yang memiliki property Key dan sekaligus merupakan barisan isi grup. Grup-lah yang ditelusuri foreach, dan isi masing-masing grup ditelusuri di dalamnya. Pada contoh berikut, catatan sensor suhu dikelompokkan menurut ruangannya.

csharp Contoh: GroupBy mengelompokkan catatan suhu menurut ruangan
// Contoh: GroupBy mengelompokkan catatan suhu menurut ruangan
List<CatatanSuhu> catatan = new()
{
    new CatatanSuhu("Lab", 24.5),
    new CatatanSuhu("Kantor", 26.0),
    new CatatanSuhu("Lab", 25.5),
    new CatatanSuhu("Gudang", 30.0),
    new CatatanSuhu("Kantor", 27.5)
};
foreach (var grup in catatan.GroupBy(c => c.Ruang))
{
    Console.WriteLine($"{grup.Key}: {string.Join(", ", grup.Select(c => c.Suhu))}");
}

record CatatanSuhu(string Ruang, double Suhu);
text Keluaran
Lab: 24.5, 25.5
Kantor: 26, 27.5
Gudang: 30

Tiga grup muncul sesuai tiga nilai kunci yang berbeda, dan tiap grup memuat catatan yang ruangannya sama. Pada .NET, urutan grup mengikuti urutan kemunculan kuncinya di sumber, sehingga Lab tampil lebih dahulu karena catatan pertamanya paling awal. Penyebutan var pada perulangan luar menghindarkan penulisan tipe IGrouping<string, CatatanSuhu> secara penuh.

Urutan grup dapat diatur setelah pengelompokan dilakukan dengan menambahkan OrderBy pada Key, misalnya catatan.GroupBy(c => c.Ruang).OrderBy(g => g.Key). Isi di dalam satu grup juga dapat diurutkan dengan OrderBy pada penelusuran dalamnya, sehingga laporan per ruangan dapat menampilkan suhu dari terendah ke tertinggi. Pola ini menjaga pemisahan tanggung jawab, yaitu pengelompokan menentukan keanggotaan, sedangkan pengurutan menentukan tampilan.

Agregasi per grup#

Agregasi merangkum setiap grup menjadi satu nilai, dan operator yang digunakan adalah Count untuk banyaknya elemen, Average untuk rata-rata, Max dan Min untuk batasnya, serta Sum untuk jumlahnya. Rantai GroupBy lalu Select membentuk satu baris ringkasan per grup, dan bentuk new { ... } mengemas beberapa agregasi sekaligus ke dalam satu objek anonymous type, yaitu objek tanpa deklarasi kelas.

csharp Contoh: rata-rata dan jumlah catatan per ruangan
// Contoh: agregasi rata-rata dan jumlah per grup
List<CatatanSuhu> catatan = new()
{
    new CatatanSuhu("Lab", 24.5),
    new CatatanSuhu("Kantor", 26.0),
    new CatatanSuhu("Lab", 25.5),
    new CatatanSuhu("Gudang", 30.0),
    new CatatanSuhu("Kantor", 27.5)
};
var ringkasan = catatan
    .GroupBy(c => c.Ruang)
    .Select(g => new { Ruang = g.Key, RataRata = g.Average(c => c.Suhu), Jumlah = g.Count() });
foreach (var baris in ringkasan)
{
    Console.WriteLine($"{baris.Ruang}: rata-rata {baris.RataRata:F1} ({baris.Jumlah} catatan)");
}

record CatatanSuhu(string Ruang, double Suhu);
text Keluaran
Lab: rata-rata 25.0 (2 catatan)
Kantor: rata-rata 26.8 (2 catatan)
Gudang: rata-rata 30.0 (1 catatan)

Rata-rata Kantor bernilai 26.8 karena 26.0 dan 27.5 dibagi dua, sedangkan Lab menghasilkan 25.0 dari 24.5 dan 25.5. Format F1 membatasi satu angka di belakang koma, sehingga keluarannya stabil untuk dibaca. Bentuk query memiliki padanan klausanya, yaitu group ... by ... into g, yang menghasilkan grup dengan nama g untuk dilanjutkan ke select.

Agregasi lain dapat ditambahkan pada baris ringkasan yang sama, misalnya SuhuTertinggi = g.Max(c => c.Suhu) dan Total = g.Sum(c => c.Suhu), karena satu objek tanpa nama menampung properti sebanyak kebutuhan laporannya. Grup kosong tidak pernah muncul pada hasil GroupBy, karena setiap grup terbentuk dari minimal satu elemen sumber. Dengan demikian, Average dan Max pada grup hasil pengelompokan tidak pernah dihadapkan pada barisan kosong.

csharp Contoh: group-by-into dalam bentuk query
// Contoh: pengelompokan dalam bentuk query dengan group-by-into
List<CatatanSuhu> catatan = new()
{
    new CatatanSuhu("Lab", 24.5),
    new CatatanSuhu("Kantor", 26.0),
    new CatatanSuhu("Lab", 25.5)
};
var ringkasan =
    from c in catatan
    group c by c.Ruang into g
    select new { Ruang = g.Key, RataRata = g.Average(x => x.Suhu) };
foreach (var baris in ringkasan)
{
    Console.WriteLine($"{baris.Ruang}: rata-rata {baris.RataRata:F1}");
}

record CatatanSuhu(string Ruang, double Suhu);
text Keluaran
Lab: rata-rata 25.0
Kantor: rata-rata 26.0

Join untuk menyandingkan dua sumber#

Metode Join menggabungkan dua barisan berdasarkan kunci yang cocok, dengan empat argumen, yaitu barisan dalam, pemilih kunci sumber pertama, pemilih kunci sumber kedua, dan lambda pembentuk hasilnya. Hubungan yang terbentuk adalah inner join, artinya hanya pasangan yang kuncinya cocok pada kedua sumber yang masuk hasil. Contoh berikut menyandingkan antrean tiket loket dengan daftar layanannya.

csharp Contoh: Join menyandingkan tiket dengan layanan
// Contoh: Join menggabungkan antrean tiket dengan daftar layanan
List<Tiket> antrean = new()
{
    new Tiket("A-01", "KRT"),
    new Tiket("A-02", "SRT"),
    new Tiket("A-03", "KRT")
};
List<Layanan> daftarLayanan = new()
{
    new Layanan("KRT", "Pencetakan kartu", 5000),
    new Layanan("SRT", "Pengambilan surat", 3000)
};
var gabungan = antrean.Join(
    daftarLayanan,
    t => t.KodeLayanan,
    l => l.Kode,
    (t, l) => $"{t.Nomor}: {l.Nama}, tarif {l.Tarif}");
foreach (string baris in gabungan)
{
    Console.WriteLine(baris);
}

record Tiket(string Nomor, string KodeLayanan);
record Layanan(string Kode, string Nama, int Tarif);
text Keluaran
A-01: Pencetakan kartu, tarif 5000
A-02: Pengambilan surat, tarif 3000
A-03: Pencetakan kartu, tarif 5000

Tiga tiket masing-masing menemukan layanannya, sehingga tiga baris dihasilkan dan urutan hasilnya mengikuti sumber pertama. Tiket dengan kode layanan yang tidak terdaftar akan hilang dari hasil karena sifat inner join tersebut. Apabila satu kunci muncul beberapa kali di kedua sumber, setiap kombinasi pasangan menghasilkan satu baris, sehingga perilaku ini berbeda dengan pengelompokan yang selalu menyatukan elemen berkunci sama menjadi satu grup. Bentuk query menyandingkan pasangan kuncinya dengan kata kunci join ... on ... equals, dan hasil keduanya sama.

Kunci penggabungan dapat berupa gabungan beberapa properti melalui objek tanpa nama, misalnya new { t.Gedung, t.KodeLayanan }, dan kedua sumber wajib menuliskannya dengan bentuk yang sama persis. Kebutuhan semacam ini lazim ketika satu kode layanan berulang di beberapa gedung, sehingga pencocokannya menuntut dua kunci sekaligus.

csharp Contoh: join-on-equals dalam bentuk query
// Contoh: Join dalam bentuk query dengan klausa join-on-equals
List<Tiket> antrean = new()
{
    new Tiket("A-01", "KRT"),
    new Tiket("A-02", "SRT"),
    new Tiket("A-03", "KRT")
};
List<Layanan> daftarLayanan = new()
{
    new Layanan("KRT", "Pencetakan kartu", 5000),
    new Layanan("SRT", "Pengambilan surat", 3000)
};
var gabungan =
    from t in antrean
    join l in daftarLayanan on t.KodeLayanan equals l.Kode
    select $"{t.Nomor}: {l.Nama}";
foreach (string baris in gabungan)
{
    Console.WriteLine(baris);
}

record Tiket(string Nomor, string KodeLayanan);
record Layanan(string Kode, string Nama, int Tarif);
text Keluaran
A-01: Pencetakan kartu
A-02: Pengambilan surat
A-03: Pencetakan kartu

Contoh program utuh#

Program berikut merangkum pengelompokan, agregasi per grup, dan penggabungan dalam satu berkas.

csharp Program.cs
// Program contoh GroupBy, agregasi per grup, dan Join

// Data catatan sensor suhu per ruangan
List<CatatanSuhu> catatan = new()
{
    new CatatanSuhu("Lab", 24.5),
    new CatatanSuhu("Kantor", 26.0),
    new CatatanSuhu("Lab", 25.5),
    new CatatanSuhu("Gudang", 30.0),
    new CatatanSuhu("Kantor", 27.5)
};

// GroupBy: isi tiap grup ditelusuri lewat g
foreach (var grup in catatan.GroupBy(c => c.Ruang))
{
    Console.WriteLine($"{grup.Key}: {string.Join(", ", grup.Select(c => c.Suhu))}");
}

// Agregasi: rata-rata dan jumlah catatan per ruangan
var ringkasan = catatan
    .GroupBy(c => c.Ruang)
    .Select(g => new { Ruang = g.Key, RataRata = g.Average(c => c.Suhu), Jumlah = g.Count() });
foreach (var baris in ringkasan)
{
    Console.WriteLine($"{baris.Ruang}: rata-rata {baris.RataRata:F1} ({baris.Jumlah} catatan)");
}

// Join: antrean tiket loket digabung dengan daftar layanan
List<Tiket> antrean = new()
{
    new Tiket("A-01", "KRT"),
    new Tiket("A-02", "SRT"),
    new Tiket("A-03", "KRT")
};
List<Layanan> daftarLayanan = new()
{
    new Layanan("KRT", "Pencetakan kartu", 5000),
    new Layanan("SRT", "Pengambilan surat", 3000)
};
var gabungan =
    from t in antrean
    join l in daftarLayanan on t.KodeLayanan equals l.Kode
    select $"{t.Nomor}: {l.Nama}, tarif {l.Tarif}";
foreach (string baris in gabungan)
{
    Console.WriteLine(baris);
}

record CatatanSuhu(string Ruang, double Suhu);
record Tiket(string Nomor, string KodeLayanan);
record Layanan(string Kode, string Nama, int Tarif);
text Keluaran
Lab: 24.5, 25.5
Kantor: 26, 27.5
Gudang: 30
Lab: rata-rata 25.0 (2 catatan)
Kantor: rata-rata 26.8 (2 catatan)
Gudang: rata-rata 30.0 (1 catatan)
A-01: Pencetakan kartu, tarif 5000
A-02: Pengambilan surat, tarif 3000
A-03: Pencetakan kartu, tarif 5000

Jalankan program tersebut dari folder topik dengan perintah berikut.

bash
# aktifkan .NET yang terpasang di sandbox
export DOTNET_ROOT=/home/kal/.dotnet PATH=/home/kal/.dotnet:$PATH DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1
# jalankan program contoh dari folder topik
dotnet run --file Program.cs
text Keluaran
Lab: 24.5, 25.5
Kantor: 26, 27.5
Gudang: 30
Lab: rata-rata 25.0 (2 catatan)
Kantor: rata-rata 26.8 (2 catatan)
Gudang: rata-rata 30.0 (1 catatan)
A-01: Pencetakan kartu, tarif 5000
A-02: Pengambilan surat, tarif 3000
A-03: Pencetakan kartu, tarif 5000

Kesalahan umum#

  • Mengurutkan data dengan asumsi urutan grup dari GroupBy yang diatur sendiri. Urutan grup mengikuti kemunculan kunci, sedangkan urutan tampilan tertentu dicapai dengan menambahkan OrderBy setelahnya.
  • Mengira Join menyertakan baris yang tidak punya pasangan. Sifat inner join menyaring baris itu, sehingga kebutuhan penyertaan lengkap diarahkan ke GroupJoin disertai DefaultIfEmpty().
  • Menumpuk perulangan foreach bersarang untuk mencocokkan dua daftar secara manual. Pola tersebut berjalan jauh lebih lambat pada data besar, sedangkan Join memetakan kuncinya sekali untuk seluruh proses.

Ringkasan#

  • GroupBy menghasilkan grup bertipe IGrouping yang membawa kunci pada Key dan isinya sebagai barisan, dengan urutan grup mengikuti kemunculan kunci.
  • Agregasi per grup menggunakan Count, Average, Max, Min, dan Sum di dalam Select, dan bentuk query menyediakan klausanya melalui group ... by ... into.
  • Join menyandingkan dua sumber menurut kunci yang cocok dengan sifat inner join, baik dalam bentuk metode maupun klausa join ... on ... equals.
  • Kebutuhan menyertakan baris tanpa pasangan diarahkan ke GroupJoin disertai DefaultIfEmpty().

Referensi#