summarize dan group_by

Pola split-apply-combine untuk statistik per kelompok.

Daftar isi

Topik ini membahas dua verba dplyr yang bekerja berpasangan, yaitu summarize() untuk meringkas tabel menjadi statistik agregat dan group_by() untuk mengelompokkan baris agar perhitungan tersebut dijalankan per kelompok. Keduanya membentuk pola split-apply-combine, yaitu memecah data menjadi kelompok, menerapkan satu perhitungan pada setiap kelompok, lalu menggabungkan hasilnya kembali. Pembahasan ditutup dengan penyusunan rantai verba lengkap dalam satu pipe. Dataset contoh didefinisikan ulang di awal topik agar seluruh contoh tetap dapat dijalankan ulang secara mandiri.

r
library(tidyverse)
nilai <- tibble::tibble(
  nama = c("Ali", "Budi", "Citra", "Dewi", "Eka", "Fajar", "Gita", "Hana"),
  kelas = c("7A", "7B", "7A", "7B", "7A", "7B", "7A", "7B"),
  nilai = c(88, 74, 92, 65, 81, 79, 95, 70),
  kehadiran = c(0.95, 0.88, 1.00, 0.75, 0.92, 0.85, 0.98, 0.80)
)
text Keluaran
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.1     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.3     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Merangkum tabel dengan summarize()#

Verba summarize() menerima tabel sebagai argumen pertamanya, lalu mengembalikan tabel baru yang berisi satu baris dengan kolom-kolom hasil perhitungan. Setiap argumennya berupa ekspresi penugasan sebagaimana mutate(), tetapi ekspresinya harus menghasilkan satu nilai, biasanya berupa fungsi statistik yang diterapkan pada satu kolom.

r
nilai |>
  summarize(
    rata = mean(nilai),
    sd = sd(nilai),
    terendah = min(nilai),
    tertinggi = max(nilai)
  )
text Keluaran
# A tibble: 1 × 4
   rata    sd terendah tertinggi
  <dbl> <dbl>    <dbl>     <dbl>
1  80.5  10.7       65        95

Hasilnya satu baris yang meringkas seluruh baris tabel asalnya, yaitu rata-rata 80,5 dengan simpangan baku sekitar 10,7. Fungsi statistik yang dipakai adalah fungsi bawaan R seperti mean(), sd(), min(), dan max(), sehingga verba summarize() sendiri tidak menghitung apa pun dan hanya mengatur cara hasilnya tersusun menjadi tabel.

Penghitung banyaknya baris dan nilai unik#

Dua fungsi khusus sering dipakai di dalam summarize() karena maknanya bergantung pada tabel yang sedang diringkas. Fungsi n() menghitung banyaknya baris tanpa memerlukan argumen, sedangkan n_distinct() menghitung banyaknya nilai berbeda pada kolom yang disebutkan.

r
nilai |>
  summarize(
    banyak_baris = n(),
    banyak_kelas = n_distinct(kelas),
    banyak_nama = n_distinct(nama)
  )
text Keluaran
# A tibble: 1 × 3
  banyak_baris banyak_kelas banyak_nama
         <int>        <int>       <int>
1            8            2           8

Hasilnya menyatakan bahwa tabel memuat delapan baris, dua kelas, dan delapan nama yang semuanya berbeda. Apabila data berisi nama ganda, n_distinct(nama) tetap menghitung sekali untuk setiap nama, sehingga hasilnya bisa lebih kecil daripada n().

Pengelompokan dengan group_by()#

Fungsi group_by() tidak mengubah isi tabel maupun urutan barisnya, melainkan menandai bahwa baris-barisnya terbagi menjadi kelompok-kelompok menurut kolom yang disebutkan. Tanda pengelompokan itu terlihat pada cetakannya.

r
per_kelas <- nilai |> group_by(kelas)
per_kelas
text Keluaran
# A tibble: 8 × 4
# Groups:   kelas [2]
  nama  kelas nilai kehadiran
  <chr> <chr> <dbl>     <dbl>
1 Ali   7A       88      0.95
2 Budi  7B       74      0.88
3 Citra 7A       92      1   
4 Dewi  7B       65      0.75
5 Eka   7A       81      0.92
6 Fajar 7B       79      0.85
7 Gita  7A       95      0.98
8 Hana  7B       70      0.8 

Baris Groups: kelas [2] menyatakan bahwa tabel tergrup menjadi dua kelompok menurut kolom kelas. Apabila summarize() dijalankan pada tabel tergrup, perhitungannya dijalankan pada setiap kelompok secara terpisah, kemudian hasilnya digabung menjadi satu baris untuk setiap kelompok. Inilah pola split-apply-combine pada praktiknya, yaitu kelas 7A dan 7B dipecah, statistiknya dihitung masing-masing, lalu hasilnya disatukan. Perhitungan hanya ditulis satu kali untuk seluruh kelompok, tanpa memilah baris secara manual untuk setiap kelas.

r
per_kelas |>
  summarize(
    banyak_siswa = n(),
    rata_nilai = mean(nilai),
    sd_nilai = sd(nilai)
  )
text Keluaran
# A tibble: 2 × 4
  kelas banyak_siswa rata_nilai sd_nilai
  <chr>        <int>      <dbl>    <dbl>
1 7A               4         89     6.06
2 7B               4         72     5.94

Fungsi n() kini menghasilkan banyaknya baris per kelompok, yaitu empat siswa untuk masing-masing kelas, dan rata-rata dihitung terpisah untuk 7A dan 7B. Hasil keluaran di atas juga tidak lagi memuat baris Groups, karena summarize() melepas satu lapis pengelompokan setelah perhitungannya selesai. Apabila pengelompokannya bertingkat dengan dua kolom, satu lapis dilepas dan sisanya tetap menempel pada hasilnya.

Melepas pengelompokan dengan ungroup()#

Tabel tergrup yang disimpan dalam variabel tetap tergrup sampai pengelompokannya dilepas secara eksplisit. Fungsi ungroup() menghapus tanda kelompok tersebut dan mengembalikan tabel biasa tanpa mengubah isinya.

r
per_kelas |> ungroup()
text Keluaran
# A tibble: 8 × 4
  nama  kelas nilai kehadiran
  <chr> <chr> <dbl>     <dbl>
1 Ali   7A       88      0.95
2 Budi  7B       74      0.88
3 Citra 7A       92      1   
4 Dewi  7B       65      0.75
5 Eka   7A       81      0.92
6 Fajar 7B       79      0.85
7 Gita  7A       95      0.98
8 Hana  7B       70      0.8 

Dampaknya terlihat pada perhitungan yang melibatkan seluruh tabel. Ekspresi berikut dimaksudkan menghitung porsi setiap siswa terhadap jumlah nilai seluruh tabel, tetapi tabel per_kelas masih tergrup, sehingga sum(nilai) dihitung di dalam setiap kelompoknya.

r
per_kelas |>
  mutate(bagian = nilai / sum(nilai))
text Keluaran
# A tibble: 8 × 5
# Groups:   kelas [2]
  nama  kelas nilai kehadiran bagian
  <chr> <chr> <dbl>     <dbl>  <dbl>
1 Ali   7A       88      0.95  0.247
2 Budi  7B       74      0.88  0.257
3 Citra 7A       92      1     0.258
4 Dewi  7B       65      0.75  0.226
5 Eka   7A       81      0.92  0.228
6 Fajar 7B       79      0.85  0.274
7 Gita  7A       95      0.98  0.267
8 Hana  7B       70      0.8   0.243

Porsi Ali yang bernilai 88 dihitung terhadap 356, yaitu jumlah nilai kelas 7A, dan bukan terhadap 644 sebagai jumlah seluruh tabel. Perilaku ini bisa dimanfaatkan apabila memang diinginkan, tetapi menjadi jebakan apabila pengelompokannya tersisa tanpa sengaja. Pemanggilan ungroup() lebih dahulu menghasilkan porsi terhadap jumlah seluruh tabel.

Rantai verba dalam satu pipe#

Seluruh verba pada modul ini dapat disambung dalam satu rantai pipe, dan kombinasi group_by() dengan summarize() biasanya menjadi intinya. Rantai berikut menghitung statistik per kelas, lalu mengurutkan hasilnya dari rata-rata tertinggi.

r
nilai |>
  group_by(kelas) |>
  summarize(
    banyak_siswa = n(),
    rata_nilai = mean(nilai)
  ) |>
  arrange(desc(rata_nilai))
text Keluaran
# A tibble: 2 × 3
  kelas banyak_siswa rata_nilai
  <chr>        <int>      <dbl>
1 7A               4         89
2 7B               4         72

Verba pada rantai itu dijalankan berurutan, yaitu pengelompokan, peringkasan, lalu pengurutan, sehingga arrange() bekerja pada tabel hasil peringkasan yang kolomnya baru saja dibentuk. Verba filter() juga dapat disisipkan untuk memilih kelompok yang hasil agregatnya memenuhi kondisi tertentu.

r
nilai |>
  group_by(kelas) |>
  summarize(
    banyak_siswa = n(),
    rata_nilai = mean(nilai),
    rata_kehadiran = mean(kehadiran)
  ) |>
  filter(rata_nilai >= 75) |>
  arrange(desc(rata_kehadiran))
text Keluaran
# A tibble: 1 × 4
  kelas banyak_siswa rata_nilai rata_kehadiran
  <chr>        <int>      <dbl>          <dbl>
1 7A               4         89          0.962

Kelas 7B tersaring karena rata-rata nilainya 72 berada di bawah batas 75, sehingga yang tersisa hanya kelas 7A dengan rata-rata kehadiran 0,962. Kondisi pada filter() di sini menguji kolom hasil agregat, bukan baris data mentahnya, dan pemisahan dua tahap inilah yang membuat statistik per kelompok dapat dibangun bertingkat dengan rapi. Rantai semacam ini juga mudah diperluas, misalnya menambah kolom agregat lain pada summarize() tanpa mengubah tahapan lainnya.

Ringkasan#

  • summarize() meringkas tabel menjadi satu baris per kelompok, dengan setiap kolom hasilnya berupa fungsi statistik seperti mean(), sd(), min(), atau max() yang menghasilkan satu nilai.
  • n() menghitung banyaknya baris pada kelompoknya, sedangkan n_distinct() menghitung banyaknya nilai berbeda pada kolom yang disebutkan.
  • group_by() menandai pembagian kelompok tanpa mengubah isi tabel, dan summarize() pada tabel tergrup menjalankan pola split-apply-combine.
  • summarize() melepas satu lapis pengelompokan, sedangkan ungroup() melepas seluruhnya, dan tabel tergrup yang lupa dilepas membuat verba berikutnya bekerja per kelompok.
  • Rantai group_by(), summarize(), filter(), dan arrange() menyusun perhitungan per kelompok secara bertingkat dalam satu pipe.

Referensi#