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.

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

text
      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.0

Mari kita telaah blok tersebut:

  1. Baris 5-16 (list baris): setiap transaksi disimpan sebagai tuple — satu baris data. Nilai np.nan menandai sel yang tidak terekam; pandas menampilkannya sebagai NaN.
  2. Baris 17 (pd.DataFrame(baris, columns=kolom)): list of tuples berubah menjadi DataFrame sepuluh baris.
  3. Baris 21-23 (ringkas): isna().sum() menghitung sel kosong per kolom, isna().mean() mengubahnya menjadi proporsi. Kolom jumlah kehilangan 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.

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

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

  1. 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.
  2. Cetakan bersih: yang bertahan hanyalah transaksi yang terekam sempurna. Untuk analisis yang mensyaratkan data lengkap, inilah yang diinginkan.
  3. Baris 24 (dropna(subset=["jumlah"])): hanya kolom jumlah yang diperhatikan, sehingga cukup dua baris yang dibuang dan delapan tersisa. Semakin sempit subset, 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”.

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

text
pelanggan
Budi              4
Siti              2
Dewi              1
Rani              1
Agus              1
Tidak tercatat    1
Name: count, dtype: int64

Hasilnya: 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.

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

text
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: 1

Mari kita telaah dua keputusan yang berbeda dalam satu blok:

  1. Baris 19-21 (fillna(median_jumlah)): median kolom jumlah adalah 2,0 — angka wajar untuk transaksi kafe — sehingga kedua sel kosong diisi 2,0. Strategi ini cocok karena nilai antar transaksi relatif seragam.
  2. Baris 23-25 (harga matcha latte): sel kosong ada di baris matcha latte. Median seluruh kolom harga (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.
  3. Baris 29 (isna().sum().sum()): penjumlahan ganda menghitung seluruh sel kosong dari semua kolom — hasilnya 1, karena kolom pelanggan memang 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.

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

text
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.5

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

StrategiPerintahCocok saatRisiko utama
Hapus barisdropna()Nilai hilang sedikit; baris bermasalah tak bisa diperbaikiKehilangan data — 4 sel kosong membuang 4 baris
Hapus selektifdropna(subset=[...])Hanya kolom kritis tertentu yang wajib terisiKolom non-kritis ikut tak diperiksa
Konstantafillna("Tidak tercatat")Kolom kategori; kekosongan perlu terlihatAgregasi bisa menampilkan label tersebut
Statistik kolomfillna(median)Nilai antar baris relatif seragamNilai buatan bisa menyesatkan bila kolom heterogen
Nilai baris lainffill(), harga produk sejenisDeret waktu; kolom punya acuan paralelPerubahan 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() dan isna().mean() mengubah kekosongan menjadi hitungan dan persentase — dasar untuk memilih strategi.
  • dropna() menghapus baris bermasalah secara diam-diam; bandingkan shape sebelum dan sesudah, dan pertimbangkan subset untuk membatasi kerusakan.
  • fillna() dengan konstanta menjaga baris tetap hidup sambil menandai kekosongan secara jujur, misalnya label Tidak 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; parameter limit mencegah 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 .