Pengenalan purrr

Iterasi gaya tidy dengan keluarga fungsi map.

Daftar isi

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:

r
library(tidyverse)
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

Dataset contoh memuat delapan baris nilai ujian dari empat siswa pada dua kelas. Tujuan iterasinya adalah menghitung rata-rata ujian untuk setiap kelas:

r
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
text Keluaran
# 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       76

Dengan 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:

r
per_kelas <- split(nilai, nilai$kelas)
names(per_kelas)
class(per_kelas[["7A"]])
text Keluaran
[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:

r
map(per_kelas, ~ mean(.x$ujian))
text Keluaran
$`7A`
[1] 85.75

$`7B`
[1] 82.5

Perulangan 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():

r
map_dbl(per_kelas, ~ mean(.x$ujian))
text Keluaran
   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:

r
map_int(per_kelas, nrow)
map_chr(per_kelas, ~ paste0("rata-rata ", round(mean(.x$ujian), 1)))
text Keluaran
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:

r
map_dfr(per_kelas, ~ tibble(
  kelas = .x$kelas[1],
  rata_rata = mean(.x$ujian),
  jumlah_siswa = nrow(.x)
))
text Keluaran
# A tibble: 2 × 3
  kelas rata_rata jumlah_siswa
  <chr>     <dbl>        <int>
1 7A         85.8            4
2 7B         82.5            4

Pola 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:

r
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)))
text Keluaran
[1] 86.00 82.75

Elemen 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:

r
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))
text Keluaran
# A tibble: 2 × 3
  kelas ujian     rata_rata
  <chr> <list>        <dbl>
1 7A    <dbl [4]>      85.8
2 7B    <dbl [4]>      82.5

Satu 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():

r
sapply(per_kelas, function(df) mean(df$ujian))
text Keluaran
   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:

FungsiBentuk 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, sedangkan map_dbl(), map_int(), map_chr(), dan map_lgl() mengembalikan vektor bertipe.
  • Formula anonim ~ .x adalah singkatan dari fungsi satu argumen, dan map2() mengolah dua list yang sejajar dengan .x dan .y.
  • map_dfr() menyusun hasil berupa tibble dari setiap iterasi menjadi satu tibble utuh.
  • Berbeda dengan sapply() yang menebak bentuk keluarannya, keluarga map mengunci bentuk keluaran pada namanya sehingga perilakunya dapat diandalkan.

Referensi#