Data nyata hampir tidak pernah tinggal dalam satu file. Master produk ada di satu berkas, detail kategori di berkas lain, dan pembaruan stok datang setiap minggu dari sistem yang berbeda. Menggabungkan dataset adalah keterampilan menyatukan potongan-potongan itu menjadi satu gambaran utuh — dan sekaligus keterampilan bertahan dari dua risikonya: kunci yang tidak cocok dan nilai yang saling bertentangan. Chapter ini merangkai pola join, concat, dan operasi himpunan dari chapter Set dan Dictionary Comprehensions menjadi teknik penggabungan yang bisa dipertanggungjawabkan.
Data Latihan #
Kita melanjutkan data produk.csv dari chapter Membaca Data CSV, kini dilengkapi master kategori. Simpan berikut sebagai kategori.csv:
kategori,diskon_persen,gudang
minuman,10,Gudang A
bahan pokok,5,Gudang B
camilan,15,Gudang A
elektronik,0,Gudang CPerhatikan dua jebakan yang sengaja ditanam: produk P012 di produk.csv memiliki kategori aksesoris yang tidak terdaftar di sini, dan kategori elektronik tidak dimiliki satu pun produk. Keduanya akan tertangkap pemeriksaan integritas di akhir chapter.
Join Berbasis Kunci dengan Dictionary Index #
Join menggabungkan dua list record berdasarkan kunci yang sama. Langkah pertamanya selalu: bangun dictionary index dari sisi yang dicari, lalu akses lewat kuncinya:
import csv
def baca_produk(nama_berkas):
hasil = []
with open(nama_berkas, newline="", encoding="utf-8") as berkas:
for baris in csv.DictReader(berkas):
try:
hasil.append({
"id": baris["id"],
"nama": baris["nama"],
"harga": int(baris["harga"]),
"kategori": baris["kategori"],
"stok": int(baris["stok"]),
})
except ValueError:
continue
return hasil
def baca_kategori(nama_berkas):
hasil = []
with open(nama_berkas, newline="", encoding="utf-8") as berkas:
for baris in csv.DictReader(berkas):
hasil.append({
"kategori": baris["kategori"],
"diskon_persen": int(baris["diskon_persen"]),
"gudang": baris["gudang"],
})
return hasil
produk = baca_produk("produk.csv")
kategori = baca_kategori("kategori.csv")
indeks_kategori = {k["kategori"]: k for k in kategori}
hasil_join = []
for p in produk:
detail = indeks_kategori.get(p["kategori"])
if detail is None:
continue
hasil_join.append({
**p,
"diskon_persen": detail["diskon_persen"],
"gudang": detail["gudang"],
})
print(f"{len(hasil_join)} dari {len(produk)} produk berhasil digabung")
print(hasil_join[0])Output yang Dihasilkan:
9 dari 10 produk berhasil digabung
{'id': 'P001', 'nama': 'Kopi Gayo', 'harga': 85000, 'kategori': 'minuman', 'stok': 120, 'diskon_persen': 10, 'gudang': 'Gudang A'}Method get() mengembalikan None untuk kunci tak dikenal, sehingga join tidak pernah melempar KeyError — baris tanpa pasangan bisa diputuskan secara sadar: di sini kita lewati, dan nanti kita laporkan. Pattern {**p, ...} menyalin record produk lalu menempel kolom baru dari sisi kategori, persis seperti field turunan di chapter Transformasi Data.
Concat dan Resolusi Konflik #
Concat menempel list demi list — cara paling murah menggabungkan data periode yang sama strukturnya. Konflik muncul ketika kunci yang sama muncul di kedua list, misalnya data kuartal kedua berisi P005 yang stoknya sudah diperbarui:
id,nama,harga,kategori,stok
P005,Keripik Singkong,15000,camilan,180
P013,Teh Hijau,38000,minuman,150
P014,Roti Tawar,18000,bahan pokok,60Simpan sebagai produk_q2.csv, lalu gabungkan:
produk_q2 = baca_produk("produk_q2.csv")
semua = produk + produk_q2
print(f"Concat: {len(produk)} + {len(produk_q2)} = {len(semua)} baris")
def gabung_rekaman(baris_list, kunci, strategi="last"):
hasil = {}
for baris in baris_list:
if strategi == "first" and baris[kunci] in hasil:
continue
hasil[baris[kunci]] = baris
return list(hasil.values())
last_wins = {p["id"]: p for p in semua}
first_wins = gabung_rekaman(semua, "id", strategi="first")
print(f"P005 last-wins stok : {last_wins['P005']['stok']}")
indeks_first = {p["id"]: p for p in first_wins}
print(f"P005 first-wins stok: {indeks_first['P005']['stok']}")Output yang Dihasilkan:
Concat: 10 + 3 = 13 baris
P005 last-wins stok : 180
P005 first-wins stok: 250Dua strategi klasik menghasilkan dua jawaban: assignment dictionary (hasil[kunci] = baris) menimpa sehingga data terakhir menang, sedangkan continue saat kunci sudah ada mempertahankan data pertama. Pilihan “last wins” tepat untuk snapshot terbaru; “first wins” tepat untuk data yang tidak boleh berubah setelah terekam. Yang penting adalah keputusannya sadar dan terdokumentasi, bukan kebetulan urutan.
Operasi Himpunan pada Kunci #
Sebelum menggabungkan, kita sering perlu tahu apa saja bedanya. Konversi ke set membuka operasi himpunan langsung pada kunci:
id_q1 = {p["id"] for p in produk}
id_q2 = {p["id"] for p in produk_q2}
print(f"Baru di Q2 : {sorted(id_q2 - id_q1)}")
print(f"Ada di keduanya : {sorted(id_q1 & id_q2)}")
print(f"Total id unik : {len(id_q1 | id_q2)}")Output yang Dihasilkan:
Baru di Q2 : ['P013', 'P014']
Ada di keduanya : ['P005']
Total id unik : 12Operasi - (selisih) menemukan id baru, & (irisan) menemukan id yang tumpang tindih — kandidat konflik, dan | (gabungan) menghitung cakupan total. Hasil selisih dan irisan kita urutkan dengan sorted() karena urutan iterasi set tidak dijamin.
Menggabungkan Pembaruan: Nilai Pertama yang Tidak Kosong #
Kasus ketiga adalah patch: berkas pembaruan yang hanya berisi sebagian kolom, di mana nilai kosong berarti “tidak berubah”. Simpan berikut sebagai update_stok.csv:
id,stok
P003,60
P004,
P005,180Aturan penggabungannya: perbarui hanya kolom yang berisi nilai; None atau string kosong berarti pertahankan nilai lama — pola first non-null:
with open("update_stok.csv", newline="", encoding="utf-8") as berkas:
pembaruan = [
{"id": b["id"], "stok": int(b["stok"]) if b["stok"] else None}
for b in csv.DictReader(berkas)
]
def terapkan_pembaruan(baris_list, daftar_pembaruan):
hasil = {b["id"]: dict(b) for b in baris_list}
for perubahan in daftar_pembaruan:
target = hasil[perubahan["id"]]
for kolom, nilai in perubahan.items():
if kolom == "id" or nilai is None:
continue
target[kolom] = nilai
return list(hasil.values())
hasil_update = terapkan_pembaruan(produk, pembaruan)
indeks_hasil = {p["id"]: p for p in hasil_update}
for id_produk in ("P003", "P004", "P005"):
print(f"{id_produk}: stok = {indeks_hasil[id_produk]['stok']}")Output yang Dihasilkan:
P003: stok = 60
P004: stok = 40
P005: stok = 180Stok P003 dan P005 diganti dengan nilai baru, sedangkan P004 yang kosong di berkas pembaruan tetap mempertahankan stok lamanya (40). Guard if kolom == "id" or nilai is None adalah satu-satunya baris yang membedakan patch dari timpaan.
Pemeriksaan Integritas: Kunci Yatim #
Setiap penggabungan wajib diakhiri pertanyaan: ada kunci yang tidak menemukan pasangannya? Perbedaan himpunan pada kedua arah menjawabnya:
def laporan_yatim(daftar_produk, daftar_kategori):
terdaftar = {k["kategori"] for k in daftar_kategori}
dipakai = {p["kategori"] for p in daftar_produk}
return sorted(dipakai - terdaftar), sorted(terdaftar - dipakai)
tanpa_kategori, tanpa_produk = laporan_yatim(produk, kategori)
print(f"Produk dengan kategori tak terdaftar: {tanpa_kategori}")
print(f"Kategori tanpa satu pun produk : {tanpa_produk}")Output yang Dihasilkan:
Produk dengan kategori tak terdaftar: ['aksesoris']
Kategori tanpa satu pun produk : ['elektronik']Padanan pandas: merge #
pandas merangkum seluruh pola di atas dalam satu method merge(). Parameter how="outer" mempertahankan kedua sisi, dan indicator=True menandai asal tiap baris:
import pandas as pd
df_produk = pd.read_csv("produk.csv")
df_kategori = pd.read_csv("kategori.csv")
df_gabung = df_produk.merge(df_kategori, on="kategori", how="outer", indicator=True)
print(df_gabung["_merge"].value_counts().to_dict())
print("left_only :", df_gabung.loc[df_gabung["_merge"] == "left_only", "id"].tolist())
print("right_only:", df_gabung.loc[df_gabung["_merge"] == "right_only", "kategori"].tolist())Output yang Dihasilkan:
{'both': 11, 'left_only': 1, 'right_only': 1}
left_only : ['P012']
right_only: ['elektronik']Kolom _merge menunjukkan 11 pasangan cocok, satu produk tanpa kategori (P012), dan satu kategori tanpa produk (elektronik) — laporan yatim versi pandas, selaras dengan hasil standard library kita. Untuk menyusun data yang sudah bersih dan digabung menjadi informasi final, tiga pertanyaan tersisa: bagaimana jika datanya besar, dan bagaimana merangkai semua ini menjadi pipeline.