Topik ini membahas purrr, yaitu paket tidyverse untuk iterasi yang menggantikan perulangan for pada banyak keperluan analisis data. Pembahasannya mencakup keluarga fungsi map beserta turunan bertipe keluarannya, formula anonim ~ .x, perpaduan dua masukan dengan map2(), dan pemanfaatan purrr pada kolom list tibble. Seluruh contoh berjalan di atas satu dataset nilai ujian yang dipecah per kelas, sehingga setiap keluaran dapat dihitung ulang dengan mudah.
Dari for ke map#
Iterasi diperlukan setiap kali satu operasi harus diulang untuk setiap anggota sebuah kumpulan, misalnya menghitung rata-rata untuk setiap kelas dalam dataset. Paket tidyverse dimuat lebih dahulu agar tibble() dan seluruh fungsi purrr tersedia:
library(tidyverse)── 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 errorsDataset contoh memuat delapan baris nilai ujian dari empat siswa pada dua kelas. Tujuan iterasinya adalah menghitung rata-rata ujian untuk setiap kelas:
nilai <- tibble(
siswa = c("Ali", "Budi", "Citra", "Dewi", "Ali", "Budi", "Citra", "Dewi"),
kelas = rep(c("7A", "7B"), each = 4),
ujian = c(85, 78, 92, 88, 80, 84, 90, 76)
)
nilai# A tibble: 8 × 3
siswa kelas ujian
<chr> <chr> <dbl>
1 Ali 7A 85
2 Budi 7A 78
3 Citra 7A 92
4 Dewi 7A 88
5 Ali 7B 80
6 Budi 7B 84
7 Citra 7B 90
8 Dewi 7B 76Dengan split(), tibble dipecah menjadi list berisi satu tibble per kelas. List semacam ini adalah masukan yang paling wajar untuk map(), karena setiap elemennya adalah satu unit kerja yang lengkap:
per_kelas <- split(nilai, nilai$kelas)
names(per_kelas)
class(per_kelas[["7A"]])[1] "7A" "7B"
[1] "tbl_df" "tbl" "data.frame"Fungsi map() memanggil satu fungsi untuk setiap elemen masukannya dan mengumpulkan seluruh hasil menjadi list. Pemanggilan berikut menghitung rata-rata kolom ujian pada setiap tibble kelas, dan formula anonim ~ mean(.x$ujian) menyatakan pekerjaan yang diulang:
map(per_kelas, ~ mean(.x$ujian))$`7A`
[1] 85.75
$`7B`
[1] 82.5Perulangan for dari basis R mampu menghasilkan hal yang sama, tetapi menuntut penyiapan wadah hasil, pengelolaan indeks, dan penamaannya sendiri, sehingga maksud kode tersamar di balik mesin iterasinya. map() menangani mekanisme itu secara internal, sehingga yang tertulis hanya pasangan masukan dan operasi yang diulang. Pemisahan ini membuat maksud setiap baris iterasi terbaca dari satu tempat, sekalipun operasinya bertambah rumit.
Keluarga map dan tipe keluaran#
map() selalu mengembalikan list, sedangkan turunannya mengembalikan vektor bertipe. map_dbl() menuntut hasil numerik double, map_int() menuntut bilangan bulat, map_chr() menuntut teks, dan map_lgl() menuntut nilai logika. Apabila salah satu hasil tidak dapat dikonversi ke tipe yang dijanjikan, fungsi tersebut melaporkan error alih-alih membiarkan tipe keluaran berubah-ubah. Versi bertipe dari perhitungan rata-rata di atas adalah map_dbl():
map_dbl(per_kelas, ~ mean(.x$ujian)) 7A 7B
85.75 82.50 Hasilnya kini vektor numerik dengan nama elemen yang diwarisi dari nama list masukannya, sehingga langsung dapat dipakai dalam perhitungan berikutnya. Dua turunan lain memperlihatkan pola yang sama pada tipe berbeda:
map_int(per_kelas, nrow)
map_chr(per_kelas, ~ paste0("rata-rata ", round(mean(.x$ujian), 1)))7A 7B
4 4
7A 7B
"rata-rata 85.8" "rata-rata 82.5" Formula anonim ~ .x pada contoh-contoh di atas adalah singkatan dari definisi fungsi tanpa nama. Tulisan ~ mean(.x$ujian) setara dengan function(x) mean(x$ujian), yaitu fungsi satu argumen yang memperlakukan elemen yang sedang diproses sebagai .x. Bentuk singkat ini membuat maksud setiap pemanggilan map() terbaca tanpa perlu melompat ke definisi fungsi lain, dan itulah alasan gaya ini dipakai secara luas dalam kode tidyverse.
Menyusun ulang hasil dengan map_dfr#
Apabila setiap iterasi menghasilkan satu tibble, map_dfr() menggabungkan seluruhnya dengan menempelkan baris demi baris menjadi satu tibble. Contoh berikut membuat ringkasan per kelas yang memuat nama kelas, rata-rata, dan banyak siswanya:
map_dfr(per_kelas, ~ tibble(
kelas = .x$kelas[1],
rata_rata = mean(.x$ujian),
jumlah_siswa = nrow(.x)
))# A tibble: 2 × 3
kelas rata_rata jumlah_siswa
<chr> <dbl> <int>
1 7A 85.8 4
2 7B 82.5 4Pola ini menggantikan kebiasaan mengisi kerangka hasil di dalam perulangan for dengan satu pemanggilan yang menyatakan bentuk keluarannya secara langsung. Tabel yang dibentuk setiap iterasi tidak harus sebatas ringkasan sederhana, karena kolom apa pun yang dibutuhkan dapat disertakan selama strukturnya sama antar elemen. Kolomnya juga menyesuaikan tipe hasil setiap iterasi, sehingga kolom rata_rata tampil sebagai numerik dan jumlah_siswa sebagai bilangan bulat. Struktur tibble hasilnya konsisten, sehingga rantai dplyr berikutnya dapat menyambung tanpa penyesuaian tambahan.
Dua masukan dengan map2#
Ketika iterasi membutuhkan dua kumpulan yang sejajar, map2() memproses pasangan elemennya berdampingan dengan .x sebagai elemen pertama dan .y sebagai elemen kedua. Contoh berikut menghitung rata-rata gabungan dua ujian untuk setiap kelas, dengan nilai kedua ujian disimpan dalam dua list yang panjangnya sama:
ujian_pertama <- list(c(85, 78, 92, 88), c(80, 84, 90, 76))
ujian_kedua <- list(c(88, 82, 90, 85), c(78, 86, 88, 80))
map2_dbl(ujian_pertama, ujian_kedua, ~ mean(c(.x, .y)))[1] 86.00 82.75Elemen pertama kedua list digabungkan lalu dirata-ratakan untuk kelas pertama, dan elemen kedua diperlakukan sama untuk kelas berikutnya. Kedua masukan harus panjangnya sama, kecuali salah satunya berupa satu nilai tunggal yang dipakai ulang untuk setiap iterasi. Keluarga map2 tersedia dalam varian bertipe yang sama dengan map, seperti map2_dbl() pada contoh di atas. Hasilnya berupa vektor numerik dengan panjang sama seperti masukannya, sehingga langsung dapat ditugaskan sebagai kolom baru pada tibble.
purrr pada kolom list tibble#
Kolom tibble juga dapat menampung list, dan purrr adalah pasangan alaminya untuk mengolah kolom semacam itu. Contoh berikut menyimpan nilai ujian setiap kelas sebagai kolom list, lalu map_dbl() dipanggil di dalam mutate() untuk membentuk kolom rata-ratanya:
rekap <- tibble(
kelas = c("7A", "7B"),
ujian = list(c(85, 78, 92, 88), c(80, 84, 90, 76))
)
rekap |> mutate(rata_rata = map_dbl(ujian, mean))# A tibble: 2 × 3
kelas ujian rata_rata
<chr> <list> <dbl>
1 7A <dbl [4]> 85.8
2 7B <dbl [4]> 82.5Satu baris tabel kini membawa satu kumpulan nilai utuh, dan satu pemanggilan map_dbl() merangkum setiap kumpulan menjadi satu angka. Kolom ujian tampil bertipe list pada keluaran tibble, dan setiap selnya memuat vektor utuh empat nilai. Kumpulan pada setiap sel boleh berbeda panjang, dan kolom list tetap menyimpannya tanpa penyamaan panjang maupun pengisian kosong, berbeda dari kolom biasa yang menuntut panjang seragam antar baris. Pola ini berguna apabila satu observasi secara alami memuat data berukuran bervariasi, misalnya daftar nilai siswa per kelas.
Perbandingan dengan sapply#
Base R menyediakan sapply() dengan tujuan yang sama, yaitu menerapkan satu fungsi pada setiap elemen list dan menyederhanakan hasilnya. Pada kasus di atas, hasilnya sama dengan map_dbl():
sapply(per_kelas, function(df) mean(df$ujian)) 7A 7B
85.75 82.50 Perbedaan dengan keluarga map terletak pada kontrak keluaran. sapply() menebak bentuk keluaran yang paling sederhana dari hasil iterasi, sehingga bentuknya dapat berubah antara vektor, list, atau matriks begitu data berubah, dan perubahan itu tidak disertai pesan apa pun. Keluarga map menyatakan bentuk keluarannya pada namanya sendiri, sebagaimana dirangkum pada tabel berikut:
| Fungsi | Bentuk keluaran yang dijanjikan |
|---|---|
map() | list |
map_dbl() | vektor numerik double |
map_int() | vektor bilangan bulat |
map_chr() | vektor character |
sapply() | bergantung pada hasil iterasi |
Kontrak yang tetap membuat kode lebih mudah dipelihara, karena analisis di hulu tidak berubah perilaku hanya karena datanya berubah bentuk di hilir. Perilaku penyesuaian sapply() tetap nyaman untuk eksplorasi cepat di konsol, tetapi risikonya tidak sepadan pada kode yang dipakai lama, dan keluarga map menjadi pilihan baku pada analisis tidyverse.
Ringkasan#
map()menerapkan satu fungsi pada setiap elemen list dan mengembalikan list, sedangkanmap_dbl(),map_int(),map_chr(), danmap_lgl()mengembalikan vektor bertipe.- Formula anonim
~ .xadalah singkatan dari fungsi satu argumen, danmap2()mengolah dua list yang sejajar dengan.xdan.y. map_dfr()menyusun hasil berupa tibble dari setiap iterasi menjadi satu tibble utuh.- Berbeda dengan
sapply()yang menebak bentuk keluarannya, keluargamapmengunci bentuk keluaran pada namanya sehingga perilakunya dapat diandalkan.
Referensi#
- purrr: Functional Programming Tools — tidyverse — Referensi resmi keluarga fungsi map beserta varian bertipe dan map2.
- R for Data Science, edisi ke-2 — Wickham, Çetinkaya-Rundel, Grolemund — Bab iteration membedah map, formula anonim, dan pola kolom list secara bertahap.