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.

csv
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,Farhan
csv
tanggal,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,Tono

Perhatikan 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.

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.

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

text
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      Tono

Mari kita telaah penumpukannya:

  1. Baris 8 (pd.concat([...], ignore_index=True)): daftar berkas diletakkan dalam list; urutan list menjadi urutan baris hasil. ignore_index=True menyusun ulang penomoran baris dari nol sampai 13 tanpa bolong.
  2. 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.
  3. Bentuk hasil: 8 + 6 = 14 baris, 5 kolom — sama seperti sebelum ditumpuk. pd.concat memanjangkan, bukan melebar; melebarkan tabel (menempel kolom berdampingan) juga bisa lewat axis=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.

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

text
      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    NaN

Mari kita telaah penggabungannya:

  1. 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.
  2. Baris 10 (pd.merge(..., on="pelanggan", how="left")): parameter on menyebut kolom kunci yang dicocokkan di kedua tabel — kolom yang sudah kita seragamkan bentuknya pada chapter Menangani Data Tidak Konsisten .
  3. Baris transaksi Tono: how="left" mempertahankan seluruh baris Februari, sehingga transaksi Tono tetap tampil dengan kota dan member kosong. 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.

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

text
how='left' : (6, 7)
how='inner': (5, 7)

Baris yang dibuang inner join:
      tanggal          menu pelanggan
5  2026-02-10  es kopi susu      Tono

Mari kita telaah dua hasilnya:

  1. Baris 10-11 (how="left" vs how="inner"): hasil left join 6 baris, inner join 5 baris — selisih satu baris adalah transaksi Tono yang tidak memiliki pasangan di tabel pelanggan.
  2. 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 howBaris yang dihasilkan
leftSeluruh baris tabel kiri; kolom tabel kanan kosong bila tanpa pasangan
rightSeluruh baris tabel kanan; kebalikan dari left
innerHanya baris yang punya pasangan di kedua tabel
outerSemua 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.

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

text
  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 merge

Mari kita telaah dua nasib penggabungan itu:

  1. 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.
  2. Baris 23-27 (try/except MergeError): pelanggan_dobel memuat Budi dua kali dengan kota berbeda — entri ganda pada sisi yang seharusnya tunggal. validate menolak melanjutkan dan pesannya menyebut arah pelanggarannya: kunci tidak unik di tabel kanan.
  3. Tanpa validate: penggabungan kedua justru berhasil diam-diam — transaksi Budi tergandakan dua baris karena menemui dua baris Budi. Pendapatan menggelembung tanpa jejak. validate murah, pasang saja setiap kali kita mengantre merge baru.

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”.

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

text
_merge
both          5
left_only     1
right_only    0
Name: count, dtype: int64

      tanggal          menu pelanggan
5  2026-02-10  es kopi susu      Tono

Kedua, 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.

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

text
  pelanggan kota_transaksi kota_profil
0      Budi        Bandung      Cimahi
1      Siti       Surabaya    Surabaya

Mari kita telaah kedua peralatan itu:

  1. Kolom _merge: lima transaksi berpasangan (both), satu lagi left_only — transaksi Tono, sesuai ramalan. Angka right_only 0 ikut tampil karena kolom _merge berjenis kategori yang menyebut ketiga kemungkinan; nol berarti tidak ada pelanggan yang menganggur tanpa transaksi.
  2. suffixes=("_transaksi", "_profil"): tanpa akhiran, pandas menamai kolom benturan kota_x dan kota_y — sah, tetapi maknanya harus dihafal. Akhiran buatan sendiri membuat nama kolom langsung terbaca maknanya, seperti kota_transaksi dan kota_profil pada 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.concat menumpuk tabel berstruktur sama; ignore_index=True merapikan penomoran baris hasil tumpukan.
  • pd.merge merangkai 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 dari left saat 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=True mengaudit baris yang tak berpasangan lewat kolom _merge; suffixes menjernihkan 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 .