Dari pemeriksaan pada chapter sebelumnya, kita tahu berkas penjualan_kotor.csv menyimpan dua nilai hilang. Pertanyaan berikutnya jauh lebih penting dari sekadar menemukannya: apa yang sebaiknya kita lakukan terhadap sel yang kosong itu? Menghapus barisnya terasa mudah, tetapi bisa memangkas data; mengisinya terasa murah hati, tetapi bisa mengarang nilai yang tidak pernah ada. Pada chapter ini kita membedah empat strategi utama menangani missing values — menghapus, mengisi dengan konstanta, mengisi dengan statistik kolom, dan mengisi dari baris tetangga — beserta kapan masing-masing menjadi pilihan yang tepat.
Dataset Latihan: Transaksi dengan Sel Kosong #
Kita memakai catatan lima hari penjualan Toko Kopi Kita berikutnya, 21 sampai 25 Januari 2026. Sama seperti berkas sebelumnya, beberapa sel gagal terekam: dua transaksi kehilangan jumlah cangkir, satu kehilangan harga, dan satu lagi tidak mencatat nama pelanggan. Karena setiap blok kode pada chapter ini berdiri sendiri, DataFrame yang sama dibangun ulang di tiap blok.
import numpy as np
import pandas as pd
kolom = ["tanggal", "produk", "jumlah", "harga", "pelanggan"]
baris = [
("2026-01-21", "kopi susu", 2, 24000, "Budi"),
("2026-01-21", "teh tawar", 1, 20000, "Siti"),
("2026-01-22", "kopi susu", np.nan, 24000, "Dewi"),
("2026-01-22", "matcha latte", 2, np.nan, "Rani"),
("2026-01-23", "americano", 1, 22000, "Budi"),
("2026-01-23", "kopi susu", 3, 24000, "Agus"),
("2026-01-24", "teh tawar", 2, 20000, "Siti"),
("2026-01-24", "matcha latte", np.nan, 28000, "Budi"),
("2026-01-25", "americano", 2, 22000, np.nan),
("2026-01-25", "kopi susu", 4, 24000, "Budi"),
]
transaksi = pd.DataFrame(baris, columns=kolom)
print(transaksi)
print()
ringkas = pd.DataFrame(
{"hilang": transaksi.isna().sum(), "persen": (transaksi.isna().mean() * 100).round(1)}
)
print(ringkas)Output yang Dihasilkan:
tanggal produk jumlah harga pelanggan
0 2026-01-21 kopi susu 2.0 24000.0 Budi
1 2026-01-21 teh tawar 1.0 20000.0 Siti
2 2026-01-22 kopi susu NaN 24000.0 Dewi
3 2026-01-22 matcha latte 2.0 NaN Rani
4 2026-01-23 americano 1.0 22000.0 Budi
5 2026-01-23 kopi susu 3.0 24000.0 Agus
6 2026-01-24 teh tawar 2.0 20000.0 Siti
7 2026-01-24 matcha latte NaN 28000.0 Budi
8 2026-01-25 americano 2.0 22000.0 NaN
9 2026-01-25 kopi susu 4.0 24000.0 Budi
hilang persen
tanggal 0 0.0
produk 0 0.0
jumlah 2 20.0
harga 1 10.0
pelanggan 1 10.0Mari kita telaah blok tersebut:
- Baris 5-16 (list
baris): setiap transaksi disimpan sebagai tuple — satu baris data. Nilainp.nanmenandai sel yang tidak terekam; pandas menampilkannya sebagaiNaN. - Baris 17 (
pd.DataFrame(baris, columns=kolom)): list of tuples berubah menjadi DataFrame sepuluh baris. - Baris 21-23 (
ringkas):isna().sum()menghitung sel kosong per kolom,isna().mean()mengubahnya menjadi proporsi. Kolomjumlahkehilangan 20% isinya — informasi yang menentukan strategi.
Menghapus Baris dengan dropna #
Strategi paling lugas: buang setiap baris yang memuat sel kosong. dropna() tanpa parameter melakukannya terhadap seluruh kolom; parameter subset mempersempit target ke kolom tertentu saja. Perhatikan bentuk data sebelum dan sesudah — di sinilah letak bahayanya.
import numpy as np
import pandas as pd
kolom = ["tanggal", "produk", "jumlah", "harga", "pelanggan"]
baris = [
("2026-01-21", "kopi susu", 2, 24000, "Budi"),
("2026-01-21", "teh tawar", 1, 20000, "Siti"),
("2026-01-22", "kopi susu", np.nan, 24000, "Dewi"),
("2026-01-22", "matcha latte", 2, np.nan, "Rani"),
("2026-01-23", "americano", 1, 22000, "Budi"),
("2026-01-23", "kopi susu", 3, 24000, "Agus"),
("2026-01-24", "teh tawar", 2, 20000, "Siti"),
("2026-01-24", "matcha latte", np.nan, 28000, "Budi"),
("2026-01-25", "americano", 2, 22000, np.nan),
("2026-01-25", "kopi susu", 4, 24000, "Budi"),
]
transaksi = pd.DataFrame(baris, columns=kolom)
bersih = transaksi.dropna()
print("Sebelum:", transaksi.shape, "-> dropna():", bersih.shape)
print()
print(bersih)
print()
print("dropna(subset=['jumlah']):", transaksi.dropna(subset=["jumlah"]).shape)Output yang Dihasilkan:
Sebelum: (10, 5) -> dropna(): (6, 5)
tanggal produk jumlah harga pelanggan
0 2026-01-21 kopi susu 2.0 24000.0 Budi
1 2026-01-21 teh tawar 1.0 20000.0 Siti
4 2026-01-23 americano 1.0 22000.0 Budi
5 2026-01-23 kopi susu 3.0 24000.0 Agus
6 2026-01-24 teh tawar 2.0 20000.0 Siti
9 2026-01-25 kopi susu 4.0 24000.0 Budi
dropna(subset=['jumlah']): (8, 5)Mari kita telaah hasilnya:
- Baris 19 (
dropna()): empat baris terbuang sekaligus — baris 2, 3, 7, dan 8 pada indeks — karena masing-masing memuat satu sel kosong. Dari sepuluh transaksi hanya enam yang selamat: 40% data terbuang karena empat sel bermasalah. - Cetakan
bersih: yang bertahan hanyalah transaksi yang terekam sempurna. Untuk analisis yang mensyaratkan data lengkap, inilah yang diinginkan. - Baris 24 (
dropna(subset=["jumlah"])): hanya kolomjumlahyang diperhatikan, sehingga cukup dua baris yang dibuang dan delapan tersisa. Semakin sempitsubset, semakin sedikit data yang terbuang.
Mengisi dengan Konstanta #
Menghapus seluruh baris karena satu kolom kosong terasa berlebihan ketika kolom itu tidak kritis. Nama pelanggan yang tidak terekam, misalnya, tidak menghalangi analisis penjualan. Untuk kasus semacam ini kita mengisi dengan konstanta — sebuah label yang jujur menyatakan “data ini tidak ada”.
import numpy as np
import pandas as pd
kolom = ["tanggal", "produk", "jumlah", "harga", "pelanggan"]
baris = [
("2026-01-21", "kopi susu", 2, 24000, "Budi"),
("2026-01-21", "teh tawar", 1, 20000, "Siti"),
("2026-01-22", "kopi susu", np.nan, 24000, "Dewi"),
("2026-01-22", "matcha latte", 2, np.nan, "Rani"),
("2026-01-23", "americano", 1, 22000, "Budi"),
("2026-01-23", "kopi susu", 3, 24000, "Agus"),
("2026-01-24", "teh tawar", 2, 20000, "Siti"),
("2026-01-24", "matcha latte", np.nan, 28000, "Budi"),
("2026-01-25", "americano", 2, 22000, np.nan),
("2026-01-25", "kopi susu", 4, 24000, "Budi"),
]
transaksi = pd.DataFrame(baris, columns=kolom)
transaksi["pelanggan"] = transaksi["pelanggan"].fillna("Tidak tercatat")
print(transaksi["pelanggan"].value_counts())Output yang Dihasilkan:
pelanggan
Budi 4
Siti 2
Dewi 1
Rani 1
Agus 1
Tidak tercatat 1
Name: count, dtype: int64Hasilnya: transaksi tanpa nama pelanggan kini berlabel Tidak tercatat — satu kategori eksplisit, bukan sel kosong. Keuntungannya ganda: baris tetap bisa dianalisis, dan siapa pun yang membaca laporan tahu persis bahwa satu transaksi memang tidak mencatat pelanggannya. Label semacam ini juga lebih jujur daripada mengisi dengan nama yang paling sering muncul, yang berarti mengarang identitas pelanggan yang tidak pernah diketahui.
Mengisi dengan Statistik Kolom #
Kolom angka menuntut pendekatan berbeda: kita tidak bisa mengisi jumlah cangkir dengan teks. Kandidat pengisi yang lazim adalah median — nilai tengah kolom — atau modus — nilai yang paling sering muncul. Median dipilih karena tahan terhadap pencilan; satu transaksi raksasa tidak akan menariknya. Namun statistik kolom tidak selalu cocok untuk semua kolom, seperti kasus harga berikut.
import numpy as np
import pandas as pd
kolom = ["tanggal", "produk", "jumlah", "harga", "pelanggan"]
baris = [
("2026-01-21", "kopi susu", 2, 24000, "Budi"),
("2026-01-21", "teh tawar", 1, 20000, "Siti"),
("2026-01-22", "kopi susu", np.nan, 24000, "Dewi"),
("2026-01-22", "matcha latte", 2, np.nan, "Rani"),
("2026-01-23", "americano", 1, 22000, "Budi"),
("2026-01-23", "kopi susu", 3, 24000, "Agus"),
("2026-01-24", "teh tawar", 2, 20000, "Siti"),
("2026-01-24", "matcha latte", np.nan, 28000, "Budi"),
("2026-01-25", "americano", 2, 22000, np.nan),
("2026-01-25", "kopi susu", 4, 24000, "Budi"),
]
transaksi = pd.DataFrame(baris, columns=kolom)
median_jumlah = transaksi["jumlah"].median()
print("Median kolom jumlah:", median_jumlah)
transaksi["jumlah"] = transaksi["jumlah"].fillna(median_jumlah)
harga_matcha = transaksi.loc[transaksi["produk"] == "matcha latte", "harga"].dropna()
print("Harga matcha latte di baris lain:", sorted(harga_matcha.unique().tolist()))
transaksi["harga"] = transaksi["harga"].fillna(28000)
print()
print(transaksi)
print()
print("Total sel kosong tersisa:", transaksi.isna().sum().sum())Output yang Dihasilkan:
Median kolom jumlah: 2.0
Harga matcha latte di baris lain: [28000.0]
tanggal produk jumlah harga pelanggan
0 2026-01-21 kopi susu 2.0 24000.0 Budi
1 2026-01-21 teh tawar 1.0 20000.0 Siti
2 2026-01-22 kopi susu 2.0 24000.0 Dewi
3 2026-01-22 matcha latte 2.0 28000.0 Rani
4 2026-01-23 americano 1.0 22000.0 Budi
5 2026-01-23 kopi susu 3.0 24000.0 Agus
6 2026-01-24 teh tawar 2.0 20000.0 Siti
7 2026-01-24 matcha latte 2.0 28000.0 Budi
8 2026-01-25 americano 2.0 22000.0 NaN
9 2026-01-25 kopi susu 4.0 24000.0 Budi
Total sel kosong tersisa: 1Mari kita telaah dua keputusan yang berbeda dalam satu blok:
- Baris 19-21 (
fillna(median_jumlah)): median kolomjumlahadalah 2,0 — angka wajar untuk transaksi kafe — sehingga kedua sel kosong diisi 2,0. Strategi ini cocok karena nilai antar transaksi relatif seragam. - Baris 23-25 (harga
matcha latte): sel kosong ada di baris matcha latte. Median seluruh kolomharga(24.000) akan salah, karena matcha latte tidak pernah dijual 24.000 — kita periksa baris lainnya dan menemukan harganya selalu 28.000, lalu mengisi dengan nilai itu. Pelajarannya: statistik kolom hanya sah bila makna nilainya seragam. - Baris 29 (
isna().sum().sum()): penjumlahan ganda menghitung seluruh sel kosong dari semua kolom — hasilnya 1, karena kolompelangganmemang belum diisi pada blok ini (setiap blok berdiri sendiri). Nol berarti pembersihan tuntas.
Mengisi Deret Waktu dengan ffill #
Catatan yang dicatat tiap hari — stok, suhu, kunjungan — punya satu keistimewaan: nilainya berubah pelan, sehingga nilai hari sebelumnya adalah tebakan yang masuk akal untuk hari yang terlewat. Metode ffill (forward fill) membawa nilai terakhir yang valid ke seluruh sel kosong setelahnya. Toko Kopi Kita menimbang stok biji kopi setiap pagi; empat pagi terlewat dari catatan dua belas hari.
import numpy as np
import pandas as pd
stok = pd.DataFrame({
"tanggal": ["2026-01-21", "2026-01-22", "2026-01-23", "2026-01-24",
"2026-01-25", "2026-01-26", "2026-01-27", "2026-01-28",
"2026-01-29", "2026-01-30", "2026-01-31", "2026-02-01"],
"stok_kg": [12.0, 12.0, np.nan, 11.5, 11.5, np.nan, np.nan, 10.5,
10.0, 10.0, np.nan, 9.5],
})
print("Sebelum ffill:")
print(stok)
stok["stok_kg"] = stok["stok_kg"].ffill()
print()
print("Sesudah ffill:")
print(stok)Output yang Dihasilkan:
Sebelum ffill:
tanggal stok_kg
0 2026-01-21 12.0
1 2026-01-22 12.0
2 2026-01-23 NaN
3 2026-01-24 11.5
4 2026-01-25 11.5
5 2026-01-26 NaN
6 2026-01-27 NaN
7 2026-01-28 10.5
8 2026-01-29 10.0
9 2026-01-30 10.0
10 2026-01-31 NaN
11 2026-02-01 9.5
Sesudah ffill:
tanggal stok_kg
0 2026-01-21 12.0
1 2026-01-22 12.0
2 2026-01-23 12.0
3 2026-01-24 11.5
4 2026-01-25 11.5
5 2026-01-26 11.5
6 2026-01-27 11.5
7 2026-01-28 10.5
8 2026-01-29 10.0
9 2026-01-30 10.0
10 2026-01-31 10.0
11 2026-02-01 9.5Empat sel kosong terisi dari baris di atasnya: 23 Januari mewarisi 12,0 kg dari 22 Januari; 26 dan 27 Januari sama-sama mewarisi 11,5 kg; 31 Januari mewarisi 10,0 kg. Metode kebalikannya, bfill (backward fill), menarik nilai dari baris di bawahnya — berguna ketika data justru paling valid di titik setelahnya. Keduanya menerima parameter limit untuk membatasi seberapa jauh nilai dikopi; ffill(limit=1) hanya mengisi satu sel kosong beruntun, mencegah satu nilai tua menular terlalu jauh.
Memilih Strategi yang Tepat #
Tidak ada strategi tunggal yang menang untuk semua situasi. Tabel berikut merangkum pertanyaan penentu dan risiko masing-masing strategi yang telah kita coba.
| Strategi | Perintah | Cocok saat | Risiko utama |
|---|---|---|---|
| Hapus baris | dropna() | Nilai hilang sedikit; baris bermasalah tak bisa diperbaiki | Kehilangan data — 4 sel kosong membuang 4 baris |
| Hapus selektif | dropna(subset=[...]) | Hanya kolom kritis tertentu yang wajib terisi | Kolom non-kritis ikut tak diperiksa |
| Konstanta | fillna("Tidak tercatat") | Kolom kategori; kekosongan perlu terlihat | Agregasi bisa menampilkan label tersebut |
| Statistik kolom | fillna(median) | Nilai antar baris relatif seragam | Nilai buatan bisa menyesatkan bila kolom heterogen |
| Nilai baris lain | ffill(), harga produk sejenis | Deret waktu; kolom punya acuan paralel | Perubahan nyata tertunda atau tersembunyi |
Pola yang menyatukan kelima strategi: keputusan bergantung pada makna kolom, bukan pada tipe datanya semata. Kolom jumlah boleh diisi median karena transaksi kafe seragam; kolom harga harus mengikuti harga produknya; kolom pelanggan paling jujur diisi label eksplisit. Panduan lengkap seluruh perilaku missing data tersedia di dokumentasi pandas missing data
, dan ringkasan fungsi pandas lainnya ada di pandas user guide
.
Rangkuman #
isna().sum()danisna().mean()mengubah kekosongan menjadi hitungan dan persentase — dasar untuk memilih strategi.dropna()menghapus baris bermasalah secara diam-diam; bandingkanshapesebelum dan sesudah, dan pertimbangkansubsetuntuk membatasi kerusakan.fillna()dengan konstanta menjaga baris tetap hidup sambil menandai kekosongan secara jujur, misalnya labelTidak tercatat.- Pengisian dengan statistik sah hanya bila nilai kolom seragam; median tahan pencilan, dan harga produk lebih tepat diisi dari baris produk yang sama.
ffill()/bfill()memanfaatkan urutan waktu; parameterlimitmencegah nilai menular terlalu jauh.
Nilai kosong kini tertangani, tetapi daftar masalah dari pemeriksaan awal kita masih panjang. Masalah berikutnya menempel diam-diam pada data yang tampak sehat: baris yang sama tercatat dua kali, menggandakan pendapatan semu. Kita buru duplikat itu pada chapter berikutnya, Menangani Data Duplikat .