Manipulasi Data

Menggabungkan data dengan join, mengelompokkan dengan groupby, dan menyusun pipeline transformasi data.

Daftar isi

Manipulasi data jarang selesai dalam satu operasi. Topik ini membahas tiga kelompok operasi DataFrames.jl yang paling sering dirangkai, yaitu join untuk menggabungkan dua tabel berdasarkan kolom kunci, subset dan transform untuk menyaring baris serta menambah kolom, dan penyusunan semuanya menjadi pipeline transformasi yang terbaca dari atas ke bawah. Contohnya tetap pada ranah nilai ujian dan daftar siswa agar perubahan datanya mudah diikuti.

Menggabungkan tabel dengan join#

Join menggabungkan baris dua tabel yang cocok pada kolom kuncinya. Fungsi innerjoin hanya mengambil pasangan baris yang kuncinya ada pada kedua tabel, sedangkan leftjoin mempertahankan seluruh baris tabel kiri, dan baris kiri tanpa pasangan mendapat nilai missing pada kolom tabel kanan. Kolom kunci disebut melalui argumen kata kunci on. Urutan baris hasil join tidak dijamin oleh DataFrames.jl, sehingga contoh di bawah mengurutkannya dengan sort sebelum ditampilkan.

julia Contoh: innerjoin dan leftjoin
using DataFrames

siswa = DataFrame(
    id = [1, 2, 3, 4],
    nama = ["Ali", "Budi", "Citra", "Dewi"],
)
nilai = DataFrame(
    id = [2, 3, 5],
    nilai = [85, 91, 77],
)

gabung = sort(innerjoin(siswa, nilai; on = :id), :id)
show(stdout, gabung; allrows = true, allcols = true)
println()
kiri = sort(leftjoin(siswa, nilai; on = :id), :id)
show(stdout, kiri; allrows = true, allcols = true)
text Keluaran
2×3 DataFrame
 Row │ id     nama    nilai
     │ Int64  String  Int64
─────┼──────────────────────
   1 │     2  Budi       85
   2 │     3  Citra      91
4×3 DataFrame
 Row │ id     nama    nilai
     │ Int64  String  Int64?
─────┼────────────────────────
   1 │     1  Ali     missing
   2 │     2  Budi         85
   3 │     3  Citra        91
   4 │     4  Dewi    missing

Perhatikan tipe kolom nilai pada hasil leftjoin yang tertulis Int64?. Tanda tanya menandai kolom yang boleh memuat missing di samping bilangan bulat, karena Ali dan Dewi tidak memiliki pasangan pada tabel nilai. Pada hasil innerjoin, keduanya tidak muncul dan tipe kolom kembali menjadi Int64 murni. Pemilihan jenis join dengan demikian turut menentukan tipe kolom hasilnya.

DataFrames.jl menyediakan jenis join lain di samping keduanya, yaitu outerjoin yang mempertahankan baris dari kedua tabel, semijoin dan antijoin yang menyaring baris tabel kiri tanpa menambahkan kolom kanan, serta crossjoin yang memasangkan setiap baris tabel pertama dengan setiap baris tabel kedua. Topik ini membatasi pembahasannya pada innerjoin dan leftjoin karena keduanya paling sering dipakai pada pekerjaan data.

Menyaring baris dengan subset#

Fungsi subset menyaring baris berdasarkan syarat per kolom. Setiap pasangan :kolom => fungsi menuntut fungsi yang mengembalikan vektor Bool dengan panjang yang sama dengan jumlah baris, dan baris yang berpasangan dengan true dipertahankan. Operator perbandingan ditulis dengan titik di depannya, yaitu .>=, sehingga perbandingan berjalan secara broadcast untuk seluruh elemen kolom. Sama seperti select, fungsi subset mengembalikan salinan, sedangkan subset! memutasi DataFrame aslinya.

julia Contoh: subset dengan ambang batas
using DataFrames

nilai = DataFrame(
    nama = ["Ali", "Budi", "Citra", "Dewi", "Eko", "Fajar"],
    kelas = ["7A", "7A", "7A", "7B", "7B", "7B"],
    nilai = [78, 85, 91, 82, 74, 88],
)

lulus = subset(nilai, :nilai => n -> n .>= 80)
show(stdout, lulus; allrows = true, allcols = true)
text Keluaran
4×3 DataFrame
 Row │ nama    kelas   nilai
     │ String  String  Int64
─────┼───────────────────────
   1 │ Budi    7A         85
   2 │ Citra   7A         91
   3 │ Dewi    7B         82
   4 │ Fajar   7B         88

Empat baris tersisa adalah Budi, Citra, Dewi, dan Fajar, dengan urutan baris aslinya dipertahankan. Ali dengan nilai 78 dan Eko dengan nilai 74 tersaring karena berada di bawah ambang batas. Syarat berlapis ditulis dengan menambahkan pasangan kolom berikutnya, misalnya :kelas => k -> k .== "7A" untuk menyaring kelasnya sekaligus.

Menambah kolom dengan transform#

Fungsi transform menambah atau mengganti kolom dari hasil fungsi tanpa mengubah jumlah baris. Pembungkus ByRow menjalankan fungsi skalar per baris, sehingga ekspresi bersyarat seperti penentuan status lulus cukup ditulis untuk satu nilai dan diterapkan ke seluruh kolom. Pola :kolom => fungsi => :kolom_baru pada transform sama dengan pola yang dipakai oleh combine, sehingga keduanya terasa konsisten.

julia Contoh: transform dengan ByRow
using DataFrames

nilai = DataFrame(
    nama = ["Ali", "Budi", "Citra", "Dewi", "Eko", "Fajar"],
    kelas = ["7A", "7A", "7A", "7B", "7B", "7B"],
    nilai = [78, 85, 91, 82, 74, 88],
)

berstatus = transform(
    nilai,
    :nilai => ByRow(n -> n >= 80 ? "Lulus" : "Remedial") => :status,
)
println(berstatus.status)
text Keluaran
["Remedial", "Lulus", "Lulus", "Lulus", "Remedial", "Lulus"]

Kolom status bernilai Remedial untuk Ali dan Eko, sedangkan empat siswa lainnya bernilai Lulus. Ekspresi n >= 80 ? "Lulus" : "Remedial" memakai operator ternary Julia yang mengembalikan salah satu dari dua nilai sesuai syaratnya. Kolom baru diletakkan pada posisi terakhir, dan nama kolomnya ditentukan melalui pasangan penamaan di ujung argumen. Fungsi transform menerima beberapa pasangan dalam satu panggilan, sehingga beberapa kolom baru dapat dibentuk sekaligus, dan varian transform! memutasi DataFrame aslinya.

Menyusun pipeline transformasi#

Operasi satu langkah jarang cukup, sehingga DataFrames.jl dirancang agar hasil satu fungsi menjadi masukan fungsi berikutnya. Pipeline pada contoh berikut terdiri atas empat tahap yang masing-masing menyimpan hasil antara ke variabel, sehingga setiap tahap dapat diperiksa sebelum dilanjutkan. Package DataFramesMeta menyediakan makro @chain untuk menulis rangkaian semacam ini dengan gaya berantai, tetapi contoh di sini sengaja memakai fungsi bawaan DataFrames.jl agar dependensinya minim.

julia pipeline.jl
# pipeline.jl
# Pipeline transformasi: transform, subset, groupby, combine, select.

using DataFrames, Statistics

nilai = DataFrame(
    nama = ["Ali", "Budi", "Citra", "Dewi", "Eko", "Fajar"],
    kelas = ["7A", "7A", "7A", "7B", "7B", "7B"],
    nilai = [78, 85, 91, 82, 74, 88],
)

# 1. tambahkan kolom status dari nilai per baris
berstatus = transform(
    nilai,
    :nilai => ByRow(n -> n >= 80 ? "Lulus" : "Remedial") => :status,
)

# 2. ambil baris siswa yang lulus saja
lulus = subset(berstatus, :status => s -> s .== "Lulus")

# 3. agregasi per kelas
ringkasan = combine(
    groupby(lulus, :kelas),
    :nilai => mean => :rata_rata,
    :nilai => length => :jumlah,
)

# 4. susun kolom yang disajikan
hasil = select(ringkasan, :kelas, :jumlah, :rata_rata)
println(names(hasil))
show(stdout, hasil; allrows = true, allcols = true)
text Keluaran
["kelas", "jumlah", "rata_rata"]
2×3 DataFrame
 Row │ kelas   jumlah  rata_rata
     │ String  Int64   Float64
─────┼───────────────────────────
   1 │ 7A           2       88.0
   2 │ 7B           2       85.0

Keluarannya menunjukkan dua baris hasil, yaitu kelas 7A dengan dua siswa lulus dan rata-rata 88.0, serta kelas 7B dengan dua siswa lulus dan rata-rata 85.0. Ali dan Eko tersaring pada tahap subset karena statusnya Remedial. Tahap terakhir select menyusun ulang kolom agar jumlah tampil sebelum rata_rata, dan names(hasil) pada keluaran mengonfirmasi urutannya.

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat operasi tabel dirangkai. Yang pertama dapat direproduksi pada contoh berikut.

julia Contoh: join dengan nama kolom kunci yang salah
using DataFrames

siswa = DataFrame(
    id = [1, 2, 3, 4],
    nama = ["Ali", "Budi", "Citra", "Dewi"],
)
nilai = DataFrame(
    id = [2, 3, 5],
    nilai = [85, 91, 77],
)

try
    innerjoin(siswa, nilai; on = :nomor)
catch e
    println(typeof(e))
end
text Keluaran
ArgumentError
  • Menulis nama kolom kunci yang tidak ada pada salah satu tabel. Contoh di atas memanggil innerjoin dengan :nomor, dan eksekusinya berhenti dengan ArgumentError. Cocokkan kunci kedua tabel melalui names terlebih dahulu.
  • Lupa operator titik pada fungsi subset. Fungsi yang mengembalikan nilai tunggal seperti n -> n >= 80 berhenti dengan MethodError, sedangkan bentuk broadcast n -> n .>= 80 mengembalikan vektor Bool yang diminta.
  • Memproses kolom hasil leftjoin tanpa menangani missing. Bungkus perhitungannya dengan skipmissing apabila baris tanpa pasangan memang ingin dikesampingkan.

Ringkasan#

  • innerjoin mengambil pasangan yang cocok pada kedua tabel, sedangkan leftjoin mempertahankan seluruh baris kiri dan mengisi kolom kanan dengan missing.
  • subset menyaring baris dengan fungsi yang mengembalikan vektor Bool, dan transform menambah kolom, baik per kolom maupun per baris melalui ByRow.
  • Pipeline transformasi dirangkai dari fungsi yang mengembalikan salinan, sehingga setiap tahap dapat disimpan dan diperiksa.
  • Urutan baris hasil join tidak dijamin, sehingga hasilnya diurutkan dengan sort apabila urutannya penting.

Referensi#