Data penjualan kita kini bersih kolom demi kolom, tetapi satu kendala masih menghadang: data nyata jarang datang sebagai satu tabel yang rapi. Toko Kopi Kita menyimpan rekap Januari dan Februari di dua berkas terpisah, sementara data pelanggan beserta kota dan status keanggotaannya hidup di tabel lain. Dua kebutuhan yang berbeda — menumpuk tabel yang strukturnya sama, dan merangkai tabel yang saling melengkapi — membutuhkan dua peralatan yang berbeda pula. Pada chapter ini kita memakai pd.concat untuk yang pertama dan pd.merge
untuk yang kedua, lengkap dengan validate, indicator, dan suffixes untuk menjaga penggabungan tetap terkendali.
Dua Berkas Bulanan dan Satu Tabel Pelanggan #
Berkas latihan kita terdiri dari dua rekap bulanan berkolom sama persis: tanggal, menu, jumlah, harga, dan pelanggan. Tanggal sudah tersimpan dalam bentuk baku ISO sesuai chapter Menangani Format Tanggal
, sehingga kedua berkas siap digabungkan kapan pun.
tanggal,menu,jumlah,harga,pelanggan
2026-01-15,kopi susu,3,24000,Budi
2026-01-15,teh tawar,2,20000,Siti
2026-01-16,kopi susu,1,24000,Dewi
2026-01-17,americano,2,22000,Budi
2026-01-18,matcha latte,4,28000,Rani
2026-01-19,teh tawar,1,20000,Agus
2026-01-20,kopi susu,2,24000,Siti
2026-01-21,es kopi susu,3,26000,Farhantanggal,menu,jumlah,harga,pelanggan
2026-02-01,kopi susu,2,24000,Dewi
2026-02-02,teh tawar,2,20000,Budi
2026-02-03,americano,1,22000,Rani
2026-02-05,matcha latte,3,28000,Siti
2026-02-07,kopi susu,4,24000,Agus
2026-02-10,es kopi susu,2,26000,TonoPerhatikan satu detail di baris terakhir Februari: pelanggan Tono tidak akan kita daftarkan di tabel pelanggan. Satu nama tanpa berkas induk itu sengaja dipelihara — ia menjadi bahan uji ketika kita membedah baris yang gagal berpasangan di akhir chapter.
Menumpuk Berkas Bulanan dengan pd.concat #
pd.concat menyambungkan tabel-tabel yang strukturnya sepadan — berkas bulanan, laporan per cabang, hasil ekspor yang terpotong. Ia bekerja seperti menumpuk kertas: seluruh baris Januari diikuti seluruh baris Februari, kolom tetap yang lima itu juga.
import pandas as pd
januari = pd.read_csv("penjualan_januari.csv")
februari = pd.read_csv("penjualan_februari.csv")
print("Januari:", januari.shape, "| Februari:", februari.shape)
dua_bulan = pd.concat([januari, februari], ignore_index=True)
print("Setelah ditumpuk:", dua_bulan.shape)
print()
print(dua_bulan)Output yang Dihasilkan:
Januari: (8, 5) | Februari: (6, 5)
Setelah ditumpuk: (14, 5)
tanggal menu jumlah harga pelanggan
0 2026-01-15 kopi susu 3 24000 Budi
1 2026-01-15 teh tawar 2 20000 Siti
2 2026-01-16 kopi susu 1 24000 Dewi
3 2026-01-17 americano 2 22000 Budi
4 2026-01-18 matcha latte 4 28000 Rani
5 2026-01-19 teh tawar 1 20000 Agus
6 2026-01-20 kopi susu 2 24000 Siti
7 2026-01-21 es kopi susu 3 26000 Farhan
8 2026-02-01 kopi susu 2 24000 Dewi
9 2026-02-02 teh tawar 2 20000 Budi
10 2026-02-03 americano 1 22000 Rani
11 2026-02-05 matcha latte 3 28000 Siti
12 2026-02-07 kopi susu 4 24000 Agus
13 2026-02-10 es kopi susu 2 26000 TonoMari kita telaah penumpukannya:
- Baris 8 (
pd.concat([...], ignore_index=True)): daftar berkas diletakkan dalam list; urutan list menjadi urutan baris hasil.ignore_index=Truemenyusun ulang penomoran baris dari nol sampai 13 tanpa bolong. - Tanpa
ignore_index: indeks Februari tetap berlanjut dari 0, sehingga baris bernomor 0 muncul dua kali — tidak salah secara hitungan, tetapi membingungkan saat kita mengambil baris dengan.loc. Penomoran ulang adalah kebiasaan yang aman untuk data hasil tumpukan. - Bentuk hasil: 8 + 6 = 14 baris, 5 kolom — sama seperti sebelum ditumpuk.
pd.concatmemanjangkan, bukan melebar; melebarkan tabel (menempel kolom berdampingan) juga bisa lewataxis=1, tetapi itu hanya tepat bila setiap baris benar-benar berpasangan.
Menggabungkan Dua Tabel Berbeda dengan merge #
Kebutuhan kedua berbeda sifatnya: tabel transaksi dan tabel pelanggan tidak bisa ditumpuk karena kolomnya berbeda — tetapi keduanya dihubungkan kolom pelanggan. pd.merge merangkai keduanya menjadi satu tabel kaya: setiap transaksi ikut membawa kota dan status member pembelinya. Inilah inti penggabungan berbasis kunci.
import pandas as pd
februari = pd.read_csv("penjualan_februari.csv")
pelanggan = pd.DataFrame({
"pelanggan": ["Agus", "Budi", "Dewi", "Farhan", "Rani", "Siti"],
"kota": ["Bandung", "Bandung", "Cimahi",
"Bandung", "Surabaya", "Bandung"],
"member": ["ya", "ya", "tidak", "ya", "tidak", "ya"],
})
februari_kaya = pd.merge(februari, pelanggan, on="pelanggan", how="left")
print(februari_kaya[["tanggal", "menu", "pelanggan", "kota", "member"]])Output yang Dihasilkan:
tanggal menu pelanggan kota member
0 2026-02-01 kopi susu Dewi Cimahi tidak
1 2026-02-02 teh tawar Budi Bandung ya
2 2026-02-03 americano Rani Surabaya tidak
3 2026-02-05 matcha latte Siti Bandung ya
4 2026-02-07 kopi susu Agus Bandung ya
5 2026-02-10 es kopi susu Tono NaN NaNMari kita telaah penggabungannya:
- Baris 4-8 (
pelanggan = pd.DataFrame({...})): tabel induk pelanggan — satu baris per orang. Dalam praktik ia biasanya dibaca dari berkas atau basis data; di sini kita tulis langsung agar contoh berdiri sendiri. - Baris 10 (
pd.merge(..., on="pelanggan", how="left")): parameteronmenyebut kolom kunci yang dicocokkan di kedua tabel — kolom yang sudah kita seragamkan bentuknya pada chapter Menangani Data Tidak Konsisten . - Baris transaksi Tono:
how="left"mempertahankan seluruh baris Februari, sehingga transaksi Tono tetap tampil dengankotadanmemberkosong. Kualitas penggabungan seperti inilah yang membuat normalisasi kunci sebelum merge menjadi wajib, bukan pemanis.
Memilih Sisi Kebenaran: left atau inner #
Parameter how menentukan baris mana yang berhak masuk hasil. Pilihan left mempertahankan seluruh tabel kiri; pilihan inner hanya menerima baris yang punya pasangan di kedua tabel. Perbedaannya tampak justru pada nama tanpa berkas induk kita: Tono.
import pandas as pd
februari = pd.read_csv("penjualan_februari.csv")
pelanggan = pd.DataFrame({
"pelanggan": ["Agus", "Budi", "Dewi", "Farhan", "Rani", "Siti"],
"kota": ["Bandung", "Bandung", "Cimahi",
"Bandung", "Surabaya", "Bandung"],
"member": ["ya", "ya", "tidak", "ya", "tidak", "ya"],
})
kiri = pd.merge(februari, pelanggan, on="pelanggan", how="left")
dalam = pd.merge(februari, pelanggan, on="pelanggan", how="inner")
print("how='left' :", kiri.shape)
print("how='inner':", dalam.shape)
print()
hilang = kiri[kiri["kota"].isna()]
print("Baris yang dibuang inner join:")
print(hilang[["tanggal", "menu", "pelanggan"]])Output yang Dihasilkan:
how='left' : (6, 7)
how='inner': (5, 7)
Baris yang dibuang inner join:
tanggal menu pelanggan
5 2026-02-10 es kopi susu TonoMari kita telaah dua hasilnya:
- Baris 10-11 (
how="left"vshow="inner"): hasil left join 6 baris, inner join 5 baris — selisih satu baris adalah transaksi Tono yang tidak memiliki pasangan di tabel pelanggan. - Baris 15-17 (
kiri[kiri["kota"].isna()]): cara cepat mengintip baris yang dibuang inner join: saring hasil left join pada kolom yang kosong. Transaksi Tono tidak masuk hasil inner tanpa pesan apa pun — lagi-lagi kegagalan senyap.
Daftar lengkap nilai how beserta janjinya:
Nilai how | Baris yang dihasilkan |
|---|---|
left | Seluruh baris tabel kiri; kolom tabel kanan kosong bila tanpa pasangan |
right | Seluruh baris tabel kanan; kebalikan dari left |
inner | Hanya baris yang punya pasangan di kedua tabel |
outer | Semua baris dari kedua tabel; kolom mengisi apa adanya |
Menegakkan Aturan Kunci dengan validate #
Penggabungan mengandaikan hubungan antar tabel yang jelas: satu pelanggan bisa punya banyak transaksi, tetapi satu baris tabel pelanggan hanya mewakili satu orang. Parameter validate menegakkan dugaan itu — bila kenyataan melanggar, pd.merge menolak jalan dan menyeret MergeError ke permukaan.
import pandas as pd
from pandas.errors import MergeError
transaksi = pd.DataFrame({
"pelanggan": ["Budi", "Siti", "Budi"],
"menu": ["kopi susu", "teh tawar", "americano"],
})
pelanggan = pd.DataFrame({
"pelanggan": ["Budi", "Siti", "Dewi"],
"kota": ["Bandung", "Bandung", "Cimahi"],
})
aman = pd.merge(transaksi, pelanggan, on="pelanggan",
how="left", validate="many_to_one")
print(aman)
print()
pelanggan_dobel = pd.DataFrame({
"pelanggan": ["Budi", "Budi", "Siti", "Dewi"],
"kota": ["Bandung", "Jakarta", "Bandung", "Cimahi"],
})
try:
pd.merge(transaksi, pelanggan_dobel, on="pelanggan",
how="left", validate="many_to_one")
except MergeError as galat:
print("MergeError:", galat)Output yang Dihasilkan:
pelanggan menu kota
0 Budi kopi susu Bandung
1 Siti teh tawar Bandung
2 Budi americano Bandung
MergeError: Merge keys are not unique in right dataset; not a many-to-one mergeMari kita telaah dua nasib penggabungan itu:
- Baris 13 (
validate="many_to_one"): kita menyatakan sumpahnya — banyak transaksi untuk satu pelanggan. Penggabungan pertama sah karena tabel kanan memang satu baris per pelanggan, dan transaksi Budi dua kali sama-sama menemui baris Budi yang sama. - Baris 23-27 (
try/except MergeError):pelanggan_dobelmemuat Budi dua kali dengan kota berbeda — entri ganda pada sisi yang seharusnya tunggal.validatemenolak melanjutkan dan pesannya menyebut arah pelanggarannya: kunci tidak unik di tabel kanan. - Tanpa
validate: penggabungan kedua justru berhasil diam-diam — transaksi Budi tergandakan dua baris karena menemui dua baris Budi. Pendapatan menggelembung tanpa jejak.validatemurah, pasang saja setiap kali kita mengantremergebaru.
Selain many_to_one, tersedia one_to_one (kedua sisi unik, misal transaksi bergabung tabel struk) dan many_to_many (dua-duanya boleh berganda — hampir selalu tanda desain yang perlu dipikirkan ulang).
Memeriksa Kecocokan dengan indicator dan Menyiasati Kolom Bentrok #
Dua peralatan kecil melengkapi kotak merge kita. Pertama, indicator=True menambah kolom _merge yang mencatat asal setiap baris: both, left_only, atau right_only — alat audit yang tepat untuk menagih janji “semua pelanggan dikenal”.
import pandas as pd
februari = pd.read_csv("penjualan_februari.csv")
pelanggan = pd.DataFrame({
"pelanggan": ["Agus", "Budi", "Dewi", "Farhan", "Rani", "Siti"],
"kota": ["Bandung", "Bandung", "Cimahi",
"Bandung", "Surabaya", "Bandung"],
})
gabung = pd.merge(februari, pelanggan, on="pelanggan",
how="left", indicator=True)
print(gabung["_merge"].value_counts())
print()
tanpa_pasangan = gabung[gabung["_merge"] == "left_only"]
print(tanpa_pasangan[["tanggal", "menu", "pelanggan"]])Output yang Dihasilkan:
_merge
both 5
left_only 1
right_only 0
Name: count, dtype: int64
tanggal menu pelanggan
5 2026-02-10 es kopi susu TonoKedua, suffixes mengurus nama kolom yang berbenturan. Bila kedua tabel sama-sama punya kolom kota — satu berarti kota cabang transaksi, satu berarti kota domisili pelanggan — pd.merge menempel akhiran pada masing-masing alih-alih menimpa salah satunya.
import pandas as pd
transaksi = pd.DataFrame({
"pelanggan": ["Budi", "Siti"],
"kota": ["Bandung", "Surabaya"], # kota cabang
})
profil = pd.DataFrame({
"pelanggan": ["Budi", "Siti"],
"kota": ["Cimahi", "Surabaya"], # kota domisili
})
gabung = pd.merge(transaksi, profil, on="pelanggan",
suffixes=("_transaksi", "_profil"))
print(gabung)Output yang Dihasilkan:
pelanggan kota_transaksi kota_profil
0 Budi Bandung Cimahi
1 Siti Surabaya SurabayaMari kita telaah kedua peralatan itu:
- Kolom
_merge: lima transaksi berpasangan (both), satu lagileft_only— transaksi Tono, sesuai ramalan. Angkaright_only0 ikut tampil karena kolom_mergeberjenis kategori yang menyebut ketiga kemungkinan; nol berarti tidak ada pelanggan yang menganggur tanpa transaksi. suffixes=("_transaksi", "_profil"): tanpa akhiran, pandas menamai kolom benturankota_xdankota_y— sah, tetapi maknanya harus dihafal. Akhiran buatan sendiri membuat nama kolom langsung terbaca maknanya, sepertikota_transaksidankota_profilpada hasil di atas.
Kombinasi indicator dan value_counts() pada kolom _merge adalah laporan audit tiga baris yang layak menjadi kebiasaan setelah setiap penggabungan: berapa baris berpasangan, berapa yang menganggur, dan ke arah mana.
Rangkuman #
pd.concatmenumpuk tabel berstruktur sama;ignore_index=Truemerapikan penomoran baris hasil tumpukan.pd.mergemerangkai tabel berbeda lewat kolom kunci (on); normalisasikan kunci kedua sisi lebih dulu seperti pada chapter Menangani Data Tidak Konsisten.how="left"mempertahankan seluruh tabel kiri,how="inner"(bawaan) membuang baris tanpa pasangan tanpa pesan — mulai darileftsaat memeriksa data.validate="many_to_one"dan kawan-kawannya menolak penggabungan yang melanggar aturan kunci; tanpa itu, entri ganda di sisi kanan menggandakan baris diam-diam.indicator=Truemengaudit baris yang tak berpasangan lewat kolom_merge;suffixesmenjernihkan kolom yang berbenturan nama.
Kini Januari dan Februari bersatu, dan setiap transaksi membawa profil pelanggannya. Namun perhatikan bentuk tabel itu: bila kita ingin rekap penjualan per menu per bulan, barisnya harus ditekuk menjadi kolom — dan kebalikannya. Pergeseran bentuk data itulah materi chapter berikutnya, Mengubah Bentuk Data .