Nilai hilang setelah ditangani, satu masalah klasik masih menunggu: baris yang terekam dua kali. Duplikat jarang terlihat di cuplikan data, tetapi dampaknya nyata — pendapatan menggelembung, jumlah pelanggan dihitung ganda, dan model belajar dari contoh yang sama berulang. Pada chapter ini kita membedah duplikat pada data gabungan kasir dan rekap manual Toko Kopi Kita: menandainya dengan duplicated(), mengendalikan parameter keep, menghapusnya dengan drop_duplicates(), dan yang terpenting — membedakan duplikat sungguhan dari transaksi sah yang kebetulan mirip.

Ketika Dua Sistem Mencatat Hal yang Sama #

Toko Kopi Kita menggabungkan dua catatan: aplikasi kasir sebagai sumber utama dan rekap manual sebagai cadangan. Proses penggabungan yang terburu-buru membuat sebagian transaksi terekam dua kali. Data hasilnya memuat dua jenis duplikat yang perlu dibedakan sejak awal.

JenisCiriContoh pada data kita
Duplikat penuhSeluruh kolom identik dengan baris lainT005 tercatat dua kali persis sama
Duplikat parsialSebagian kolom sama, sebagian berbedaT008 tercatat dua kali dengan jumlah berbeda — kasir mengoreksi catatan

Duplikat penuh bisa dihapus begitu saja. Duplikat parsial menuntut keputusan: baris mana yang benar? Tanpa kunci pengenal seperti kolom id, dua transaksi kopi susu dari pelanggan yang sama di hari yang sama bisa dianggap duplikat padahal keduanya sah — dan itu bukan pembersihan data, melainkan penghapusan transaksi nyata.

Menandai Baris Duplikat dengan duplicated #

Fungsi duplicated() mengembalikan nilai True untuk setiap baris yang sama persis dengan baris sebelumnya. Dengan keep=False, semua baris yang terlibat penggandaan ditandai — bukan hanya salinannya — sehingga kita bisa memeriksa keduanya berdampingan.

python
import pandas as pd

baris = [
    ("T001", "2026-01-21", "kopi susu", 2, "Budi"),
    ("T002", "2026-01-21", "teh tawar", 1, "Siti"),
    ("T003", "2026-01-22", "americano", 2, "Dewi"),
    ("T004", "2026-01-22", "matcha latte", 3, "Rani"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T006", "2026-01-23", "teh tawar", 2, "Budi"),
    ("T007", "2026-01-24", "americano", 1, "Siti"),
    ("T008", "2026-01-24", "kopi susu", 2, "Dewi"),
    ("T008", "2026-01-24", "kopi susu", 3, "Dewi"),
    ("T009", "2026-01-25", "matcha latte", 2, "Agus"),
]
transaksi = pd.DataFrame(baris, columns=["id", "tanggal", "produk", "jumlah", "pelanggan"])

print(transaksi)
print()
print("Baris duplikat penuh:", transaksi.duplicated().sum())
print()
print("Baris yang terlibat duplikat penuh:")
print(transaksi[transaksi.duplicated(keep=False)])

Output yang Dihasilkan:

text
      id     tanggal        produk  jumlah pelanggan
0   T001  2026-01-21     kopi susu       2      Budi
1   T002  2026-01-21     teh tawar       1      Siti
2   T003  2026-01-22     americano       2      Dewi
3   T004  2026-01-22  matcha latte       3      Rani
4   T005  2026-01-23     kopi susu       1      Agus
5   T005  2026-01-23     kopi susu       1      Agus
6   T006  2026-01-23     teh tawar       2      Budi
7   T007  2026-01-24     americano       1      Siti
8   T008  2026-01-24     kopi susu       2      Dewi
9   T008  2026-01-24     kopi susu       3      Dewi
10  T009  2026-01-25  matcha latte       2      Agus

Baris duplikat penuh: 1

Baris yang terlibat duplikat penuh:
     id     tanggal     produk  jumlah pelanggan
4  T005  2026-01-23  kopi susu       1      Agus
5  T005  2026-01-23  kopi susu       1      Agus

Mari kita telaah hasilnya:

  1. Baris 20 (duplicated().sum()): satu baris dinyatakan duplikat — salinan kedua T005. Baris pertamanya tidak dihitung karena secara bawaan duplicated() menandai baris setelah kemunculan pertama.
  2. Baris 22-23 (duplicated(keep=False)): saringan memperlihatkan kedua baris T005 sekaligus — yang asli dan salinannya — supaya kita bisa memastikan keduanya memang identik sebelum menghapus.
  3. Pasangan T008 tidak tertangkap: baris 9 dan 10 berbagi id tetapi berbeda di kolom jumlah (2 lawan 3), sehingga keduanya bukan duplikat penuh. Duplikat parsial semacam ini butuh perlakuan khusus di bagian berikutnya.

Mengatur Baris Asli dengan Parameter keep #

Parameter keep menentukan kemunculan mana yang dianggap “asli”. Tiga pilihannya mengubah hitungan duplikat secara langsung — dan pilihan yang keliru bisa membuat kita membuang baris yang justru benar.

python
import pandas as pd

baris = [
    ("T001", "2026-01-21", "kopi susu", 2, "Budi"),
    ("T002", "2026-01-21", "teh tawar", 1, "Siti"),
    ("T003", "2026-01-22", "americano", 2, "Dewi"),
    ("T004", "2026-01-22", "matcha latte", 3, "Rani"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T006", "2026-01-23", "teh tawar", 2, "Budi"),
    ("T007", "2026-01-24", "americano", 1, "Siti"),
    ("T008", "2026-01-24", "kopi susu", 2, "Dewi"),
    ("T008", "2026-01-24", "kopi susu", 3, "Dewi"),
    ("T009", "2026-01-25", "matcha latte", 2, "Agus"),
]
transaksi = pd.DataFrame(baris, columns=["id", "tanggal", "produk", "jumlah", "pelanggan"])

print('keep="first":', transaksi.duplicated(keep="first").sum(), "baris")
print('keep="last" :', transaksi.duplicated(keep="last").sum(), "baris")
print("keep=False  :", transaksi.duplicated(keep=False).sum(), "baris")

Output yang Dihasilkan:

text
keep="first": 1 baris
keep="last" : 1 baris
keep=False  : 2 baris

Ketiganya menghitung pasangan T005 yang sama, tetapi menandai baris yang berbeda:

Nilai keepBaris yang dianggap asliBaris yang ditandai
"first" (bawaan)Kemunculan pertamaSalinan kedua dan seterusnya
"last"Kemunculan terakhirSemua salinan sebelumnya
FalseTidak adaSeluruh baris yang terlibat, termasuk yang pertama

Untuk T005 yang salinannya identik, ketiga pilihan menghasilkan data akhir yang sama. Pilihan baru berdampak ketika salinannya berbeda — seperti T008 yang jumlahnya dikoreksi dari 2 menjadi 3. Di sana, keep="last" berarti “percayai catatan terbaru”, dan itu tepat karena kasir memang mengoreksi catatan lamanya.

Menghapus dengan drop_duplicates #

Penghapusan memakai drop_duplicates(). Tanpa parameter, fungsi ini hanya membuang duplikat penuh; parameter subset mempersempit perbandingan ke kolom kunci — di data kita, id — sehingga duplikat parsial ikut tertangani.

python
import pandas as pd

baris = [
    ("T001", "2026-01-21", "kopi susu", 2, "Budi"),
    ("T002", "2026-01-21", "teh tawar", 1, "Siti"),
    ("T003", "2026-01-22", "americano", 2, "Dewi"),
    ("T004", "2026-01-22", "matcha latte", 3, "Rani"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T006", "2026-01-23", "teh tawar", 2, "Budi"),
    ("T007", "2026-01-24", "americano", 1, "Siti"),
    ("T008", "2026-01-24", "kopi susu", 2, "Dewi"),
    ("T008", "2026-01-24", "kopi susu", 3, "Dewi"),
    ("T009", "2026-01-25", "matcha latte", 2, "Agus"),
]
transaksi = pd.DataFrame(baris, columns=["id", "tanggal", "produk", "jumlah", "pelanggan"])

tanpa_duplikat_penuh = transaksi.drop_duplicates()
print("Dedup penuh   :", transaksi.shape, "->", tanpa_duplikat_penuh.shape)

per_id = transaksi.drop_duplicates(subset=["id"], keep="last")
print("Dedup per id  :", transaksi.shape, "->", per_id.shape)
print()
print(per_id)

Output yang Dihasilkan:

text
Dedup penuh   : (11, 5) -> (10, 5)
Dedup per id  : (11, 5) -> (9, 5)

      id     tanggal        produk  jumlah pelanggan
0   T001  2026-01-21     kopi susu       2      Budi
1   T002  2026-01-21     teh tawar       1      Siti
2   T003  2026-01-22     americano       2      Dewi
3   T004  2026-01-22  matcha latte       3      Rani
5   T005  2026-01-23     kopi susu       1      Agus
6   T006  2026-01-23     teh tawar       2      Budi
7   T007  2026-01-24     americano       1      Siti
9   T008  2026-01-24     kopi susu       3      Dewi
10  T009  2026-01-25  matcha latte       2      Agus

Mari kita telaah dua hasil yang berbeda itu:

  1. Baris 20 (drop_duplicates()): hanya satu baris yang terbuang — salinan kedua T005. Sepuluh baris tersisa, tetapi T008 masih ada dua kali karena barisnya tidak identik.
  2. Baris 23 (drop_duplicates(subset=["id"], keep="last")): perbandingan dibatasi ke kolom id, sehingga kedua baris T008 dianggap sama dan hanya catatan terakhir yang dipertahankan — yang berjumlah 3 cangkir. Sembilan baris tersisa: satu baris per transaksi.
  3. Cetakan per_id: hasil akhirnya rapi — T005 tunggal, T008 tunggal dengan koreksi terbaru, urutan lain tetap utuh.

Berapa Besar Dampaknya ke Angka #

Pembersihan tanpa pengukuran hanyalah keyakinan. Kita hitung dampak tiap strategi terhadap tiga angka yang dipakai manajemen toko: jumlah transaksi, total cangkir terjual, dan pendapatan. Harga produk diambil dari daftar harga tetap toko.

python
import pandas as pd

baris = [
    ("T001", "2026-01-21", "kopi susu", 2, "Budi"),
    ("T002", "2026-01-21", "teh tawar", 1, "Siti"),
    ("T003", "2026-01-22", "americano", 2, "Dewi"),
    ("T004", "2026-01-22", "matcha latte", 3, "Rani"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T005", "2026-01-23", "kopi susu", 1, "Agus"),
    ("T006", "2026-01-23", "teh tawar", 2, "Budi"),
    ("T007", "2026-01-24", "americano", 1, "Siti"),
    ("T008", "2026-01-24", "kopi susu", 2, "Dewi"),
    ("T008", "2026-01-24", "kopi susu", 3, "Dewi"),
    ("T009", "2026-01-25", "matcha latte", 2, "Agus"),
]
transaksi = pd.DataFrame(baris, columns=["id", "tanggal", "produk", "jumlah", "pelanggan"])

harga_produk = {"kopi susu": 24000, "teh tawar": 20000,
                "americano": 22000, "matcha latte": 28000}


def ringkas(df, label):
    pendapatan = (df["jumlah"] * df["produk"].map(harga_produk)).sum()
    tebal = f"Rp{int(pendapatan):,}".replace(",", ".")
    print(f"{label}: {len(df)} transaksi, {df['jumlah'].sum()} cangkir, {tebal}")


ringkas(transaksi, "Sebelum dedup        ")
ringkas(transaksi.drop_duplicates(), "Dedup penuh         ")
ringkas(transaksi.drop_duplicates(subset=["id"], keep="last"), "Dedup per id (akhir)")

Output yang Dihasilkan:

text
Sebelum dedup        : 11 transaksi, 20 cangkir, Rp482.000
Dedup penuh         : 10 transaksi, 19 cangkir, Rp458.000
Dedup per id (akhir): 9 transaksi, 17 cangkir, Rp410.000

Tiga baris ringkasan tersebut bercerita banyak. Data mentah melaporkan 11 transaksi dan pendapatan Rp482.000 — padahal satu transaksi tercatat dua kali dan satu catatan T008 yang usang ikut terhitung. Dedup penuh memperbaiki sebagian (Rp458.000), dan dedup per id menghasilkan angka yang benar: 9 transaksi, 17 cangkir, Rp410.000. Selisih Rp72.000 adalah pendapatan semu yang tanpa pembersihan akan masuk ke laporan manajemen.

Rangkuman #

  • duplicated() menandai baris identik dengan baris sebelumnya; keep=False menampilkan semua baris yang terlibat untuk diperiksa sebelum dihapus.
  • Parameter keep mengatur kemunculan mana yang dianggap asli — "first" bawaan, "last" untuk memercayai catatan terbaru.
  • drop_duplicates() tanpa parameter hanya menangani duplikat penuh; subset pada kolom kunci seperti id menangani duplikat parsial.
  • subset yang longgar dapat menghapus transaksi sah yang mirip; batasi pada kolom yang benar-benar unik.
  • Ukur dampaknya: hitung ulang metrik utama sebelum dan sesudah dedup — perbedaannya adalah kesalahan yang berhasil dicegah.

Dengan duplikat bersih, baris-baris data kini mewakili kejadian yang sesungguhnya. Namun bila kita mencoba menghitung pendapatan pada berkas penjualan_kotor.csv dari chapter pertama, penjumlahannya justru merekatkan teks alih-alih menjumlahkan angka — tanda tipe kolomnya belum benar. Kita perbaiki berbagai jenis tipe yang salah itu pada chapter berikutnya, Memperbaiki Tipe Data .